Chapter 22 · Security, Privacy, Row/Column Policies, Masking, and Least-Privilege Analytics

Threat-Model a Warehouse Including BI Exports, Shared Credentials, Staging Data, Backups, and Over-Broad Analyst Roles

Threat-model the complete AtlasMart analytical path—including identities, raw/staging data, BI exports, backups, service accounts, and privileged roles—and turn findings into reproducible security acceptance tests and operator evidence.

Intermediate → Advanced205–250 minutesEnd-to-end warehouse threat-model lab10 lines · 820 USD · restore overlay PASSLast reviewed: September 2026

Learning outcomes

01

Threat-model warehouse assets, identities, trust boundaries, copies, and recovery paths.

02

Convert shared credentials, over-broad roles, staging exposure, exports, and backups into testable findings.

03

Run the complete local policy/privacy lab and interpret its audit, copy, and erasure evidence.

04

Preserve warehouse control totals while proving least privilege and restore-safe erasure behavior.

05

Create a production runbook with owners, rollback paths, and explicit non-guarantees.

Continuity and explicit security-layer addition

Chapter 22 begins from Chapter 21's certified warehouse/domain truth: 10 current paid lines, 8 orders, 12 units, 820 USD gross revenue, 495 USD cost, and 325 USD gross profit. The canonical fact grain remains one current paid order line. The governed metrics and conformed dimensions are not redefined. This chapter adds identity, authorization, privacy, copy-control, and audit mechanisms around those same assets. A security control that changes revenue merely because a different user queried it is a modeling bug unless the metric contract explicitly defines a security-scoped population.

Lab contract and limits

Runtime executed for this chapter: Python 3.13.5 and SQLite 3.46.1. Environment: local/in-process, synthetic, no-cost. Storage: one disposable SQLite file plus JSON evidence. Security engine: SQLite does not provide a warehouse-style role/RLS/CLS subsystem, so the lab implements a deterministic policy evaluator and secure-view simulation while keeping the policy semantics vendor-neutral; PostgreSQL/cloud-warehouse row policies are referenced only as product-specific examples. Token key: supplied at runtime through ATLASMART_TOKEN_KEY; no production secret is embedded in SQL or source. Time/currency: existing UTC and USD metric semantics remain unchanged. Privacy: all names/emails are synthetic. The erasure workflow is a technical teaching model, not legal advice or a universal retention rule.

1. Start with assets and paths, not a generic threat list

A warehouse threat model identifies valuable assets, actors, access paths, trust boundaries, plausible failures, and controls. For AtlasMart, the assets include raw customer PII, conformed dimensions, paid-order facts, semantic metrics, certified marts, BI exports, token keys/mappings, credentials, audit logs, and backups. The actors include analysts, engineers, service identities, privacy operators, security administrators, and downstream consumers.

AtlasMart threat register
asset/path                         threat/failure                         evidence/controlhuman identity                      shared account / weak attribution             disable shared identities; audit unique subjectservice identity                    over-broad BI/admin privileges                separate pipeline_writer from export/admin rolessecure semantic/customer view       policy bypass through raw table               enumerate effective raw/staging grantsraw/staging customer data           direct identifier exposure                   restricted sensitivity + narrow service grantsmasked BI export                    copied data escapes live policy                export role + copy registry + retention ownerimmutable backup                    deleted subject can reappear on restore       quarantine/expiry + erasure overlay on restoreprivacy operator                    can erase and grant own privileges            separation of duties: erase yes, ADMIN nosecurity administrator              can manage policy and read all PII by default avoid implicit data access; separate admin/data rolesdev environment                     accidental production access                 environment-bound identities/resources

2. Failure injection: prove the model catches real weaknesses

The fixture intentionally starts with two findings: an enabled shared human credential and a marketing role that can bypass its masked view by reading raw PII. A useful threat model should produce executable checks for both. After repair, those findings are empty while required business access still works.

Acceptance evidence
initial shared_credentials = [shared_analyst]initial bypass_paths = [marketing_analyst -> prod.raw_customer]repaired shared_credentials = []repaired bypass_paths = []repaired separation_of_duties = []finance mart SELECT = ALLOWfinance PII SELECT = DENYETL raw INSERT = ALLOWETL security ADMIN = DENY

