Role-Based Access Control in Metabase - A Developer's Guide
Role-based access control (RBAC) in Metabase is implemented through permission groups ā named collections of users that share a common set of data acc...
Role-Based Access Control in Metabase: A Developer's Guide
Role-based access control (RBAC) in Metabase is implemented through permission groups ā named collections of users that share a common set of data access and collection permissions. Instead of assigning permissions to individual users, you assign them to groups and add users to groups. Every user belongs to the built-in "All Users" group as a baseline, with additional groups granting or restricting access on top of that baseline.
Understanding Metabase's permission model in detail is essential for any deployment serving multiple user types ā from internal teams with different data access needs to embedded analytics products where customers should only see their own data.
---
The Permission Model
Metabase permissions operate across two independent dimensions:
1. Data Permissions
Controls which databases, schemas, and tables a group can query.
Database access levels (per group, per database):
āāā Unrestricted ā query any table, any schema āāā Granular ā specify access per schema or per table ā āāā Schema: unrestricted / granular / no access ā āāā Table: unrestricted / sandboxed / no access āāā No access ā can't see the database exists
2. Collection Permissions
Controls which folders (collections) of dashboards and questions a group can see and edit.
Collection access levels (per group, per collection):
āāā Curate (write) ā view, create, edit, move content āāā View (read) ā view content only, can't edit āāā No access ā collection is invisible to the group
These two dimensions are independent. A group can have full collection access (edit all dashboards) but restricted data access (only query specific tables). Or full data access but read-only collection access (can run queries, can't modify saved dashboards).
---
Built-in Groups
Every Metabase instance has two built-in groups that cannot be deleted:
All Users (group ID: 1)
- Every user belongs to this group automatically
Administrators (group ID: 2)
---
Designing Your Group Structure
Principle: Start Restrictive
Set All Users to minimal permissions, then grant access explicitly through specific groups. This is the "deny by default" approach ā safer than granting broad access and trying to restrict exceptions.
All Users group:
- Data: No access to all databases - Collections: View access to a shared "Public Reports" collection only
Specific groups add permissions on top: - Analytics Team: Unrestricted data + Curate access to all collections - Executives: View-only data + View access to executive collections - Customer Portal (embedded): Sandboxed data + View specific collection
Common Group Patterns
Pattern 1: Department-Based Access
All Users ā no data access, view shared reports collection
Analytics Team ā unrestricted data, curate all collections Sales Team ā unrestricted access to sales DB only, curate sales collection Engineering ā unrestricted all data, curate engineering collection Executives ā unrestricted read on warehouse, view executive collection
Pattern 2: Role-Based Access
All Users ā no access
Analysts ā full data access, curate assigned collections Report Viewers ā view-only data, view collections Admins ā Metabase Administrators group
Pattern 3: Multi-Tenant Embedded
All Users ā no access
Internal Team ā full access Customer Tier (embedded) ā sandboxed data, view their collection
---
Configuring Data Permissions
Setting Database-Level Access
In Admin ā Permissions ā Data:
Select a group (left column), then configure access for each database (right panel).
Unrestricted access: The group can query any table in any schema.
Granular access: Opens per-schema and per-table controls. Use this when a group should only see specific parts of a database.
No access: The database is invisible to users in this group.
Setting Table-Level Access (Granular)
When a database is set to "Granular" for a group, you can configure each table:
Unrestricted: Query this table normally.
Sandboxed: Apply row-level security. Requires configuring a sandbox policy (see the Row-Level Security guide). Metabase will prompt you to configure the sandbox when you select this option.
No access: The table is invisible and unqueryable by this group.
Native Query Permissions
Within data permissions, you can also control whether a group can use the native SQL editor:
Restrict native query access for groups that shouldn't be able to run arbitrary SQL (e.g., external users, junior analysts, embedded end-users).
---
Configuring Collection Permissions
In Admin ā Permissions ā Collections:
For each group, set permissions on each collection:
Curate: Users can view, create, edit, move, and archive content. Can create sub-collections.
View: Users can see and run dashboards and questions. Cannot create or modify content.
No access: The collection and its contents are invisible.
Collection Permission Inheritance
Collection permissions cascade to sub-collections by default, but can be overridden at the sub-collection level. A group with "View" on a parent collection doesn't automatically get the same access on manually-restricted sub-collections.
The Root Collection
Content not in any named collection lives in the "Our analytics" root collection. Set root collection permissions carefully ā overly permissive root access undermines more specific collection restrictions.
---
Programmatic Permission Management
For deployments managing permissions at scale (many groups, automated provisioning), use the API.
Reading the Current Permissions Graph
javascript
async function getPermissionsGraph() { const res = await fetch(${METABASE_URL}/api/permissions/graph, { headers: { "x-api-key": API_KEY }, }); return res.json(); }
The graph structure:
json
{ "revision": 42, "groups": { "3": { // group ID "2": { // database ID "schemas": "all" // unrestricted access }, "3": { "schemas": { // granular access "public": { "orders": "all", "customers": { "read": "all" // sandboxed table } } } } } } }
Granting and Revoking Access Programmatically
javascript
// Grant unrestricted access to a database async function grantDatabaseAccess(groupId, databaseId) { const graph = await getPermissionsGraph();
if (!graph.groups[groupId]) graph.groups[groupId] = {}; graph.groups[groupId][databaseId] = { schemas: "all" };
return updatePermissionsGraph(graph); }
// Grant access to specific tables only async function grantTableAccess(groupId, databaseId, schemaName, tableIds) { const graph = await getPermissionsGraph();
if (!graph.groups[groupId]) graph.groups[groupId] = {}; if (!graph.groups[groupId][databaseId]) graph.groups[groupId][databaseId] = {};
const tablePermissions = {}; for (const tableId of tableIds) { tablePermissions[tableId] = "all"; }
graph.groups[groupId][databaseId] = { schemas: { [schemaName]: tablePermissions } };
return updatePermissionsGraph(graph); }
// Revoke all access to a database async function revokeDatabaseAccess(groupId, databaseId) { const graph = await getPermissionsGraph();
if (graph.groups[groupId]) { delete graph.groups[groupId][databaseId]; }
return updatePermissionsGraph(graph); }
async function updatePermissionsGraph(graph) { const res = await fetch(${METABASE_URL}/api/permissions/graph, { method: "PUT", headers: { "x-api-key": API_KEY, "Content-Type": "application/json", }, body: JSON.stringify(graph), }); return res.json(); }
Managing Collection Permissions Programmatically
javascript
async function setCollectionPermission(groupId, collectionId, level) { // level: "write" (curate), "read" (view), "none" const res = await fetch(${METABASE_URL}/api/collection/graph, { headers: { "x-api-key": API_KEY }, }); const graph = await res.json();
if (!graph.groups[groupId]) graph.groups[groupId] = {}; graph.groups[groupId][collectionId] = level;
await fetch(${METABASE_URL}/api/collection/graph, { method: "PUT", headers: { "x-api-key": API_KEY, "Content-Type": "application/json", }, body: JSON.stringify(graph), }); }
---
Access Control for Embedded Analytics
Embedded users (customers accessing analytics via your SaaS product) require a specific permission pattern:
Group: "Embedded Viewers"
Data permissions: - Production DB: Granular - orders table: Sandboxed (filter by organization_id = user attribute) - customers table: Sandboxed - products table: Unrestricted (shared reference data) - other tables: No access
Collection permissions: - Customer Analytics collection: View - All other collections: No access
Native queries: No (disabled)
This configuration ensures embedded users:
---
RBAC Audit and Maintenance
Reviewing the Permissions Graph
Periodically audit who has access to what:
javascript
async function auditPermissions() { const [graph, groups, users] = await Promise.all([ getPermissionsGraph(), fetch(${METABASE_URL}/api/permissions/group, { headers: { "x-api-key": API_KEY } }).then(r => r.json()), fetch(${METABASE_URL}/api/user, { headers: { "x-api-key": API_KEY } }).then(r => r.json()), ]);
const groupById = Object.fromEntries(groups.map(g => [g.id, g]));
for (const [groupId, dbPerms] of Object.entries(graph.groups)) { const group = groupById[groupId]; if (!group) continue;
console.log(\nGroup: ${group.name} (${group.member_count} members)); for (const [dbId, access] of Object.entries(dbPerms)) { const accessSummary = typeof access.schemas === "string" ? access.schemas : "granular"; console.log( Database ${dbId}: ${accessSummary}); } } }
Offboarding Users
When a user leaves, deactivate them in Metabase and remove them from groups:
javascript
async function offboardUser(email) { const user = await getUserByEmail(email); if (!user) return;
// Deactivate user (soft delete ā preserves their content) await fetch(${METABASE_URL}/api/user/${user.id}, { method: "DELETE", headers: { "x-api-key": API_KEY }, });
console.log(Deactivated user: ${email}); }
If you're using SSO, users deprovisioned in your IdP automatically lose Metabase access ā they can't authenticate. Deactivation in Metabase is still good practice to prevent edge cases.
---
Common Permission Mistakes
Leaving All Users with unrestricted data access The default configuration gives everyone full data access. Change All Users to "No access" for all databases immediately after setup and grant access explicitly.
Granting administrator access too broadly Administrator access bypasses all data and collection restrictions. Only the Metabase admin team should be Administrators ā not all engineers or analysts.
Forgetting to restrict native query access Users with native query access can run arbitrary SQL. Restrict this for any group that includes end users or external users.
Not testing with a non-admin user Permission configurations look correct to admins (who bypass restrictions). Always verify permissions by logging in as a user in the restricted group.
---
Summary
Metabase RBAC uses permission groups with two independent permission dimensions: data access (which databases and tables to query) and collection access (which dashboards and questions to view or edit). The correct approach is deny-by-default: set All Users to minimal access, then grant permissions through specific groups. For embedded analytics, create a dedicated group with sandboxed data access and view-only collection permissions with no native SQL. Manage permissions programmatically through the permissions graph API for deployments with many groups or automated user provisioning.