feat: more role backend and admin/writer roles additions and adjustments
This commit is contained in:
1 parent
8c6c6456da
commit
de142f6c5f
131 files changed
+2663
-435
No files matched your search
@@ -1,4 +1,5 @@
|
||||
Feature: The back-office keeps an append-only activity log that owners can read and retention can prune
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As the owner of a RenewCMS site
|
||||
I want every important action recorded with who, what, when and from where
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: activity-log-backend
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Go only, no SPA. Package `test` (black-box), new files `test/activity_log_domain_test.go`, `test/activity_log_persistence_test.go`, `test/activity_log_http_test.go` and `test/activity_cleanup_test.go`. Fake and helper type names must be unique across the package. No live server, no real SMTP, no real SQLite file, no real sleeping, no `time.Ticker`. No env var is added. Never put a secret value in a test name or log assertion helper beyond the fixed test strings.
|
||||
|
||||
## Setup shared by every case
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Activity screen, filter cards as toggles and the Export menu
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As an owner or admin
|
||||
I want the Failed actions and Security events cards to toggle their filter, and Export to offer the current view or all activity as CSV
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA: Activity filter cards as toggles and the Export menu
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
## Implementation note for coder (informational)
|
||||
|
||||
SPA slice with one small backend change: accept `range=all` in the list, summary and export range validation (use case range handling and the handler `validate` tag). An omitted range stays 7d. The range table in features/activity-log-backend.feature is updated accordingly.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Recent activity card on Home and Retention card on Settings
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As a RenewCMS user
|
||||
I want Home to show my last five activity events and the owner to choose how long activity is kept on Settings
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA: Recent activity card on Home and Retention card on Settings
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
## Implementation note for coder (informational, not a QA assertion)
|
||||
|
||||
- Backend: add NextCleanupAt (nullable time) to the audit settings entity with a domain method computing the next 03:00 (audit.CleanupHour) strictly after a given time, in that time's location; nil when RetentionDays is audit.ForeverRetentionDays. GetActivityRetentionUseCase and UpdateActivityRetentionUseCase get an injected `now func() time.Time` (wired in api/dependecyInjection.go) and populate it. settingsResponse in api/handlers/settings gains `activity_next_cleanup_at` (`*time.Time`, RFC 3339, null when nil). No logic in the handler, none in the SPA.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Developers authenticate against /v1 with named API keys instead of the jwt cookie
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As a developer integrating with RenewCMS
|
||||
I want to create named API keys and send them as a Bearer token
|
||||
@@ -115,7 +116,7 @@ Feature: Developers authenticate against /v1 with named API keys instead of the
|
||||
When the client sends POST /v1/api-key with body {"name": "sneaky", "user_id": 1, "role": "owner"}
|
||||
Then the response status is 201
|
||||
And the stored key belongs to user 2
|
||||
And using that key on POST /v1/user/invite returns 403 with apperr.OwnerRequiredErrorMessage
|
||||
And using that key on POST /v1/user/invite returns 403 with apperr.MissingPermissionErrorMessage
|
||||
|
||||
Scenario Outline: Key management needs a verified real session
|
||||
Given <caller>
|
||||
@@ -186,7 +187,7 @@ Feature: Developers authenticate against /v1 with named API keys instead of the
|
||||
Given an active key created by admin 2
|
||||
When the client sends POST /v1/user/invite with that key and body {"email": "new@example.com"}
|
||||
Then the response status is 403
|
||||
And the response body is apperr.OwnerRequiredErrorMessage
|
||||
And the response body is apperr.MissingPermissionErrorMessage
|
||||
And no invite exists
|
||||
|
||||
Scenario Outline: A key that can't authenticate gets the same 401 as a bad jwt
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: API keys
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Package: `test` (black-box). No live server, no real SQLite file, no real SMTP.
|
||||
|
||||
## Setup shared by every case
|
||||
@@ -38,7 +40,7 @@ Package: `test` (black-box). No live server, no real SQLite file, no real SMTP.
|
||||
"yesterday" -> `400` `BodyErrorMessage`; past `expires_at` -> `400` `apperr.APIKeyExpiryErrorMessage`.
|
||||
Each: repository still empty.
|
||||
12. Body `{"name":"sneaky","user_id":1,"role":"owner"}` with admin 2 -> `201`; stored `UserID` 2; that
|
||||
key on `POST /v1/user/invite` -> `403` `apperr.OwnerRequiredErrorMessage`, no invite.
|
||||
key on `POST /v1/user/invite` -> `403` `apperr.MissingPermissionErrorMessage`, no invite.
|
||||
13. Access table: no credentials -> `401`; user 4 cookie -> `403` `apperr.VerificationRequiredErrorMessage`;
|
||||
stale `token_version` -> `401`. Each: repository empty.
|
||||
|
||||
@@ -59,7 +61,7 @@ Package: `test` (black-box). No live server, no real SQLite file, no real SMTP.
|
||||
`200`. No credentials -> list has only 6, `GET /5` is `404`. `Bearer rcms_unknown` on `GET /v1/article`
|
||||
-> `401` `apperr.UnauthorizedErrorMessage`.
|
||||
18. Owner key `POST /v1/user/invite` -> `201`, `invited_by` 1. Admin key -> `403`
|
||||
`apperr.OwnerRequiredErrorMessage`, no invite and no mail.
|
||||
`apperr.MissingPermissionErrorMessage`, no invite and no mail.
|
||||
19. Table of 401 cases on `GET /v1/settings`, all with body `apperr.UnauthorizedErrorMessage` exactly (the
|
||||
stored-hash case has no `rcms_` prefix, so it takes the jwt path: assert status `401` only):
|
||||
unknown well-formed key; expired (clock one second past `expires_at`); revoked; creator deleted from
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Authors backend, public bylines that articles credit in order
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As an owner or admin of a RenewCMS installation
|
||||
I want to manage authors and credit them on articles in a chosen order
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: Authors backend
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Spec: `features/authors_backend.feature`. Go tests only, in `test/` (package `test`), run with `task test`. No live server, no real SMTP, no real SQLite file outside the temp-dir repository-contract harness. Call `validator.InitValidator()` before any handler call. Every error assertion compares the plain-text body to the exact code token or constant named in the spec.
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Container-aware update guidance
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As the owner of a RenewCMS site running in Docker
|
||||
I want Settings to say "update by pulling a new image" instead of "Not prepared"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA: Container update guidance
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
## Automated (Go, test/, httptest)
|
||||
1. Fakes: in-memory update status store, fake FeedClient, fake HelperDetector, fake JobStore and restart writer, validator.InitValidator(). Mount NewSystemRouter on a fresh chi.NewRouter() with jwtauth.Verifier + Authenticator and a test jwtauth.New("HS256", ...).
|
||||
2. Parse RENEWCMS_CONTAINER through config.NewSettingsFromEnv with t.Setenv over the table in the feature (true, TRUE, false, empty, yes).
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Owners and admins define custom content types, and users manage entries of those types through a generic /v1 API
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As a site owner or admin
|
||||
I want to define content types made of ordered scalar fields, and then create entries of those types
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: custom content types (backend core)
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Package: `test` (black-box). No live server, no real SQLite file, no real SMTP.
|
||||
|
||||
## Setup shared by every case
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Content types carry an optional icon, built-in types are readable, and verified users can resolve user display names
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As an owner, admin or verified user working in the SPA
|
||||
I want an icon per content type, a read-only view of the built-in types and the display names of entry authors
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: custom content types support (icon, built-in read model, user lookup)
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Package: `test` (black-box). No live server, no real SQLite file, no real SMTP.
|
||||
|
||||
## Setup shared by every case
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Image variants are generated on first request, cached, and managed by admins
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As an admin or owner of a RenewCMS site
|
||||
I want to define named variants of the original images and have them generated and cached on demand
|
||||
@@ -48,12 +49,12 @@ Feature: Image variants are generated on first request, cached, and managed by a
|
||||
| GET | /v1/image-variants | no cookie | 401 | apperr.UnauthorizedErrorMessage |
|
||||
| GET | /v1/image-variants | a stale token_version cookie | 401 | apperr.UnauthorizedErrorMessage |
|
||||
| GET | /v1/image-variants | the cookie of an unverified user| 403 | apperr.VerificationRequiredErrorMessage |
|
||||
| GET | /v1/image-variants | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| POST | /v1/image-variants | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| PUT | /v1/image-variants/hero | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| PATCH | /v1/image-variants/hero | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| DELETE | /v1/image-variants/hero | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| POST | /v1/image-variants/hero/purge | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| GET | /v1/image-variants | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| POST | /v1/image-variants | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| PUT | /v1/image-variants/hero | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| PATCH | /v1/image-variants/hero | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| DELETE | /v1/image-variants/hero | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| POST | /v1/image-variants/hero/purge | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| GET | /v1/image-variants | a valid API key | 401 | apperr.UnauthorizedErrorMessage |
|
||||
|
||||
Scenario Outline: Owners and admins are both allowed
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: image-variants
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Go only, no SPA. Package `test` (black-box). New files: `test/image_variants_admin_http_test.go`, `test/image_variants_serving_http_test.go`, `test/image_variants_geometry_test.go`, `test/image_variants_audit_test.go`, `test/image_variants_config_test.go`. Fake type names must be unique across the package. No live server, no real SMTP, no real SQLite file, no `time.Sleep`.
|
||||
|
||||
## Setup shared by every case
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Image variants cache panel: cache statistics, purge by scope, request tracking and the Cache screen
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As an owner or admin of a RenewCMS site
|
||||
I want to see what the variant cache holds and to purge all of it, the unused part, one variant or one image
|
||||
@@ -42,8 +43,8 @@ Feature: Image variants cache panel: cache statistics, purge by scope, request t
|
||||
| GET | /v1/image-variants/cache | no cookie | 401 | apperr.UnauthorizedErrorMessage |
|
||||
| GET | /v1/image-variants/cache | a stale token_version cookie | 401 | apperr.UnauthorizedErrorMessage |
|
||||
| GET | /v1/image-variants/cache | the cookie of an unverified user | 403 | apperr.VerificationRequiredErrorMessage |
|
||||
| GET | /v1/image-variants/cache | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| POST | /v1/image-variants/cache/purge | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| GET | /v1/image-variants/cache | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| POST | /v1/image-variants/cache/purge | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| GET | /v1/image-variants/cache | a valid API key | 401 | apperr.UnauthorizedErrorMessage |
|
||||
| POST | /v1/image-variants/cache/purge | a valid API key | 401 | apperr.UnauthorizedErrorMessage |
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: image_variants_cache
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Two parts: Go for the API and tracking, Vitest for the Cache screen. No live server, no real SQLite file, no real SMTP, no `time.Sleep` (use an injected clock and manual flushes).
|
||||
|
||||
## Go (package `test`, black-box)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Image variants per image: library fields, per-image routes and generate on upload
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As an owner or admin of a RenewCMS site
|
||||
I want to see, generate and purge the variants of one image, and to have variants generated when I upload
|
||||
@@ -36,10 +37,10 @@ Feature: Image variants per image: library fields, per-image routes and generate
|
||||
| GET | /v1/media/1/variants | no cookie | 401 | apperr.UnauthorizedErrorMessage |
|
||||
| GET | /v1/media/1/variants | a stale token_version cookie | 401 | apperr.UnauthorizedErrorMessage |
|
||||
| GET | /v1/media/1/variants | the cookie of an unverified user | 403 | apperr.VerificationRequiredErrorMessage |
|
||||
| GET | /v1/media/1/variants | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| POST | /v1/media/1/variants | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| POST | /v1/media/1/variants/hero | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| DELETE | /v1/media/1/variants/hero | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| GET | /v1/media/1/variants | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| POST | /v1/media/1/variants | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| POST | /v1/media/1/variants/hero | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| DELETE | /v1/media/1/variants/hero | the cookie of a plain user | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| GET | /v1/media/1/variants | a valid API key | 401 | apperr.UnauthorizedErrorMessage |
|
||||
| POST | /v1/media/1/variants | a valid API key | 401 | apperr.UnauthorizedErrorMessage |
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: image_variants_per_image_api
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Go only, no SPA. Package `test` (black-box). New files: `test/image_variants_per_image_http_test.go`, `test/image_variants_generate_on_upload_test.go`, `test/media_variants_list_http_test.go`, `test/image_variants_per_image_audit_test.go`. Extend `test/image_variants_support_test.go`; fake type names must stay unique in the package. No live server, no real SQLite file, no real SMTP, no `time.Sleep`.
|
||||
|
||||
## Setup shared by every case
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Integration screen (API reference)
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As a developer building a frontend or an API-key client for a RenewCMS site
|
||||
I want /integration to be a complete, searchable API reference
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA: Integration screen (API reference)
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
## Automated (Vitest + Vue Test Utils, `task test:web`)
|
||||
|
||||
Mount IntegrationPage inside DashboardShell with a resolved verified session and a test router at
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Integration page documents every route an API key can call
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As a developer building on RenewCMS
|
||||
I want the API reference to cover content types, entries, built-in types, sites, authors and media purge
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: Integration page, every API-key route documented
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
No Go test: SPA only, no /v1 call, no API change. Samples and counts follow the real API; where the board differs (purge response, built-in type samples) the API wins.
|
||||
|
||||
## Automated SPA (Vitest + Vue Test Utils, `task test:web`)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: A verified user can invite another person by email, and the users list shows users and open invites
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As a member of the shared RenewCMS workspace
|
||||
I want to invite someone by email and see who has an account or a pending invite
|
||||
@@ -11,7 +12,7 @@ Feature: A verified user can invite another person by email, and the users list
|
||||
And POST /v1/user/invite and GET /v1/user/ are mounted in NewUserRouter, inside the group that
|
||||
requires a valid jwt cookie, a current token_version and a verified user
|
||||
And POST /v1/user/invite additionally requires the owner role (403
|
||||
apperr.OwnerRequiredErrorMessage for an admin); GET /v1/user/ is open to every verified user
|
||||
apperr.MissingPermissionErrorMessage for an admin); GET /v1/user/ is open to every verified user
|
||||
And wherever a scenario has verified user id 7 post an invite, user 7 is the owner
|
||||
And an invite has an id, an email stored in lower case, a hashed code, an expires_at equal to
|
||||
its creation time plus 3 days, an invited_by user id, a nullable accepted_at and a created_at
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: invitations
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Package: `test` (black-box). New fakes must have unique type names across the package:
|
||||
`mockInviteRepo` (implements `invite.Repository` with an in-memory slice and a `failSwitch`),
|
||||
plus a `fixedClock` only if the use case takes one. Reuse `mockUserRepo` and `stubHasher`
|
||||
@@ -50,7 +52,7 @@ plus a `fixedClock` only if the use case takes one. Reuse `mockUserRepo` and `st
|
||||
failing send -> `500` and no invite for that address remains.
|
||||
13. Auth table (`POST`): no cookie -> `401`; cookie of unverified user 8 -> `403`; cookie with stale
|
||||
`token_version` -> `401`; cookie of a verified admin (seeded with `Role: admin`) -> `403`, plain
|
||||
text body exactly `apperr.OwnerRequiredErrorMessage`. Each asserts no invite and no mail.
|
||||
text body exactly `apperr.MissingPermissionErrorMessage`. Each asserts no invite and no mail.
|
||||
14. Rate limit: limiter of 2 per minute, `RemoteAddr` `203.0.113.20:1234`, three valid POSTs -> `201`,
|
||||
`201`, `429`; third body is exactly `apperr.TooManyRequestsErrorMessage`, has `Retry-After`, and adds no
|
||||
invite or mail. `GET /` from the same address afterwards is not `429`.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: The owner can cancel a pending invite and renew a pending or expired one
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As the owner of the shared RenewCMS workspace
|
||||
I want to cancel an invite I no longer want, or renew one that is stale or expired
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: invite management
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Package: `test` (black-box). Reuse `mockUserRepo`, `stubHasher`, `mockMailRepo` and `mockInviteRepo`.
|
||||
Extend `mockInviteRepo` with the new renew method and a `failSwitch` entry for it (unique type names
|
||||
across the package). No live server, no real SQLite (except case 27), no real SMTP.
|
||||
@@ -45,7 +47,7 @@ across the package). No live server, no real SQLite (except case 27), no real SM
|
||||
|
||||
## Access and ids (table over both endpoints)
|
||||
22. No cookie -> `401`; stale `token_version` -> `401`; unverified -> `403`; admin -> `403` with body
|
||||
`apperr.OwnerRequiredErrorMessage`; admin on an unknown id -> `403`. Each: invite unchanged, no mail.
|
||||
`apperr.MissingPermissionErrorMessage`; admin on an unknown id -> `403`. Each: invite unchanged, no mail.
|
||||
23. Ids `abc`, `-1`, `4294967296` -> `400` `apperr.IdUint32ErrorMessage` on both endpoints.
|
||||
|
||||
## Rate limit
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Media cleanup in the SPA: Settings card, purge dialog and Media Duplicates button
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As a RenewCMS owner or admin
|
||||
I want to turn on automatic duplicate purging and to purge duplicates by hand
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA: Media cleanup in the SPA (Settings card, purge dialog, Media Duplicates button)
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
No Go test: the API contract is already covered by media_purge.qa.md and media_purge_scheduler.qa.md. This slice has no Go change. The SPA never reaches `persistence`, `mailer` or `gorm`, and every `/v1` call goes through `useApi`.
|
||||
|
||||
Settled decisions: a non-admin sees the Read-only card with placeholders and no settings GET; Partial is a 200 with `failed_groups > 0`; a 403 on PATCH reverts silently, refreshes the session and switches the card to Read-only; dates use the browser timezone.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Owners and admins can merge duplicate images in use, with persisted purge settings
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As a RenewCMS owner or admin
|
||||
I want to see what a duplicate purge would do, run it, and keep the automatic-purge settings
|
||||
@@ -35,7 +36,7 @@ Feature: Owners and admins can merge duplicate images in use, with persisted pur
|
||||
RequireVerifiedUser), then a group guarded by the new middleware.RequireOwnerOrAdmin, and
|
||||
registered before the /{id} routes
|
||||
And RequireOwnerOrAdmin is an allowlist: only the roles owner and admin pass; any other value,
|
||||
including an empty role, answers 403 with apperr.OwnerOrAdminRequiredErrorMessage
|
||||
including an empty role, answers 403 with apperr.MissingPermissionErrorMessage
|
||||
("Only owners and admins can do this.") as plain text from http.Error
|
||||
And a stale fingerprint answers 409 with apperr.PurgeConflictErrorMessage ("The library changed,
|
||||
so nothing was purged. Check the new summary and try again.") and a busy run answers 409 with
|
||||
@@ -59,14 +60,14 @@ Feature: Owners and admins can merge duplicate images in use, with persisted pur
|
||||
| POST | /v1/media/purge | no cookie | 401 | the existing jwtauth response (status only) |
|
||||
| GET | /v1/media/purge-settings | a stale token_version cookie | 401 | apperr.UnauthorizedErrorMessage |
|
||||
| GET | /v1/media/purge-settings | cookie of an unverified owner | 403 | apperr.VerificationRequiredErrorMessage |
|
||||
| GET | /v1/media/purge-settings | cookie of a verified user, role "editor" | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| PATCH | /v1/media/purge-settings | cookie of a verified user, role "editor" | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| GET | /v1/media/purge-preview | cookie of a verified user, role "editor" | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| POST | /v1/media/purge | cookie of a verified user, role "editor" | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| GET | /v1/media/purge-settings | cookie of a verified user, empty role | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| PATCH | /v1/media/purge-settings | cookie of a verified user, empty role | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| GET | /v1/media/purge-preview | cookie of a verified user, empty role | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| POST | /v1/media/purge | cookie of a verified user, empty role | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
||||
| GET | /v1/media/purge-settings | cookie of a verified user, role "editor" | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| PATCH | /v1/media/purge-settings | cookie of a verified user, role "editor" | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| GET | /v1/media/purge-preview | cookie of a verified user, role "editor" | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| POST | /v1/media/purge | cookie of a verified user, role "editor" | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| GET | /v1/media/purge-settings | cookie of a verified user, empty role | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| PATCH | /v1/media/purge-settings | cookie of a verified user, empty role | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| GET | /v1/media/purge-preview | cookie of a verified user, empty role | 403 | apperr.MissingPermissionErrorMessage |
|
||||
| POST | /v1/media/purge | cookie of a verified user, empty role | 403 | apperr.MissingPermissionErrorMessage |
|
||||
|
||||
Scenario Outline: Owners and admins both pass the role guard
|
||||
When the client sends <method> <path> with a valid cookie of a verified <role>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: media_purge
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Package: `test` (black-box). New files `test/media_purge_http_test.go` and `test/media_purge_test.go` (use case level). Fake type names must be unique in the package. Go only: no SPA, no scheduler, no env var.
|
||||
|
||||
## Setup shared by every case
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
Feature: Roles and permissions enforcement: permission checks replace the owner and admin gates
|
||||
|
||||
As the owner of a RenewCMS site
|
||||
I want every owner and admin gate replaced by a check of the caller's role permissions
|
||||
So that built-in roles are enforced by what they hold, API keys follow their creator's live role,
|
||||
and later slices can add custom roles without touching the routes again
|
||||
|
||||
Background:
|
||||
Given the spec of record is docs/design/screens/roles.md (catalogue, "Built-in roles", "HTTP status to copy", section 7)
|
||||
And features/roles_permissions_foundation.feature provides the catalogue, the Owner (34), Admin (29) and Writer (9) roles,
|
||||
users.role_id, the legacy fallback and GetMyAccessUseCase
|
||||
And "ok" in a table means the request passes the permission gate. A later 400, 404 or 409 from the handler is not a gate failure
|
||||
And "403" in a table means status 403, body "missing_permission\n" (http.Error, plain text, like the other snake_case codes
|
||||
in api/pkg/apperr) and the use case or handler body is not invoked
|
||||
And the code is apperr.MissingPermissionErrorMessage = "missing_permission"
|
||||
And the response carries the header X-Missing-Permission with the catalogue key that failed, so the SPA can word
|
||||
"Your role doesn't include {permission name}"
|
||||
And api/middleware gains RequirePermission(key) for on/off keys and for the minimum "not none" check of a scoped key. It uses
|
||||
a use case that depends on domain interfaces only. It resolves the caller's role live on every request, with no cache
|
||||
And a request that fails the gate answers only after the existing steps: authtoken.Authenticator, RequireCurrentToken (401),
|
||||
RequireVerifiedUser (403 verification), RequireSession (403 session_required for an API key) and RejectAPIKey (401)
|
||||
And RequireOwner, RequireOwnerOrAdmin, apperr.OwnerRequiredErrorMessage and apperr.OwnerOrAdminRequiredErrorMessage are removed
|
||||
And no use case, handler or middleware reads user.Role for an authorization decision. user.Role stays only for the
|
||||
role_id fallback, for CreateUser and AcceptInvite assigning it, for ListUsersAndInvites output and for migrations
|
||||
And a scoped decision is taken from the caller's resolved grants. Use cases receive them, not user.Role
|
||||
And the role is resolved as in GetMyAccessUseCase: role_id first, else the built-in named by the legacy string
|
||||
And a role that cannot be resolved (role_id unknown, or no role_id and a legacy string other than "owner" or "admin")
|
||||
is refused with 403 missing_permission on every gated route. A repository failure other than role.ErrRoleNotFound
|
||||
answers 500 "Internal server error"
|
||||
And enforcement honours any grant it finds, including an owner-only key placed on a custom role by hand. Refusing to
|
||||
grant owner-only keys belongs to slice 3 (ValidateGrants at write time)
|
||||
And the users, tokens and cookies are the ones of the foundation feature: user 3 Owner, user 5 Admin, user 9 Writer,
|
||||
all verified with a valid jwt cookie and token_version 0
|
||||
And routes with no obvious catalogue key reuse existing keys: media rename = media.upload; purge settings GET = settings.view,
|
||||
PATCH = settings.edit_general; purge preview and run = media.delete at scope all (own fails closed, images record no creator);
|
||||
per-image variant mutations and image-variants configuration = settings.edit_general; media variants GET and image-variants
|
||||
list = media.view; image-variants cache stats = settings.view; builtin types GET = schema.view
|
||||
And GET /v1/site stays open to any verified user
|
||||
And settings.manage_sites covers PATCH /v1/site/{id} including site SEO defaults, so Admin may edit them
|
||||
|
||||
Scenario: A refused request carries the missing_permission contract
|
||||
Given user 9 (Writer)
|
||||
When the client sends POST /v1/user/invite with body {"email": "new@example.com"} and user 9's cookie
|
||||
Then the response status is 403 and the body is "missing_permission\n"
|
||||
And the header X-Missing-Permission is "users.invite"
|
||||
And no invite is created and no mail is sent
|
||||
|
||||
Scenario: Verification and token checks still come first
|
||||
Given user 12 is not verified and holds the Admin role
|
||||
When the client sends POST /v1/user/invite with user 12's cookie
|
||||
Then the status is 403 and the body is apperr.VerificationRequiredErrorMessage
|
||||
And a stale token_version gives 401 apperr.UnauthorizedErrorMessage and no cookie gives 401, as before
|
||||
|
||||
Scenario Outline: Users and invites
|
||||
Given the caller is <role>
|
||||
When the client sends <call> with that caller's cookie
|
||||
Then the gate result is <result>
|
||||
|
||||
Examples:
|
||||
| role | call | key | result |
|
||||
| Owner | GET /v1/user | users.view | ok |
|
||||
| Admin | GET /v1/user | users.view | ok |
|
||||
| Writer | GET /v1/user | users.view | 403 |
|
||||
| Owner | POST /v1/user/invite | users.invite | ok |
|
||||
| Admin | POST /v1/user/invite | users.invite | ok |
|
||||
| Writer | POST /v1/user/invite | users.invite | 403 |
|
||||
| Admin | DELETE /v1/user/invite/1 | users.invite | ok |
|
||||
| Writer | DELETE /v1/user/invite/1 | users.invite | 403 |
|
||||
| Admin | POST /v1/user/invite/1/renew | users.invite | ok |
|
||||
| Writer | POST /v1/user/invite/1/renew | users.invite | 403 |
|
||||
|
||||
Scenario Outline: Settings, sites and system
|
||||
Given the caller is <role>
|
||||
When the client sends <call> with that caller's cookie (and a real session, not an API key, where the route requires one)
|
||||
Then the gate result is <result>
|
||||
|
||||
Examples:
|
||||
| role | call | key | result |
|
||||
| Owner | GET /v1/settings | settings.view | ok |
|
||||
| Admin | GET /v1/settings | settings.view | ok |
|
||||
| Writer | GET /v1/settings | settings.view | 403 |
|
||||
| Owner | PATCH /v1/settings | system.edit_retention | ok |
|
||||
| Admin | PATCH /v1/settings | system.edit_retention | 403 |
|
||||
| Writer | PATCH /v1/settings | system.edit_retention | 403 |
|
||||
| Owner | POST /v1/site | settings.manage_sites | ok |
|
||||
| Admin | POST /v1/site | settings.manage_sites | ok |
|
||||
| Writer | POST /v1/site | settings.manage_sites | 403 |
|
||||
| Admin | PATCH /v1/site/2 | settings.manage_sites | ok |
|
||||
| Admin | PATCH /v1/site/2/default | settings.manage_sites | ok |
|
||||
| Writer | PATCH /v1/site/2 | settings.manage_sites | 403 |
|
||||
| Owner | DELETE /v1/site/2 | settings.delete_site | ok |
|
||||
| Admin | DELETE /v1/site/2 | settings.delete_site | 403 |
|
||||
| Writer | DELETE /v1/site/2 | settings.delete_site | 403 |
|
||||
| Writer | GET /v1/site | (none) | ok |
|
||||
| Owner | GET /v1/system/update | settings.view | ok |
|
||||
| Admin | GET /v1/system/update | settings.view | ok |
|
||||
| Writer | GET /v1/system/update | settings.view | 403 |
|
||||
| Owner | POST /v1/system/update/check | system.apply_updates | ok |
|
||||
| Admin | POST /v1/system/update/check | system.apply_updates | 403 |
|
||||
| Owner | POST /v1/system/update/download | system.apply_updates | ok |
|
||||
| Admin | POST /v1/system/update/download | system.apply_updates | 403 |
|
||||
| Owner | POST /v1/system/update/restart | system.apply_updates | ok |
|
||||
| Admin | POST /v1/system/update/restart | system.apply_updates | 403 |
|
||||
| Writer | POST /v1/system/update/restart | system.apply_updates | 403 |
|
||||
|
||||
Scenario: The permission gate runs before the update rate limiters
|
||||
Given the update-check limiter allows 5 attempts per minute per user
|
||||
When the Admin sends POST /v1/system/update/check 6 times
|
||||
Then every response is 403 missing_permission and none is 429
|
||||
|
||||
Scenario: The permission gate runs before the invite rate limiter
|
||||
Given the invite limiter is exhausted for user 9
|
||||
When the Writer sends POST /v1/user/invite
|
||||
Then the response is 403 missing_permission and none is 429
|
||||
|
||||
Scenario: An admin reads the update status but cannot apply it
|
||||
Given the Admin and the Owner each send GET /v1/system/update
|
||||
Then both get 200
|
||||
And the Admin's body has can_apply false
|
||||
And the Owner's body has can_apply true when the update helper is installed, false otherwise, as before
|
||||
|
||||
Scenario: An admin reads the activity retention but cannot change it
|
||||
Given the Admin sends GET /v1/settings
|
||||
Then the status is 200 and the body holds activity_retention_days and activity_retention_max_gb
|
||||
And PATCH /v1/settings with {"activity_retention_days": 30} gives 403 missing_permission and nothing is stored
|
||||
|
||||
Scenario Outline: Authors, schema, media and API key creation
|
||||
Given the caller is <role>
|
||||
When the client sends <call> with that caller's cookie
|
||||
Then the gate result is <result>
|
||||
|
||||
Examples:
|
||||
| role | call | key | result |
|
||||
| Owner | GET /v1/author | authors.view | ok |
|
||||
| Writer | GET /v1/author | authors.view | ok |
|
||||
| Admin | POST /v1/author | authors.create | ok |
|
||||
| Writer | POST /v1/author | authors.create | 403 |
|
||||
| Admin | DELETE /v1/author/4 | authors.delete | ok |
|
||||
| Writer | DELETE /v1/author/4 | authors.delete | 403 |
|
||||
| Writer | POST /v1/author/me | (none) | ok |
|
||||
| Admin | GET /v1/content-type | schema.view | ok |
|
||||
| Writer | GET /v1/content-type | schema.view | ok |
|
||||
| Writer | GET /v1/content-type/events | schema.view | ok |
|
||||
| Admin | POST /v1/content-type | schema.edit | ok |
|
||||
| Writer | POST /v1/content-type | schema.edit | 403 |
|
||||
| Writer | PUT /v1/content-type/events | schema.edit | 403 |
|
||||
| Admin | DELETE /v1/content-type/events | schema.delete | ok |
|
||||
| Writer | DELETE /v1/content-type/events | schema.delete | 403 |
|
||||
| Writer | GET /v1/builtin-type | schema.view | ok |
|
||||
| Writer | GET /v1/media | media.view | ok |
|
||||
| Writer | GET /v1/media/1 | media.view | ok |
|
||||
| Writer | GET /v1/media/1/usage | media.view | ok |
|
||||
| Writer | POST /v1/media (multipart image) | media.upload | ok |
|
||||
| Writer | PATCH /v1/media/1 | media.upload | ok |
|
||||
| Writer | DELETE /v1/media/1 | media.delete | 403 |
|
||||
| Writer | POST /v1/media/merge | media.delete | 403 |
|
||||
| Admin | DELETE /v1/media/1 | media.delete | ok |
|
||||
| Writer | GET /v1/media/purge-settings | settings.view | 403 |
|
||||
| Writer | PATCH /v1/media/purge-settings | settings.edit_general| 403 |
|
||||
| Writer | GET /v1/media/purge-preview | media.delete | 403 |
|
||||
| Writer | POST /v1/media/purge | media.delete | 403 |
|
||||
| Admin | POST /v1/media/purge | media.delete | ok |
|
||||
| Writer | GET /v1/media/1/variants | media.view | ok |
|
||||
| Writer | POST /v1/media/1/variants | settings.edit_general| 403 |
|
||||
| Writer | DELETE /v1/media/1/variants/hero | settings.edit_general| 403 |
|
||||
| Writer | GET /v1/image-variants | media.view | ok |
|
||||
| Writer | GET /v1/image-variants/cache | settings.view | 403 |
|
||||
| Writer | POST /v1/image-variants | settings.edit_general| 403 |
|
||||
| Writer | POST /v1/image-variants/cache/purge | settings.edit_general| 403 |
|
||||
| Admin | PUT /v1/image-variants/hero | settings.edit_general| ok |
|
||||
| Admin | POST /v1/api-key | apikeys.create | ok |
|
||||
| Writer | POST /v1/api-key | apikeys.create | 403 |
|
||||
|
||||
Scenario: Routes that keep their old behaviour for API keys
|
||||
Given a valid API key whose creator is the Admin
|
||||
Then POST /v1/api-key, GET /v1/activity and POST /v1/system/update/check answer 403 apperr.SessionRequiredErrorMessage
|
||||
And GET /v1/media/1/variants answers 401 apperr.UnauthorizedErrorMessage (RejectAPIKey runs before the permission check)
|
||||
|
||||
Scenario Outline: Roles are judged by the keys they hold, with no implied keys
|
||||
Given user 20 has a custom role that grants only <grants>
|
||||
When the client sends <call> with user 20's cookie
|
||||
Then the gate result is <result>
|
||||
|
||||
Examples:
|
||||
| grants | call | result |
|
||||
| schema.edit=all | POST /v1/content-type | ok |
|
||||
| schema.edit=all | GET /v1/content-type | 403 |
|
||||
| schema.view=all | PUT /v1/content-type/events | 403 |
|
||||
| users.invite=all | GET /v1/user | 403 |
|
||||
| users.view=all | POST /v1/user/invite | 403 |
|
||||
| settings.manage_sites=all | DELETE /v1/site/2 | 403 |
|
||||
| settings.delete_site=all | PATCH /v1/site/2 | 403 |
|
||||
| media.upload=all | GET /v1/media | 403 |
|
||||
| media.view=all | POST /v1/media | 403 |
|
||||
| apikeys.create=all | GET /v1/api-key | 403 |
|
||||
| settings.edit_general=all | PATCH /v1/media/purge-settings | ok |
|
||||
| system.apply_updates=all | POST /v1/system/update/check | ok |
|
||||
| (no grants) | GET /v1/author | 403 |
|
||||
|
||||
Scenario: A user whose role cannot be resolved is refused everywhere that is gated
|
||||
Given user 13 has role_id 99 (no such role) and legacy role ""
|
||||
When user 13 sends GET /v1/user, GET /v1/content-type and POST /v1/media
|
||||
Then every response is 403 missing_permission
|
||||
|
||||
Scenario: A repository failure while resolving the role is a server error
|
||||
Given the role repository fails with a non-sentinel error
|
||||
When user 3 sends GET /v1/user
|
||||
Then the status is 500 and the body is "Internal server error"
|
||||
|
||||
Scenario Outline: The legacy role string is no longer read for authorization
|
||||
Given user <id> has legacy role "<legacy>" and role_id <role_id>
|
||||
When the client sends <call> with that user's cookie
|
||||
Then the gate result is <result>
|
||||
|
||||
Examples:
|
||||
| id | legacy | role_id | call | result |
|
||||
| 14 | owner | Writer's id | POST /v1/user/invite | 403 |
|
||||
| 15 | admin | Writer's id | POST /v1/content-type| 403 |
|
||||
| 16 | "" | Admin's id | POST /v1/content-type| ok |
|
||||
| 17 | owner | 0 | POST /v1/user/invite | ok |
|
||||
| 18 | admin | 0 | POST /v1/content-type| ok |
|
||||
| 19 | admin | 0 | PATCH /v1/settings | 403 |
|
||||
|
||||
Scenario: A session follows its role live
|
||||
Given user 9 is on the Writer role and GET /v1/media/purge-settings gives 403
|
||||
When Writer's grants are edited in the repository to add settings.view=all
|
||||
Then the next GET /v1/settings with the same cookie answers ok, with no re-login and no cache
|
||||
|
||||
Scenario: Admin relaxations
|
||||
Given the Admin
|
||||
Then the Admin can now call POST /v1/user/invite, DELETE /v1/user/invite/{id} and POST /v1/user/invite/{id}/renew
|
||||
And can now call POST /v1/site, PATCH /v1/site/{id} and PATCH /v1/site/{id}/default
|
||||
And can now list every API key with created_by_name and revoke any key
|
||||
And can read every setting: GET /v1/settings, GET /v1/system/update and the retention values
|
||||
|
||||
Scenario: Owner-only items stay owner-only
|
||||
Given the Admin
|
||||
Then DELETE /v1/site/{id}, PATCH /v1/settings, POST /v1/system/update/check, /download and /restart answer 403 missing_permission
|
||||
And the Owner passes all of them
|
||||
|
||||
Scenario: Writer is effective
|
||||
Given user 9 on the Writer role
|
||||
Then GET /v1/content-type and GET /v1/content-type/events answer ok
|
||||
And GET /v1/media, GET /v1/media/1 and POST /v1/media answer ok
|
||||
And GET /v1/author answers ok and the author items carry no user_email
|
||||
And GET /v1/user, GET /v1/settings, GET /v1/system/update, POST /v1/user/invite, POST /v1/site and POST /v1/api-key answer 403
|
||||
And GET /v1/activity answers ok and returns only the events whose actor is user 9
|
||||
And GET /v1/permission, GET /v1/auth/me, PATCH /v1/user/me and POST /v1/author/me answer ok
|
||||
|
||||
Scenario Outline: activity.view scope decides what the list returns
|
||||
Given events by user 3, user 5 and user 9 exist
|
||||
And the caller's activity.view grant is <scope>
|
||||
When the client sends GET /v1/activity<query>
|
||||
Then the result is <result>
|
||||
|
||||
Examples:
|
||||
| scope | query | result |
|
||||
| all | | 200, events of users 3, 5 and 9 |
|
||||
| all | ?actor=user:5 | 200, events of user 5 only |
|
||||
| own | | 200, only the caller's own events |
|
||||
| own | ?actor=user:9 | 200, only the caller's own events (self is allowed) |
|
||||
| own | ?actor=user:3 | 403 missing_permission |
|
||||
| none | | 403 missing_permission |
|
||||
|
||||
Scenario Outline: Activity summary and export follow the scope
|
||||
Given the caller holds <grants>
|
||||
When the client sends <call>
|
||||
Then the result is <result>
|
||||
|
||||
Examples:
|
||||
| grants | call | result |
|
||||
| activity.view=all | GET /v1/activity/summary| 200 |
|
||||
| activity.view=own | GET /v1/activity/summary| 403 missing_permission |
|
||||
| activity.view=all, activity.export=all | GET /v1/activity/export | 200 CSV of every event |
|
||||
| activity.view=own, activity.export=all | GET /v1/activity/export | 200 CSV of the caller's own events only |
|
||||
| activity.view=none, activity.export=all | GET /v1/activity/export | 403 missing_permission |
|
||||
| activity.view=all | GET /v1/activity/export | 403 missing_permission |
|
||||
|
||||
Scenario Outline: apikeys.view scope decides which keys are listed
|
||||
Given keys 1 (created by user 3), 2 (user 5) and 3 (user 9) exist
|
||||
And the caller's apikeys.view grant is <scope>
|
||||
When the client sends GET /v1/api-key
|
||||
Then the result is <result>
|
||||
|
||||
Examples:
|
||||
| scope | result |
|
||||
| all | 200, keys 1, 2 and 3, each with created_by_name |
|
||||
| own | 200, only the caller's own keys, without created_by_name |
|
||||
| none | 403 missing_permission |
|
||||
|
||||
Scenario Outline: apikeys.revoke scope decides which keys can be revoked
|
||||
Given key 2 was created by user 5
|
||||
And the caller is user 9 and holds apikeys.revoke = <scope>
|
||||
When the client sends DELETE /v1/api-key/2
|
||||
Then the result is <result>
|
||||
|
||||
Examples:
|
||||
| scope | result |
|
||||
| all | 204 and key 2 is revoked |
|
||||
| own | 404 apperr.APIKeyNotFoundErrorMessage and key 2 is not revoked |
|
||||
| none | 403 missing_permission, key 2 is not revoked and no lookup occurs |
|
||||
|
||||
Scenario: The Admin lists and revokes every key
|
||||
Given the Admin and a key created by the Owner
|
||||
When the Admin sends GET /v1/api-key and DELETE /v1/api-key/<that id>
|
||||
Then the list contains the Owner's key with created_by_name, and the delete returns 204
|
||||
|
||||
Scenario Outline: authors.edit scope decides who can edit an author
|
||||
Given author 4 is linked to user 9 and author 5 is linked to user 3
|
||||
And the caller is user 9 with authors.edit = <scope>
|
||||
When the client sends PATCH <path> with body <body>
|
||||
Then the result is <result>
|
||||
|
||||
Examples:
|
||||
| scope | path | body | result |
|
||||
| all | /v1/author/5 | {"bio": "x"} | 200 |
|
||||
| all | /v1/author/5 | {"user_id": 9} | 200 or the existing link errors |
|
||||
| own | /v1/author/4 | {"bio": "x"} | 200 |
|
||||
| own | /v1/author/5 | {"bio": "x"} | 403 author_rights (existing contract) |
|
||||
| own | /v1/author/4 | {"user_id": null} | 403 author_rights (existing contract) |
|
||||
| none | /v1/author/4 | {"bio": "x"} | 403 missing_permission |
|
||||
| none | /v1/author/999 | {"bio": "x"} | 403 missing_permission (before any lookup) |
|
||||
|
||||
Scenario Outline: user_email on authors needs users.view
|
||||
Given author 4 is linked to a user with an email
|
||||
And the caller holds authors.view and users.view = <users_view>
|
||||
When the client sends GET /v1/author
|
||||
Then user_email is <shown>
|
||||
|
||||
Examples:
|
||||
| users_view | shown |
|
||||
| all | present |
|
||||
| none | absent |
|
||||
|
||||
Scenario: An API key resolves its creator's current role on every request
|
||||
Given a valid API key created by user 5 (Admin)
|
||||
And GET /v1/content-type with that key answers 200 and POST /v1/user/invite answers ok
|
||||
When user 5's role_id is changed to the Writer role in the repository
|
||||
Then the next POST /v1/user/invite with the same key answers 403 missing_permission
|
||||
And GET /v1/content-type with the same key still answers 200
|
||||
And POST /v1/content-type with the same key answers 403 missing_permission
|
||||
And no new key and no new login is needed for the change to apply
|
||||
|
||||
Scenario: A key grows when its creator's role grows
|
||||
Given a valid API key created by user 9 (Writer), and POST /v1/content-type with it gives 403
|
||||
When user 9's role_id is changed to the Admin role
|
||||
Then the next POST /v1/content-type with the same key passes the gate
|
||||
|
||||
Scenario: Editing the creator's role grants shrinks the key as well
|
||||
Given a valid API key created by user 20 whose custom role grants media.view and media.upload
|
||||
When the role's media.upload grant is removed in the repository
|
||||
Then POST /v1/media with the key gives 403 missing_permission and GET /v1/media still passes
|
||||
|
||||
Scenario: A key whose creator's role cannot be resolved is refused by the gate
|
||||
Given a valid API key created by user 13 (role_id 99)
|
||||
When the client sends GET /v1/media with the key
|
||||
Then the status is 403 missing_permission
|
||||
|
||||
Scenario: Key authentication failures are unchanged
|
||||
Then an unknown, revoked or expired key, or one whose creator is deleted or unverified, still gives 401 apperr.UnauthorizedErrorMessage
|
||||
|
||||
Scenario: Public and self-service routes keep no permission gate
|
||||
Given no cookie
|
||||
Then GET /v1/article, GET /v1/article/{id}, GET /v1/article/{id}/seo and GET /v1/site/{key}/sitemap answer as before
|
||||
And user 9 on a role with no grants still gets 200 on GET /v1/auth/me, GET /v1/permission, GET /v1/site and GET /v1/user/lookup?ids=1, and ok on PATCH /v1/user/me
|
||||
|
||||
Scenario: Articles and entries are not gated in this slice
|
||||
Given user 20 has a custom role with no content grants
|
||||
Then the article and entry routes answer exactly as before this slice (own/all, publish and draft are slice 2b)
|
||||
|
||||
Scenario Outline: Existing owner and admin behaviour does not regress
|
||||
Given the caller is <role>
|
||||
When the client sends <call>
|
||||
Then the status is <status> exactly as before this slice
|
||||
|
||||
Examples:
|
||||
| role | call | status |
|
||||
| Owner | GET /v1/user | 200 |
|
||||
| Admin | GET /v1/user | 200 |
|
||||
| Owner | POST /v1/content-type | as before |
|
||||
| Admin | POST /v1/content-type | as before |
|
||||
| Admin | GET /v1/builtin-type | 200 |
|
||||
| Admin | GET /v1/activity/summary | 200 |
|
||||
| Admin | GET /v1/activity/export | 200 CSV |
|
||||
| Admin | GET /v1/media/purge-settings | 200 |
|
||||
| Admin | PATCH /v1/media/purge-settings | as before |
|
||||
| Admin | GET /v1/image-variants | 200 |
|
||||
| Admin | GET /v1/media/1/variants | as before |
|
||||
| Owner | PATCH /v1/settings | as before |
|
||||
| Admin | DELETE /v1/author/4 | as before |
|
||||
|
||||
Scenario: The owner and admin gates are gone from the code
|
||||
Then api/router.go and api/middleware hold no RequireOwner or RequireOwnerOrAdmin
|
||||
And no file under api/ or internal/infrastructure/useCases compares a user.Role value for authorization,
|
||||
apart from the role_id fallback in GetMyAccessUseCase, CreateUser and AcceptInvite assigning a role, and the ListUsersAndInvites output
|
||||
And the apperr constants OwnerRequiredErrorMessage and OwnerOrAdminRequiredErrorMessage no longer exist
|
||||
@@ -0,0 +1,152 @@
|
||||
# QA procedure: roles_permissions_enforcement
|
||||
|
||||
Package: `test` (black-box). Name new fakes uniquely, for example `enforcementRoleRepo`, `enforcementUserRepo`.
|
||||
Do not copy the old `Role ==` checks into new code.
|
||||
|
||||
## Deferred to later slices
|
||||
|
||||
- 2b: own/all enforcement after loading an article or entry, publish separate from update, Draft behaviour. Articles and entries stay ungated here.
|
||||
- 3: /v1/role CRUD, no-escalation, ValidateGrants at write time, per-type override keys.
|
||||
- 4: role change, invite with a role, transfer ownership, removal of the legacy role column.
|
||||
- SPA (sidebar, read-only pages, 403 toasts) is out of scope. It reacts to the status code only.
|
||||
|
||||
## Design notes for the user
|
||||
|
||||
- docs/design/screens/roles.md "Owner-only items" disables View roles and Manage roles for an admin. This slice follows the user's decision (Admin has 29 keys). The design needs a fix by the user.
|
||||
- docs/design/change-list.md item 21 ("Sites: owner only, hidden for other roles") and its SEO "Permissions" paragraph ("only the owner edits site SEO defaults", 403 `owner_only`), plus the `owner_only` clause in routes.md "SEO", disagree with this slice. Admin has `settings.manage_sites`, so an admin can now edit sites and their SEO defaults.
|
||||
- roles.md is inconsistent about Writer and Schema. The "Writer: Articles list" state hides Schema from the sidebar, while the catalogue, the built-in table and change-list ("Schema read-only for view-only roles") give Writer `schema.view`.
|
||||
- change-list line 100 mentions a "restricted `user` role" that does not exist.
|
||||
- The docs name `GET /v1/me`. The implemented route is `GET /v1/auth/me` (resolved in slice 1).
|
||||
- change-list item 15 and routes.md word activity export as "owner and admin"; it is now `activity.export` plus `activity.view`.
|
||||
- change-list item 22 says GET /v1/author works for any role; under 2a a role without `authors.view` gets 403.
|
||||
- The Home activity card may call activity summary, so a Writer would get 403 there; check before the SPA slice.
|
||||
|
||||
## Inventory (route, current gate, new key, Owner / Admin / Writer)
|
||||
|
||||
Routes gated today by RequireOwner or RequireOwnerOrAdmin:
|
||||
|
||||
| Route | New key (scope) | Owner / Admin / Writer | Change |
|
||||
|---|---|---|---|
|
||||
| POST /v1/user/invite, DELETE /v1/user/invite/{id}, POST /v1/user/invite/{id}/renew | users.invite | ok / ok / 403 | Admin gains |
|
||||
| PATCH /v1/settings | system.edit_retention | ok / 403 / 403 | none |
|
||||
| POST /v1/site, PATCH /v1/site/{id}, PATCH /v1/site/{id}/default | settings.manage_sites | ok / ok / 403 | Admin gains |
|
||||
| DELETE /v1/site/{id} | settings.delete_site | ok / 403 / 403 | none |
|
||||
| POST /v1/author | authors.create | ok / ok / 403 | none |
|
||||
| DELETE /v1/author/{id} | authors.delete | ok / ok / 403 | none |
|
||||
| POST, PUT /v1/content-type | schema.edit | ok / ok / 403 | none |
|
||||
| DELETE /v1/content-type/{key} | schema.delete | ok / ok / 403 | none |
|
||||
| GET /v1/builtin-type, /{key} | schema.view | ok / ok / ok | Writer gains |
|
||||
| GET /v1/activity/summary | activity.view at all | ok / ok / 403 | none |
|
||||
| GET /v1/activity/export | activity.export plus activity.view (same scoping as the list) | ok / ok / 403 | none |
|
||||
| POST /v1/system/update/check, /download, /restart | system.apply_updates | ok / 403 / 403 | none |
|
||||
| GET /v1/media/purge-settings | settings.view | ok / ok / 403 | none |
|
||||
| PATCH /v1/media/purge-settings | settings.edit_general | ok / ok / 403 | none |
|
||||
| GET /v1/media/purge-preview, POST /v1/media/purge | media.delete at all | ok / ok / 403 | none |
|
||||
| GET /v1/media/{id}/variants (after RejectAPIKey) | media.view | ok / ok / ok | Writer gains |
|
||||
| POST, DELETE /v1/media/{id}/variants... (after RejectAPIKey) | settings.edit_general | ok / ok / 403 | none |
|
||||
| GET /v1/image-variants | media.view | ok / ok / ok | Writer gains |
|
||||
| GET /v1/image-variants/cache | settings.view | ok / ok / 403 | none |
|
||||
| other image-variants mutations and cache purge | settings.edit_general | ok / ok / 403 | none |
|
||||
|
||||
Gated by a legacy `Role ==` inside a use case or handler:
|
||||
|
||||
| Place | New |
|
||||
|---|---|
|
||||
| ListActivity.scopeToViewer, activityHandler.writeActivityError | activity.view: all unfiltered, own forced to self (another actor gives 403 missing_permission), none gives 403 |
|
||||
| ListAPIKeys | apikeys.view: all lists everything with created_by_name, own only the caller's keys without it, none 403 |
|
||||
| RevokeAPIKey | apikeys.revoke: all any key, own only the caller's (404 API key not found on another's), none 403 |
|
||||
| ListAuthors (CallerRole, isAdminRole) | gate authors.view; user_email only if the caller holds users.view |
|
||||
| UpdateAuthor / AuthorCaller.isAdmin | authors.edit: all any author and the link, own the linked author only (else author_rights), none 403 missing_permission |
|
||||
| updateHandler.isOwner | can_apply true only with system.apply_updates (and helper installed, as today) |
|
||||
| middleware.RequireOwner, RequireOwnerOrAdmin | deleted; replaced by RequirePermission(key) and a scope-aware variant resolving the live role |
|
||||
|
||||
Previously ungated or verification-only:
|
||||
|
||||
| Route | New |
|
||||
|---|---|
|
||||
| GET /v1/user | users.view |
|
||||
| GET /v1/settings | settings.view |
|
||||
| GET /v1/system/update | settings.view |
|
||||
| GET /v1/author | authors.view |
|
||||
| GET /v1/content-type, /{key} | schema.view |
|
||||
| GET /v1/media, /{id}, /{id}/usage | media.view |
|
||||
| POST /v1/media | media.upload |
|
||||
| PATCH /v1/media/{id} | media.upload |
|
||||
| DELETE /v1/media/{id}, POST /v1/media/merge | media.delete at all (own fails closed) |
|
||||
| POST /v1/api-key | apikeys.create (RequireSession stays) |
|
||||
| GET /v1/api-key, DELETE /v1/api-key/{id} | apikeys.view, apikeys.revoke (scoped) |
|
||||
| GET /v1/activity | activity.view (scoped) |
|
||||
| GET /v1/site, GET /v1/user/lookup, PATCH /v1/user/me, POST /v1/author/me, GET /v1/permission, GET /v1/auth/me | ungated |
|
||||
| /v1/article/*, /v1/content-type/{key}/entry/*, public reads | ungated in 2a |
|
||||
|
||||
## Test infrastructure first
|
||||
|
||||
1. `enforcementRoleRepo` implements the role `Repository` in memory, seeded with the three built-ins through the same Go function the migration uses. It supports editing grants and a switchable failure.
|
||||
2. The user fake carries `RoleID`. Add users 3 (Owner), 5 (Admin), 9 (Writer), 12 (unverified Admin), 13 (role_id 99), 14-19 (legacy and role_id combinations from the Outline), 20 (custom role).
|
||||
3. A helper `mintCookie(userID, tokenVersion)` and a helper to mint API keys backed by the existing `apikey` fakes (see `test/api_keys_http_test.go`).
|
||||
4. HTTP harness. Call `validator.InitValidator()`. Build each router with the same middleware chain as `api/router.go` for that group, now with the permission middleware, over use cases wired to fakes.
|
||||
- Follow whichever approach the closest existing HTTP test uses. Existing tests for `NewUserRouter`, `NewSettingsRouter`, `NewSiteRouter`, `NewAuthorRouter`, `NewContentTypeRouter`, `NewBuiltinTypeRouter`, `NewActivityRouter`, `NewSystemRouter`, `NewAPIKeyRouter`, `NewMediaRouter` and `NewImageVariantRouter` need the role repository added to their fixtures.
|
||||
- Mount `authtoken.Verifier` and `container.apiKeyAuth()` equivalents where the real router has them, so key scenarios traverse the same order.
|
||||
- No live server, no real SQLite file, no real SMTP, no real update helper.
|
||||
5. A gate-matrix helper `assertGate(t, rec, want)`. For "ok" assert the status is not the `missing_permission` 403 and the handler or use case spy was reached. For "403" assert status 403, body `missing_permission\n`, header `X-Missing-Permission` equals the key, and the spy was not reached.
|
||||
|
||||
## A. Domain and use cases (fakes only)
|
||||
|
||||
6. Permission check use case (name left to coder, for example `CheckPermission`): table-test owner, admin, writer, a custom role, unresolvable role (expect `role.ErrMissingPermission` via `errors.Is`; the failing key travels in a typed error via `errors.As`), a failing repository (expect a non-sentinel error), and a live grant change between two calls.
|
||||
7. `ListActivity`: all, own (self filter forced, other actor returns `audit.ErrActorNotAllowed`), none. No `user.Role`.
|
||||
8. `ListAPIKeys` and `RevokeAPIKey`: all, own, none. `created_by_name` follows scope all. Own on another's key returns `apikey.ErrAPIKeyNotFound`.
|
||||
9. `ListAuthors` and `UpdateAuthor`: `user_email` follows `users.view`. `authors.edit` own, all, none, including `author.ErrAuthorRights` for own violations.
|
||||
10. The `can_apply` computation in the update status follows `system.apply_updates`.
|
||||
11. Hygiene tests, in the style of the existing `*_hygiene_test.go`:
|
||||
- `api/router.go` and `api/middleware` contain no `RequireOwner`.
|
||||
- Non-test Go under `api/` and `internal/infrastructure/useCases` has no `user.RoleOwner` or `user.RoleAdmin` comparison apart from the allowed spots listed in the feature.
|
||||
- `internal/domain/role` still imports only the stdlib.
|
||||
|
||||
## B. HTTP
|
||||
|
||||
12. Gate matrices: run every Scenario Outline row. One test per outline, subtests per row, named after the row.
|
||||
13. Contract: status 403, body `missing_permission\n`, `X-Missing-Permission`, no side effect (no invite stored, no mail sent, no site created, retention unchanged).
|
||||
14. Order: stale token 401, no cookie 401, unverified 403 verification, API key on session-only routes 403 `session_required`, API key on `/v1/media/1/variants` 401, all before the permission check. Limiter order: 6 admin update checks give 6 times 403; exhausted invite limiter gives 403 for a Writer.
|
||||
15. Scoped: activity (list, summary, export), api keys (list, revoke), authors (edit, `user_email`) per the Outlines.
|
||||
16. API key live role: change `role_id` between requests, edit grants between requests, and unresolvable role. Use the same key value throughout.
|
||||
17. Session live role: edit grants between two requests with the same cookie.
|
||||
18. Legacy string: the six rows of the "legacy role string" Outline.
|
||||
19. Ungated: public routes, `/v1/auth/me`, `/v1/permission`, `/v1/site`, `/v1/user/lookup`, `PATCH /v1/user/me`, `POST /v1/author/me` answer for a user whose role has no grants. Article and entry routes behave as in their existing tests.
|
||||
20. Regression: run the existing suites for the owner and admin cases in the "does not regress" Outline unchanged, after the assertion edits listed below.
|
||||
|
||||
## Existing tests that must change (found by grep, so recheck with `go vet`)
|
||||
|
||||
Assertions of the removed constants become `missing_permission`, or the expectation flips where Admin gains access.
|
||||
|
||||
| File (lines) | Change |
|
||||
|---|---|
|
||||
| test/roles_permissions_foundation_http_test.go (around 320, the "Existing role checks are untouched" Outline) | Superseded. The admin rows for invite and `/system/update/check` move to this slice (invite becomes ok, update check becomes `missing_permission`). Keep the owner rows. |
|
||||
| test/update_http_test.go:248; test/update_download_http_test.go:187; test/update_restart_http_test.go:263-264 | Admin 403 body becomes `missing_permission` |
|
||||
| test/activity_log_backend_http_test.go:280, 735; test/activity_filter_cards_and_export_http_test.go:75; test/activity_next_cleanup_test.go:199 | Message becomes `missing_permission`. The "editor" fixtures need a Writer or custom role_id. Admin retention read stays 200. |
|
||||
| test/user_roles_test.go:280; test/invite_management_test.go:410; test/api_keys_http_test.go:434, 573 | Admin invite is now allowed. Other Owner-required assertions become `missing_permission`. API key list and revoke fixtures need roles. |
|
||||
| test/seo_output_sitemap_http_test.go:167; test/seo_data_site_http_test.go; test/site_acceptance_test.go; test/authors_backend_http_test.go | Admin PATCH /v1/site/{id} is now allowed. Message assertions for non-admins become `missing_permission`. |
|
||||
| test/custom_content_types_test.go:131 | "member without a role" gets 403 `missing_permission`. The fixture needs an unresolvable or empty role. |
|
||||
| test/media_purge_http_test.go:160-167; test/media_purge_qa_test.go:31; test/image_variants_per_image_http_test.go:152-155; test/image_variants_cache_http_test.go:32-33; test/image_variants_admin_http_test.go:42-47 | The "editor", "plain" and "empty role" users need role_ids. Expected body becomes `missing_permission`. |
|
||||
| every test that builds users with a legacy `Role` and no `RoleID` | Add `RoleID` (or rely on the fallback for "owner" and "admin" only). Users with legacy roles like "editor" or "" are now unresolvable. |
|
||||
|
||||
## Existing features to amend or mark superseded
|
||||
|
||||
Edit the owner/admin 403 clauses in place or add a "superseded by roles_permissions_enforcement" note.
|
||||
|
||||
- roles_permissions_foundation (last Outline)
|
||||
- user_roles, invite_management, invitations
|
||||
- sites_backend, sites_authors_integration, seo_data_backend, seo_output_backend, seo_sites_articles_integration
|
||||
- authors_backend
|
||||
- activity-log-backend, activity_filter_cards_and_export, activity_home_card_and_retention
|
||||
- api_keys, integration_api_key_routes, integration
|
||||
- media_purge, media_cleanup_spa, image-variants, image_variants_cache, image_variants_per_image_api
|
||||
- updates_download_stage, updates_restart_helper, updates_release_feed, container_update_guidance
|
||||
- custom_content_types, custom_content_types_support
|
||||
|
||||
Also check the matching `.qa.md` files and these non-test texts: `web/src/data/apiCatalogue.ts` (OWNER_REQUIRED_MESSAGE, OWNER_OR_ADMIN_MESSAGE), `web/src/views/IntegrationView.vue` ("Owner and admin routes answer 403…") and anything under `internal/infrastructure/llmdocs` that documents the old messages. The SPA stores handle 403 by status only, so they should be unaffected; this is a manual check.
|
||||
|
||||
## Manual verification
|
||||
|
||||
- Start `task dev:api` on a copy of a migrated database. As the admin, check that settings are readable and software and retention updates return 403. As the owner, check everything still works.
|
||||
- Create an API key as an admin, change that user to Writer by editing `users.role_id` in the database copy, and confirm the same key now gets 403 on a schema write.
|
||||
- The SPA is untouched, so the admin will still see owner-only UI hidden. That is expected until the SPA slices.
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Roles and permissions foundation: catalogue, built-in roles, backfills and read endpoints
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As the owner of a RenewCMS site
|
||||
I want permissions to be a fixed catalogue and roles to be stored sets of (permission, scope)
|
||||
@@ -287,7 +288,7 @@ Feature: Roles and permissions foundation: catalogue, built-in roles, backfills
|
||||
Examples:
|
||||
| id | legacy | call | status |
|
||||
| 3 | owner | POST /v1/user/invite | allowed (not 403) |
|
||||
| 5 | admin | POST /v1/user/invite | 403 apperr.OwnerRequiredErrorMessage |
|
||||
| 5 | admin | POST /v1/user/invite | allowed (not 403) |
|
||||
| 5 | admin | POST /v1/content-type | allowed (not 403) |
|
||||
| 5 | admin | PATCH /v1/settings | 403 apperr.OwnerRequiredErrorMessage |
|
||||
| 5 | admin | POST /v1/system/update/check | 403 apperr.OwnerRequiredErrorMessage |
|
||||
| 5 | admin | PATCH /v1/settings | 403 apperr.MissingPermissionErrorMessage |
|
||||
| 5 | admin | POST /v1/system/update/check | 403 apperr.MissingPermissionErrorMessage |
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: roles_permissions_foundation
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Package: `test` (black-box). Name new fakes uniquely, for example `mockRoleRepo`, `rolesFoundationUserRepo`.
|
||||
Do not copy the old `Role ==` checks into new code.
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: SEO data backend, article and site search fields, unique slugs and media usage
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As an editor or the owner of a RenewCMS installation
|
||||
I want to store search and sharing data on articles and sites, with a slug that is unique per site
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: SEO data backend (slice 1 of 4)
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Spec: `features/seo_data_backend.feature`. Go tests only, in `test/` (package `test`), run with `task test`. No live server, no real SMTP, no real SQLite file outside the temp-dir repository-contract harness. Call `validator.InitValidator()` before any handler call. Error assertions compare the plain-text body to the exact token or constant named in the spec. The 409 `slug_taken` assertion checks the plain-text body `slug_taken` and the `X-Slug-Suggestion` header.
|
||||
|
||||
## Divergences from docs/design
|
||||
@@ -7,7 +9,7 @@ Spec: `features/seo_data_backend.feature`. Go tests only, in `test/` (package `t
|
||||
`docs/design` is read-only; the SPA slices (3 and 4) align to the code, not to these design words.
|
||||
|
||||
- **Site image and robots names:** the API uses `default_social_image_id` and `default_robots` (existing site contract). The design writes `social_image_id` for the site and, in places, `robots`. The SPA uses the code names.
|
||||
- **Non-owner site writes:** the API answers 403 with the existing text "Only the owner can do this." (`apperr.OwnerRequiredErrorMessage`). The design mentions a 403 `owner_only` token, which does not exist.
|
||||
- **Non-owner site writes:** the API answers 403 with the existing text "Only the owner can do this." (`apperr.MissingPermissionErrorMessage`). The design mentions a 403 `owner_only` token, which does not exist.
|
||||
- **Empty-slug fallback:** one rule everywhere. A title with no letter or digit becomes `article`, then `article-2`, `article-3` on conflict, in the API and in the migration. `docs/design/seo-slices.md` (decision 5) says `article-<id>` for the migration; that wording is not followed.
|
||||
- **Slug conflict transport:** the conflict follows the authors slug conflict (plain text `slug_taken`, 409); the suggestion travels in the `X-Slug-Suggestion` response header, not in a JSON body. The `slug.suggestion` of the slice 2 check endpoint is separate.
|
||||
- **Empty slug:** `slug: ""` regenerates the slug from the title (the SPA "Reset to match the title" sends `""`); 409 only if the regenerated slug belongs to another article on the site.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: SEO output backend, resolved SEO object, non-persisting check, list status and sitemap
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As a frontend developer or the RenewCMS back-office editor
|
||||
I want the resolved SEO data of an article, a non-persisting check, an SEO status on the article list and a sitemap per site
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: SEO output backend (slice 2 of 4)
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Spec: `features/seo_output_backend.feature`. Go tests only, in `test/` (package `test`), run with `task test`. No live server, no real SMTP, no real SQLite file outside the temp-dir repository-contract harness. Call `validator.InitValidator()` before any handler call. Error assertions compare the plain-text body to the exact token or constant named in the spec. Success bodies are JSON. Assert exact key sets by decoding into `map[string]any` and comparing sorted key lists.
|
||||
|
||||
## Divergences from docs/design
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: SEO round, slice 4: Sites defaults, Articles search status, Integration "Search and sharing" and suggestion dismissal
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As the owner of a RenewCMS install, and as anyone who edits articles or builds a frontend on the API
|
||||
I want to set the site-wide SEO defaults, see which articles need attention, dismiss suggestions I do not want, and read how the SEO routes work
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA: SEO round, slice 4 (Sites defaults, Articles search status, Integration, suggestion dismissal)
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
SPA slice only. No Go change here: the API of `features/seo_data_backend.feature` and `features/seo_output_backend.feature` already exists, and `counts.suggestions` is covered by the httptest part of `features/articles_pagination_api.qa.md`, so this file has no httptest part. A wrong contract is reported as a backend defect.
|
||||
|
||||
Roles: the domain defines only `owner` and `admin`. "verified" is a verified account without either role.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Sites and authors in the Editor, Articles and Profile screens
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As a RenewCMS user who can edit articles
|
||||
I want to choose an article's site and bylines in the editor, see them in the articles list, and create my own author profile
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA: Sites and authors in Editor, Articles and Profile
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
SPA slice only. No Go change: the API already exists (commits "Sites feature backend" and "Authors feature backend"). A wrong contract is reported as a backend defect.
|
||||
|
||||
Roles: the domain defines only `owner` and `admin` (`internal/domain/user`). There is no "member" constant. "verified" in the feature and below is a verified account without the owner or admin role, which has no author rights. The earlier `sites_authors_screens` files still say "member" for the same account; that wording is left as is.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Sites backend, the owner manages sites and every article belongs to exactly one
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As the owner of a RenewCMS installation
|
||||
I want to create, edit, delete and choose a default among several sites, and assign each article to one
|
||||
@@ -14,7 +15,7 @@ Feature: Sites backend, the owner manages sites and every article belongs to exa
|
||||
And GET /v1/site/ accepts a session or an API key; every write is session-only and goes through RequireOwner, like PATCH /v1/settings
|
||||
And PATCH /v1/article/{id} with site_id stays callable with an API key
|
||||
And error responses are plain text from http.Error whose body is exactly the code token: site_key_taken, site_has_articles, default_site, key_immutable, invalid_site_url, invalid_locale, site_not_found, image_not_found, use_set_default, invalid_reassign
|
||||
And a non-owner write answers 403 with the existing RequireOwner body "Only the owner can do this." (apperr.OwnerRequiredErrorMessage), with no owner_only token
|
||||
And a non-owner write answers 403 with the existing RequireOwner body "Only the owner can do this." (apperr.MissingPermissionErrorMessage), with no owner_only token
|
||||
And a request body that cannot be decoded answers 400 with apperr.BodyErrorMessage ("The request cannot be processed due to a mismatch in the format of the body."), a malformed id answers 400 with apperr.IdUint32ErrorMessage, and a validate-tag failure answers 400 with the validator error text, like POST /v1/article
|
||||
And a missing article answers 404 "Article not found" (apperr.ArticleNotFoundErrorMessage)
|
||||
And the roles are "owner" and "admin" only, and a "verified" account is one with is_verified true
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: Sites backend
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Spec: `features/sites_backend.feature`. Go tests only, in `test/` (package `test`), run with `task test`. No live server, no real SMTP, no real SQLite file outside the temp-dir repository-contract harness. Call `validator.InitValidator()` before any handler call. Every error assertion compares the plain-text body to the exact code token.
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Updates part 2: the Update button (download, verify, stage)
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As the owner of a RenewCMS site
|
||||
I want one button that downloads the new release, proves it is genuine, backs up the database and stages it
|
||||
@@ -100,7 +101,7 @@ Feature: Updates part 2: the Update button (download, verify, stage)
|
||||
|
||||
Scenario: Only the owner
|
||||
When an admin sends POST /v1/system/update/download
|
||||
Then the status is 403 and the body is apperr.OwnerRequiredErrorMessage
|
||||
Then the status is 403 and the body is apperr.MissingPermissionErrorMessage
|
||||
And no job starts and the fetcher is not called
|
||||
|
||||
Scenario Outline: POST is guarded like the other system routes
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA: Updates part 2 (download, verify, stage)
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
## Automated Go (`task test`, package `test`, black-box, no live network, no real SMTP)
|
||||
|
||||
Files: `test/update_download_test.go` (domain, use case), `test/update_download_http_test.go` (HTTP),
|
||||
@@ -29,7 +31,7 @@ the TLS cases, a small size cap and a short timeout. Never relax the policy by e
|
||||
5. Persistence: JSON job store round trip in a temp dir; "downloading" at startup reads failed/"download"; "ready" with the staged
|
||||
binary deleted reads idle; a missing or corrupt file reads idle without error. The real helper detector reads
|
||||
`<data dir>/update-helper.json`: absent file gives false, a present valid file gives true.
|
||||
6. Guards: admin 403 with `apperr.OwnerRequiredErrorMessage+"\n"`; no cookie 401; stale token 401; unverified 403; none start a job.
|
||||
6. Guards: admin 403 with `apperr.MissingPermissionErrorMessage+"\n"`; no cookie 401; stale token 401; unverified 403; none start a job.
|
||||
7. Status codes: available false or nothing stored gives 400; detector off gives 409; state downloading, verifying, ready each give
|
||||
409; after "failed" a new POST gives 202 with a new started_at. Order check: detector off and available false gives 409.
|
||||
8. Concurrency: two simultaneous POSTs (goroutines) give one 202 and one 409; each asset fetched once (run with `-race`).
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Updates part 1: signed releases, release feed and read-only update notification
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As the owner of a RenewCMS site
|
||||
I want to see when a newer signed release exists, and have admins see the same status read-only
|
||||
@@ -142,7 +143,7 @@ Feature: Updates part 1: signed releases, release feed and read-only update noti
|
||||
Scenario: An admin cannot check
|
||||
Given the caller is an admin
|
||||
When the client sends POST /v1/system/update/check
|
||||
Then the status is 403 and the body is apperr.OwnerRequiredErrorMessage
|
||||
Then the status is 403 and the body is apperr.MissingPermissionErrorMessage
|
||||
And the feed is not contacted
|
||||
|
||||
Scenario: Feed failure keeps the last result
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA: Updates part 1 (signed releases, release feed, notification)
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
## Automated Go (`task test`, package `test`, black-box)
|
||||
|
||||
Files: `test/update_test.go` (domain, use cases), `test/update_http_test.go` (HTTP), `test/update_feed_test.go`
|
||||
@@ -24,7 +26,7 @@ server, no SQLite, no SMTP.
|
||||
`apperr.VerificationRequiredErrorMessage`.
|
||||
6. `POST /v1/system/update/check` as owner: 200, store updated with latest, `release_url` and `checked_at`, feed called
|
||||
once, a later GET equals it. Nothing newer: `available` false. Running version "dev": `available` false.
|
||||
7. POST check as admin: 403, body `apperr.OwnerRequiredErrorMessage+"\n"`, feed not called.
|
||||
7. POST check as admin: 403, body `apperr.MissingPermissionErrorMessage+"\n"`, feed not called.
|
||||
8. Feed error: 500, store unchanged (latest and `checked_at` as before), body contains no feed error text.
|
||||
9. Rate limit: owner A sends 6 checks: first 5 are 200, the 6th is 429 with `apperr.TooManyRequestsErrorMessage+"\n"`
|
||||
and an integer `Retry-After` of at least 1; feed called 5 times; owner B's first check still 200; a login-limiter
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Updates part 3: Restart, the privileged helper and rollback
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As the owner of a RenewCMS site
|
||||
I want one Restart button that installs the staged release, or just restarts the site, and puts the old version back if the new one does not come up
|
||||
@@ -122,7 +123,7 @@ Feature: Updates part 3: Restart, the privileged helper and rollback
|
||||
| no jwt cookie | 401 | the existing 401 text |
|
||||
| a stale token version | 401 | the existing 401 text |
|
||||
| an unverified owner | 403 | the existing unverified text |
|
||||
| an admin | 403 | apperr.OwnerRequiredErrorMessage |
|
||||
| an admin | 403 | apperr.MissingPermissionErrorMessage |
|
||||
|
||||
Scenario Outline: Restart is refused with 409
|
||||
Given the owner and <condition>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA: Updates part 3 (restart, helper, rollback)
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
## Automated Go (`task test`, package `test`, black-box, no live systemd, no real SMTP, no network)
|
||||
|
||||
Files: `test/update_restart_http_test.go` (HTTP), `test/update_restart_usecase_test.go` (request and status use cases),
|
||||
@@ -41,7 +43,7 @@ plus the fakes where noted. Helper time is driven by `fakeClock`: no `time.Sleep
|
||||
"Restart site never installs": owner A's body `{"kind":"restart"}` after the fake job store turned "ready" gives 409, no marker, staged files untouched.
|
||||
4. Bad body: `{"kind":"upgrade"}`, `{"kind":""}`, `{"kind":7}`, truncated JSON, `not json` give 400 plain text, no marker. The struct uses
|
||||
`validate:"omitempty,oneof=update restart"` and goes through `validator.Validate.Struct`; an empty body and `{}` are accepted.
|
||||
5. Guards: admin 403 with `apperr.OwnerRequiredErrorMessage+"\n"`; no cookie 401; stale token 401; unverified 403; none writes a marker or deletes the result.
|
||||
5. Guards: admin 403 with `apperr.MissingPermissionErrorMessage+"\n"`; no cookie 401; stale token 401; unverified 403; none writes a marker or deletes the result.
|
||||
Order: an admin with a bad body gets 403, an owner with a bad body and can_apply off gets 400, can_apply off and a kind mismatch gives 409.
|
||||
6. 409 table: can_apply off, downloading, verifying, marker pending, result "running": no second marker, existing files untouched.
|
||||
7. Concurrency (`-race`): two simultaneous POSTs give one 202, one 409, one marker.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Feature: Users have a role, owner or admin, and only the owner can invite
|
||||
Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
As the person who registered the workspace
|
||||
I want my account to be the owner and everyone I invite to be an admin
|
||||
@@ -22,7 +23,7 @@ Feature: Users have a role, owner or admin, and only the owner can invite
|
||||
created_at; invite items are unchanged: id, kind, email, status, created_at
|
||||
And POST /v1/user/invite is owner-only: after the jwt, current-token and verified checks, a
|
||||
RequireOwner check loads the user with GetUserUseCase and refuses any role other than
|
||||
"owner" with 403 and the plain-text body apperr.OwnerRequiredErrorMessage
|
||||
"owner" with 403 and the plain-text body apperr.MissingPermissionErrorMessage
|
||||
"Only the owner can do this.", before the rate limiter, the body decoding and the validation
|
||||
And GET /v1/user/ stays open to every verified user, owner and admin alike
|
||||
And error responses are plain text from http.Error, success responses are JSON
|
||||
@@ -82,7 +83,7 @@ Feature: Users have a role, owner or admin, and only the owner can invite
|
||||
Given a verified admin id 2 "jonas" and a valid jwt cookie for user id 2
|
||||
When the client sends POST /v1/user/invite with body {"email": "sam.oliveira@renew.example"}
|
||||
Then the response status is 403
|
||||
And the response body is exactly apperr.OwnerRequiredErrorMessage, as plain text
|
||||
And the response body is exactly apperr.MissingPermissionErrorMessage, as plain text
|
||||
And the invite repository is still empty
|
||||
And no email was sent
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# QA procedure: user_roles
|
||||
|
||||
> Superseded in part by roles_permissions_enforcement: the owner and admin 403 clauses now answer 403 "missing_permission" with the header X-Missing-Permission, decided by the caller's role permissions. An Admin can now invite and write sites, and a role without the needed permission is refused on routes that were open to every verified user.
|
||||
|
||||
Package: `test` (black-box). Reuse `mockUserRepo`, `stubHasher`, `mockMailRepo`, `mockInviteRepo`.
|
||||
No live server, no real SQLite file, no real SMTP.
|
||||
|
||||
@@ -34,7 +36,7 @@ No live server, no real SQLite file, no real SMTP.
|
||||
|
||||
## POST /v1/user/invite
|
||||
13. Owner cookie, valid body -> 201, one invite, one "invitation" mail.
|
||||
14. Admin cookie, valid body -> 403, body exactly `apperr.OwnerRequiredErrorMessage`, `Content-Type`
|
||||
14. Admin cookie, valid body -> 403, body exactly `apperr.MissingPermissionErrorMessage`, `Content-Type`
|
||||
is not JSON-decoded (plain text), no invite, no mail.
|
||||
15. Admin cookie with `{"email":"not-an-email"}` and with `not json` -> 403 both.
|
||||
16. Rate limit 2: admin sends 3 requests -> 403, 403, 403.
|
||||
|
||||
Reference in new issue
Block a user