3. Run the complete deterministic lab

Run commands
# Bash / Git Bashexport ATLASMART_TOKEN_KEY="synthetic-lab-key-2026"python ch22_lab.py# PowerShell$env:ATLASMART_TOKEN_KEY = "synthetic-lab-key-2026"python .\ch22_lab.py

The script creates a disposable SQLite database, seeds the current 10-line AtlasMart fixture, builds identity/role/resource/policy catalogs, executes positive and negative access tests, repairs the deliberate findings, runs a synthetic C004 erasure, verifies backup restore overlay behavior, and writes report.json, access_matrix.json, and deletion_manifest.json.

Complete executed lab source
from __future__ import annotationsimport hashlib, hmac, json, os, sqlite3, sysfrom pathlib import PathOUT=Path('/mnt/data/atlasmart_ch22_lab'); OUT.mkdir(parents=True,exist_ok=True)DB=OUT/'atlasmart_ch22.sqlite'if DB.exists(): DB.unlink()KEY=os.environ.get('ATLASMART_TOKEN_KEY')if not KEY:    raise SystemExit('Set ATLASMART_TOKEN_KEY to a synthetic lab-only value before running.')SALES=[('O1001',1,'2026-09-18','P100','D-CUST-001','web',2,10000,6000,'USD','paid'),('O1001',2,'2026-09-18','P200','D-CUST-001','web',1,2500,1500,'USD','paid'),('O1002',1,'2026-09-18','P300','D-CUST-002','mobile',1,19000,12000,'USD','paid'),('O1003',1,'2026-09-19','P400','D-CUST-001','web',1,10000,6500,'USD','paid'),('O1005',1,'2026-09-20','P100','D-CUST-004','mobile',1,5000,3000,'USD','paid'),('O1005',2,'2026-09-20','P400','D-CUST-004','mobile',1,10000,6500,'USD','paid'),('O1007',1,'2026-09-21','P300','D-CUST-003','sales',1,7500,4500,'USD','paid'),('O1008',1,'2026-09-21','P200','D-CUST-002','mobile',2,5000,2500,'USD','paid'),('O1009',1,'2026-09-22','P100','D-CUST-005','web',1,5000,2500,'USD','paid'),('O1010',1,'2026-09-22','P100','D-CUST-003','sales',1,8000,4500,'USD','paid')]CUSTOMERS=[('D-CUST-001','C001','Ada Retail','ada@example.test','Mid-Market','G-NORTH','active',1),('D-CUST-002','C002','Ben Home','ben@example.test','Consumer','G-EAST','active',1),('D-CUST-003','C003','Cyra Labs','cyra@example.test','Enterprise','G-EAST','active',1),('D-CUST-004','C004','Dara Studio','dara@example.test','Consumer','G-SOUTH','inactive',0),('D-CUST-005','C005','__INFERRED__',None,'__UNKNOWN__','__UNKNOWN__','active',0)]EXPECTED={'lines':10,'orders':8,'units':12,'revenue_usd':820,'cost_usd':495,'profit_usd':325}IDENTITIES=[('alice.marketing','human','prod',0,1),('frank.finance','human','prod',0,1),('olivia.ops','human','prod',0,1),('svc_etl_prod','service','prod',0,1),('svc_bi_export_prod','service','prod',0,1),('priya.privacy','human','prod',0,1),('sara.security','human','prod',0,1),('dev.dana','human','dev',0,1),('shared_analyst','human','prod',1,1)]ROLE_BINDINGS=[('alice.marketing','marketing_analyst','prod'),('frank.finance','finance_analyst','prod'),('olivia.ops','operations_analyst','prod'),('svc_etl_prod','pipeline_writer','prod'),('svc_bi_export_prod','bi_exporter','prod'),('priya.privacy','privacy_operator','prod'),('sara.security','security_admin','prod'),('dev.dana','developer','dev'),('shared_analyst','marketing_analyst','prod')]RESOURCES=[('prod.fact_sales','prod','internal'),('prod.dim_customer','prod','restricted'),('prod.raw_customer','prod','restricted'),('prod.secure_customer_view','prod','restricted'),('prod.finance_mart','prod','internal'),('prod.bi_export_masked','prod','restricted'),('prod.security_catalog','prod','confidential'),('dev.dim_customer','dev','synthetic')]# Initial grants deliberately contain one bypass: marketing_analyst can read prod.raw_customer.GRANTS=[('marketing_analyst','prod.secure_customer_view','SELECT','ALLOW'),('marketing_analyst','prod.raw_customer','SELECT','ALLOW'),('finance_analyst','prod.finance_mart','SELECT','ALLOW'),('operations_analyst','prod.fact_sales','SELECT','ALLOW'),('pipeline_writer','prod.raw_customer','INSERT','ALLOW'),('pipeline_writer','prod.dim_customer','UPSERT','ALLOW'),('bi_exporter','prod.secure_customer_view','SELECT','ALLOW'),('bi_exporter','prod.bi_export_masked','INSERT','ALLOW'),('privacy_operator','prod.dim_customer','SELECT_PII','ALLOW'),('privacy_operator','prod.dim_customer','ERASE_PII','ALLOW'),('privacy_operator','prod.raw_customer','ERASE_PII','ALLOW'),('security_admin','prod.security_catalog','ADMIN','ALLOW'),('developer','dev.dim_customer','SELECT','ALLOW')]PII=[('prod.dim_customer','customer_name','direct_identifier','restricted'),('prod.dim_customer','email','contact_identifier','restricted'),('prod.dim_customer','durable_customer_id','pseudonymous_identifier','restricted'),('prod.dim_customer','segment','business_attribute','internal'),('prod.raw_customer','customer_name','direct_identifier','restricted'),('prod.raw_customer','email','contact_identifier','restricted')]PURPOSE_POLICIES=[('marketing_campaign_analysis','marketing_analyst','token,segment,geography_code,masked_email','no direct name/email; consent policy evaluated separately'),('finance_reporting','finance_analyst','aggregate revenue/cost/profit','no customer direct identifiers required'),('privacy_request','privacy_operator','direct identifiers only for verified privacy workflow','case-bound access; audited'),('pipeline_processing','pipeline_writer','raw/integration fields required by governed load','non-human service identity; no ad-hoc BI export')]COPIES=[('COPY-LIVE-DIM','prod.dim_customer','live_table','active',1,None),('COPY-RAW','prod.raw_customer','raw_generation','active',1,None),('COPY-EXPORT','prod.bi_export_masked','export','active',1,None),('COPY-BACKUP-20260920','backup://prod/2026-09-20','immutable_backup','active',1,'2026-10-21')]def sha(v:str)->str: return hashlib.sha256(v.encode()).hexdigest()def token(durable_id:str)->str:    return 'tok_'+hmac.new(KEY.encode(),durable_id.encode(),hashlib.sha256).hexdigest()[:20]def masked_email(email):    if not email: return None    local,domain=email.split('@',1)    return (local[:1]+'***@'+domain) if local else '***@'+domaindef setup():    c=sqlite3.connect(DB)    c.row_factory=sqlite3.Row    c.executescript('''    PRAGMA foreign_keys=ON;    CREATE TABLE fact_sales(order_id TEXT,line_no INT,order_date TEXT,product_id TEXT,durable_customer_id TEXT,channel TEXT,quantity INT,amount_cents INT,cost_cents INT,currency_code TEXT,status TEXT,PRIMARY KEY(order_id,line_no));    CREATE TABLE dim_customer(durable_customer_id TEXT PRIMARY KEY,source_customer_id TEXT,customer_name TEXT,email TEXT,segment TEXT,geography_code TEXT,lifecycle_status TEXT,marketing_consent INT);    CREATE TABLE raw_customer(durable_customer_id TEXT PRIMARY KEY,source_customer_id TEXT,customer_name TEXT,email TEXT,raw_generation INT NOT NULL,erased INT NOT NULL DEFAULT 0);    CREATE TABLE token_vault(durable_customer_id TEXT PRIMARY KEY,customer_token TEXT UNIQUE NOT NULL);    CREATE TABLE identity_registry(identity TEXT PRIMARY KEY,identity_type TEXT,environment TEXT,is_shared INT,enabled INT);    CREATE TABLE role_binding(identity TEXT,role TEXT,environment TEXT,PRIMARY KEY(identity,role,environment));    CREATE TABLE resource_catalog(resource TEXT PRIMARY KEY,environment TEXT,sensitivity TEXT);    CREATE TABLE permission(role TEXT,resource TEXT,action TEXT,effect TEXT,PRIMARY KEY(role,resource,action));    CREATE TABLE pii_catalog(resource TEXT,column_name TEXT,classification TEXT,sensitivity TEXT,PRIMARY KEY(resource,column_name));    CREATE TABLE purpose_policy(purpose TEXT,role TEXT,allowed_fields TEXT,restriction TEXT,PRIMARY KEY(purpose,role));    CREATE TABLE access_audit(event_id INTEGER PRIMARY KEY AUTOINCREMENT,event_time TEXT,identity TEXT,resource TEXT,action TEXT,decision TEXT,reason TEXT);    CREATE TABLE copy_registry(copy_id TEXT PRIMARY KEY,location TEXT,copy_type TEXT,status TEXT,contains_subject INT,retention_until TEXT);    CREATE TABLE deletion_manifest(case_id TEXT PRIMARY KEY,durable_customer_id TEXT,requested_at TEXT,executed_at TEXT,scope TEXT,before_hash TEXT,after_hash TEXT,backup_disposition TEXT,status TEXT);    CREATE TABLE erasure_tombstone(durable_customer_id TEXT PRIMARY KEY,case_id TEXT,applies_from TEXT);    CREATE TABLE operator_note(note_id TEXT PRIMARY KEY,case_id TEXT,note TEXT);    CREATE VIEW finance_mart AS SELECT order_date,SUM(amount_cents)/100.0 revenue_usd,SUM(cost_cents)/100.0 cost_usd,SUM(amount_cents-cost_cents)/100.0 profit_usd FROM fact_sales WHERE status='paid' AND currency_code='USD' GROUP BY order_date;    ''')    c.executemany('INSERT INTO fact_sales VALUES (?,?,?,?,?,?,?,?,?,?,?)',SALES)    c.executemany('INSERT INTO dim_customer VALUES (?,?,?,?,?,?,?,?)',CUSTOMERS)    c.executemany('INSERT INTO raw_customer VALUES (?,?,?,?,1,0)',[(x[0],x[1],x[2],x[3]) for x in CUSTOMERS])    c.executemany('INSERT INTO token_vault VALUES (?,?)',[(x[0],token(x[0])) for x in CUSTOMERS])    c.executemany('INSERT INTO identity_registry VALUES (?,?,?,?,?)',IDENTITIES)    c.executemany('INSERT INTO role_binding VALUES (?,?,?)',ROLE_BINDINGS)    c.executemany('INSERT INTO resource_catalog VALUES (?,?,?)',RESOURCES)    c.executemany('INSERT INTO permission VALUES (?,?,?,?)',GRANTS)    c.executemany('INSERT INTO pii_catalog VALUES (?,?,?,?)',PII)    c.executemany('INSERT INTO purpose_policy VALUES (?,?,?,?)',PURPOSE_POLICIES)    c.executemany('INSERT INTO copy_registry VALUES (?,?,?,?,?,?)',COPIES)    c.commit(); return cdef controls(c):    r=c.execute("SELECT count(*),count(distinct order_id),sum(quantity),sum(amount_cents),sum(cost_cents),sum(amount_cents-cost_cents) FROM fact_sales WHERE status='paid' AND currency_code='USD'").fetchone()    return {'lines':r[0],'orders':r[1],'units':r[2],'revenue_usd':r[3]//100,'cost_usd':r[4]//100,'profit_usd':r[5]//100}def roles(c,identity,env):    return {r[0] for r in c.execute('SELECT role FROM role_binding WHERE identity=? AND environment=?',(identity,env))}def authorize(c,identity,resource,action,now='2026-09-21T13:00:00Z'):    i=c.execute('SELECT environment,is_shared,enabled FROM identity_registry WHERE identity=?',(identity,)).fetchone()    rr=c.execute('SELECT environment FROM resource_catalog WHERE resource=?',(resource,)).fetchone()    if not i or not i['enabled']:        decision,reason='DENY','unknown or disabled identity'    elif not rr:        decision,reason='DENY','unknown resource'    elif i['environment']!=rr['environment']:        decision,reason='DENY','environment isolation'    else:        rs=roles(c,identity,rr['environment'])        hits=c.execute("SELECT role,effect FROM permission WHERE resource=? AND action=? AND role IN (%s)" % (','.join('?'*len(rs)) if rs else "''"),(resource,action,*sorted(rs)) if rs else (resource,action)).fetchall()        decision='ALLOW' if any(h['effect']=='ALLOW' for h in hits) else 'DENY'        reason='role grant' if decision=='ALLOW' else 'no matching least-privilege grant'    c.execute('INSERT INTO access_audit(event_time,identity,resource,action,decision,reason) VALUES (?,?,?,?,?,?)',(now,identity,resource,action,decision,reason)); c.commit()    return decision,reasondef secure_customer_view(c,identity,purpose='marketing_campaign_analysis'):    dec,_=authorize(c,identity,'prod.secure_customer_view','SELECT')    if dec!='ALLOW': return []    rs=roles(c,identity,'prod')    out=[]    for r in c.execute('SELECT * FROM dim_customer ORDER BY durable_customer_id'):        # Marketing sees only consented customers; BI exporter receives the same policy-filtered rows.        if ('marketing_analyst' in rs or 'bi_exporter' in rs) and not r['marketing_consent']:            continue        out.append({'customer_token':token(r['durable_customer_id']),'segment':r['segment'],'geography_code':r['geography_code'],'masked_email':masked_email(r['email'])})    return outdef credential_findings(c):    rows=c.execute('SELECT identity,identity_type,environment FROM identity_registry WHERE is_shared=1 AND enabled=1').fetchall()    return [dict(r) for r in rows]def separation_findings(c):    forbidden=[('pipeline_writer','security_admin'),('privacy_operator','security_admin')]    findings=[]    for ident in [r[0] for r in c.execute('SELECT identity FROM identity_registry WHERE enabled=1')]:        rs=roles(c,ident,'prod')        for a,b in forbidden:            if a in rs and b in rs: findings.append({'identity':ident,'roles':[a,b]})    return findingsdef bypass_findings(c):    findings=[]    # Any analytical/export role able to read raw direct identifiers is a bypass.    for role in ('marketing_analyst','finance_analyst','operations_analyst','bi_exporter'):        row=c.execute("SELECT effect FROM permission WHERE role=? AND resource='prod.raw_customer' AND action='SELECT'",(role,)).fetchone()        if row and row[0]=='ALLOW': findings.append({'role':role,'resource':'prod.raw_customer','reason':'bypasses secure view and exposes direct identifiers'})    return findingsdef repair_identity_and_bypass(c):    c.execute("UPDATE identity_registry SET enabled=0 WHERE identity='shared_analyst'")    c.execute("DELETE FROM permission WHERE role='marketing_analyst' AND resource='prod.raw_customer' AND action='SELECT'")    c.commit()def direct_pii_snapshot(c,did):    r=c.execute('SELECT durable_customer_id,source_customer_id,customer_name,email,segment,geography_code,marketing_consent FROM dim_customer WHERE durable_customer_id=?',(did,)).fetchone()    return dict(r) if r else Nonedef erase_subject(c,did='D-CUST-004',case_id='ERASE-20260921-004'):    prior=c.execute('SELECT status FROM deletion_manifest WHERE case_id=?',(case_id,)).fetchone()    if prior: return 'replay_ignored'    before=direct_pii_snapshot(c,did); before_hash=sha(json.dumps(before,sort_keys=True))    c.execute('BEGIN')    c.execute("UPDATE dim_customer SET source_customer_id=NULL,customer_name='[ERASED]',email=NULL,marketing_consent=0 WHERE durable_customer_id=?",(did,))    # Governed exception to raw immutability: active raw direct identifiers are rewritten and the generation advances.    c.execute("UPDATE raw_customer SET source_customer_id=NULL,customer_name='[ERASED]',email=NULL,raw_generation=raw_generation+1,erased=1 WHERE durable_customer_id=?",(did,))    c.execute('DELETE FROM token_vault WHERE durable_customer_id=?',(did,))    c.execute('INSERT INTO erasure_tombstone VALUES (?,?,?)',(did,case_id,'2026-09-21T13:30:00Z'))    # Active copies can be sanitized; immutable backup is quarantined/pending expiry, not falsely reported as deleted in place.    c.execute("UPDATE copy_registry SET contains_subject=0,status='sanitized' WHERE copy_type IN ('live_table','raw_generation','export')")    c.execute("UPDATE copy_registry SET status='quarantined_pending_expiry' WHERE copy_type='immutable_backup' AND contains_subject=1")    after=direct_pii_snapshot(c,did); after_hash=sha(json.dumps(after,sort_keys=True))    scope='dim_customer, active raw generation, token vault, active exports; backup tracked separately'    backup='immutable backup quarantined; expiry/rewrite depends on approved retention/legal policy; restore hook must reapply tombstone'    c.execute('INSERT INTO deletion_manifest VALUES (?,?,?,?,?,?,?,?,?)',(case_id,did,'2026-09-21T13:20:00Z','2026-09-21T13:30:00Z',scope,before_hash,after_hash,backup,'executed_with_backup_followup'))    c.execute('INSERT INTO operator_note VALUES (?,?,?)',('NOTE-ERASE-004',case_id,'Technical lab workflow only; controller/legal policy decides grounds, exceptions, retention, backups, downstream processors, and proof requirements.'))    c.commit(); return {'before_hash':before_hash,'after_hash':after_hash,'status':'executed_with_backup_followup'}def restore_overlay_test(c,did='D-CUST-004'):    # Simulate a restore row that would reintroduce PII, then apply the erasure tombstone before release.    restored={'durable_customer_id':did,'source_customer_id':'C004','customer_name':'Dara Studio','email':'dara@example.test'}    tomb=c.execute('SELECT case_id FROM erasure_tombstone WHERE durable_customer_id=?',(did,)).fetchone()    if tomb:        restored.update({'source_customer_id':None,'customer_name':'[ERASED]','email':None})    return {'tombstone_found':bool(tomb),'released_name':restored['customer_name'],'released_email':restored['email'],'status':'PASS' if restored['customer_name']=='[ERASED]' and restored['email'] is None else 'FAIL'}def main():    c=setup(); ctl_before=controls(c); assert ctl_before==EXPECTED,ctl_before    initial_creds=credential_findings(c); assert [x['identity'] for x in initial_creds]==['shared_analyst']    initial_bypass=bypass_findings(c); assert initial_bypass and initial_bypass[0]['role']=='marketing_analyst'    assert separation_findings(c)==[]    # Masked view test: direct names are never returned; consented rows only.    mkt=secure_customer_view(c,'alice.marketing'); assert len(mkt)==3 and all('customer_name' not in r for r in mkt) and all(r['masked_email'] for r in mkt)    # Initial bypass is real even though the secure view masks.    d1,_=authorize(c,'alice.marketing','prod.raw_customer','SELECT'); assert d1=='ALLOW'    # Environment isolation: a dev identity cannot query prod restricted data.    d2,reason2=authorize(c,'dev.dana','prod.dim_customer','SELECT_PII'); assert d2=='DENY' and reason2=='environment isolation'    # Service identity is narrow: can write raw but cannot administer security or export BI.    assert authorize(c,'svc_etl_prod','prod.raw_customer','INSERT')[0]=='ALLOW'    assert authorize(c,'svc_etl_prod','prod.security_catalog','ADMIN')[0]=='DENY'    assert authorize(c,'svc_etl_prod','prod.bi_export_masked','INSERT')[0]=='DENY'    repair_identity_and_bypass(c)    assert credential_findings(c)==[] and bypass_findings(c)==[]    assert authorize(c,'alice.marketing','prod.raw_customer','SELECT')[0]=='DENY'    # Finance gets governed aggregate, not PII.    assert authorize(c,'frank.finance','prod.finance_mart','SELECT')[0]=='ALLOW'    assert authorize(c,'frank.finance','prod.dim_customer','SELECT_PII')[0]=='DENY'    # Privacy operator can perform case-bound PII workflow but cannot administer grants.    assert authorize(c,'priya.privacy','prod.dim_customer','ERASE_PII')[0]=='ALLOW'    assert authorize(c,'priya.privacy','prod.security_catalog','ADMIN')[0]=='DENY'    er=erase_subject(c); assert er!='replay_ignored'    assert erase_subject(c)=='replay_ignored'    after=direct_pii_snapshot(c,'D-CUST-004'); assert after['customer_name']=='[ERASED]' and after['email'] is None and after['source_customer_id'] is None    assert c.execute("SELECT count(*) FROM token_vault WHERE durable_customer_id='D-CUST-004'").fetchone()[0]==0    raw=c.execute("SELECT customer_name,email,raw_generation,erased FROM raw_customer WHERE durable_customer_id='D-CUST-004'").fetchone(); assert tuple(raw)==('[ERASED]',None,2,1)    ctl_after=controls(c); assert ctl_after==EXPECTED    restore=restore_overlay_test(c); assert restore['status']=='PASS'    copies=[dict(r) for r in c.execute('SELECT * FROM copy_registry ORDER BY copy_id')]    assert [r['status'] for r in copies if r['copy_type']=='immutable_backup']==['quarantined_pending_expiry']    manifest=dict(c.execute('SELECT * FROM deletion_manifest').fetchone())    access=[dict(r) for r in c.execute('SELECT * FROM access_audit ORDER BY event_id')]    report={      'runtime':{'python':sys.version.split()[0],'sqlite':sqlite3.sqlite_version},      'controls_before':ctl_before,'controls_after_erasure':ctl_after,      'initial_findings':{'shared_credentials':initial_creds,'bypass_paths':initial_bypass,'separation_of_duties':[]},      'repaired_findings':{'shared_credentials':credential_findings(c),'bypass_paths':bypass_findings(c),'separation_of_duties':separation_findings(c)},      'masked_marketing_rows':mkt,      'access_matrix_tests':access,      'pii_catalog':[dict(r) for r in c.execute('SELECT * FROM pii_catalog ORDER BY resource,column_name')],      'purpose_policies':[dict(r) for r in c.execute('SELECT * FROM purpose_policy ORDER BY purpose')],      'copy_registry':copies,'deletion_manifest':manifest,'restore_overlay_test':restore,      'erasure_replay':'replay_ignored',      'privacy_note':'Technical teaching workflow only. Jurisdiction, controller policy, legal basis, exceptions, retention, backups, processors, and proof requirements must be decided outside this lab.',      'tokenization_note':'Tokens are pseudonymous identifiers, not proof of anonymization; auxiliary mapping/key access changes re-identification risk.'    }    (OUT/'report.json').write_text(json.dumps(report,indent=2,sort_keys=True),encoding='utf-8')    (OUT/'access_matrix.json').write_text(json.dumps(access,indent=2),encoding='utf-8')    (OUT/'deletion_manifest.json').write_text(json.dumps(manifest,indent=2,sort_keys=True),encoding='utf-8')    print(json.dumps(report,indent=2,sort_keys=True)); c.close()if __name__=='__main__': main()

