Use RBAC with ABAC

Important

RBAC is in Public Preview. ABAC is generally available. This page describes how the two work together.

Role-based access control (RBAC) and attribute-based access control (ABAC) in Unity Catalog are complementary controls designed to work together. They answer different questions:

  • RBAC controls which identity a user acts as for a session. A user assumes a role to act with that role's permissions instead of their own. Use RBAC to give one user several distinct permission sets they switch between explicitly — for example, separating access across clinical trials, projects, or sensitivity tiers.
  • ABAC controls what data the active identity can see, row by row or column by column. Policies attach to data through governed tags and apply to whichever identity runs the query. Use ABAC for consistent filtering or masking across many tables driven by data attributes.

RBAC sets the active identity for the session, and ABAC evaluates its policies against that identity. This page covers how that interaction plays out in practice, the behavior of identity-related SQL functions, and combined-use patterns.

How identity functions behave when assuming a role

The Unity Catalog identity-related SQL functions resolve against the session's active identity, not the underlying authenticating user. When a user assumes a role, the active session identity becomes the role:

Function When user acts as their user identity When user assumes a role
current_user() Returns the user's username Returns the assumed role's name
is_member(group) Returns true if the user is a member of the group (a workspace-local group, or an account group assigned to the workspace) Returns true only if the assumed role is itself a member of group. Returns false for groups the underlying user is a member of but the assumed role is not.
is_account_group_member(group) Returns true if the user is a member of the account-level group Same as is_member: returns true only based on the assumed role's group memberships, not the underlying user's.

ABAC policies that reference these functions evaluate against the assumed role, not the user. The assumed role is the active identity for ABAC policy evaluation, Unity Catalog grant resolution, and audit attribution. As a result, assuming a role changes the behavior of existing policies and views that were built around per-user identity.

Note

A role is not automatically a member of itself. When a user assumes role G, current_user() returns G, but is_member('G') and is_account_group_member('G') return false unless G was explicitly added as a member of itself. To match on the assumed role in a policy, compare against current_user() rather than testing membership with is_member or is_account_group_member.

Common pitfall: row-level security views built on current_user()

A common pattern across ABAC and table-level row filters is to filter rows by joining against a provisioning table (also called a mapping table or access-control list) keyed on the username returned by current_user(). For example:

CREATE OR REPLACE FUNCTION facility_filter(facility_id STRING)
RETURN EXISTS (
  SELECT 1
  FROM governance.user_facility_provisioning p
  WHERE p.user = current_user()
    AND p.facility_id = facility_id
);

When the same user assumes a role, current_user() no longer returns their username. It returns the role's name. Since the role isn't in the provisioning table, the filter returns no rows and the user appears to lose access to data they were granted.

Add the role to the provisioning table

Treat the role as another principal in your provisioning data: insert one row per role with the facilities (or other attributes) that the role should see. The filter then matches whether the active identity is the user or the assumed role.

Combined-use patterns

The following are examples of how customers use RBAC and ABAC together to solve real access-control problems. These are starting points, not exhaustive recipes.

Per-project row filters keyed on the assumed role

Filtering rows by project is a common need in clinical trial research, contract marketing, client consulting, and other settings where one team works across several isolated projects. The following example uses clinical trials, but the pattern generalizes to any per-project data isolation.

A clinical research org runs several concurrent trials, each in its own access role. Tag each table with the project identifier. Users see only the rows for the project whose role they have currently assumed.

Setup:

  • Tables under clinical_trials.* have a project_id column tagged with the governed tag key project.
  • Each project has a corresponding access role named role-<project> (for example, role-alpha, role-beta).
  • Users have Assume permission only on the roles for projects they work on.

Row filter UDF:

CREATE FUNCTION project_match(project_id STRING) RETURNS BOOLEAN
DETERMINISTIC
RETURN current_user() = CONCAT('role-', project_id);

Policy:

CREATE POLICY per_project_row_filter
ON CATALOG clinical_trials
ROW FILTER project_match
TO `account users`
FOR TABLES
MATCH COLUMNS has_tag('project') AS project
USING COLUMNS (project);

This UDF correlates the active identity with each row's project tag value, so principal-targeting clauses like TO / EXCEPT can't express it — those target principals, not row content. As the principal-targeting guidance explains, prefer TO / EXCEPT for simple principal scoping and reserve identity functions inside a UDF for cases like this one, where a single rule depends on both the active identity and the row's content.

A single policy covers every project: USING COLUMNS (project) passes each row's project tag value into the UDF, so you don't need a separate policy per project. For the general form of this technique — driving row access from a lookup table instead of role-name matching — see Use mapping tables for dynamic access control.

