Enforce per-resource dashboard grants (RBAC matrix's own/granted qualifier)
api/dashboards' handler previously enforced only tenant-baseline role
(RoleEditor+), so any Editor could edit/delete any dashboard in their
tenant -- the matrix's "(own/granted)" qualifier was explicitly named
as unbuilt in this handler's own doc comment. This closes that gap.
New core interface api/dashboards.PermissionStore (nil-safe, same "not
wired == no-op" shape as authz.Authorizer) resolves a per-resource
dashboard_permissions grant. canEditDashboard now requires the
identity be Admin/Owner, the dashboard's creator, or hold a grant of at
least Editor; canManageGrants is deliberately stricter (creator or
Admin/Owner only, never grant-derived access) so a user who can edit a
dashboard only because of a grant can't extend or re-grant that access
to themselves or others. Wired handlers: PUT/DELETE
/dashboards/{id}/permissions/{userId}, GET .../permissions.
Two real bugs found and fixed while wiring this up, before any of it
touched a live database:
- handleCreate/handleImport never stamped created_by from the
authenticated identity, so every dashboard was owned by "anonymous"
regardless of who made it -- the ownership check would have been
meaningless. Also fixed: ImportDashboard trusted the exported JSON's
created_by verbatim, so re-importing someone else's export would
leave the actual importer unable to edit their own copy.
- metadata/migrations/0024_create_dashboard_permissions.sql's CHECK
constraint diverged from /docs/phase-4-rbac-design.md's schema
(allowed role='admin', nullable granted_by). Reconciled via
0033_restrict_dashboard_permissions_role.sql: Admin/Owner already
have tenant-wide access so a resource-level "admin" grant is
meaningless, and every real grant now always has an attributable
granter.
enterprise/internal/rbacstore gets the storage side: raw CRUD
(dashboard_permissions.go) plus DashboardPermissions
(dashboards_adapter.go), an adapter implementing
api/dashboards.PermissionStore -- same pattern as audit.QueryAPILogger
over queryapi.AuditLogger. Wired into enterprise/cmd/enterprise-api
only; plain api/cmd/api passes nil (ownership/Admin checks still work
via the nil-permissions fallback, just without the "granted" bonus).
Verified: the full own/granted/admin/creator matrix, including the
granted-editor-cannot-manage-grants regression, passes against a fake
PermissionStore (api/dashboards/handler_test.go, all existing tests
also still pass unmodified in behavior). Real integration tests exist
in enterprise/internal/rbacstore/rbacstore_test.go (skip-gated on
RBACSTORE_TEST_POSTGRES_ADDR, same convention as every other
Postgres-backed piece this phase) but have not run against a live
database in this environment -- disclosed in threat-model.md,
phase-4-runbook.md, and enterprise/README.md alongside every other
piece carrying the same gap. Also fixed a stale path in
phase-4-runbook.md's dashboards-tenant-scoping section
(./internal/dashboards/... -> ./dashboards/..., stale since that
package moved out of api/internal/ earlier in this phase).
This commit is contained in:
+39
-3
@@ -232,15 +232,43 @@ tenant. Verify the real SQL, not just the fake-store unit tests:
|
||||
docker run --rm --network sentry_default -v $(pwd)/api:/src -w /src \
|
||||
-e DASHBOARDS_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
|
||||
-e DASHBOARDS_TEST_POSTGRES_PASSWORD=sentry-dev-only \
|
||||
golang:1.25-alpine go test ./internal/dashboards/... -run Integration -v
|
||||
golang:1.25-alpine go test ./dashboards/... -run Integration -v
|
||||
```
|
||||
|
||||
(Path fixed from an earlier `./internal/dashboards/...` -- stale since
|
||||
`dashboards` moved from `api/internal/dashboards` to `api/dashboards`
|
||||
earlier in Phase 4, once `enterprise/cmd/enterprise-api` needed to
|
||||
import it: Go's compiler-enforced `internal/` visibility rule meant a
|
||||
separate module like `enterprise/` could never import anything under
|
||||
`api/internal/...`, regardless of the AGPL/commercial licensing
|
||||
boundary, which only forbids the reverse direction.)
|
||||
|
||||
Expect all `TestIntegration*` tests to pass, including
|
||||
`TestIntegrationDashboardTenantForeignKeyRejectsUnknownTenant` (the
|
||||
`tenant_id` foreign key added in
|
||||
`metadata/migrations/0027_add_dashboards_tenant_fk.sql` rejecting a
|
||||
dashboard for a tenant that doesn't exist).
|
||||
|
||||
## 5a. Per-resource dashboard grants (new -- built and unit-tested, not yet run live)
|
||||
|
||||
`enterprise/internal/rbacstore`'s `dashboard_permissions` CRUD and its
|
||||
`DashboardPermissions` adapter (implementing `api/dashboards.
|
||||
PermissionStore`) have real integration tests, same skip-gated shape as
|
||||
§6 below:
|
||||
|
||||
```sh
|
||||
docker run --rm --network sentry_default -v $(pwd)/enterprise:/src -w /src \
|
||||
-e RBACSTORE_TEST_POSTGRES_ADDR=metadata-postgres:5432 \
|
||||
-e RBACSTORE_TEST_POSTGRES_PASSWORD=sentry-dev-only \
|
||||
golang:1.25-alpine go test ./internal/rbacstore/... -run DashboardPermission -v
|
||||
```
|
||||
|
||||
Expect all `TestDashboardPermission*`/`TestSetDashboardPermission*`/
|
||||
`TestGetDashboardPermission*`/`TestRevokeDashboardPermission*`/
|
||||
`TestListDashboardPermissions` tests to pass. This only takes effect
|
||||
when `enterprise-api` (not plain `api`) is serving traffic -- see
|
||||
§8/§10.
|
||||
|
||||
## 6. `enterprise/internal/rbacstore` and `internal/audit` (already verified — reconfirm here)
|
||||
|
||||
```sh
|
||||
@@ -422,8 +450,16 @@ Full accounting: `/docs/security/threat-model.md`. Headline items:
|
||||
identity either (refused outright) for either protocol.
|
||||
- No admin UI to create a `tenant_memberships` row -- §3a's manual SQL
|
||||
bootstrap is the only way to grant a logged-in identity access today.
|
||||
- **No per-resource dashboard grants** (`dashboard_permissions` has a
|
||||
schema, no handler reads it).
|
||||
- **Per-resource dashboard grants are now enforced** (`api/dashboards`'
|
||||
handler reads `dashboard_permissions` via
|
||||
`enterprise/internal/rbacstore.DashboardPermissions`, only when
|
||||
`enterprise-api` -- not plain `api` -- serves traffic), but there's
|
||||
still no UI or `sentryctl` command to create a grant -- `PUT
|
||||
/dashboards/{id}/permissions/{userId}` has to be called directly.
|
||||
Verified against a fake store; the real-Postgres integration tests
|
||||
(`enterprise/internal/rbacstore/rbacstore_test.go`) haven't run
|
||||
against a live database in this environment, same gap as the rest of
|
||||
this phase's Postgres-backed pieces.
|
||||
- Three of the four adversarial ClickHouse/Tantivy probes named in
|
||||
`/docs/phase-4-isolation-design.md`'s verification plan are closed
|
||||
(§8, §9); the last (mid-provisioning-race handling) is still stubbed
|
||||
|
||||
Reference in New Issue
Block a user