4. The security result must preserve the analytical contract

Evidence Result
Warehouse before security/privacy actions 10 lines · 8 orders · 12 units · 820 revenue · 495 cost · 325 profit
Warehouse after C004 erasure Exactly the same controls
Shared credentials after repair 0
Analytical raw bypasses after repair 0
Forbidden separation-of-duties combinations 0
Erasure replay replay_ignored
Restore overlay PASS

Security can legitimately filter a consumer's visible rows, but it must not mutate the underlying governed metric definition invisibly. If a dashboard shows a security-scoped result, the scope belongs in the query/security contract.

5. BI exports and shared credentials amplify blast radius

A shared login weakens attribution; an export creates a portable copy; combining them makes investigation and revocation especially difficult. Use individual or workload identities, dedicated export services, destination controls, copy inventories, and retention owners. Do not assume revoking a warehouse role recalls files already downloaded elsewhere.

6. Staging and raw paths are production data paths

Raw/staging schemas, object storage, temporary tables, caches, and debug extracts are often less polished than presentation marts, which makes them attractive bypass targets. Include them in the same sensitivity classification and access review. If engineers need emergency debugging access, use a time-bound audited workflow rather than permanent broad analyst roles.

7. Backup and disaster recovery tests must include privacy overlays

A technically successful restore can be a privacy/security regression if it resurrects a deleted direct identifier or an old over-broad grant. AtlasMart's restore test checks the erasure tombstone before release. A production recovery exercise should also reapply current authorization, key rotation, retention, and policy state appropriate to the restored point.

