Security & Auth

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...

šŸ“…
šŸ“–9 min read

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
  • Defines the baseline permissions for all users
  • Default: unrestricted access to all databases and collections
  • Best practice: restrict All Users to minimal access and grant more through specific groups
  • Administrators (group ID: 2)

  • Full access to everything including the Admin panel
  • Can manage users, groups, databases, and settings
  • Users in this group bypass all data and collection restrictions
  • ---

    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:

  • Allowed: Full SQL access, including write operations if the database user has write permissions
  • No native queries: Query builder only — no SQL editor access
  • 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:

  • Can only see their own organization's data (sandboxing)
  • Can only access dashboards in their designated collection
  • Cannot run arbitrary SQL
  • ---

    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.