feat: /v1/role list, get, create, update and delete. Reassign members on delete. Write-time validation (ValidateGrants, duplicates, typed keys against real content types). Name uniqueness (role_name_taken), role_locked, role_has_members, role_conflict on version mismatch. No-escalation: nobody grants a permission they don't hold, and roles.edit_owner_only guards the Owner role. Activity events for role changes. Typed-key exposure in /auth/me. Cleanup of typed rows when a content type is deleted. | Backend only. Will carry the deferred ListGrants decision.
This commit is contained in:
1 parent
a043f7f41a
commit
094df63a7f
51 files changed
+4822
-115
No files matched your search
@@ -6,9 +6,9 @@ Do not copy any `Role.Allows`, `Role ==` or hand-rolled own/all comparison into
|
||||
|
||||
## Deferred to later slices
|
||||
|
||||
- 3: /v1/role CRUD, no-escalation, writing typed grants, cleanup of typed rows of deleted content types.
|
||||
- 3: /v1/role CRUD, no-escalation, writing typed grants, cleanup of typed rows of deleted content types. Done by roles_permissions_roles_api.
|
||||
- 4: role change, invite with role, ownership transfer, removal of the legacy role column.
|
||||
- 5-7: SPA (View only mark, "Ask an editor to publish.", disabled Delete). The SPA reacts to the status code and the X-Missing-Permission header only. /auth/me exposing typed grants.
|
||||
- 5-7: SPA (View only mark, "Ask an editor to publish.", disabled Delete). The SPA reacts to the status code and the X-Missing-Permission header only. /auth/me exposing typed grants (the API side is done by roles_permissions_roles_api).
|
||||
- Not changed: GET /v1/media/{id}/usage still lists articles and entries for any media.view holder. media.delete own fails closed. Entry relation summaries are not gated by the related type.
|
||||
|
||||
## Design notes for the user
|
||||
@@ -44,7 +44,7 @@ Routes that no key gates after 2b among the content routes: none. Entry routes n
|
||||
|
||||
## Test infrastructure first
|
||||
|
||||
1. `contentScopeRoleRepo`: in-memory role `Repository`, seeded with the three built-ins through the same Go function the migration uses, plus the custom roles and users 21-27 of the feature Background. Supports editing grants and role_id between calls, a switchable failure, and call counters for `Get`, `GetByName` and `ListGrants`. Its `Get` returns the role with `Grants` filled, like the persistence repository.
|
||||
1. `contentScopeRoleRepo`: in-memory role `Repository`, seeded with the three built-ins through the same Go function the migration uses, plus the custom roles and users 21-27 of the feature Background. Supports editing grants and role_id between calls, a switchable failure, and call counters for `Get`, `GetByName` and `List` (ListGrants was removed by roles_permissions_roles_api). Its `Get` returns the role with `Grants` filled, like the persistence repository.
|
||||
2. User fake with a `Get` call counter (existing `mockUserRepo` plus a counter, or a wrapper). Users 3, 5, 9, 12 (unverified Admin), 13 (role_id 99), 20 (hand-placed owner-only), 21-27.
|
||||
3. Article fake and entry fake seeded with the ids, states and created_by values of the Background. Both record writes, so "article unchanged", "entry stored" and "nothing stored" are asserted on the store. A recording `audit.Recorder` to assert "no audit event".
|
||||
4. HTTP harness: `validator.InitValidator()`. Build `NewArticleRouter` and `NewContentTypeRouter` equivalents mounted on a fresh `chi.NewRouter()` mirroring `api/router.go`: `authtoken.Verifier`, `container.apiKeyAuth()` equivalent, the new optional-access middleware on the public article group, `Authenticator`, `RequireCurrentToken`, `RequireVerifiedUser`, the typed permission gate, and the handlers over real use cases wired to the fakes. No live server, no real SQLite, no real SMTP.
|
||||
@@ -68,7 +68,7 @@ Routes that no key gates after 2b among the content routes: none. Entry routes n
|
||||
## B. Use cases (fakes only)
|
||||
|
||||
13. `CheckPermission` (typed variant) table: Owner, Admin, Writer, each custom role, an unresolvable role (`role.ErrMissingPermission` via `errors.Is`, key via `errors.As`), a failing repository (non-sentinel error), and a live grant change between two calls.
|
||||
14. Counters: for one `Require` call, `ListGrants` is 0, `Get` (or `GetByName` for a legacy owner/admin) is 1. Same for `GetMyAccessUseCase.Get`.
|
||||
14. Counters: for one `Require` call, `List` is 0 (was `ListGrants`), `Get` (or `GetByName` for a legacy owner/admin) is 1. Same for `GetMyAccessUseCase.Get`.
|
||||
15. Article use cases (get, list, update, update-is-online, delete, create, SEO check): table of caller grants × article × expected error or result, per the feature Outlines. Assert for refusals: error is `role.MissingPermissionError{Key: "content.update"}` and so on, the store is unchanged, the recorder has no event, and no site/author/image lookup happened (counters on those fakes).
|
||||
16. Entry use cases (list, get, create, update, delete): same, including `created_by` unchanged on update, own-filtered list totals and `?q=` over the own set, and `entry.ErrEntryNotFound` for a non-owned read under own.
|
||||
17. Order: scope check after the load and before validation: PUT with an invalid body for a non-owned entry gives the scope error, not `FieldError`.
|
||||
@@ -86,7 +86,7 @@ Routes that no key gates after 2b among the content routes: none. Entry routes n
|
||||
26. Live role: edit grants and `role_id` between requests with the same cookie and the same API key.
|
||||
27. API key: article created through the key has `created_by` of the creator.
|
||||
28. Order: unverified 403, stale token 401, no cookie 401 before the permission gate on the authed article and entry routes.
|
||||
29. Loads per request: with the counting fakes, a gated session request performs at most 1 user `Get` and exactly 1 role `Get`, 0 `ListGrants`; with an API key at most 2 user `Get`. Run on one article route and one entry route.
|
||||
29. Loads per request: with the counting fakes, a gated session request performs at most 1 user `Get` and exactly 1 role `Get`, 0 `List` (was `ListGrants`); with an API key at most 2 user `Get`. Run on one article route and one entry route.
|
||||
30. Owner-only: user 20 with a hand-placed `system.apply_updates=all` gets 403 on `POST /v1/system/update/check`; `/auth/me` omits it.
|
||||
31. Media rows of the Outline and the "usage not filtered" assertion.
|
||||
32. Regression: run the 2a Outline "Existing owner and admin behaviour does not regress", the authors.edit Outline and the activity/api-key scope Outlines unchanged.
|
||||
@@ -116,5 +116,5 @@ Add a "superseded by roles_permissions_content_scope" note or edit in place:
|
||||
|
||||
- Start `task dev:api` on a copy of a migrated database. As the Writer: publish returns 403, saving a draft works and stays a draft. As the Admin: publish works.
|
||||
- With two accounts on a custom role holding content.update=own, confirm account B gets 403 on A's article and entry, and A can edit them.
|
||||
- Set a typed grant by editing `role_permission` in the database copy (`content.update:article` with scope none) and confirm article edits return 403 while entry edits still work. This is the only way to try typed grants until slice 3.
|
||||
- Set a typed grant by editing `role_permission` in the database copy (`content.update:article` with scope none) and confirm article edits return 403 while entry edits still work. Typed grants can now also be written through PUT /v1/role/{id} (roles_permissions_roles_api).
|
||||
- Confirm no password or secret appears in any scenario. Env vars are referenced by name only (JWT_SECRET).
|
||||
Reference in new issue
Block a user