Use RBAC with ABAC

Role-based access control (RBAC) sets the active identity for a session. Attribute-based access control (ABAC) in Unity Catalog applies row filters and column masks to that identity:

  • RBAC: A user assumes a role to act with that role's permissions instead of their own. Use roles to separate access across projects or sensitivity tiers.
  • ABAC row filter and column mask policies: Filter rows or mask column values for tables the active identity can already access. Policies are defined on securable objects, such as catalogs or schemas, and use governed tags to identify matching tables and columns. See Core concepts for attribute-based access control (ABAC).

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
session_user() Returns the user's username Returns the assumed role's name
is_member(group) Returns true if the user is a direct or indirect member of 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 direct or indirect member of the account group Returns true based on the assumed role's direct and indirect account-group memberships, including groups not assigned to the workspace.

The assumed role is the active identity for ABAC policy evaluation, Unity Catalog grant resolution, and audit attribution. Assuming a role can therefore change the behavior of existing policies and views built around per-user identity.

Note

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

The SQL reference recommends session_user() instead of the deprecated current_user() alias. Use is_account_group_member() to test account-group membership. See session_user function and is_member function.

Common pitfall: row-level security views built on session_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 session_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 = session_user()
    AND p.facility_id = facility_filter.facility_id
);

The facility_filter.facility_id reference identifies the function parameter. Without the function name, facility_id resolves to the provisioning table's column instead. See Name resolution.

When the same user assumes a role, session_user() no longer returns their username. It returns the role's name. Because 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 examples combine RBAC with ABAC row filters and column masks.

Requirements

Before you use the examples, complete these steps:

  • Use compute that supports ABAC.
  • Create the governed tags and grant the permissions needed to apply tags, create user-defined functions (UDFs), and manage policies. See Separation of duties and permissions.
  • Grant the querying identities USE CATALOG, USE SCHEMA, and SELECT on the relevant data. Row filters and column masks do not grant these privileges. See Usage privileges.
  • Create each example UDF in a schema where you have permission, and use that schema as the current schema when creating its policy, or fully qualify the function name. For a complete walkthrough, see Tutorial: Configure ABAC with SQL.

Per-project row filters keyed on the assumed role

A clinical research organization runs several concurrent trials, each with its own access role. A row filter limits each role to its trial's data. The same pattern applies to other projects that need separate data access. Tag the project_id column in each table with the governed tag key project. The column values identify the project for each row.

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 the 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 session_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 compares the active identity with each row's project_id column value. MATCH COLUMNS uses the governed tag to find that column. The TO and EXCEPT clauses select the principals a policy applies to. They do not compare row values. 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 value from the matched column into the UDF. The alias project names the matched column, not a tag value. For an example that uses a lookup table to determine row access, see Use mapping tables for dynamic access control.

Behavior:

  • A user acting as their user identity sees no rows in any clinical_trials table. session_user() returns their username. Choose role names that cannot match a user's username or a service principal's application ID.
  • A user assuming role-alpha sees only rows where project_id equals alpha. After switching to role-beta, run the query again to see rows where project_id equals beta.

Mask personally identifiable information unless a role is assumed

In this example, personally identifiable information (PII), such as Social Security numbers and email addresses, appears masked by default. To see the raw values, a user must assume role-pii-cleared. Audit events identify the role used to access the data and the identity that initiated the event.

Setup:

  • Tag sensitive columns with the governed tag key pii (allowed values such as ssn, email, phone).
  • Create the role-pii-cleared group with no members. Grant the Assume permission on that group.
    • Users authorized to view raw PII can assume this access role to view unmasked data. Do not add members to the group, because the policy's EXCEPT clause would allow them to see unmasked data without assuming the role.

Create the column mask UDF, using this SQL:

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 the 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 events for these queries identify the authorizing role in identity_metadata.run_as and its name in identity_metadata.run_as_display_name. See Audit attribution.

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 session_user() = 'role-sens-restricted' THEN TRUE
  WHEN session_user() = 'role-sens-confidential' THEN row_sensitivity IN ('internal', 'confidential')
  WHEN session_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 session_user() against each role name rather than testing membership with is_account_group_member(). See the note in How identity functions behave when assuming a role 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.
  • 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 tier they need for the session. The filter applies to tables with the tagged column and returns the rows allowed by that role.

Audit attribution

In the audit log system table, identity_metadata.run_by identifies who initiated an event, and identity_metadata.run_as identifies the identity used for authorization. The corresponding run_by_display_name and run_as_display_name fields contain human-readable names. For queries made interactively while assuming a role, these identify the user and role respectively. Job workloads can have a different initiator. See Set Run as to a group.

Use audit events to review activity under a role. To inspect which policies apply to a table, use SHOW EFFECTIVE POLICIES as described in Plan for dynamic policy evaluation. For audit access, schema, and availability, see Verify audit attribution.

Additional resources

Resource Description
Tutorial: Model exclusive access with RBAC Configure and verify separate data access for two clinical trials using roles.
Switch RBAC roles Assume a role using the role switcher, dedicated access mode clusters, the Databricks CLI, the API, or third-party BI tools.
Manage permissions on a group Grant or revoke the Assume permission on a group so users can assume the corresponding role.
Attribute-based access control in Unity Catalog Learn how governed tags, policies, and policy evaluation work.