Skip to content

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:

  1. Looks up the user's entitlements from the entitlement table (cached with a configurable TTL).
  2. For each source table with security filters configured, checks which values the user is authorized to access.
  3. AND-combines all filter conditions for the table.
  4. 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:

  1. Enable/disable. The enabled flag.
  2. Entitlement source. Where to find the entitlement table.
  3. 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 person table is filtered by both team (resource type Team) and level (resource type Level). A user must have entitlements for both resource types to see any persons. The filters are AND-combined.
  • software, knows, and created are not listed in tableSecurityFilter, so they are unrestricted.

For example, when user sso:alice (entitled to Team={graph} and Level={senior}) runs:

MATCH (p:person) RETURN p.name, p.team, p.level

The engine internally filters to only return rows where:

  • team is in {graph}
  • AND level is 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).