Row-Level Security for Embedded Dashboards
Row-level security (RLS) in Metabase — implemented through a feature called data sandboxing — restricts which rows of a database table a given user ca...
Row-Level Security for Embedded Dashboards
Row-level security (RLS) in Metabase — implemented through a feature called data sandboxing — restricts which rows of a database table a given user can query, regardless of which dashboard or question they're using. Instead of filtering data at the dashboard level (which can be bypassed if a user has access to the query builder), sandboxing enforces access control at the query execution layer: Metabase automatically appends a WHERE clause to every query against a sandboxed table based on the current user's attributes.
Row-level security is the technically correct approach to data isolation in embedded analytics. It is available in Metabase Pro and Enterprise.
---
Why Row-Level Security Matters for Embedded Analytics
Without row-level security, data access in embedded Metabase is controlled only by:
- Which dashboards a user can see (permission groups)
This is sufficient for simple use cases — a dashboard that's pre-filtered to a single customer via a locked parameter works fine. But it breaks down when:
Row-level security closes these gaps. Even if a user somehow accesses a dashboard without the intended filter, or runs an ad-hoc SQL query, they can only see rows they're authorized to see. The restriction is enforced at query execution time, not at the dashboard configuration level.
---
How Metabase Data Sandboxing Works
When you configure a sandbox policy on a table, Metabase intercepts queries against that table and wraps them:
sql
-- What the user's question generates: SELECT order_date, SUM(amount) as revenue FROM orders GROUP BY order_date
-- What Metabase actually executes against the database: SELECT order_date, SUM(amount) as revenue FROM ( SELECT * FROM orders WHERE organization_id = 99 ) AS sandboxed_orders GROUP BY order_date
The wrapping is transparent to the user — they see the same query interface, but the results are automatically restricted to their allowed rows.
---
Setting Up Data Sandboxing
Prerequisites
Step 1: Create a Group for Sandboxed Users
In Admin → People → Groups, create a group (e.g., Customer Portal Users or Embedded Viewers). Users who should be sandboxed belong to this group.
Step 2: Assign User Attributes
User attributes are key-value pairs that Metabase uses to evaluate sandbox policies. For JWT-authenticated embedded users, attributes are passed in the JWT SSO token.
For JWT SSO (embedded users):
javascript
const ssoPayload = { email: user.email, first_name: user.firstName, last_name: user.lastName, groups: ["Customer Portal Users"], // Attributes used in sandbox policies: organization_id: String(user.organizationId), role: user.role, exp: Math.round(Date.now() / 1000) + 600 };
const ssoToken = jwt.sign(ssoPayload, METABASE_JWT_SHARED_SECRET); // Redirect: /auth/sso?jwt=<token>
Important: All attribute values in the JWT must be strings, even if the column they map to is an integer. Metabase handles type coercion.
For SAML users: Attributes are passed as SAML assertions. Configure attribute mapping in Admin → Settings → Authentication → SAML.
For manually managed users: Set attributes in Admin → People → [User] → User attributes.
Step 3: Configure Permissions
In Admin → Permissions → Data:
Customer Portal Users)Unrestricted to SandboxedStep 4: Configure the Sandbox Policy
After setting a table to Sandboxed, Metabase prompts you to configure the sandbox policy. You have two options:
---
#### Option A: Filter by Column (Simple)
The simplest sandbox: filter the table where column = user_attribute.
Configuration:
organization_idorganization_idEffect: Every query against this table automatically includes WHERE organization_id = [user's organization_id attribute].
This works for the most common multi-tenant pattern where each row belongs to one tenant and the tenant identifier is a direct column on the table.
---
#### Option B: Custom Sandbox Question (Advanced)
For complex access logic, define a SQL query that returns the rows a user is allowed to see. The custom question is parameterized with user attributes using {{attribute_name}} syntax.
Example: A user can only see orders that belong to their organization AND were created by team members they manage:
sql
SELECT o.* FROM orders o JOIN organization_members om ON o.created_by = om.user_id AND o.organization_id = om.organization_id WHERE om.manager_id = {{current_user_id}} AND o.organization_id = {{organization_id}}
The {{current_user_id}} and {{organization_id}} are substituted with the current user's attributes at query time.
Custom sandbox questions can reference other sandboxed tables (Metabase applies the outer sandbox policy), and they can implement arbitrarily complex logic — joins, subqueries, CTEs.
---
Sandboxing Multiple Tables
A realistic data model has multiple related tables. You need to configure sandbox policies for every table that contains tenant-specific data:
| Table | Sandbox Type | Filter |
|---|---|---|
orders | Column filter | organization_id = {{organization_id}} |
customers | Column filter | organization_id = {{organization_id}} |
invoices | Column filter | organization_id = {{organization_id}} |
audit_log | Custom question | Complex join to verify org membership |
products | None (shared across tenants) | No sandbox needed |
---
Row-Level Security vs. Locked Parameters
Both approaches restrict what data a user sees. Understanding the difference is important:
| Dimension | Locked Parameters | Data Sandboxing (RLS) |
|---|---|---|
| Enforcement layer | Dashboard configuration | Query execution |
| Applies to SQL editor | No | Yes |
| Applies to ad-hoc questions | No | Yes |
| Works if filter is accidentally removed | No | Yes |
| Setup complexity | Low | Medium |
| Requires Pro/Enterprise | Yes (for locked params) | Yes |
| Can express complex logic | Limited | Yes (custom sandbox) |
For embedded analytics where users only see pre-built dashboards and have no access to the query builder, locked parameters are often sufficient. For deployments where users have broader Metabase access, data sandboxing is the more robust choice.
---
Combining Both Approaches
The most secure configuration uses both locked parameters and data sandboxing together:
javascript
// JWT for embedded user with both locked param and sandbox attributes const ssoPayload = { email: user.email, groups: ["Customer Portal Users"], organization_id: String(user.organizationId), // used by sandbox policy exp: Math.round(Date.now() / 1000) + 600 };
// Separate embedding token const embedPayload = { resource: { dashboard: DASHBOARD_ID }, params: { organization_id: user.organizationId // locked filter on the dashboard }, exp: Math.round(Date.now() / 1000) + 600 };
---
Testing Row-Level Security
After configuring sandboxing, verify it's working correctly:
1. Test With a Sandboxed User Account
Create a test user account in the sandboxed group with known attribute values. Log in as this user and:
SELECT * FROM orders LIMIT 100organization_id are returned2. Verify the SQL Editor Is Restricted
If the sandboxed user has access to the native SQL editor:
SELECT * FROM orders WHERE organization_id = WHERE clause and replaces it with the sandbox filter)3. Test Attribute Injection
Run a query and check Metabase's query logs to confirm the sandbox filter is being applied. In Admin → Troubleshooting → Logs, look for the actual SQL being executed against your database — it should include the sandbox WHERE clause.
4. Test Edge Cases
---
Performance Considerations
Data sandboxing wraps queries in a subquery, which has minor performance implications:
For column filter sandboxing, the overhead is minimal. The WHERE clause on the inner query is semantically equivalent to adding it to the outer query, and most database query planners optimize this correctly.
For custom sandbox questions, performance depends on the complexity of the custom query. Complex joins or subqueries in the sandbox definition run for every query against the sandboxed table. Ensure the sandbox query itself is well-optimized with appropriate indexes.
Monitor slow query logs after enabling sandboxing to identify any performance regressions.
---
Common Issues
"Sandbox attribute not found" The user attribute referenced in the sandbox policy ({{organization_id}}) doesn't exist on the current user. Verify the attribute is set on the user's profile or included in the JWT SSO token.
Users see all rows despite sandboxing Check that the user is actually in the sandboxed group. Users in groups with Unrestricted access to the database bypass sandbox policies.
Sandbox returns no results for valid users The attribute value type may not match the column type. Ensure numeric IDs are passed as strings in JWT attributes ("99" not 99) and that Metabase is converting them correctly.
Custom sandbox query fails Test the custom sandbox query directly against your database, substituting real attribute values for the {{variable}} placeholders. Verify it returns the expected rows.
---
Summary
Row-level security in Metabase — implemented as data sandboxing — restricts query results at the execution layer by wrapping every query against a sandboxed table with a user-specific WHERE clause. It is enforced regardless of how the query is constructed, making it more robust than dashboard-level filter locking for deployments where users have broad Metabase access. For simple multi-tenant embedded dashboards, locked parameters may be sufficient; for any deployment involving the SQL editor or ad-hoc querying, data sandboxing is the correct approach.