You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Custom-alert resolve-on-delete silently skips the recovery row on mcp/viewer paths (config_alert_log grant gap in #3305) #3334
CustomAlertEvaluator.ResolveAndDeleteRuleAsync (#3305) force-resolves an open incident on rule delete by writing a "Custom: Resolved" recovery row via INSERT INTO config.config_alert_log. But DarlingManagedRoles grants INSERT/UPDATE/DELETE ON ALL TABLES IN SCHEMA config to admin only; viewer and mcp get SELECT-everywhere plus narrow write grants on specific tables (custom_views, custom_alert_rules, config_mute_rules, config_monitored_servers, config_alert_settings UPDATE, analysis_* INSERT) — NOT config_alert_log.
So the recovery INSERT fails permission-denied on both least-privilege delete paths:
The write is failure-isolated (caught + logged), so the delete still succeeds (rule + state cascade away, alert stops firing) — but the resolution row #3305 intends is silently skipped. A deleted firing rule leaves a dangling "open" incident in alert history / the open-incidents view forever, which is exactly what #3305 set out to prevent.
CustomAlertTeardownLiveTests connect as the owner/superuser (which has INSERT on all config tables), so the least-privilege write path was never exercised. Any test that connects as viewer/mcp would have caught it.
Scope
Affects the deployed #3305 (MCP path) and #3333 (web path). Low impact on DARLING01 today (no custom rules exist there yet), but wrong on any real fleet. The ReconcileStateAsync disable/out-of-scope sweep runs as the service (owner) so it is NOT affected — only the caller-initiated DELETE paths.
Fix options (security-surface decision — flagging rather than unilaterally granting)
A.GRANT INSERT ON config.config_alert_log TO viewer, mcp — simplest, but broad: it lets these roles inject arbitrary alert-history rows (fabricated fires/resolutions), an integrity expansion of the deliberately-restricted role model.
B (recommended). A SECURITY DEFINER function owned by the store owner, e.g. config.resolve_custom_alert_incident(rule_id), that writes only the scoped resolution row(s) with owner privilege; GRANT EXECUTE to viewer + mcp; ResolveAndDeleteRuleAsync calls it. Confines the privileged write to exactly its purpose (resolving a rule's own incident as it is deleted) without a blanket INSERT grant.
C. Don't write the resolve on delete; make the open-incidents consumers filter out incidents whose rule no longer exists. Avoids the write but leaves history literally incomplete and relies on every consumer.
Also
Add a resolve-on-delete test that connects as viewer/mcp (not owner) so this class of grant gap can't hide again.
Part of #3285; regression in #3305 (surfaced reviewing #3333).
Fixed in #3347 (merged to dev). The resolve-on-delete recovery row is now written through a SECURITY DEFINER function config.record_custom_alert_resolution (owner-privileged, EXECUTE-granted to viewer/mcp, shape-locked to a no-channel resolution row so no fire can be forged), so the teardown resolution lands even when the delete runs on the least-privilege mcp/viewer pool. A gated live proof test connects as a disposable low-privilege role, asserts the direct config_alert_log INSERT is denied (42501), then confirms both teardown paths (caller delete + sweep reconcile) write the row via the function. Will auto-close-verify again at the next dev->main promotion.
Activity
Merge pull request #3347 from erikdarlingdata/feature/3334-custom-ale…
erikdarlingdata commentedon Sep 12, 2026
Fixed in #3347 (merged to
dev). The resolve-on-delete recovery row is now written through aSECURITY DEFINERfunctionconfig.record_custom_alert_resolution(owner-privileged, EXECUTE-granted to viewer/mcp, shape-locked to a no-channel resolution row so no fire can be forged), so the teardown resolution lands even when the delete runs on the least-privilege mcp/viewer pool. A gated live proof test connects as a disposable low-privilege role, asserts the directconfig_alert_logINSERT is denied (42501), then confirms both teardown paths (caller delete + sweep reconcile) write the row via the function. Will auto-close-verify again at the next dev->main promotion.Resolve custom-alert incidents on delete via a SECURITY DEFINER funct…