DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Using AUTHID Parameter in Oracle PL/SQL
  • Enhancing SQL Server Security With AI-Driven Anomaly Detection
  • Data Privacy and Security: A Developer's Guide to Handling Sensitive Data With DuckDB
  • Strengthening Cloud Environments Through Python and SQL Integration

Trending

  • Jakarta NoSQL: Why JPA Is Not Enough for the AI Era
  • Slopsquatting: A New Supply Chain Threat From AI Coding Agents
  • Building Cross-Team SLO Contracts for Performance Accountability
  • HTTP QUERY in Java: The Missing Method for Complex REST API Searches
  1. DZone
  2. Data Engineering
  3. Databases
  4. Scaling Row-Level Security With ABAC on Databricks Unity Catalog

Scaling Row-Level Security With ABAC on Databricks Unity Catalog

Learn how to scale row-level security in Databricks Unity Catalog using a tag-driven ABAC pattern that reduces maintenance and simplifies onboarding.

By 
Sriram Vadlamani user avatar
Sriram Vadlamani
·
Jul. 20, 26 · Analysis
Likes (0)
Comment
Save
Tweet
Share
557 Views

Join the DZone community and get the full member experience.

Join For Free

Onboarding a new table into row-level security should be four lines of metadata. Not two new objects, a code review, and a platform-team ticket. This post describes a tag-driven attribute-based access control (ABAC) pattern built on Databricks Unity Catalog primitives that achieves the objective of one UDF per filter shape, one policy per shape, and a single control table that drives all per-group authorization logic.

I work as a solutions architect with large enterprises running hundreds of tables across multiple regions, product lines, and source systems, where row-level security follows a pattern: Group A sees records from System X. Group B sees the regions China and India. Group C sees plant key 333. Group D combines two source systems. Group E sees everything except specific values. The domain (finance, healthcare, etc.) doesn't matter. The pattern remains the same.

A traditional implementation looks like this:

  1. One row-filter UDF per table
  2. One row-filter policy per (table and group) combination
  3. A SQL query layer that joins every table to an identity mapping table

Every new table required writing two new objects (UDF + policy) and updating every existing group definition. Every new group required touching every UDF. Onboarding a new product line meant rebuilding the whole machine. Hundreds of tables × dozens of groups = a maintenance nightmare.

Instead, what they needed was a pattern where:

  • A new table joins the RLS scheme with only metadata changes — no new UDFs, no new policies
  • A new business group is just a data write — no DDL, no code review
  • Misconfigured rules fail closed, not open

This post describes the four-layer pattern we landed on. It's built entirely on Unity Catalog ABAC primitives (governed tags, row-filter policies, attribute-based binding) and a single control table that drives per-group filter logic.

ABAC-based row-level security workflow in Databricks Unity Catalog

The Four Layers

Each layer does exactly one thing and one thing only:

  • Layer 1: Tags are declarative metadata. A table-level tag declares which shape a table is. A column-level tag tells the row filter which physical column corresponds to which logical attribute. All tags do is describe the data. They facilitate the action for the next layers.
  • Layer 2: UDF is the decision logic. Given a row's attribute values, it returns a boolean answer of TRUE or FALSE for current_user(). It doesn't know anything about which table it's filtering; it just answers "is this row visible?"
  • Layer 3: Policy is the binding. It says, "for tables tagged with shape X, call UDF Y with these columns." It uses tag-matching expressions so it auto-attaches to new tables as they're tagged. This is what enables us to avoid per-table DDL. 
  • Layer 4: Row-Level Security is what the customer experiences. Their SQL doesn't change; filtered rows just come back.

Layer 1 + 2: Tags Describe, UDFs Decide

Two governed tags do all the work. A rls_tag on the table says "this is a table_1_filter shape." An rls_attr tag on each column says "this column is the src_sys_cd attribute" or "this column is the region attribute." The column tag is the load-bearing piece — it lets you have a column literally named ws_region_cd and still have the policy treat it as the logical region attribute. Physical naming is decoupled from policy semantics.

