notifications (3/5): notification:read and notification:manage permissions#454
Conversation
🔐 Codex Security Review
Review SummaryOverall Risk: MEDIUM Findings[MEDIUM] Notification permissions are hidden and can be stripped by the role editor
NotesNo cryptostealing/pool hijack, command execution, network discovery, plugin, protobuf wire-format, or infrastructure changes were present in the scoped diff. The notification Connect services are not mounted in Generated by Codex Security Review | |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 507a4e593d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
507a4e5 to
3bd492f
Compare
|
Codex finding [MEDIUM] services in RBAC contract but not mounted is a split artifact: this PR adds the permission map + |
d9857a9 to
c49d919
Compare
3bd492f to
66e55db
Compare
|
Re the re-review:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 66e55db8f5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…to the mount PR Two review fixes: - Renumber the permission-seed migration 000087 -> 000088 so it sits above the keyset-index migration (now 000087) that merges first in the stack, keeping numeric order aligned with merge order (see #453). - Move the notification entries out of registeredServices (the RBAC contract fixture): TestRPCContract_RegisteredServicesMatchMainMux compares it against the handlers mounted in main.go, and the mounts land in the next stack PR, so listing them here failed the test standalone. The ProcedurePermissions policy map (and its catalog keys) stay here; only the mount-coupled registration moves to the server PR. Addresses Codex connector findings (P1/P2) on #454. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
66e55db to
d2685fc
Compare
|
codex-connector triage (both fixed):
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d2685fc9dd
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
6076695 to
03bb87c
Compare
…to the mount PR Two review fixes: - Renumber the permission-seed migration 000087 -> 000088 so it sits above the keyset-index migration (now 000087) that merges first in the stack, keeping numeric order aligned with merge order (see #453). - Move the notification entries out of registeredServices (the RBAC contract fixture): TestRPCContract_RegisteredServicesMatchMainMux compares it against the handlers mounted in main.go, and the mounts land in the next stack PR, so listing them here failed the test standalone. The ProcedurePermissions policy map (and its catalog keys) stay here; only the mount-coupled registration moves to the server PR. Addresses Codex connector findings (P1/P2) on #454. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d2685fc to
23cd610
Compare
03bb87c to
b89405f
Compare
…to the mount PR Two review fixes: - Renumber the permission-seed migration 000087 -> 000088 so it sits above the keyset-index migration (now 000087) that merges first in the stack, keeping numeric order aligned with merge order (see #453). - Move the notification entries out of registeredServices (the RBAC contract fixture): TestRPCContract_RegisteredServicesMatchMainMux compares it against the handlers mounted in main.go, and the mounts land in the next stack PR, so listing them here failed the test standalone. The ProcedurePermissions policy map (and its catalog keys) stay here; only the mount-coupled registration moves to the server PR. Addresses Codex connector findings (P1/P2) on #454. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
23cd610 to
8cd5d9a
Compare
b89405f to
c8602d1
Compare
8cd5d9a to
341655e
Compare
Add the notification_history keyset Lister/Store, the sqlc query, and the CONCURRENTLY-built (organization_id, id DESC) keyset index (migration 000087). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c8602d1 to
6f51c39
Compare
Add the notification:read and notification:manage permission keys to the authz catalog, gate the notification RPCs in the permission middleware, and seed the permissions onto builtin ADMIN roles (migration 000088). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
341655e to
9548ca8
Compare
ankitgoswami
left a comment
There was a problem hiding this comment.
approving based on the PR description
Summary
This PR makes the Notifications surface (channels, alert rules, maintenance windows, delivery history) permission-gated. It introduces a new
notificationauthorization resource with two permissions —notification:read(view) andnotification:manage(mutate) — wires every Notifications RPC to the permission it requires, and seeds the permission rows while granting them to existing ADMIN roles. No Notifications handlers are mounted yet (that lands in the next stack PR); this PR establishes the authorization policy those handlers will enforce.It is PR 3 of a 5-PR stack: #452 (proto + generated) → #453 (history-listing DB) → #454 (this PR, authz) → #455 (service + handler mounts + wiring) → #456 (client UI).
How it works
There are two independent paths: the request-time enforcement policy (in code) and the permission data in the database (rows + role grants).
Request-time enforcement. Every authenticated request flows through an auth interceptor that resolves the caller's
EffectivePermissions(the union of permissions granted by the caller's roles) and stashes it on the request context. A handler gates itself by callingRequirePermission(key, scope), which fails closed: missing context value →Internal; no session →Unauthenticated; caller lacks the key →PermissionDenied. TheProcedurePermissionsmap is the central policy table that says which catalog key each gated procedure needs. This PR adds the Notifications entries to that map: all list/read procedures →notification:read; every mutation →notification:manage.TestChannelis classified as a mutation (:manage) because it triggers a real outbound delivery, not just a read.Permission data reaching the database. Two mechanisms populate the
permissionandrole_permissiontables:authz.Reconcile, runs once per server start after migrations): upserts every catalog permission row (refreshing description text), and seeds built-in roles. Crucially, the reconciler seeds ADMIN/FIELD_TECH role grants only when the role row is first created — it never re-asserts the seed set onto an already-existing role, so operators' revocations are preserved. That means new catalog keys do NOT auto-propagate onto already-seeded ADMIN roles.000088_seed_notification_permissions): does the one-time job the reconciler deliberately won't do — backfill the two new permissions onto existing ADMIN roles. It also inserts the permission rows itself (ON CONFLICTmakes this redundant-but-safe alongside the reconciler), scopes the role grant tobuiltin_key = 'ADMIN'so custom roles are untouched, and usesON CONFLICT DO NOTHINGso it is replay-safe.The migration is numbered
000088so it sorts above the keyset-index migration000087from #453 (which merges first), keeping golang-migrate's numeric order aligned with merge order.Diagrams
Request authorization flow:
flowchart TD client["Client RPC call"] --> interceptor["Auth interceptor"] interceptor -->|"resolves caller roles -> EffectivePermissions"| ctx["Request context (stashed permissions)"] ctx --> handler["Notifications handler"] handler -->|"RequirePermission(key, scope)"| gate{"Caller has key?"} policy["ProcedurePermissions map (read -> notification:read, mutations -> notification:manage)"] -.->|"supplies required key"| gate gate -->|"yes"| allow["Handler executes (proxies to Grafana)"] gate -->|"no"| deny["PermissionDenied"] gate -->|"no session"| unauth["Unauthenticated"] gate -->|"missing ctx value"| internal["Internal (fail-closed)"]Two ways the permissions reach the database:
flowchart LR catalog["catalog.go (in-code permission definitions)"] subgraph boot["Every server boot"] reconcile["authz.Reconcile"] end subgraph migrate["One-time migration 000088"] seed["seed_notification_permissions.up.sql"] end permTable[("permission rows")] rpTable[("role_permission grants")] catalog --> reconcile reconcile -->|"upsert all catalog rows"| permTable reconcile -->|"seed grants ONLY on first role creation"| rpTable seed -->|"insert permission rows (ON CONFLICT safe)"| permTable seed -->|"backfill grants onto EXISTING ADMIN roles"| rpTableLifecycle of a
notification:*permission on an existing-DB upgrade:stateDiagram-v2 [*] --> CatalogDefined: "added to catalog.go" CatalogDefined --> RowUpserted: "reconciler upserts permission row on boot" RowUpserted --> GrantedToAdmin: "migration 000088 backfills existing ADMIN roles" CatalogDefined --> GrantedToAdmin: "migration also inserts rows if reconciler has not run" GrantedToAdmin --> [*]: "ADMIN callers now pass RequirePermission" note right of RowUpserted Reconciler will NOT grant new keys onto already-existing roles, hence the migration does the backfill. end noteAreas of the code involved
server/internal/domain/authz/catalog.gonotificationresource constant;notification:read/notification:managepermission constants; twoCatalogEntryrows.server/internal/handlers/middleware/rpc_permissions.gonotificationsv1connect; adds 13ProcedurePermissionsentries (Channel/Rule/MaintenanceWindow/History services) mapping reads →:read, mutations →:manage.:manage, each read is:read, and thatTestChannelis:manage(outbound delivery).server/migrations/000088_seed_notification_permissions.up.sqlON CONFLICTupsert) and backfills them onto existingADMINroles (ON CONFLICT DO NOTHING).builtin_key = 'ADMIN',deleted_at IS NULL, and replay-safety.server/migrations/000088_seed_notification_permissions.down.sqlserver/internal/domain/authz/catalog_test.gonotificationresource to the completeness / grouping / ordering contract fixtures.No DB table for permissions-on-channels exists or is added: channels, rules, and maintenance windows live in Grafana, not in this database.
Key technical decisions & trade-offs
TestChannelgated on:manage, not:read— chosen because it triggers an outbound delivery (a side-effecting action), over treating "test" as a read-only diagnostic.ON CONFLICT) over relying solely on the reconciler, so the migration is self-contained and the grant step has its FK target present regardless of boot ordering.000088(above notifications (2/5): notification_history keyset listing #453's000087) — chosen to keep golang-migrate numeric order aligned with stack merge order, over a lower number that an already-upgraded DB would skip.builtin_key = 'ADMIN'withDO NOTHING— chosen over a broad grant so custom/operator-tuned roles are untouched and re-runs are idempotent.registeredServicesRPC-contract fixture — those entries land in notifications (4/5): Grafana-backed service, handlers, and server wiring #455 alongside the realmain.gohandler mounts, because the contract test comparesregisteredServicesagainst the live mux; the procedure→permission policy stays here.Testing & validation
catalog_test.go): assert the new permissions and resource appear in the completeness set, are grouped undernotification, and respect declaration order.rpc_permissions_test.go): every key referenced byProcedurePermissionsmust exist in the catalog, and every registered procedure must be classified in exactly one bucket.backfill_test.go) exercise the additive-on-first-creation policy that motivates the migration.Explicitly NOT covered here:
main.goyet (that is notifications (4/5): Grafana-backed service, handlers, and server wiring #455), so there is no integration test exercising a real gated Notifications request in this PR.downmigration is dev-only and assumes no custom-role hand-grants.🤖 Generated with Claude Code