Observed erasure/restore evidence
ERASE-20260921-004before direct identifiers: source_customer_id=C004, customer_name=Dara Studio, email=dara@example.testafter direct identifiers:  source_customer_id=NULL, customer_name=[ERASED], email=NULLtoken_vault mapping:       removedactive raw generation:     rewritten under authorized privacy exception; generation 1 -> 2active export copy:         sanitizedimmutable backup:           quarantined_pending_expiry (not falsely reported deleted in place)restore-overlay test:       PASS; erasure tombstone reapplied before restored data is releasedrerun same case:            replay_ignoredwarehouse controls:         unchanged at 10 lines / 8 orders / 12 units / 820 / 495 / 325 USD

8. Operator runbook

  1. Detect: identify leaked/over-broad path, identity, resource, copy, and last-known-good policy version.
  2. Contain: revoke or disable the narrowest identity/grant/export route that stops exposure without destroying evidence.
  3. Scope: enumerate raw/staging/mart/BI/export/backup copies and affected query/audit events.
  4. Repair: apply reviewed policy/credential/privacy change; rotate credentials if compromise is plausible.
  5. Replay/recover: rerun affected data jobs only if required; security repair alone should not rewrite business measures.
  6. Verify: run positive and negative identity tests, bypass scans, data reconciliation, and restore overlays.
  7. Communicate: notify owners/consumers/security/privacy/legal channels according to organizational policy.
  8. Preserve evidence: retain non-sensitive hashes, audit events, case IDs, policy diffs, and decision records under approved retention.