Behavior:

  • A user acting as their user identity sees no rows in any clinical_trials table. current_user() returns their username, which never matches the role-* naming pattern. This is the intended default-deny.
  • A user assuming role-alpha sees only rows where project_id equals alpha. Switching to role-beta swaps the visible data without re-querying anything else.

PII masking relaxed for users acting as a designated role

By default, PII columns (SSN, email, phone) appear masked for everyone. To see the raw values, a user must explicitly assume a designated PII-cleared role. Audit logs record the role-assumption event, so "I needed to look at real PII" becomes an auditable opt-in instead of an ambient permission.

Setup:

  • Sensitive columns are tagged with the governed tag key pii (allowed values such as ssn, email, phone).
  • An access role named role-pii-cleared is granted Assume to the users who are cleared to view raw PII.

Column mask UDF (static — the policy targets which principals to mask):

CREATE FUNCTION mask_pii(val STRING) RETURNS STRING
DETERMINISTIC
RETURN '***';

Policy:

CREATE POLICY pii_default_mask
ON CATALOG customer_data
COLUMN MASK mask_pii
TO `account users`
EXCEPT `role-pii-cleared`
FOR TABLES
MATCH COLUMNS has_tag('pii') AS pii_col
ON COLUMN pii_col;

The EXCEPT clause excludes role-pii-cleared from the policy entirely, so the UDF is never invoked when that role is the active identity. See Prefer TO/EXCEPT for principal targeting for the general guidance on principal targeting via TO / EXCEPT.

Behavior:

  • A user acting as their user identity sees *** in every PII column. This is the default state for everyone, including users who have Assume permission on role-pii-cleared.
  • After assuming role-pii-cleared, the policy no longer applies to the session, and the same user sees the raw values.
  • Audit log entries for the session record identity_metadata.run_as = role-pii-cleared, so reviewers can see exactly when PII was unmasked and by whom.

Sensitivity-tier policies that vary by assumed role

Data is classified into sensitivity tiers (internal, confidential, restricted). Each tier has a corresponding access role, with restricted implying access to confidential and internal as well. A single row filter UDF gates row visibility by comparing each row's tier against the user's assumed role.

Setup:

  • Tables have a sensitivity_level column tagged with the governed tag key sensitivity (allowed values: internal, confidential, restricted).
  • Three access roles: role-sens-internal, role-sens-confidential, role-sens-restricted.

Row filter UDF:

CREATE FUNCTION sensitivity_allowed(row_sensitivity STRING) RETURNS BOOLEAN
DETERMINISTIC
RETURN CASE
  WHEN current_user() = 'role-sens-restricted' THEN TRUE
  WHEN current_user() = 'role-sens-confidential' THEN row_sensitivity IN ('internal', 'confidential')
  WHEN current_user() = 'role-sens-internal' THEN row_sensitivity = 'internal'
  ELSE FALSE
END;

Policy:

CREATE POLICY sensitivity_filter
ON CATALOG corp_data
ROW FILTER sensitivity_allowed
TO `account users`
FOR TABLES
MATCH COLUMNS has_tag('sensitivity') AS sens
USING COLUMNS (sens);

Because a role is not a member of itself, this UDF compares current_user() against each role name rather than testing membership with is_account_group_member(). See the note above for why membership tests don't match the assumed role. See Performance considerations for row filter and column mask policies for performance characteristics of identity functions in UDFs.

Behavior:

  • A user acting as their user identity sees no rows. The ELSE FALSE branch matches anything that isn't one of the three roles. As in the per-project example above, this is the intended default-deny.
  • Assuming role-sens-internal reveals only internal rows.
  • Assuming role-sens-confidential reveals internal and confidential rows.
  • Assuming role-sens-restricted reveals all rows.

Users assume the highest tier they need for the session; the filter automatically excludes everything above that tier without requiring the user to know which tables hold which classifications.

Audit attribution

Both ABAC policy evaluations and the underlying queries respect the RBAC run_as / run_by attribution. Audit log entries record identity_metadata.run_by as the authenticating user and identity_metadata.run_as as the assumed role, regardless of which ABAC policies were applied during evaluation. See Audit log system table reference for the full audit-log schema.

Next steps

  • Model exclusive access: Apply patterns for setting up exclusive access using either an account-local group or a group synced from Microsoft Entra ID. See Model exclusive access.
  • Switch roles: Assume a role using the role switcher, dedicated access mode clusters, the CLI, the API, or third-party BI tools. See Switch roles.
  • Manage Assume permissions: Grant or revoke Assume on a group so users can assume the corresponding role. See Manage permissions on a group.
  • Review ABAC core concepts: Learn how governed tags, policies, and policy evaluation work. See Attribute-based access control in Unity Catalog.