How to Implement Column-Level Security and Dataset-Level Access Control in Apache Superset

Column-level security in Apache Superset is enforced through Row Level Security (RLS) filters that inject SQL clauses into queries, while dataset-level access control uses FAB permissions evaluated via get_dataset_access_filters() in the security manager.

Apache Superset implements granular data protection through two complementary mechanisms living in the security manager layer. This guide explains how to configure both dataset-level access control and column-level security using the native RowLevelSecurityFilter system and permission model. You will learn to leverage the REST API, Python client, and underlying source code in superset/security/manager.py to enforce fine-grained access policies.

Understanding the Two Security Layers

Superset separates data access into dataset-level permissions and column-level (row-level) filters. Both mechanisms are evaluated during query construction in SQL Lab, Explore, and chart rendering to ensure the database never returns unauthorized data.

Dataset-Level Access Control

Dataset-level security protects entire tables or virtual datasets using Flask-AppBuilder (FAB) permissions. The SupersetSecurityManager.can_access_datasource() method (line 649 in superset/security/manager.py) calls raise_for_access() which uses get_dataset_access_filters() from superset/utils/filters.py to generate a SQLAlchemy OR clause. This clause checks if the user owns a database permission, catalog permission, schema permission, or explicit datasource permission string such as "[my_db].[my_table](id:42)".

Column-Level Security via Row Level Security (RLS)

Column-level restrictions are implemented through RowLevelSecurityFilter objects stored in the row_level_security_filters table. During query execution (lines 2728-2849 in superset/security/manager.py), the security manager joins the user’s roles with RLS filters via the rls_filter_roles association table and injects the filter’s clause text into the WHERE clause. The filter_type field (REGULAR or BASE) determines whether the clause is AND-ed with other filters or serves as a default base filter when no regular filter exists.

Implementing Dataset-Level Access Control

Every SqlaTable computes a permission string via get_perm() in superset/connectors/sqla/models.py. To grant access, assign this permission to a role.

  1. Identify the dataset permission string through the UI at Datasets → List → … → Permissions or programmatically via DatasetDAO.find_by_id(id).perm.
  2. Grant the permission to a role using the Security → List Roles UI or the REST API.
  3. Validate access by attempting to query the dataset; unauthorized users receive a DATASOURCE_SECURITY_ACCESS_ERROR defined in superset/exceptions.py.

Granting Dataset Permissions via REST API


# Authenticate and obtain JWT

TOKEN=$(curl -s -X POST http://localhost:8088/api/v1/security/login \
      -H "Content-Type: application/json" \
      -d '{"username":"admin","password":"admin"}' | jq -r .access_token)

# Grant permission for dataset id 42 to role id 5

curl -X POST "http://localhost:8088/api/v1/security/role/5/permissions" \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"permissions":["[my_db].[my_table](id:42)"]}'

The helper get_dataset_access_filters() in superset/utils/filters.py (lines 24-44) automatically combines permissions. It grants access if the user holds all_datasource_access, explicit datasource_access, or broader catalog_access, schema_access, or database_access permissions covering the dataset’s location.

Implementing Column-Level Security with RLS

RLS filters allow you to inject arbitrary SQL expressions referencing specific columns into every query. This effectively restricts which rows and column values users can see based on their role memberships.

  1. Create the RLS filter with a clause targeting specific columns (e.g., department = 'finance').
  2. Assign to roles via the Security → Row Level Security UI or the REST API endpoint defined in superset/row_level_security/api.py.
  3. Filter application happens automatically during query building; regular filters are AND-ed together while base filters apply only when no regular filter exists for a user's roles.

Adding RLS Filters via Python Client

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

# Identify target dataset

dataset = db.session.query(SqlaTable).filter_by(id=42).one()

# Create RLS filter restricting to finance department

rls = RowLevelSecurityFilter(
    clause="department = 'finance'",
    filter_type=utils.RowLevelSecurityFilterType.REGULAR,
    tables=[dataset],
)
db.session.add(rls)
db.session.flush()

# Associate with Finance role

finance_role = security_manager.find_role("Finance")
finance_role.row_level_security_filters.append(rls)
db.session.commit()

The RowLevelSecurityFilter model is defined at line 2082 in superset/connectors/sqla/models.py. The security manager queries these filters and the rls_filter_roles association table during query construction to build the final SQL.

Using Low-Level Filter Helpers

For custom integrations or middleware, use the filter utilities directly to respect dataset permissions:

from superset.utils.filters import get_dataset_access_filters
from superset.connectors.sqla.models import SqlaTable
from sqlalchemy import select

def build_secure_query():
    table = SqlaTable.find_by_id(42)
    base_query = select([table.table])
    permission_filter = get_dataset_access_filters(SqlaTable)
    return base_query.where(permission_filter)

Summary

  • Dataset-level access relies on FAB permission strings evaluated by get_dataset_access_filters() in superset/utils/filters.py, checking for database_access, schema_access, catalog_access, or explicit datasource_access.
  • Column-level security uses RowLevelSecurityFilter entries stored in row_level_security_filters, with SQL clauses injected into queries by the logic in superset/security/manager.py (lines 2728-2849).
  • Both mechanisms support UI configuration, REST API management via superset/row_level_security/api.py, and programmatic Python client access.
  • RLS filters support REGULAR (additive AND clauses) and BASE (default fallback) types for complex security policies.
  • Permission evaluation occurs during SQL generation, ensuring the database never sees unauthorized rows.

Frequently Asked Questions

What is the difference between dataset-level and column-level security in Superset?

Dataset-level security controls access to entire tables or virtual datasets through FAB permissions like datasource_access or schema_access, evaluated by SupersetSecurityManager.can_access_datasource(). Column-level security uses Row Level Security (RLS) filters that inject SQL WHERE clauses to restrict which rows and column values users see based on their roles.

How does Superset enforce RLS filters during query execution?

When building a query, the security manager (around lines 2728-2849 in superset/security/manager.py) selects applicable RowLevelSecurityFilter objects from the row_level_security_filters table by joining through the rls_filter_roles association table. It concatenates the clause strings with AND operators and injects them into the generated SQL WHERE clause before sending the query to the database.

Can RLS filters restrict access to specific columns rather than just rows?

While RLS filters technically operate on rows, you can achieve column-level restrictions by filtering on column values that only exist in specific rows (e.g., department = 'finance'). The filter clause can reference any column in the table, allowing you to hide entire data segments containing sensitive column values from unauthorized roles. For true column masking, combine RLS with database-level views or calculated columns.

Where is the dataset permission logic implemented in the Superset codebase?

The core permission evaluation resides in superset/security/manager.py in the can_access_datasource() and raise_for_access() methods. The SQLAlchemy filter generation logic is in superset/utils/filters.py inside get_dataset_access_filters(). Permission strings are generated by SqlaTable.get_perm() in superset/connectors/sqla/models.py, which also defines the RowLevelSecurityFilter model at line 2082.

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 →