9. Correctness, freshness, retry, observability, cost, compatibility, and rollback

Correctness: controls remain 820/495/325 USD after privacy actions. Freshness/history: authorization state and deletion cases need timestamps/versions where reconstruction matters. Idempotency: the same erasure case replays safely. Data quality: privacy/security policy is not a substitute for Chapter 13 quality tests. Observability: audit decisions and copy state are first-class evidence. Performance/cost: this lab makes no invented policy-overhead claim; benchmark selected engine behavior. Compatibility: native RLS/masking/IAM syntax differs by engine. Rollback: restore the last reviewed policy, but never roll back an authorized privacy action by resurrecting direct identifiers without an explicit approved decision.

10. What the lab proves—and does not prove

Proves: the deterministic fixture can detect/repair a shared identity and raw bypass, preserve required service access, erase synthetic direct identifiers/tokens, keep warehouse measures stable, and prevent a simulated restore from re-releasing erased PII. Does not prove: legal compliance, cloud IAM correctness, BI desktop export control, encryption/key management, endpoint security, backup-provider behavior, insider-risk prevention, distributed audit-log integrity, or production incident readiness. Those require platform-specific validation and organizational policy.

11. Cleanup/reset

Delete atlasmart_ch22_lab and rerun the lab with the synthetic environment key. Cleanup is limited to this disposable lab directory/database. Never point the privacy/security workflow at production or unrelated files.

12. Bridge to Chapter 23

Security depends on knowing what an object is, who owns it, where sensitive columns flow, and which consumers depend on it. Chapter 23 formalizes technical/business metadata, catalogs, lineage, documentation, ownership, certification, and impact analysis so a source or policy change can be traced through jobs, warehouse objects, metrics, reports, and data products.

Knowledge check

Acceptance questions

  1. Why does a threat model include backups and BI exports?
  2. What proves the C004 erasure did not corrupt finance metrics?
  3. Why is restore-overlay testing part of security/privacy correctness?
  4. What remains outside the local lab's guarantees?
  5. What metadata does Chapter 23 need to make impact analysis possible?
Review the answers

1. They are independent copies/access paths outside the live warehouse policy boundary.

2. Atomic control totals are identical before and after.

3. Restores can resurrect deleted PII or old policies unless current tombstones/policy are reapplied.

4. Legal compliance, native cloud/IAM enforcement, endpoints, encryption/key ops, external exports, and production incident operations.

5. Source→job/model→warehouse object→metric→report/product lineage plus owners, sensitivity, versions, and certification state.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.