Skip to content

Darling: close the BYO provision-roles.sql viewer column-ACL drift - #1639

Merged
erikdarlingdata merged 2 commits into
devfrom
feature/provision-roles-acl-drift
Jul 24, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
feature/provision-roles-acl-drift

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

The bug

The fail-closed viewer column ACL (#1262) is authored once in C# — DarlingManagedRoles.ViewerRestrictedConfigTables — and applied two ways:

  • Managed mode GENERATES the REVOKE-then-GRANT SELECT (columns) carve from that list on every service start (BuildViewerColumnAclSql), so it is correct by construction.
  • BYO mode has no service to generate anything. The operator runs Darling/tools/provision-roles.sql by hand, and that file's three GRANT SELECT (...) lists are a hand copy of the same columns.

Nothing pinned the two together, and they drifted:

Table C# NonSecretColumns provision-roles.sql Drift
config_monitored_servers 17 columns 16 missing alert_delivery_mode_override (added to C# by #1551)
config_command 11 columns 11 in sync
config_notification 12 columns 10 missing generic_body_template, generic_proxy (added to C# by #1506)

The mcp role is a deliberate asymmetry, not drift: managed mode applies the identical carve to a third role via BuildViewerColumnAclSql(config, "mcp"), and BYO correctly provisions no mcp role at all (the network MCP endpoint is managed-mode-only). That is now pinned as intentional so nobody "closes" it with a half-provisioned role.

Impact on BYO deployments

The carve is fail-CLOSED by design — viewer's table-wide SELECT is revoked and only the enumerated columns re-granted — so an unlisted column is invisible, not merely unlisted. On a bring-your-own-Postgres store a read-only viewer seat therefore took a bare SQLSTATE 42501 on the two projections written specifically for it:

  • ViewerDataService.MonitoredServersSelectSql — the Manage Servers list, which also drives the sidebar reconcile.
  • ViewerDataService.NotificationSelectNoSecretSql — the Settings prefill, where (per its own comment) one such throw blanks every later section.

Quiet in exactly the wrong direction: the columns exist, the script runs clean, psql as the owner sees everything, and only a read from the least-privilege role ever notices. Managed deployments were never affected.

The fix

  1. The three GRANT SELECT (...) lists now match the C# source of truth exactly. Only the three drifted columns were added — no grant is widened, nothing speculative (no group_id), and the secret sets are untouched. The step-2b comment also now names the source of truth and the guard.
  2. New ungated Darling/Darling.Tests/ProvisionRolesAclDriftTests.cs (5 tests, no database required) closes the actual defect — an unguarded hand mirror. It parses the shipped .sql, copied beside the test binary by the csproj (Link="Fixtures\provision-roles.sql"), so there is no second copy to go stale and no hardcoded repo path, and asserts:
    • set-equality in both directions, per table, against ViewerRestrictedConfigTables — a column on either side that the other lacks fails;
    • the paired REVOKE exists and is ordered after the blanket schema grant and before the column grant (without that ordering the carve is a silent no-op and the secrets stay readable);
    • no secret column appears in any grant, in any role;
    • the script still provisions admin + viewer only, never a half-provisioned mcp role.
  3. The guard is itself guarded. ParsedViewerAcl_Comparison_FailsOnAnInjectedDrift mutates the real file three ways — drop a column, add one, delete a whole carve — and asserts the same comparison reports each, because every way this test could silently stop working (a reformatted GRANT, a renamed file, a regex matching nothing) looks identical to "no drift".

Testing

DARLING_TEST_PG= dotnet test Darling/Darling.Tests/Darling.Tests.csproj -c Debug --nologo — 2958 passed, 0 failed, 147 skipped (gated-live, DARLING_TEST_PG cleared; no database was stood up).

Guard verified by hand, not just by the meta-test: removing alert_delivery_mode_override from the .sql failed 2 tests, restoring it went green, then removing generic_proxy failed the same 2, and restoring it went green again — so both repaired tables are genuinely covered.

🤖 Generated with Claude Code

erikdarlingdata and others added 2 commits July 24, 2026 11:00
…1639)

The fail-closed viewer column ACL (#1262) is authored once in C#
(DarlingManagedRoles.ViewerRestrictedConfigTables) and applied two ways.
Managed mode GENERATES the REVOKE-then-GRANT-columns carve from that list on
every service start, so it is correct by construction. BYO mode has no service
to generate anything: the operator runs Darling/tools/provision-roles.sql by
hand, and its three GRANT SELECT (...) lists are a hand copy of the same
columns. Nothing pinned the two together and they drifted:

  config_monitored_servers  MISSING alert_delivery_mode_override   (#1551)
  config_notification       MISSING generic_body_template          (#1506)
  config_notification       MISSING generic_proxy                  (#1506)
  config_command            in sync

Because the carve is fail-CLOSED, an unlisted column is invisible, so on a BYO
store a read-only viewer seat took a bare SQLSTATE 42501 on the two projections
written specifically FOR it: MonitoredServersSelectSql (the Manage Servers list
+ sidebar reconcile) and NotificationSelectNoSecretSql (the Settings prefill,
where one throw blanks every later section). Managed deployments were never
affected. Only the three drifted columns are added - no grant is widened.

The real defect is the unguarded hand mirror, so add an UNGATED
ProvisionRolesAclDriftTests: it parses the SHIPPED .sql (copied beside the test
binary by the csproj, so there is no stale second copy and no hardcoded repo
path), extracts every GRANT SELECT (columns) ON schema.table TO role, and
asserts set-equality against the C# list in both directions, plus the paired
REVOKE and its ordering, that no secret column is ever granted, and that the
script still provisions admin+viewer only (BYO deliberately has no mcp role).
A meta-test mutates the real file three ways and asserts the same comparison
reports each, so a parser that silently matched nothing cannot pass as
"no drift".

Darling.Tests: 2958 passed, 0 failed, 147 gated-live skipped
(DARLING_TEST_PG cleared). Verified by hand that removing either restored
column reintroduces a failing test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata merged commit 077e940 into dev Jul 24, 2026
3 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/provision-roles-acl-drift branch July 24, 2026 15:19
@claude claude Bot mentioned this pull request Jul 31, 2026
erikdarlingdata added a commit that referenced this pull request Aug 31, 2026
….sql (#2138)

The #1639 drift ratchet working as designed: the operator-facing
provisioning script's GRANT SELECT column carve is a hand mirror of
DarlingManagedRoles.ViewerRestrictedConfigTables, and
ProvisionRolesAclDriftTests parses the script and asserts set-equality -
so classifying the column in C# alone failed both CI jobs until the
script named it too. Non-secret, exactly like is_enabled beside it.
Comment kept parenthesis-free inside the GRANT list on purpose: the
parser strips -- comments before matching, but the paren-scan that finds
the list's end does not need the extra test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLaaxFenZFZs1SntKQDEg
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.

1 participant