How to Implement Row-Level Security (RLS) Filters for Fine-Grained Data Access Control in Apache Superset

Row-level security (RLS) filters in Apache Superset attach custom SQL WHERE clauses to datasets and automatically enforce them based on user roles, ensuring fine-grained data access control without modifying dashboard or chart definitions.

Row-level security (RLS) filters enable fine-grained data access control in Apache Superset by dynamically injecting user-specific SQL predicates into every query. According to the Apache Superset source code, these rules persist in the row_level_security_filters table and are applied via the apply_rls utility in superset/utils/rls.py before any SQL reaches the database, including queries against virtual datasets and complex joins.

RLS Architecture and Components

Superset's RLS implementation consists of coordinated components that store, resolve, and inject security predicates. The RowLevelSecurityFilter model in superset/connectors/sqla/models.py (lines 2082-2098) defines the core schema, storing the SQL clause, filter type, and optional group key while maintaining many-to-many relationships with tables and roles through RLSFilterTables and RLSFilterRoles association tables.

At runtime, SecurityManager.get_rls_filters in superset/security/manager.py (lines 2701-2770) resolves applicable filters for the current Flask-App-Builder user, caching results per request to avoid redundant database queries.

Filter Types: Regular vs. Base

Superset supports two distinct filter types with specific precedence rules:

  • Regular filters apply only when the current user possesses one of the filter's linked roles. Multiple Regular filters sharing a group_key are combined with OR logic.
  • Base filters act as default restrictions that apply to all users unless a Regular filter with the same group_key exists for that user. Base filters are appended with AND logic after grouped Regular predicates.

This architecture enables union-style filtering while maintaining default security boundaries.

Creating RLS Filters

You can define row-level security filters through three interfaces: the REST API, direct ORM manipulation, or the web UI. All methods ultimately persist data through the same RowLevelSecurityFilter model and enforce rules identically at runtime.

Via the REST API

The /api/v1/rowlevelsecurity/ endpoint in superset/row_level_security/api.py provides programmatic CRUD operations for infrastructure-as-code workflows. Payloads are validated against schemas defined in superset/row_level_security/schemas.py.

curl -X POST http://localhost:8088/api/v1/rowlevelsecurity/ \
  -H "Content-Type: application/json" \
  -d '{
        "name": "region_europe",
        "clause": "region = '\''EU'\''",
        "filter_type": "Regular",
        "group_key": "region",
        "tables": [123],
        "roles": [4]
      }'

Via Python ORM

For automated provisioning or custom scripts, interact directly with the SQLAlchemy model:

from superset.connectors.sqla.models import RowLevelSecurityFilter, SqlaTable
from superset import db, security_manager

table = db.session.query(SqlaTable).filter_by(table_name="orders").one()
role = security_manager.find_role("Europe Sales")

rls = RowLevelSecurityFilter(
    name="europe_only",
    clause="region = 'EU'",
    filter_type="Regular",
    group_key="region",
    tables=[table],
    roles=[role],
)
db.session.add(rls)
db.session.commit()

Via the Superset UI

Navigate to Security → Row Level Security and create a new filter. The UI captures the same fields as the API: name, SQL clause, filter type (Regular/Base), optional group key, target tables, and applicable roles.

Runtime Filter Application

When a user executes a query, Superset's security manager resolves applicable filters and rewrites the SQL before database execution.

The Execution Flow

  1. SecurityManager.get_rls_filters queries the ORM for filters linked to the user's roles, caching results per request.
  2. For virtual datasets joining multiple tables, prefetch_rls_filters (lines 2833-2850 in superset/security/manager.py) batch-loads filters for all table IDs to prevent N+1 queries.
  3. The apply_rls function (lines 32-61 in superset/utils/rls.py) traverses the parsed SQL statement, invoking get_predicates_for_table (lines 63-109) to render clause strings.
  4. Depending on the database engine's get_rls_method(), predicates are either appended to the existing WHERE clause or injected via sub-query wrapping.

