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.
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",
"enableMetaCache": "false",
"queryImpersonation": {
"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"}]
}
]
}
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 the graph schema page.
- 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:
Cleanup
Stop the local containers:
Remove the Snowflake demo database and security database when they are no longer needed: