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_keyare combined withORlogic. - Base filters act as default restrictions that apply to all users unless a Regular filter with the same
group_keyexists for that user. Base filters are appended withANDlogic 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
SecurityManager.get_rls_filtersqueries the ORM for filters linked to the user's roles, caching results per request.- For virtual datasets joining multiple tables,
prefetch_rls_filters(lines 2833-2850 insuperset/security/manager.py) batch-loads filters for all table IDs to prevent N+1 queries. - The
apply_rlsfunction (lines 32-61 insuperset/utils/rls.py) traverses the parsed SQL statement, invokingget_predicates_for_table(lines 63-109) to render clause strings. - Depending on the database engine's
get_rls_method(), predicates are either appended to the existingWHEREclause 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_filterstable and linked to roles viaRLSFilterRolesassociation tables. - The
apply_rlsfunction insuperset/utils/rls.pydynamically 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
SecurityManagercaches RLS resolutions per request viaget_rls_cache_keyand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →