Row-Level Security (RLS)
Row-Level Security (RLS) restricts which rows each user can see when querying graph data. RLS works transparently: filters are applied automatically at query time based on the authenticated user's entitlements. No changes to Cypher or Gremlin queries are required.
Overview
RLS uses an entitlement table in your data source to define which users can access which rows. Each row in the entitlement table maps a username to an authorized value for a resource type (e.g., team, level, region, tenant).
When a user runs a query, PuppyGraph:
- Looks up the user's entitlements from the entitlement table (cached with a configurable TTL).
- For each source table with security filters configured, checks which values the user is authorized to access.
- AND-combines all filter conditions for the table.
- Applies the filters to every data access, including intermediate traversal hops.
Users only see rows they are entitled to, at every step of the traversal.
Prerequisites
- Authentication must be enabled. RLS requires a username from the authenticated session. If RLS is enabled but no username is available, the query is rejected.
- An entitlement table must exist in one of your connected data sources.
Entitlement Table
The entitlement table maps usernames to authorized resource values. It must contain at least four columns:
| Column | Description | Example |
|---|---|---|
| Username | The user's login identifier | sso:alice |
| Resource Type | Category of entitlement | Team, Level |
| Resource Value | Specific allowed value | graph, senior |
| Is Authorized | Boolean authorization flag | true / false |
Example Entitlement Data
| USERNAME | RESOURCE_TYPE | RESOURCE_VALUE | IS_AUTHORIZED |
|---|---|---|---|
| sso:alice | Team | graph | true |
| sso:alice | Team | infra | true |
| sso:alice | Level | senior | true |
| sso:alice | Level | junior | true |
| sso:bob | Team | ui | true |
| sso:bob | Level | senior | true |
In this example, sso:alice can see rows whose team is graph or infra and whose level is senior or junior. sso:bob can only see rows whose team is ui and whose level is senior.
Schema Configuration
RLS is configured in the rowLevelSecurity section of your graph schema JSON. The configuration has three parts:
- Enable/disable. The
enabledflag. - Entitlement source. Where to find the entitlement table.
- Table security filters. Which columns on which tables to filter.
Configuration Reference
{
"rowLevelSecurity": {
"enabled": true,
"entitlementSource": {
"catalog": "<catalog_name>",
"schema": "<schema_name>",
"table": "<entitlement_table_name>",
"usernameColumn": "<column_name>",
"resourceTypeColumn": "<column_name>",
"resourceValueColumn": "<column_name>",
"isAuthorizedColumn": "<column_name>"
},
"tableSecurityFilter": [
{
"catalog": "<catalog_name>",
"schema": "<schema_name>",
"table": "<source_table_name>",
"filter": [
{
"column": "<security_column>",
"resourceType": "<resource_type>"
}
]
}
],
"entitlementCacheTtlSeconds": 3600
}
}
Fields
rowLevelSecurity
| Field | Required | Default | Description |
|---|---|---|---|
enabled |
Yes | Set to true to enable RLS |
|
entitlementSource |
Yes (if enabled) | Location and column mapping for the entitlement table | |
tableSecurityFilter |
Yes (if enabled) | List of tables and their security filter columns | |
entitlementCacheTtlSeconds |
No | 3600 |
How long (in seconds) to cache a user's entitlements before re-querying the entitlement table |
entitlementSource
| Field | Required | Description |
|---|---|---|
catalog |
Yes | Catalog containing the entitlement table |
schema |
Yes | Schema containing the entitlement table |
table |
Yes | Name of the entitlement table |
usernameColumn |
Yes | Column that stores the username |
resourceTypeColumn |
Yes | Column that stores the resource type |
resourceValueColumn |
Yes | Column that stores the resource value |
isAuthorizedColumn |
Yes | Column that stores the boolean authorization flag |
tableSecurityFilter
Each entry specifies a source table and the security filters to apply:
| Field | Required | Description |
|---|---|---|
catalog |
Yes | Catalog of the source table |
schema |
Yes | Schema of the source table |
table |
Yes | Name of the source table |
filter |
Yes | List of column-to-resource-type mappings |
Each filter entry:
| Field | Required | Description |
|---|---|---|
column |
Yes | Column name in the source table to filter on |
resourceType |
Yes | The resource type in the entitlement table that governs this column |
Complete Example
This example uses the TinkerPop modern graph (two node types person and software, two edge types knows and created) backed by Postgres tables. The person table is extended with team and level columns; those columns drive row-level filtering. An entitlement table in a separate security schema controls per-user access.
Source Tables
modern.person
+----+-------+-----+-------+--------+
| id | name | age | team | level |
+----+-------+-----+-------+--------+
| v1 | marko | 29 | graph | senior |
| v2 | vadas | 27 | infra | junior |
| v4 | josh | 32 | graph | senior |
| v6 | peter | 35 | ui | senior |
+----+-------+-----+-------+--------+
security.user_entitlements
+-------------+---------------+----------------+---------------+
| username | resource_type | resource_value | is_authorized |
+-------------+---------------+----------------+---------------+
| sso:alice | Team | graph | true |
| sso:alice | Level | senior | true |
| sso:bob | Team | infra | true |
| sso:bob | Team | ui | true |
| sso:bob | Level | junior | true |
| sso:bob | Level | senior | true |
+-------------+---------------+----------------+---------------+
Schema JSON (RLS section)
Given a Postgres catalog named pg_modern with the source tables in schema modern and the entitlement table in schema security:
{
"rowLevelSecurity": {
"enabled": true,
"entitlementSource": {
"catalog": "pg_modern",
"schema": "security",
"table": "user_entitlements",
"usernameColumn": "username",
"resourceTypeColumn": "resource_type",
"resourceValueColumn": "resource_value",
"isAuthorizedColumn": "is_authorized"
},
"tableSecurityFilter": [
{
"catalog": "pg_modern",
"schema": "modern",
"table": "person",
"filter": [
{"column": "team", "resourceType": "Team"},
{"column": "level", "resourceType": "Level"}
]
}
],
"entitlementCacheTtlSeconds": 3600
}
}
How It Works
With this configuration:
- Entitlement table (
pg_modern.security.user_entitlements) is queried to look up each user's authorized resource values. Results are cached for the configured TTL (1 hour in this example). - The
persontable is filtered by bothteam(resource typeTeam) andlevel(resource typeLevel). A user must have entitlements for both resource types to see any persons. The filters are AND-combined. software,knows, andcreatedare not listed intableSecurityFilter, so they are unrestricted.
For example, when user sso:alice (entitled to Team={graph} and Level={senior}) runs:
The engine internally filters to only return rows where:
teamis in{graph}- AND
levelis in{senior}
So alice sees marko and josh. The filter is also applied to every traversal hop that scans person, so a query like MATCH (p:person)-[:created]->(s:software) RETURN p.name, s.name still only originates from persons alice is entitled to see.
More Entitlement Table Examples
The shape of the entitlement table is always the same (four columns mapping (username, resource_type, resource_value, is_authorized)), but it can model very different access policies. The examples below show common patterns.
Department-Based Access
A simpler scenario: an HR analytics graph where each analyst can only see employees in their own department.
Entitlement table (hr_catalog.security.user_entitlements)
| USERNAME | RESOURCE_TYPE | RESOURCE_VALUE | IS_AUTHORIZED |
|---|---|---|---|
| sso:alice@example.com | Department | Engineering | true |
| sso:alice@example.com | Department | Product | true |
| sso:bob@example.com | Department | Sales | true |
| sso:carol@example.com | Department | Engineering | true |
Schema configuration
{
"rowLevelSecurity": {
"enabled": true,
"entitlementSource": {
"catalog": "hr_catalog",
"schema": "security",
"table": "user_entitlements",
"usernameColumn": "username",
"resourceTypeColumn": "resource_type",
"resourceValueColumn": "resource_value",
"isAuthorizedColumn": "is_authorized"
},
"tableSecurityFilter": [
{
"catalog": "hr_catalog",
"schema": "people",
"table": "employees",
"filter": [
{"column": "department", "resourceType": "Department"}
]
},
{
"catalog": "hr_catalog",
"schema": "people",
"table": "performance_reviews",
"filter": [
{"column": "employee_department", "resourceType": "Department"}
]
}
]
}
}
Alice sees employees and performance reviews from Engineering and Product. Bob sees only Sales. Carol sees only Engineering.
Multi-Tenant SaaS
For a multi-tenant SaaS where each customer (tenant) must only see their own data, the entitlement table holds a single resource type: Tenant.
Entitlement table (saas_catalog.security.tenant_access)
| USERNAME | RESOURCE_TYPE | RESOURCE_VALUE | IS_AUTHORIZED |
|---|---|---|---|
| sso:user1@acme.com | Tenant | acme-corp | true |
| sso:user2@acme.com | Tenant | acme-corp | true |
| sso:admin@globex.com | Tenant | globex-llc | true |
Schema configuration
{
"rowLevelSecurity": {
"enabled": true,
"entitlementSource": {
"catalog": "saas_catalog",
"schema": "security",
"table": "tenant_access",
"usernameColumn": "username",
"resourceTypeColumn": "resource_type",
"resourceValueColumn": "resource_value",
"isAuthorizedColumn": "is_authorized"
},
"tableSecurityFilter": [
{
"catalog": "saas_catalog",
"schema": "app",
"table": "orders",
"filter": [{"column": "tenant_id", "resourceType": "Tenant"}]
},
{
"catalog": "saas_catalog",
"schema": "app",
"table": "customers",
"filter": [{"column": "tenant_id", "resourceType": "Tenant"}]
},
{
"catalog": "saas_catalog",
"schema": "app",
"table": "events",
"filter": [{"column": "tenant_id", "resourceType": "Tenant"}]
}
]
}
}
Because every fact table includes a tenant_id column and the entitlement table only grants each user a single tenant, RLS provides hard tenant isolation across the graph, including across multi-hop traversals.
Region and Project (Multi-Type Combined)
When access is governed by more than one independent attribute (region AND project), list both resource types in the entitlement table and reference each in the corresponding filter columns. RLS will AND-combine the filters automatically.
Entitlement table
| USERNAME | RESOURCE_TYPE | RESOURCE_VALUE | IS_AUTHORIZED |
|---|---|---|---|
| sso:dana@example.com | Region | NA | true |
| sso:dana@example.com | Region | EU | true |
| sso:dana@example.com | Project | atlas | true |
| sso:dana@example.com | Project | beacon | true |
Schema configuration
{
"tableSecurityFilter": [
{
"catalog": "ops_catalog",
"schema": "delivery",
"table": "deployments",
"filter": [
{"column": "region", "resourceType": "Region"},
{"column": "project_code", "resourceType": "Project"}
]
}
]
}
Dana sees deployments only when the row's region is in {NA, EU} and project_code is in {atlas, beacon}. A row that matches one filter but not the other is filtered out.
Behavior Notes
Filter Combination
When a table has multiple security filter columns, the conditions are AND-combined. A row must satisfy all filter conditions to be visible. For example, if a table filters on both team and level, the user must have entitlements for the row's team AND the row's level.
Missing Entitlements
RLS behavior when entitlements are missing depends on what is missing. The four cases are:
| Condition | Behavior |
|---|---|
| RLS enabled, no authenticated username available | Query is rejected with an error. |
| User row missing for one or more required resource types but present for others | Tables filtered on the missing resource types return zero rows. Tables filtered only on resource types the user is entitled to are filtered normally. |
| User has no rows at all in the entitlement table | RLS is bypassed for this user. They see every row in every RLS-managed table. |
| Entitlement-table lookup fails and no cached entry exists | Query is rejected with an error. |
Users absent from the entitlement table bypass RLS
The third row above is the case to watch out for. PuppyGraph treats no entitlement rows for a user differently from no authorized values for a specific resource type. The first case bypasses RLS; the second case blocks every row from that table. The intent is that admins and system accounts can opt out of RLS by being absent from the entitlement table. If you want everyone (including admins) to be subject to RLS, ensure every user has at least one row in the entitlement table covering every resource type that any RLS-managed table filters on, even if some rows have is_authorized = false.
Local Tables
RLS table security filters also work with local tables. For local tables, set catalog and schema to "" (empty string) in tableSecurityFilter. The entitlement source table should be in an external catalog so it can be managed and updated through your existing data pipeline.
Tables Without Filters
Source tables that are not listed in tableSecurityFilter are unfiltered: all rows are visible to all users. Only tables explicitly configured with security filters are restricted.
Entitlement Caching
User entitlements are cached to avoid querying the entitlement table on every graph query. The cache TTL is controlled by entitlementCacheTtlSeconds (default: 3600 seconds / 1 hour). After the TTL expires, the next query triggers a fresh lookup from the entitlement table.
The cache is invalidated when the graph schema is reloaded.
Traversal Security
RLS filtering is applied at every step of a graph traversal, not just the starting vertices. A user cannot reach unauthorized data by traversing edges from authorized starting points. For example, if user sso:alice is not entitled to team ui, they cannot see any ui-team person vertices even if those vertices are connected (through knows or created) to persons alice can see.
Admin Users
The Admin role is not automatically exempt from RLS. Admins follow the same rules as everyone else: an admin who appears in the entitlement table is filtered by their rows, and an admin who is absent from the entitlement table bypasses RLS (see Missing Entitlements). If you want every user to be subject to RLS, ensure every user has at least one row in the entitlement table.
Relationship to RBAC
RLS and RBAC are complementary:
| Concern | RBAC | RLS |
|---|---|---|
| Controls | Whether you can query | What rows you see |
| Identity source | PuppyGraph user store | External entitlement table |
| Granularity | API/resource level | Row level per table |
A user must pass RBAC checks (e.g., query execution permission) before RLS is applied. RLS further restricts the result set.
Disabling RLS
To disable RLS, set "enabled": false in the rowLevelSecurity configuration, or remove the rowLevelSecurity section entirely. When disabled, all authenticated users see all rows (subject to RBAC permissions).