Setting Up Snowflake User Impersonation for SSO
Summary
Snowflake query impersonation lets PuppyGraph connect through one shared Snowflake service account while asking Snowflake to evaluate queries for the current SSO user.
In this tutorial, you will:
- Create Snowflake demo tables, a proxy-user stored procedure, and Snowflake access policies.
- Start PuppyGraph and a local Keycloak identity provider with Docker Compose.
- Sign in as the local PuppyGraph administrator and upload a graph schema.
- Sign in through SSO as
aliceandbob. - Run Cypher and PageRank queries and verify that Snowflake returns only the data authorized for the current SSO user.
- Optionally switch the catalog to session claims, where the user's verified JWT claims become Snowflake session variables instead of a proxy-user name.
See Connecting to Snowflake for the catalog reference and OAuth / OIDC Single Sign-On for the SSO configuration reference.
Prerequisites
- A Snowflake account and a role that can create databases, schemas, tables, stored procedures, and row access policies.
- A Snowflake service-account user configured for key-pair authentication.
- The service-account role can use the configured warehouse, read the demo tables, and call the proxy-user stored procedure.
- The private key for that service account in PKCS #8 format.
- A browser for the PuppyGraph Web UI.
Setup
Configure Snowflake
Run the complete script in a Snowflake worksheet using a role with the required setup privileges.
Administrative role
The script starts with USE ROLE ACCOUNTADMIN. Replace that role if your
organization uses a more limited administrative role for this setup.
Demo object replacement
The script creates the PUPPYGRAPH_SECURITY and PUPPYGRAPH_DEMO
databases if they do not already exist. It also uses CREATE OR REPLACE
for the proxy-user stored procedure, the PUPPYGRAPH_DEMO.MODERN tables,
and the row access policies, so existing objects with those names will be
replaced. Run it in a sandbox account, or confirm these database and object
names are safe to use in your Snowflake account.
The script:
- Creates the demo database, schema, and modern graph tables.
- Creates a proxy-user stored procedure that stores the current SSO user in a Snowflake session variable.
- Creates row access policies that read that session variable.
- Attaches the policies to the demo tables.
It uses CREATE OR REPLACE for the demo objects, so you can rerun the script
after fixing a worksheet mistake.
snowflake.sql
USE ROLE ACCOUNTADMIN;
-- ============================================================
-- 0. Create the security/utility database and schema.
-- PuppyGraph calls:
-- CALL "PUPPYGRAPH_SECURITY"."UTIL"."SET_PROXY_USER"(?)
-- ============================================================
CREATE DATABASE IF NOT EXISTS PUPPYGRAPH_SECURITY;
CREATE SCHEMA IF NOT EXISTS PUPPYGRAPH_SECURITY.UTIL;
-- ============================================================
-- 1. Create the test database and schema.
-- ============================================================
CREATE DATABASE IF NOT EXISTS PUPPYGRAPH_DEMO;
CREATE SCHEMA IF NOT EXISTS PUPPYGRAPH_DEMO.MODERN;
-- ============================================================
-- 2. Create the proxy-user stored procedure.
-- ============================================================
CREATE OR REPLACE PROCEDURE "PUPPYGRAPH_SECURITY"."UTIL"."SET_PROXY_USER"(USER_NAME STRING)
RETURNS STRING
LANGUAGE JAVASCRIPT
EXECUTE AS CALLER
AS
$$
if (USER_NAME === null || USER_NAME.trim() === "") {
throw "USER_NAME cannot be null or empty";
}
var normalizedUser = USER_NAME.trim().toLowerCase();
snowflake.createStatement({
sqlText: "SET PG_PROXY_USER = ?",
binds: [normalizedUser]
}).execute();
return "PG_PROXY_USER set to: " + normalizedUser;
$$;
-- ============================================================
-- 3. Recreate the PUPPYGRAPH_DEMO.MODERN test tables.
-- ============================================================
CREATE OR REPLACE TABLE PUPPYGRAPH_DEMO.MODERN.PERSON (
id STRING,
name STRING,
age INTEGER
);
INSERT INTO PUPPYGRAPH_DEMO.MODERN.PERSON (id, name, age) VALUES
('v1', 'marko', 29),
('v2', 'vadas', 27),
('v4', 'josh', 32),
('v6', 'peter', 35);
CREATE OR REPLACE TABLE PUPPYGRAPH_DEMO.MODERN.SOFTWARE (
id STRING,
name STRING,
lang STRING
);
INSERT INTO PUPPYGRAPH_DEMO.MODERN.SOFTWARE (id, name, lang) VALUES
('v3', 'lop', 'java'),
('v5', 'ripple', 'java');
CREATE OR REPLACE TABLE PUPPYGRAPH_DEMO.MODERN.CREATED (
id STRING,
from_id STRING,
to_id STRING,
weight DOUBLE
);
INSERT INTO PUPPYGRAPH_DEMO.MODERN.CREATED (id, from_id, to_id, weight) VALUES
('e9', 'v1', 'v3', 0.4),
('e10', 'v4', 'v5', 1.0),
('e11', 'v4', 'v3', 0.4),
('e12', 'v6', 'v3', 0.2);
CREATE OR REPLACE TABLE PUPPYGRAPH_DEMO.MODERN.KNOWS (
id STRING,
from_id STRING,
to_id STRING,
weight DOUBLE
);
INSERT INTO PUPPYGRAPH_DEMO.MODERN.KNOWS (id, from_id, to_id, weight) VALUES
('e7', 'v1', 'v2', 0.5),
('e8', 'v1', 'v4', 1.0);
-- ============================================================
-- 4. Create row access policies.
-- ============================================================
CREATE OR REPLACE ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_PERSON_READ
AS (id STRING)
RETURNS BOOLEAN ->
CASE LOWER(GETVARIABLE('PG_PROXY_USER'))
WHEN 'alice' THEN id IN ('v1', 'v2', 'v4')
WHEN 'bob' THEN id IN ('v4', 'v6')
ELSE FALSE
END;
CREATE OR REPLACE ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_KNOWS_READ
AS (id STRING)
RETURNS BOOLEAN -> LOWER(GETVARIABLE('PG_PROXY_USER')) = 'alice';
CREATE OR REPLACE ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_SOFTWARE_READ
AS (id STRING)
RETURNS BOOLEAN ->
CASE LOWER(GETVARIABLE('PG_PROXY_USER'))
WHEN 'alice' THEN id = 'v5'
WHEN 'bob' THEN id = 'v3'
ELSE FALSE
END;
CREATE OR REPLACE ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_CREATED_READ
AS (id STRING)
RETURNS BOOLEAN ->
CASE LOWER(GETVARIABLE('PG_PROXY_USER'))
WHEN 'alice' THEN id = 'e10'
WHEN 'bob' THEN id = 'e12'
ELSE FALSE
END;
-- ============================================================
-- 5. Attach row access policies to the tables.
-- ============================================================
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.PERSON
ADD ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_PERSON_READ
ON (id);
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.KNOWS
ADD ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_KNOWS_READ
ON (id);
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.SOFTWARE
ADD ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_SOFTWARE_READ
ON (id);
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.CREATED
ADD ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_CREATED_READ
ON (id);
The row access policies return no rows when PG_PROXY_USER is absent or when
the user is not allowed to read a table. This makes the Snowflake policy itself
fail closed.
Create the local deployment
Create a working directory:
Copy the Snowflake service account's private key into this directory as
snowflake_rsa_key.p8.
Create the Keycloak realm
Create puppygraph-realm.json:
puppygraph-realm.json
{
"realm": "puppygraph",
"enabled": true,
"sslRequired": "none",
"registrationAllowed": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": false,
"editUsernameAllowed": false,
"bruteForceProtected": false,
"accessTokenLifespan": 300,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"clients": [
{
"clientId": "puppygraph-app",
"name": "PuppyGraph SSO Client",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "puppygraph-sso-secret",
"redirectUris": [
"http://localhost:8081/sso_callback",
"http://localhost:8081/*"
],
"webOrigins": [
"http://localhost:8081"
],
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"publicClient": false,
"protocol": "openid-connect",
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "http://localhost:8081/*"
},
"defaultClientScopes": [
"openid",
"profile",
"email"
]
}
],
"users": [
{
"username": "alice",
"enabled": true,
"email": "alice@puppygraph.local",
"emailVerified": true,
"firstName": "Alice",
"lastName": "Engineering",
"credentials": [
{
"type": "password",
"value": "alice",
"temporary": false
}
]
},
{
"username": "bob",
"enabled": true,
"email": "bob@puppygraph.local",
"emailVerified": true,
"firstName": "Bob",
"lastName": "Sales",
"credentials": [
{
"type": "password",
"value": "bob",
"temporary": false
}
]
}
]
}
Create the Docker Compose file
Create docker-compose.yaml:
docker-compose.yaml
services:
keycloak:
image: quay.io/keycloak/keycloak:26.0
command:
- start-dev
- --import-realm
- --http-port=18080
- --hostname=http://localhost:18080
- --health-enabled=true
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- "127.0.0.1:18080:18080"
volumes:
- ./puppygraph-realm.json:/opt/keycloak/data/import/puppygraph-realm.json:ro
healthcheck:
test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/18080"]
interval: 5s
timeout: 3s
retries: 30
start_period: 20s
puppygraph:
image: puppygraph/puppygraph:latest
pull_policy: always
ports:
- "127.0.0.1:8081:8081"
- "127.0.0.1:8083:8083"
- "127.0.0.1:8182:8182"
- "127.0.0.1:7687:7687"
environment:
PUPPYGRAPH_USERNAME: puppygraph
PUPPYGRAPH_PASSWORD: puppygraph123
SSO_ENABLED: "true"
SSO_CLIENT_ID: puppygraph-app
SSO_CLIENT_SECRET: puppygraph-sso-secret
SSO_ISSUER: http://localhost:18080/realms/puppygraph
SSO_URL: http://localhost:18080/realms/puppygraph/protocol/openid-connect/auth
SSO_ACCESS_TOKEN_URL: http://keycloak:18080/realms/puppygraph/protocol/openid-connect/token
SSO_JWKS_URL: http://keycloak:18080/realms/puppygraph/protocol/openid-connect/certs
SSO_CALLBACK_URL: http://localhost:8081/sso_callback
SSO_CLAIM_AS_USER_ID: preferred_username
RBAC_ENABLED: "true"
RBAC_DEFAULT_ROLE: Analyst
BOLTSERVER_AUTHENTICATION_ENABLED: "true"
GREMLINSERVER_AUTHENTICATION_ENABLED: "true"
AUTHENTICATION_JWT_SECRETKEY: snowflake-impersonation-demo-secret
SNOWFLAKE_ACCOUNT_IDENTIFIER: <account_identifier>
SNOWFLAKE_SERVICE_ACCOUNT_USER: <service_account_username>
SNOWFLAKE_WAREHOUSE: <warehouse>
SNOWFLAKE_PRIVATE_KEY_PASSWORD: <private_key_password>
volumes:
- ./snowflake_rsa_key.p8:/home/keys/snowflake_rsa_key.p8:ro
depends_on:
keycloak:
condition: service_healthy
Replace the Snowflake environment variable values before starting the
containers. schema.json references these variables using the ${ENV:...}
syntax, so secrets such as the private key password do not need to be written
directly into the schema file.
Start Keycloak and PuppyGraph:
Wait until PuppyGraph is ready:
Create the graph schema
Create schema.json:
schema.json
{
"catalog": [
{
"name": "snowflake_puppygraph_demo",
"type": "snowflake",
"jdbc": {
"username": "${ENV:SNOWFLAKE_SERVICE_ACCOUNT_USER}",
"jdbcUri": "jdbc:snowflake://${ENV:SNOWFLAKE_ACCOUNT_IDENTIFIER}.snowflakecomputing.com/?db=PUPPYGRAPH_DEMO&warehouse=${ENV:SNOWFLAKE_WAREHOUSE}&private_key_file=/home/keys/snowflake_rsa_key.p8&private_key_file_pwd=${ENV:SNOWFLAKE_PRIVATE_KEY_PASSWORD}",
"driverClass": "net.snowflake.client.jdbc.SnowflakeDriver"
},
"identityPropagation": {
"mode": "proxyUser",
"proxyUserProcedure": "\"PUPPYGRAPH_SECURITY\".\"UTIL\".\"SET_PROXY_USER\""
}
}
],
"node": [
{
"label": "Person",
"dataSourceGroup": {
"externalDataSource": {
"enabled": true,
"catalog": "snowflake_puppygraph_demo",
"schema": "MODERN",
"table": "PERSON",
"mappedField": [
{"sourceFieldName": "ID", "targetFieldName": "id"},
{"sourceFieldName": "NAME", "targetFieldName": "name"},
{"sourceFieldName": "AGE", "targetFieldName": "age"}
]
}
},
"id": [{"name": "id", "type": "STRING"}],
"attribute": [
{"name": "name", "type": "STRING"},
{"name": "age", "type": "LONG"}
]
},
{
"label": "Software",
"dataSourceGroup": {
"externalDataSource": {
"enabled": true,
"catalog": "snowflake_puppygraph_demo",
"schema": "MODERN",
"table": "SOFTWARE",
"mappedField": [
{"sourceFieldName": "ID", "targetFieldName": "id"},
{"sourceFieldName": "NAME", "targetFieldName": "name"},
{"sourceFieldName": "LANG", "targetFieldName": "lang"}
]
}
},
"id": [{"name": "id", "type": "STRING"}],
"attribute": [
{"name": "name", "type": "STRING"},
{"name": "lang", "type": "STRING"}
]
}
],
"edge": [
{
"label": "Knows",
"fromNodeLabel": "Person",
"toNodeLabel": "Person",
"dataSourceGroup": {
"externalDataSource": {
"enabled": true,
"catalog": "snowflake_puppygraph_demo",
"schema": "MODERN",
"table": "KNOWS",
"mappedField": [
{"sourceFieldName": "ID", "targetFieldName": "id"},
{"sourceFieldName": "FROM_ID", "targetFieldName": "from_id"},
{"sourceFieldName": "TO_ID", "targetFieldName": "to_id"},
{"sourceFieldName": "WEIGHT", "targetFieldName": "weight"}
]
}
},
"id": [{"name": "id", "type": "STRING"}],
"fromKey": [{"name": "from_id", "type": "STRING"}],
"toKey": [{"name": "to_id", "type": "STRING"}],
"attribute": [{"name": "weight", "type": "DOUBLE"}]
},
{
"label": "Created",
"fromNodeLabel": "Person",
"toNodeLabel": "Software",
"dataSourceGroup": {
"externalDataSource": {
"enabled": true,
"catalog": "snowflake_puppygraph_demo",
"schema": "MODERN",
"table": "CREATED",
"mappedField": [
{"sourceFieldName": "ID", "targetFieldName": "id"},
{"sourceFieldName": "FROM_ID", "targetFieldName": "from_id"},
{"sourceFieldName": "TO_ID", "targetFieldName": "to_id"},
{"sourceFieldName": "WEIGHT", "targetFieldName": "weight"}
]
}
},
"id": [{"name": "id", "type": "STRING"}],
"fromKey": [{"name": "from_id", "type": "STRING"}],
"toKey": [{"name": "to_id", "type": "STRING"}],
"attribute": [{"name": "weight", "type": "DOUBLE"}]
}
]
}
The identityPropagation block sits on the catalog, beside jdbc. Schemas
written for earlier releases carried the same setting as
jdbc.queryImpersonation.proxyUserProcedure; they still load and mean the same
thing. Metadata caching can stay at its default: Snowflake policies filter rows,
not table and column metadata, so there is no reason to set enableMetaCache
to "false" on an impersonated catalog.
Upload the schema as the local administrator
- Open http://localhost:8081.
- Use the local username and password form to sign in as
puppygraph/puppygraph123. Do not use Sign in with SSO for this step. - Open Graph from the left navigation.
- Choose Upload Schema and select
schema.json.
The local administrator is used only to create the graph. A local user has no SSO identity, so PuppyGraph intentionally rejects data queries against an impersonation-enabled Snowflake catalog.
Run regular Cypher as an SSO user
Sign out, choose Sign in with SSO, and log in to Keycloak as
alice / alice.
Alice can read marko, vadas, and josh from PERSON. Run:
Expected result:
| name |
|---|
| josh |
| marko |
| vadas |
Alice can also read the josh to ripple Created edge:
MATCH (p:Person)-[:Created]->(software:Software)
RETURN p.name AS person,
software.name AS software
ORDER BY software
Expected result:
| person | software |
|---|---|
| josh | ripple |
Sign out and sign in through SSO as bob / bob. Bob sees a different slice
of the same PERSON table:
Expected result:
| name |
|---|
| josh |
| peter |
Bob can read Peter's Created edge to lop:
MATCH (p:Person)-[:Created]->(software:Software)
RETURN p.name AS person,
software.name AS software
ORDER BY software
Expected result:
| person | software |
|---|---|
| peter | lop |
To verify that Bob cannot read Alice's authorized software row, run:
MATCH (p:Person)-[:Created]->(software:Software {name: 'ripple'})
RETURN p.name AS person,
software.name AS software
Expected result: 0 rows.
Run a graph algorithm as an SSO user
Graph algorithms use the same query identity and Snowflake authorization as regular Cypher scans.
As Alice, run PageRank over the Knows edges Alice can read:
CALL algo.paral.pagerank({
labels: ['Person'],
relationshipTypes: ['Knows'],
maxIterations: 5
})
YIELD id, score
RETURN id, score
ORDER BY id
The result contains these node IDs:
Switch to session claims
Mode proxyUser hands Snowflake one string, the SSO account name, and leaves
the policy to look the user's entitlements up. Mode sessionClaims instead
hands Snowflake the claims of the user's verified JWT as session variables, so a
policy can read GETVARIABLE('TENANT_ID') directly and no lookup table is
needed. The two modes use procedures with different signatures, so moving a
catalog from one to the other is a deliberate change rather than an upgrade.
Give the users claims and trust the issuer
In Keycloak, add a user attribute tenant_id to alice (t-42) and to bob
(t-1), create a public client puppygraph-api with Direct access grants
enabled, and give it two protocol mappers: an audience mapper that adds
puppygraph to the token's aud, and a user-attribute mapper that puts the
tenant_id attribute into the access token as the tenant_id claim.
A browser SSO session does not carry the token's claims yet, so this part of the tutorial presents Keycloak's token directly to the REST Cypher endpoint. Register Keycloak as a trusted issuer first, as the local administrator:
curl -u puppygraph:puppygraph123 -X POST http://localhost:8081/api/trusted-issuers \
-H 'Content-Type: application/json' \
-d '{
"alias": "keycloak",
"issuer": "http://localhost:18080/realms/puppygraph",
"jwksUrl": "http://keycloak:18080/realms/puppygraph/protocol/openid-connect/certs",
"allowedAudiences": ["puppygraph"],
"allowedAlgorithms": ["RS256"],
"principalClaim": "preferred_username",
"defaultRole": "Analyst",
"isDefault": true
}'
issuer is the URL the token carries, jwksUrl the address at which the
PuppyGraph container reaches Keycloak.
Create the claim-set procedure and tenant-aware policies
The procedure receives one JSON argument,
{"set": {"TENANT_ID": "t-42"}, "unset": ["TENANT_ID"]}. It must UNSET every
variable listed under unset before it SETs the ones under set, because
PuppyGraph pools connections and a variable the previous user carried must not
survive for the next one. It returns a JSON array naming the variables it set;
PuppyGraph fails the query closed when that list differs from what it asked
for.
USE ROLE ACCOUNTADMIN;
CREATE OR REPLACE PROCEDURE PUPPYGRAPH_SECURITY.UTIL.SET_SESSION_CLAIMS(CLAIMS VARCHAR)
RETURNS VARCHAR
LANGUAGE JAVASCRIPT
EXECUTE AS CALLER
AS
$$
var spec = JSON.parse(CLAIMS);
var validName = /^[A-Z_][A-Z0-9_]*$/;
var unset = spec.unset || [];
var set = spec.set || {};
var names = Object.keys(set).sort();
unset.concat(names).forEach(function (name) {
if (!validName.test(name)) { throw "invalid session variable name: " + name; }
});
if (unset.length > 0) {
// UNSET fails for a variable that was never set on this session, so give each one a value first.
var nulls = unset.map(function () { return "NULL"; });
snowflake.createStatement({sqlText: "SET (" + unset.join(", ") + ") = (" + nulls.join(", ") + ")"}).execute();
snowflake.createStatement({sqlText: "UNSET (" + unset.join(", ") + ")"}).execute();
}
if (names.length > 0) {
var marks = names.map(function () { return "?"; });
var values = names.map(function (name) { return String(set[name]); });
snowflake.createStatement({sqlText: "SET (" + names.join(", ") + ") = (" + marks.join(", ") + ")", binds: values}).execute();
}
return JSON.stringify(names);
$$;
-- The proxy-user policies read PG_PROXY_USER, which the claim-set procedure
-- neither sets nor clears: a pooled connection can still carry the variable a
-- previous proxy-user call left, so a table left on such a policy could answer
-- from that stale value. Replace every proxy-user policy. A row access policy
-- also restricts the rows DML can reach, so detach each one before the
-- backfill or the UPDATE touches nothing.
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.PERSON DROP ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_PERSON_READ;
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.KNOWS DROP ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_KNOWS_READ;
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.SOFTWARE DROP ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_SOFTWARE_READ;
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.CREATED DROP ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_CREATED_READ;
-- A tenant partition of the demo data, deliberately different from the
-- overlapping proxy-user slices above: every row belongs to exactly one tenant.
-- t-42 (alice): marko, vadas, ripple and the edges leaving marko;
-- t-1 (bob): josh, peter, lop and the edges leaving josh and peter.
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.PERSON ADD COLUMN tenant_id STRING;
UPDATE PUPPYGRAPH_DEMO.MODERN.PERSON SET tenant_id = CASE WHEN id IN ('v1', 'v2') THEN 't-42' ELSE 't-1' END;
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.SOFTWARE ADD COLUMN tenant_id STRING;
UPDATE PUPPYGRAPH_DEMO.MODERN.SOFTWARE SET tenant_id = CASE WHEN id = 'v5' THEN 't-42' ELSE 't-1' END;
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.KNOWS ADD COLUMN tenant_id STRING;
UPDATE PUPPYGRAPH_DEMO.MODERN.KNOWS SET tenant_id = 't-42';
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.CREATED ADD COLUMN tenant_id STRING;
UPDATE PUPPYGRAPH_DEMO.MODERN.CREATED SET tenant_id = CASE WHEN id = 'e9' THEN 't-42' ELSE 't-1' END;
-- One policy shape serves every table: the row belongs to the session's tenant.
CREATE OR REPLACE ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_TENANT
AS (tenant_id STRING)
RETURNS BOOLEAN -> tenant_id = GETVARIABLE('TENANT_ID');
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.PERSON ADD ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_TENANT ON (tenant_id);
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.SOFTWARE ADD ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_TENANT ON (tenant_id);
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.KNOWS ADD ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_TENANT ON (tenant_id);
ALTER TABLE PUPPYGRAPH_DEMO.MODERN.CREATED ADD ROW ACCESS POLICY PUPPYGRAPH_DEMO.MODERN.RAP_TENANT ON (tenant_id);
GETVARIABLE returns NULL when the variable is not set, so the policy returns
no rows to a connection on which the claim-set call did not run. Relationship
queries and the graph algorithm above keep working because every mapped table
now carries the same tenant policy. Do not leave a table on a PG_PROXY_USER
policy: the claim-set procedure clears only the claim variables, so a pooled
connection may still hold the proxy variable from an earlier session and such a
table could answer from it. The procedure
rethrows every error: one that swallowed a failed UNSET could report success
while a previous user's variable is still set on the pooled connection. Grant
the service account USAGE on the new procedure (Snowflake's procedure
privilege; EXECUTE is not one), plus USAGE on PUPPYGRAPH_SECURITY and its
UTIL schema if the role does not have them yet:
GRANT USAGE ON DATABASE PUPPYGRAPH_SECURITY TO ROLE <service_account_role>;
GRANT USAGE ON SCHEMA PUPPYGRAPH_SECURITY.UTIL TO ROLE <service_account_role>;
GRANT USAGE ON PROCEDURE PUPPYGRAPH_SECURITY.UTIL.SET_SESSION_CLAIMS(VARCHAR) TO ROLE <service_account_role>;
Point the catalog at the claims
Sign in again as the local administrator (puppygraph), as in
Upload the schema as the local administrator:
the schema's catalog section needs CATALOG:write, which the SSO users'
Analyst role lacks. Then replace the catalog's identityPropagation block
and upload the schema again:
"identityPropagation": {
"mode": "sessionClaims",
"claimSetProcedure": "\"PUPPYGRAPH_SECURITY\".\"UTIL\".\"SET_SESSION_CLAIMS\"",
"claims": [
{ "claim": "tenant_id", "required": true }
]
}
Each claim becomes the session variable UPPER(claim). A user whose token
lacks a claim marked required gets error IDP-02, naming the claim, before
any Snowflake query runs; there is no setting that lets such a query proceed.
Obtain a token for alice and run the query with it:
TOKEN=$(curl -s -X POST http://localhost:18080/realms/puppygraph/protocol/openid-connect/token \
-d grant_type=password -d client_id=puppygraph-api -d scope=openid \
-d username=alice -d password=alice | jq -r .access_token)
curl -s -X POST http://localhost:8081/submitCypher \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"query": "MATCH (p:Person) RETURN p.name AS name ORDER BY name"}'
The result names marko and vadas; the same call with a token for bob
names josh and peter. A token without tenant_id gets error IDP-02
naming the claim instead of rows. Both modes need a PuppyGraph release whose
DataAccess understands the request context; PuppyGraph rejects a
sessionClaims catalog with a message naming the missing support wherever it
does not.
Cleanup
Stop the local containers:
Remove the Snowflake demo database and security database when they are no longer needed: