Alle artikelen

Databricks

ABAC on Views and Time Travel: Make Policy Tests a Release Gate

Build an automated ABAC deployment gate for views and time travel, with identity checks, tag-change tests and explicit Runtime 19 preview eligibility.

  • Databricks
  • Unity Catalog
  • Delta Lake
  • Apache Spark
  • Azure
  • CI/CD

A policy test against the current table is not proof that every read path is protected. Analysts query views, applications use different identities, and reporting teams revisit historical versions. Each path needs its own access contract.

At NexOps, we have delivered Unity Catalog governance and self-service data onboarding in our RIVM engagement. That context informs our approach: policy behaviour belongs in deployment validation, not just an administrative checklist.

These Beta updates extend attribute-based access control (ABAC) to more read paths. Incomplete access-path tests leave governance gaps; we recommend making identity, tag, schema and compute contracts a release gate before exposing new routes.

What Changed

Azure Databricks announced ABAC on views on 29 September 2026. Both view support and qualifying ABAC time travel are Beta, with separate preview prerequisites.

UpdateScope and prerequisites
ABAC on viewsRow filters and column masks extend to views. A workspace admin enables ABAC on Views from the workspace settings Previews page. Requires Databricks Runtime 19 or above.
Time travel with ABACHistorical reads on qualifying Unity Catalog managed tables enforce ABAC row filters and column masks. Requires the Time Travel on UC Managed Tables with Attribute-Based Access Controls preview.

The ABAC on views release note establishes view support. The time-travel documentation supplies the more detailed eligibility rules:

  • Queries must run on standard compute using Databricks Runtime 19 or above, or on serverless compute.
  • Tables must be Unity Catalog managed Delta tables with catalog commits enabled, or managed Iceberg tables.
  • For managed Delta tables, column mapping must be enabled in both the current and requested historical versions, using the same mapping mode.

Supported SQL historical query forms include TIMESTAMP AS OF, VERSION AS OF and the @ identifier syntax. DataFrame queries support the timestampAsOf and versionAsOf options.

Databricks compares the historical and current schemas and exposes only columns present in both. It applies the table's current policies to historical data for the querying user. If a required policy column is missing or incompatible in the historical version, the query fails closed.

The ABAC core concepts clarify that views are included among the securable types targeted by FOR TABLES. Governed tags drive policy matching. Column tags must be applied directly: they do not inherit from their parent table.

Why It Matters for Platform Teams

Access-path coverage is now a release concern. A successful query against a protected table tells us nothing conclusive about a separately exposed view, a different execution identity or an eligible historical read. We need explicit expected behaviour for each route the organisation actually offers.

Governed tags also become security-relevant deployment inputs. A deployment that creates the correct policy but misses the intended column tag can still violate the organisation's access contract.

Historical reporting introduces a second contract: availability under current governance. Time travel retrieves old data, not an old authorisation regime. A schema change can legitimately make an earlier version unreadable when today's policy cannot be evaluated safely against it.

Our position is straightforward: deploy policy code, tag assignments, relevant schema changes and their tests as one reviewed change. A green infrastructure deployment is insufficient. The gate must prove that restricted users receive the intended rows and masks, while authorised users retain their intended access.

Use Case: Governed Public-Health Reporting

Consider a national health institute that provides regional reporting over sensitive records. This is a hypothetical scenario based on public-sector governance constraints, not a claim that NexOps has deployed these Beta features at RIVM. Regional analysts need restricted extracts, authorised stewards need broader access, and reporting teams must reproduce calculations against earlier dataset versions.

The usual approach is to validate the base table using an administrator's notebook, then expose a reporting view. That misses the analyst's actual identity and leaves historical reads outside the test. A later column rename or tag edit can also change policy applicability without changing the reporting query.

The proposed architecture keeps governed managed tables in Unity Catalog, with reporting views as a separate consumer interface. Catalog- or schema-scoped ABAC policies use governed tags to select the relevant objects and columns. A deployment pipeline prepares synthetic fixtures, runs current-table and view queries under representative identities, and runs eligible historical-table queries on the intended compute.

The gate measures contract coverage, unexpected row or mask results, legitimate historical queries that fail after schema changes, and time spent diagnosing rejected releases. The institute should also track query latency for representative reporting workloads. None of these signals requires collecting sensitive query results into CI logs.