Guest Token RLS for Embedded Analytics

For embedded dashboards, Superset supports RLS through guest tokens without requiring named user accounts. The security manager extracts rls_rules from the token and treats them as Regular filters:

guest_user = security_manager.get_guest_user_from_token({
    "user": {},
    "resources": [{"type": "dashboard", "id": "my-dashboard"}],
    "rls_rules": [
        {"dataset": 42, "clause": "customer_id = 12345"},
        {"dataset": 42, "clause": "region = 'West'"}
    ],
    "iat": 0,
    "exp": 3600,
})
g.user = guest_user

Practical Implementation Examples

Enforcing Regional Access Restrictions

To limit sales data by region for specific teams:

orders = db.session.query(SqlaTable).filter_by(table_name="sales").one()
na_role = security_manager.find_role("North America Sales")

filter_rls = RowLevelSecurityFilter(
    name="na_region_only",
    clause="region_code IN ('US', 'CA', 'MX')",
    filter_type="Regular",
    group_key": "region",
    tables=[orders],
    roles=[na_role],
)
db.session.add(filter_rls)
db.session.commit()

Setting Default Base Filters

Use Base filters to hide archived records by default, allowing specific roles to override with Regular filters sharing the same group_key:

docs = db.session.query(SqlaTable).filter_by(table_name="documents").one()
admin = security_manager.find_role("Admin")

base_filter = RowLevelSecurityFilter(
    name="hide_archived_default",
    clause="is_archived = false",
    filter_type="Base",
    group_key="archived_status",
    tables=[docs],
    roles=[admin],
)
db.session.add(base_filter)
db.session.commit()

Debugging Generated SQL

Verify that RLS predicates appear in final queries by inspecting the generated SQL string:

tbl = security_manager.find_datasource_by_name("sales")
g.user = security_manager.find_user("gamma")  # User with RLS role

sql = tbl.get_query_str({
    "groupby": ["region"], 
    "metrics": ["sum(amount)"], 
    "filter": [], 
    "is_timeseries": False,
    "columns": [], 
    "extras": {}
})
print(sql)  # Observe injected WHERE clauses

Summary

  • RLS filters in Apache Superset are stored in the row_level_security_filters table and linked to roles via RLSFilterRoles association tables.
  • The apply_rls function in superset/utils/rls.py dynamically injects SQL predicates by parsing and mutating query statements before database execution.
  • Regular filters apply only to users with specific roles; Base filters provide default restrictions unless overridden by Regular filters sharing the same group_key.
  • The SecurityManager caches RLS resolutions per request via get_rls_cache_key and supports prefetching for virtual datasets to optimize performance.
  • Guest tokens enable row-level security for embedded analytics without requiring persistent user accounts in the database.

Frequently Asked Questions

What is the difference between Regular and Base RLS filters?

Regular filters apply exclusively to users who possess one of the filter's assigned roles. Base filters apply to all users by default but are automatically excluded if the user has a Regular filter with the same group_key, making them ideal for default restrictions that specific roles can override.

How does Superset handle RLS for complex queries with joins or virtual datasets?

The prefetch_rls_filters method in superset/security/manager.py batch-loads filters for all table IDs involved in a query, preventing N+1 performance issues. The apply_rls utility then injects predicates into each referenced table individually, whether physical tables or virtual dataset sub-queries, ensuring consistent enforcement across all data sources.

Can RLS filters be managed programmatically for CI/CD workflows?

Yes. The REST API endpoint /api/v1/rowlevelsecurity/ supports full CRUD operations, allowing infrastructure-as-code approaches. Alternatively, direct ORM manipulation via the RowLevelSecurityFilter model enables Python-based automation scripts that integrate with existing provisioning systems.

How do group keys combine multiple RLS filters?

When multiple Regular filters share a group_key, Superset combines their clauses using OR logic. Base filters with matching group keys are appended using AND logic only if no Regular filter exists for that group key. This enables flexible union-style filtering while maintaining default security boundaries for unauthorized users.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →