Skip to main content
Data Engineering · lesson 17 of 23
Open the Data Engineering page →
Data Engineering advanced Lesson 17 of 23

Data Governance: Catalogs, Classification, and Access Policy

Classify every column in a catalog by sensitivity, enforce role-based access against it, and watch three roles get three different answers to one query.

By SkillByExample · Published

data-engineering governance access-control data-catalog

Classify four columns by sensitivity, then run the same query as three different roles and watch access get enforced column-by-column, not table-by-table.

A catalog: every column, classified once

CATALOG = {
    "gold.customers": {
        "customer_id": "public",
        "signup_country": "internal",
        "email": "confidential",
        "ssn": "restricted",
    },
    "gold.daily_revenue": {
        "order_date": "public",
        "country_code": "public",
        "revenue": "internal",
    },
}

def describe_table(table):
    print(f"{table}:")
    for col, classification in CATALOG[table].items():
        print(f"  {col:<16} {classification}")

describe_table("gold.customers")
$ python governance.py
== catalog: every column classified ==
gold.customers:
  customer_id      public
  signup_country   internal
  email            confidential
  ssn              restricted

The classification lives with the column, decided once, independent of who ever asks for it.

Role clearance, defined separately from the catalog

ROLE_CLEARANCE = {
    "analyst": {"public", "internal"},
    "support_agent": {"public", "internal", "confidential"},
    "compliance_officer": {"public", "internal", "confidential", "restricted"},
}

def enforce_access(role, table, requested_columns):
    clearance = ROLE_CLEARANCE.get(role, set())
    schema = CATALOG.get(table, {})
    allowed, denied = [], []
    for col in requested_columns:
        classification = schema.get(col, "unknown")
        if classification in clearance:
            allowed.append(col)
        else:
            denied.append((col, classification))
    return allowed, denied

The same query, three roles, three different results

print("== analyst wants email and ssn ==")
allowed, denied = enforce_access("analyst", "gold.customers", ["customer_id", "email", "ssn"])
print(f"allowed: {allowed}")
for col, classification in denied:
    print(f"DENIED  {col} (classification={classification}) - analyst role lacks clearance")
== access attempt: analyst wants email and ssn ==
allowed: ['customer_id']
DENIED  email (classification=confidential) - analyst role lacks clearance
DENIED  ssn (classification=restricted) - analyst role lacks clearance
print("== compliance_officer, same query ==")
allowed, denied = enforce_access("compliance_officer", "gold.customers", ["customer_id", "email", "ssn"])
print(f"allowed: {allowed}")
print(f"denied: {denied}")
== access attempt: same query, compliance_officer role ==
allowed: ['customer_id', 'email', 'ssn']
denied: []
print("== support_agent, same query ==")
allowed, denied = enforce_access("support_agent", "gold.customers", ["customer_id", "email", "ssn"])
print(f"allowed: {allowed}")
for col, classification in denied:
    print(f"DENIED  {col} (classification={classification}) - support_agent role lacks clearance")
== access attempt: support_agent, one column above clearance ==
allowed: ['customer_id', 'email']
DENIED  ssn (classification=restricted) - support_agent role lacks clearance

One query, one function, three different results — because the decision is made per column against a role’s clearance set, not as a single table-level yes/no. A support agent can see enough to help a customer (email) without ever touching ssn; an analyst gets neither. No one had to write three separate access rules — adding a fourth role only means defining its clearance set once.

Practice

1. Add a new role, `marketing`, cleared only for "public", and query gold.daily_revenue's revenue column.
allowed: ['order_date', 'country_code']
DENIED  revenue (classification=internal) - marketing role lacks clearance

revenue is internal, one level above marketing’s clearance — denied exactly like ssn was for analyst, same mechanism, different column and role.

2. Query a column that doesn't exist in the catalog, e.g. "phone_number".
DENIED  phone_number (classification=unknown) - analyst role lacks clearance

An uncatalogued column defaults to "unknown", which is in no role’s clearance set — fail closed. A catalog gap should block access, not silently grant it.

3. Add a "legal_hold" classification that no role is ever cleared for, and add a column with it.
CATALOG["gold.customers"]["litigation_note"] = "legal_hold"
allowed, denied = enforce_access("compliance_officer", "gold.customers", ["litigation_note"])
print(allowed, denied)
[] [('litigation_note', 'legal_hold')]

Even the highest clearance in ROLE_CLEARANCE doesn’t include legal_hold unless someone explicitly adds it to a role’s set — a genuinely un-grantable classification, useful for data under active legal hold that no ordinary role should ever see.

4. Why enforce access per-column instead of just denying the whole query if any column is out of clearance?

Per-column enforcement lets a query return the columns it’s entitled to instead of failing outright — the analyst still gets customer_id even though email and ssn were denied. An all-or-nothing table-level check would block that analyst from gold.customers completely, even for queries that only ever touch public columns.

Next: data security and PII — what to do with a column a role isn’t cleared to see in full, instead of just denying it outright.

Frequently Asked Questions

What does 'data governance' actually mean in a pipeline, concretely?
Three things working together: a catalog that records what data exists and what it means, a classification scheme that says how sensitive each piece is, and an access policy that enforces who can see what based on that classification. Without all three, 'governance' is a slide, not a control.
What is a data catalog?
A registry of every table and column a data platform produces, along with metadata about each one — description, owner, classification, freshness expectations. This lesson's CATALOG dict is a minimal one; production catalogs (DataHub, Amundsen, a cloud warehouse's built-in catalog) add search, lineage integration (see the data-lineage lesson), and automated discovery from query logs.
How is data classification different from access control?
Classification is a label — this column is 'confidential' — decided once, independent of who is asking. Access control is the enforcement — does this specific role's clearance include 'confidential'? — evaluated on every access attempt. Separating them means adding a new role only requires defining its clearance set, never re-classifying every column.
Where does this fit relative to data security and PII masking?
Governance decides who is *allowed* to see a value; security (the next lesson) decides what to actually *do* when someone without clearance still needs the row for some other purpose — mask it, hash it, or tokenize it rather than deny access outright. A support agent often needs to confirm 'is this the right customer' without ever seeing their full SSN.