Implementation

1. Define the matrix before configuring the preview

Start with the supported routes you intend to expose. For each case, record the identity, object, query form, tag state, compute configuration and expected outcome.

DimensionRepresentative casesAssertion
IdentityRestricted analyst, exempt steward, application service principalEach receives its approved rows and column values
ObjectCurrent table, reporting viewEach interface meets its own access contract
Historical readRequired versions and timestampsCurrent policy is enforced on eligible historical data
TagsInitial classification, addition, removal, reassignmentBehaviour matches the approved classification change
SchemaCompatible history, historical-only column, missing policy inputProjection or failure matches documented semantics
ComputeEach intended supported configurationEligibility is recorded and the contract passes

2. Establish eligibility and a controlled fixture

Record each preview setting in the deployment configuration. Check fixture eligibility against the requirements above.

Use a synthetic fixture containing records from two regions and an obviously artificial sensitive string. Configure an approved row-filter policy that restricts the analyst to one region. Add a mask returning *** for the protected string. Exempt the steward group from both policies.

Define the underlying grants through Entra ID security groups in NexOps-managed Terraform modules. Include USE CATALOG, USE SCHEMA and the required object privileges. Assign object ownership to groups.

Create a simple projection view over that fixture, exposing the same test columns without additional filtering. Establish the intended tag assignments on the relevant objects and columns, then verify the policy's actual effect on both interfaces. Do not assume a table's tag assignments automatically establish the view's required policy matching.

Retain a compatible historical table version containing the same fixture records. Capture its version number from the fixture setup.

3. Run assertions under real execution identities

Run separate test sessions authenticated as the restricted and exempt principals. Switching an expectation variable inside an administrator's session does not switch the query identity.

The following PySpark contract runs against the acceptance fixture. Keep it in a Python package and call it from a thin notebook. The environment variables are test-harness inputs, not Databricks configuration keys. Prepare the fixture, policies, tags, permissions and compute prerequisites before execution.

python
import json
import os
 
from pyspark.sql import SparkSession
 
 
def run_policy_contract(spark: SparkSession) -> None:
    # Supply harness inputs separately for each authenticated session.
    expected_principal = os.environ["ABAC_EXPECTED_PRINCIPAL"]
    profile = os.environ["ABAC_TEST_PROFILE"]
    historical_version = int(os.environ["ABAC_HISTORY_VERSION"])
 
    if historical_version < 0:
        raise ValueError("Historical version must be non-negative")
 
    actual_principal = spark.sql(
        "SELECT current_user() AS principal"
    ).first()["principal"]
 
    if actual_principal != expected_principal:
        raise RuntimeError("Test session has the wrong execution identity")
 
    # Use synthetic values; both versions contain both fixture rows.
    expected_by_profile = {
        "restricted": [
            ("north-1", "north", "***"),
        ],
        "exempt": [
            ("north-1", "north", "SYNTHETIC-NORTH"),
            ("south-1", "south", "SYNTHETIC-SOUTH"),
        ],
    }
    if profile not in expected_by_profile:
        raise ValueError("Unknown test profile")
 
    expected = sorted(expected_by_profile[profile])
    projection = "event_id, care_area, person_label"
 
    queries = {
        "current_table": (
            f"SELECT {projection} "
            "FROM abac_acceptance.fixture.patient_events"
        ),
        "current_view": (
            f"SELECT {projection} "
            "FROM abac_acceptance.fixture.analyst_events"
        ),
        "historical_table": (
            f"SELECT {projection} "
            "FROM abac_acceptance.fixture.patient_events "
            f"VERSION AS OF {historical_version}"
        ),
    }
 
    report = []
    for path, query in queries.items():
        # Collect only the small synthetic fixture into driver memory.
        # Let query errors fail the positive-access gate.
        actual = sorted(tuple(row) for row in spark.sql(query).collect())
 
        if actual != expected:
            # Keep returned values out of failure logs.
            raise AssertionError(f"{path}: access contract mismatch")
 
        report.append({
            "path": path,
            "profile": profile,
            "outcome": "passed",
        })
 
    print(json.dumps(report, indent=2))
 
 
