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

> Implement Apache Superset row-level security RLS filters to enforce fine-grained data access control with custom SQL WHERE clauses applied automatically by user role.

- Repository: [The Apache Software Foundation/superset](https://github.com/apache/superset)
- Tags: how-to-guide
- Published: 2026-03-03

---

**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`](https://github.com/apache/superset/blob/main/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`](https://github.com/apache/superset/blob/main/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`](https://github.com/apache/superset/blob/main/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`](https://github.com/apache/superset/blob/main/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`](https://github.com/apache/superset/blob/main/superset/row_level_security/schemas.py).

```bash
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:

```python
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`](https://github.com/apache/superset/blob/main/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`](https://github.com/apache/superset/blob/main/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:

```python
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:

```python
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`:

```python
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:

```python
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`](https://github.com/apache/superset/blob/main/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`](https://github.com/apache/superset/blob/main/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.