feat: more role backend and admin/writer roles additions and adjustments

This commit is contained in:
Florian Sylvain committed 2026-10-26 18:15:26 +01:00
1 parent 8c6c6456da
commit de142f6c5f
131 files changed
+2663 -435

No files matched your search

+1
View File
@@ -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
+2
View File
@@ -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.
+3 -2
View File
@@ -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
+4 -2
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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"
+2
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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
+7 -6
View File
@@ -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
+2
View File
@@ -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
+3 -2
View File
@@ -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 |
+2
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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`)
+2 -1
View File
@@ -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
+3 -1
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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.
+10 -9
View File
@@ -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>
+2
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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.
+2 -1
View File
@@ -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
+2
View File
@@ -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
+2 -1
View File
@@ -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
+3 -1
View File
@@ -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`).
+2 -1
View File
@@ -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
+3 -1
View File
@@ -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
+2 -1
View File
@@ -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>
+3 -1
View File
@@ -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.
+3 -2
View File
@@ -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
+3 -1
View File
@@ -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.