run_policy_contract(spark)

Exact row comparison catches both unexpected disclosure and unintended loss of access for this bounded fixture; a row-count check cannot do that. The identity check prevents an accidentally privileged test session from producing misleading evidence.

Extend the contract with the production service principal. Add required timestamp-based reads and DataFrame reads for interfaces used by applications.

4. Add tag-change and schema-change contracts

Run classification transitions in an isolated test schema. Define reviewed expectations for each transition. Add, remove or change a governed tag. Repeat the relevant queries under each identity.

Test historical schema behaviour independently. Include a version with a historical-only column and inspect the returned schema to verify that it is not exposed. Include a version missing a column required by the current policy and verify the documented fail-closed behaviour.

Separate expected query failures from positive-access tests. For deliberate fail-closed cases, record the approved error class or signature observed in your environment. Validate that signature in the negative-access assertion.

5. Promote the contract across DTAP

Package the tests alongside the deployment artefacts and make their outcome a required CI/CD check. Deploy the test jobs through Databricks Asset Bundles using workload identity federation and service-principal run_as identities. Our declarative Asset Bundles guide covers the deployment foundation.

Promote the same artefact through dev, test, acc and main, with main deploying to prd. Require approvals for acceptance and production. Parameterise fixture names for each environment and keep test access on private network paths.

EnvironmentGate behaviour
DevelopmentBuild synthetic fixtures and exercise tag and schema transitions
TestRun the full matrix with the intended preview settings and compute
AcceptanceValidate representative identity memberships and production-shaped query interfaces
ProductionUse bounded synthetic smoke tests; avoid disruptive tag mutations on live sensitive objects

Store evidence with the release: policy definitions, tag assignments, fixture history version, identity, compute details, preview prerequisites and assertion outcomes. Rerun affected contracts after policy-function, tag, view-definition, schema or compute changes.

Pitfalls and When Not to Use It

Beta approval is separate from test success. Passing a matrix does not override an organisation's restrictions on preview features. We would not introduce these access paths into regulated production workloads unless the organisation explicitly accepts Beta use and its operational implications.

Do not confuse ABAC with table-level row filters and column masks. Table-level filters and masks remain unsupported for time travel. Dedicated compute is also unsupported for ABAC time travel.

The ABAC requirements exclude metric views from view-policy support. A query can apply at most 20 ABAC policies across its table and view hierarchy. A query that exceeds this limit fails.

CREATE OR REPLACE VIEW removes governed tags and directly attached policies from the view. Prefer ALTER VIEW to preserve metadata. If replacement is necessary, reapply the intended tags and policies before rerunning the affected contracts.

Tag changes can take a few minutes to take effect. Use bounded retries for propagation checks and fail the gate if the approved result does not appear.

Do not promise permanent historical readability. Regulatory reproduction requirements need tested historical versions and an agreed response when access is safely blocked. A timeout, missing privilege or unavailable compute must not count as successful security enforcement.

Do not treat view support and qualifying historical-table support as a combined support statement for historical reads through views. Keep that route unapproved until official documentation establishes eligibility and tests confirm behaviour. Record unsupported routes in the inventory as not approved for deployment; successful execution does not establish support.

Finally, do not adopt a broad policy merely to simplify administration. Where classification ownership is unclear or tag completeness cannot be validated, a smaller, explicitly governed exposure surface is safer. Our preference is to expand supported access paths only as quickly as the organisation can test and operate them.

Key Takeaways

  1. Separate preview readiness from release approval. Check both feature prerequisites and the organisation's Beta policy.
  2. Exercise representative principals. Include restricted, exempt and application identities in the release evidence.
  3. Compare complete synthetic outcomes. Check returned values, visible columns and approved failure signatures.
  4. Include governance metadata in change control. Cover tag transitions and view replacement alongside policy and schema edits.
  5. Promote only verified access contracts. Retain the evidence and reject routes without documented support.

Blijf op de hoogte

Ontvang het volgende artikel per e-mail

Volgende stap

Wil je hierover praten?

We praten graag over het vak. Heeft dit artikel je op een idee gebracht, of werk je aan iets vergelijkbaars? Dan maken we graag kennis.

Open voor projecten in Q4 2026