Row-Level Security (RLS) in Geode provides fine-grained access control at the node and relationship level, allowing you to restrict which graph elements users can access based on flexible policy rules. This is essential for multi-tenant applications, data privacy compliance, and protecting sensitive information within shared graph databases.
Understanding Row-Level Security
Traditional database access control operates at the table or column level, granting or denying access to entire datasets. Row-Level Security goes further by controlling access to individual rows (in Geode’s case, nodes and relationships) based on the context of the query and the user executing it.
In graph databases, RLS enables:
- Multi-tenant isolation: Different customers see only their data
- Data compartmentalization: Users access only data relevant to their role
- Privacy compliance: Automatic filtering of sensitive information
- Hierarchical access: Organizational structures reflected in data access
- Attribute-based access: Access based on node properties and relationships
RLS Policy Basics
Geode offers two distinct mechanisms, described in full under Row-Level Security :
- Attribute policies (
CALL geode.rls.add_attribute_policy(graph, label, row_property, attribute_key)) — real per-row filtering. A row is visible only when the named property equals the caller’s principal attribute of that name. Node labels only; equality only. - Graph-scoped policies (
CREATE POLICY … ON GRAPH … FOR <actions> [USING …]) — coarse authorization over a whole graph. Actions areREAD,WRITE,MATCH,CREATE,UPDATE,DELETE,SCHEMA(notSELECT/INSERT), and theUSINGpredicate compiles only equality against a string literal.
Creating Basic Policies
-- Only allow users to see their own data
-- Person.person_id must equal the caller's 'username' principal attribute
CALL geode.rls.add_attribute_policy('app', 'Person', 'person_id', 'username');
-- Allow managers to see their team's data
-- Employee.manager_id must equal the caller's 'username' principal attribute
CALL geode.rls.add_attribute_policy('app', 'Employee', 'manager_id', 'username');
-- Allow admins to see everything
CREATE POLICY admin_full_access ON GRAPH app FOR READ, MATCH USING true;
Policy Evaluation
Attribute policies are restrictive: a row must satisfy the policy to be returned, and a principal with no value for the attribute key matches nothing, so the mechanism fails closed rather than falling back to “see everything”. The filter is applied inside the match loop, so the query text never changes:
-- The user writes this
MATCH (p:Person)
RETURN p.name, p.email;
-- With an attribute policy on (Person, person_id, username), only rows whose
-- person_id equals the caller's 'username' attribute are returned. There is no
-- rewritten predicate to inspect; use EXPLAIN to confirm the scan is filtered.
CREATE POLICY … USING (…) expression outside the supported subset is accepted
at DDL time without error, stored uncompiled, and then never grants access —
the graph reads as empty. Always verify with a read-back query. See
the USING subset
.Multi-Tenant Applications
RLS is ideal for SaaS applications serving multiple customers:
Tenant Isolation
Per-row tenant isolation is installed with an attribute policy, one per label, and resolved against a per-user attribute:
-- Row is visible only when row.tenant_id equals the caller's `tenant` attribute
CALL geode.rls.add_attribute_policy('app', 'Customer', 'tenant_id', 'tenant');
CALL geode.rls.add_attribute_policy('app', 'Order', 'tenant_id', 'tenant');
-- Give each user their tenant value
CALL geode.auth.user_set_attribute('alice', 'tenant', 'tenant_123');
Every node includes a tenant_id property:
-- Create customer data with tenant ID
CREATE (:Customer {
name: 'Acme Corp',
tenant_id: 'tenant_123',
email: 'contact@acme.com'
});
-- User from tenant_123 can see this
-- Users from other tenants cannot
Shared Reference Data
Some data may be shared across tenants. An attribute policy tests a single equality, so “shared OR mine” cannot be expressed directly. The simplest model is to leave shared labels unprotected, so every tenant can read them:
-- No attribute policy on :ReferenceData, so it stays readable by every tenant.
CALL geode.rls.policy_count();
-- Create shared data
CREATE (:ReferenceData {
type: 'country',
name: 'United States',
is_shared: true
});
Hierarchical Access Control
Model organizational hierarchies with RLS:
-- Employees can see their own data
-- Employee.employee_id must equal the caller's 'username' principal attribute
CALL geode.rls.add_attribute_policy('app', 'Employee', 'employee_id', 'username');
-- Managers can see their direct reports
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'Employee', 'access_tag', 'access_tag');
-- Directors can see entire department
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'Employee', 'access_tag', 'access_tag');
-- Executives can see everything
CREATE POLICY executive_all_access ON GRAPH app FOR READ, MATCH USING true;
Graph-Based Hierarchy
Use the graph structure itself for hierarchical access:
-- Access based on organizational graph structure
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'Employee', 'access_tag', 'access_tag');
Attribute-Based Access Control
Define access based on node properties:
-- Access based on security clearance level
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'Document', 'access_tag', 'access_tag');
-- Access based on data classification
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'DataAsset', 'access_tag', 'access_tag');
-- Time-based access
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'TemporaryData', 'access_tag', 'access_tag');
Column-Level Security with RLS
Restrict access to specific properties:
-- Policy that only exposes certain properties
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'Employee', 'access_tag', 'access_tag');
-- Sensitive properties like salary not included
MATCH (e:Employee)
RETURN e.name, e.salary; -- salary is NULL for public role
Conditional Insert/Update Policies
Write actions are gated at the graph level, by action, not per row. The action
list is READ, WRITE, MATCH, CREATE, UPDATE, DELETE, SCHEMA:
-- Only alice may create or update in the `app` graph
CREATE POLICY app_writers ON GRAPH app FOR CREATE, UPDATE
USING (current_user() = 'alice');
-- Nobody gets DELETE via this policy; it is simply not granted
CREATE POLICY app_readers ON GRAPH app FOR READ, MATCH USING true;
WITH CHECK enforcement on writes: an attribute policy
filters what a query can read, it does not validate the values a write
supplies. Enforce write-side invariants in the application, or with a uniqueness
or existence constraint. A row-valued predicate such as
USING (status != 'archived') does not compile and would silently deny all
access — see the USING subset
.Policy Functions
Create reusable functions for complex policies:
-- Define helper function
-- Geode has no user-defined functions, aggregates or procedures: the
-- grammar has no CREATE FUNCTION / AGGREGATE / PROCEDURE. Inline the
-- logic into the query, or compute it in the client.
-- Use in policy
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'Document', 'access_tag', 'access_tag');
Performance Optimization
RLS policies can impact query performance if not carefully designed:
Index Filtered Properties
An attribute policy adds an equality test on the row property to every scan of that label, so index the property the policy names:
CREATE INDEX customer_tenant FOR (c:Customer) ON (c.tenant_id);
-- The policy compares that same indexed property
CALL geode.rls.add_attribute_policy('app', 'Customer', 'tenant_id', 'tenant');
Avoid Complex Subqueries
-- Inefficient: Complex EXISTS subquery for every node
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'Document', 'access_tag', 'access_tag');
-- More efficient: Pre-compute accessible documents
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'Document', 'access_tag', 'access_tag');
Policy Caching
# Enable policy evaluation caching
geode serve --rls-cache-enabled=true \
--rls-cache-size=100MB \
--rls-cache-ttl=300s
Testing RLS Policies
Verify policies work correctly:
-- Test as specific user
SET ROLE 'employee';
SET SESSION current_user = 'alice@example.com';
MATCH (e:Employee)
RETURN count(e); -- Should only see accessible employees
-- Test as admin
SET ROLE 'admin';
MATCH (e:Employee)
RETURN count(e); -- Should see all employees
-- Verify policy enforcement
EXPLAIN MATCH (e:Employee) RETURN e;
-- Plan should show RLS filter: WHERE e.employee_id = current_user()
Automated Policy Testing
# Run RLS policy test suite
geode test-rls --policy=employee_access \
--test-users=alice,bob,admin \
--expected-results=test-cases.json
Policy Management
Viewing Policies
-- List all policies
SHOW POLICIES;
-- Show policies for specific label
SHOW POLICIES ON Employee;
-- Show policy details
DESCRIBE POLICY employee_self_access;
Modifying Policies
-- Drop policy
DROP POLICY employee_self_access;
-- Recreate with updated condition
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'Employee', 'access_tag', 'access_tag');
-- Disable policy temporarily
ALTER POLICY employee_self_access DISABLE;
-- Re-enable policy
ALTER POLICY employee_self_access ENABLE;
Best Practices
- Start with deny-all: Create restrictive policies by default, then grant access
- Use indexed properties: Filter on indexed properties for performance
- Test thoroughly: Verify policies with different user roles and scenarios
- Keep policies simple: Complex policies are hard to maintain and slow to evaluate
- Document policies: Clearly document the intent and scope of each policy
- Monitor performance: Track query performance impact of RLS policies
- Use policy functions: Extract complex logic into reusable functions
- Regular audits: Periodically review and update policies
- Principle of least privilege: Grant minimum necessary access
- Combine with RBAC: Use RLS together with role-based access control
Common Patterns
Department-Based Access
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'Employee', 'access_tag', 'access_tag');
Time-Based Access
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'SensitiveData', 'access_tag', 'access_tag');
Geographic Restrictions
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'CustomerData', 'access_tag', 'access_tag');
Data Owner Access
-- Not expressible as a Geode policy predicate (only `=` against a string
-- literal compiles); precompute the decision into a property instead:
CALL geode.rls.add_attribute_policy('app', 'Document', 'access_tag', 'access_tag');
Troubleshooting
Policy Not Applied
Check that:
- User has correct role assigned
- Policy target matches query pattern
- Policy is enabled:
SHOW POLICIES - No conflicting policies
Performance Issues
- Add indexes on filtered properties
- Simplify policy conditions
- Use policy evaluation caching
- Monitor with
EXPLAINto see added filters
Access Denied Unexpectedly
- Review all applicable policies with
SHOW POLICIES - Test policy condition manually
- Check if multiple policies conflict
- Verify user role and session context
Related Topics
- Compliance - Regulatory compliance with RLS
- Audit Logging - Track RLS policy enforcement
- Authentication - User authentication and identity
- Authorization - Role-based access control
- Performance - Optimizing RLS policy performance
- Security Overview - Security best practices