One UDF per table shape. A "shape" is a set of filterable columns. In this customer's setup, there are two shapes:

  • Table 1 shape: (src_sys_cd, plant_key, region) — three attributes. Maybe it's best to call this shape a combination of the keys. For example, all the tables that have src_sys_cd + plant_key+region fall under this shape.
  • Table 2 shape: (src_sys_cd, order_key) — two attributes

Each shape has its own UDF (rf_table_1, rf_table_2). The UDF signature takes one parameter per filterable column. The body joins a user_group_control_table (one row per group rule) to a user_group_membership mapping (or, in production, is_account_group_member()) and applies BOOL_OR across the rules — a union grant.

The key property of this UDF design: it doesn't know which table is calling it. It just answers, given attribute values, "does current_user() get this row?" That's what lets the same UDF serve many tables of the same shape.

Layer 3: The Policy that ties it all together

The policy is the only object in the system that knows about both tags and UDFs. Its four clauses each answer a separate question:

clause question it answers

`ON SCHEMA …`

Where does this policy live? (Schema-scoped — broad reach.)

`WHEN has_tag_value('rls_tag', '…')`

Which tables should it attach to? Anything tagged with the right shape.

`MATCH COLUMNS … has_tag_value('rls_attr', '…')`

Inside each table, which physical column maps to which logical attribute?

`ROW FILTER … USING COLUMNS (…)`

Which UDF to call, and in what argument order?


The auto-attachment behavior is what makes the pattern scale. The policy doesn't enumerate tables — it matches them by tag. Tag a new table tomorrow, and the policy applies to it on the next query. Zero policy edits.

Query Time: What Actually Happens

When a user runs SELECT * FROM table_1, here's what Unity Catalog does behind the scenes:

What Unity Catalog does when user runs SELECT * FROM table_1


  1. The planner sees the table and looks up policies attached to its schema. It finds rls_policy_t1.
  2. The policy's `WHEN` clause checks the table tag. Does table_1 have rls_tag=table_1_filter? Yes → the policy attaches. (If no, the policy is skipped for this table.)
  3. The policy's `MATCH COLUMNS` resolves attributes. For each logical attribute name, it scans column tags to find the physical column with that role: src_sys_cd → physical column src_sys_cd; plant_key → physical column plant_key; region → physical column region.
  4. The query is rewritten to append WHERE rf_table_1(src_sys_cd, plant_key, region) = TRUE. The customer's original SQL is unchanged.
  5. The UDF runs per row. It joins the control table to membership for current_user(), evaluates each rule, and BOOL_ORs the results — TRUE if any rule grants the row, FALSE otherwise.
  6. The engine emits only the `TRUE` rows. The customer sees only their authorized subset. They never see the UDF call or the policy mechanics.

The whole thing is transparent to the application — same SQL, filtered result.

The Scale Payoff

The reason to build the pattern this way only becomes obvious when you onboard the second, third, and hundredth table.

Adding a New Table to an Existing Shape

A new dimension table arrives that fits the Table 1 shape. The work to bring it under RLS:

Step effort shape

Create the table (customer's normal DDL)

—

Tag the table with the shape

1 line of DDL

Tag the columns with their logical roles

3 lines of DDL

Update the UDF?

None

Update the policy?

None

Update the control table?

**None** (existing groups apply automatically through their existing rules)


Four ALTER lines. That's the whole onboarding cost.

Adding a New Business Group

A new business group needs access to a specific slice of the data:

step effort

`INSERT` one row into `user_group_control_table`

1 INSERT

Add users to the AD group (outside Databricks)

—

Update the UDF?

None

Update the policy?

None

Update any table tags?

None


One INSERT. Adding a new group is a data write, not DDL — which means operations teams can self-serve through their normal change-management process, without code review or platform-team involvement.

GitHub repo: https://github.com/vbablue/databricks-abac-rls-demo/tree/main

Data definition language security sql

Opinions expressed by DZone contributors are their own.

Related

  • Using AUTHID Parameter in Oracle PL/SQL
  • Enhancing SQL Server Security With AI-Driven Anomaly Detection
  • Data Privacy and Security: A Developer's Guide to Handling Sensitive Data With DuckDB
  • Strengthening Cloud Environments Through Python and SQL Integration

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook