Skip to content

feat: add denies config so object-level DENY survives a rebuild - #802

Open
Benjamin-Knight wants to merge 1 commit into
dbt-msft:masterfrom
Benjamin-Knight:feature/denies-config
Open

feat: add denies config so object-level DENY survives a rebuild#802
Benjamin-Knight wants to merge 1 commit into
dbt-msft:masterfrom
Benjamin-Knight:feature/denies-config

Conversation

@Benjamin-Knight

@Benjamin-Knight Benjamin-Knight commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Resolve #806

A SQL Server object-level DENY is stored against object_id, so dbt discards it on every drop-and-recreate (every run for a view). The DENY is the only way to carve an exception out of a schema-level GRANT, so losing it silently leaves a fail-open posture: the broad grant persists, its exceptions evaporate, and no run reports it.

Add a model-level denies config, shaped like grants ({privilege: [principals]}), that re-applies object-level DENYs after each build, diffed against sys.database_permissions — mirroring how masks re-applies Dynamic Data Masking. Emits DENY for configured-not-present and REVOKE for present-not-configured; a converged, persisted relation issues no DDL.

  • Pure resolve/diff logic in sqlserver_deny.py (unit-tested), wired via @available resolve_denies / deny_changes and a denies config registered with MergeBehavior.Update.
  • apply_denies macros; call sites in table/incremental/snapshot and, new vs masks, view — a view is a valid securable and is recreated every run, where a DENY is lost most often.
  • Absent principal is warned-and-skipped; an unsupported privilege is warned-and-skipped too (never fails the run); grant∩deny overlap warns.

A SQL Server object-level DENY is stored against object_id, so dbt
discards it on every drop-and-recreate (every run for a view). The DENY
is the only way to carve an exception out of a schema-level GRANT, so
losing it silently leaves a fail-open posture: the broad grant persists,
its exceptions evaporate, and no run reports it.

Add a model-level `denies` config, shaped like `grants`
({privilege: [principals]}), that re-applies object-level DENYs after
each build, diffed against sys.database_permissions — mirroring how
`masks` re-applies Dynamic Data Masking. Emits DENY for
configured-not-present and REVOKE for present-not-configured; a
converged, persisted relation issues no DDL.

- Pure resolve/diff logic in sqlserver_deny.py (unit-tested), wired via
  @available resolve_denies / deny_changes and a `denies` config
  registered with MergeBehavior.Update.
- apply_denies macros; call sites in table/incremental/snapshot and, new
  vs masks, view — a view is a valid securable and is recreated every
  run, where a DENY is lost most often.
- Absent principal is warned-and-skipped; an unsupported privilege is
  warned-and-skipped too (never fails the run); grant∩deny overlap warns.
- No-op on non-SQL-Server adapters. Existing grants/masks unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Object-level DENY is lost on every rebuild — no way to express it in dbt

1 participant