feat: add Role (owner|admin) to the user domain, persistence model and mapper
This commit is contained in:
1 parent
162fea4afb
commit
461e93e3b5
30 files changed
+791
-48
No files matched your search
@@ -0,0 +1,114 @@
|
||||
Feature: Users have a role, owner or admin, and only the owner can invite
|
||||
|
||||
As the person who registered the workspace
|
||||
I want my account to be the owner and everyone I invite to be an admin
|
||||
So that only I can invite people, while every admin keeps the same access to articles,
|
||||
the users list, their own profile and Settings
|
||||
|
||||
Background:
|
||||
Given a user has a role that is exactly "owner" or "admin", stored on the user record and read
|
||||
from the user repository on every request, never from the jwt claims
|
||||
And the first registered user is the owner: CreateUser creates the user with role "owner",
|
||||
and registration is only possible while no verified user exists
|
||||
And every user created by accepting an invite is an admin: AcceptInvite creates the user with
|
||||
role "admin", whatever the inviter's role
|
||||
And there is exactly one owner and no endpoint changes, transfers or removes a role
|
||||
And AutoMigrate adds the role column to existing databases and backfills it: the user with the
|
||||
lowest id becomes "owner" and every other user "admin"; a database that already has roles
|
||||
is left unchanged by a later start
|
||||
And the public user shape (api/pkg/publicuser) is id, username, email, is_verified, created_at,
|
||||
updated_at, display_name and role
|
||||
And the users list user items are id, kind, username, display_name, email, role, status,
|
||||
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
|
||||
"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
|
||||
|
||||
Scenario: The first registered user is the owner
|
||||
Given the user repository is empty
|
||||
When the client sends POST /v1/auth/register with username "marta", password "hunter2example12",
|
||||
email "marta@renew.example"
|
||||
Then the response status is 200
|
||||
And the user repository holds one user with username "marta" and role "owner"
|
||||
|
||||
Scenario: A user created by accepting an invite is an admin
|
||||
Given a verified owner "marta" and a pending invite for "lea.martin@renew.example" with a known raw code
|
||||
When the client sends POST /v1/auth/invite/accept with that code, username "lea" and password "limewash2026"
|
||||
Then the response status is 200
|
||||
And the user repository holds user "lea" with role "admin"
|
||||
And user "marta" still has role "owner"
|
||||
|
||||
Scenario Outline: GET /v1/auth/me returns the role of the session user
|
||||
Given a verified user "<username>" with role "<role>" and a valid jwt cookie for that user
|
||||
When the client sends GET /v1/auth/me
|
||||
Then the response status is 200
|
||||
And the response body has the key role with value "<role>"
|
||||
|
||||
Examples:
|
||||
| username | role |
|
||||
| marta | owner |
|
||||
| lea | admin |
|
||||
|
||||
Scenario: An unverified owner sees their role on /auth/me
|
||||
Given an unverified user "bob" with role "owner" and a valid jwt cookie
|
||||
When the client sends GET /v1/auth/me
|
||||
Then the response status is 200
|
||||
And the response body's role is "owner" and is_verified is false
|
||||
|
||||
Scenario: The users list carries email and role on accepted users
|
||||
Given verified owner id 1 "marta" with email "marta@renew.example"
|
||||
And admin id 2 "jonas" with email "jonas@renew.example"
|
||||
And admin id 3 "ines" with display_name "Inès Duarte" and email "ines.duarte@renew.example"
|
||||
And a pending invite for "sam.oliveira@renew.example"
|
||||
When the client sends GET /v1/user/ with a valid jwt cookie for user id 2
|
||||
Then the response status is 200
|
||||
And the user items have exactly the keys id, kind, username, display_name, email, role, status, created_at
|
||||
And user 1 has email "marta@renew.example" and role "owner"; users 2 and 3 have role "admin" and their own email
|
||||
And the invite item has exactly the keys id, kind, email, status, created_at
|
||||
And no item has a key named password, password_reset_code, verification_code,
|
||||
verification_expiration, password_reset_expiration, token_version, code, hashed_code,
|
||||
invited_by or accepted_at
|
||||
|
||||
Scenario: The owner can invite
|
||||
Given a verified owner id 1 "marta" and a valid jwt cookie for user id 1
|
||||
When the client sends POST /v1/user/invite with body {"email": "sam.oliveira@renew.example"}
|
||||
Then the response status is 201
|
||||
And one invite exists and one "invitation" email was sent
|
||||
|
||||
Scenario: An admin cannot 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 invite repository is still empty
|
||||
And no email was sent
|
||||
|
||||
Scenario Outline: The owner check comes before validation and the rate limiter
|
||||
Given a verified admin id 2 and a valid jwt cookie for user id 2
|
||||
When the client sends POST /v1/user/invite with body <body>
|
||||
Then the response status is 403
|
||||
And no invite was created and no email was sent
|
||||
|
||||
Examples:
|
||||
| body |
|
||||
| {"email": "not-an-email"} |
|
||||
| not json |
|
||||
|
||||
Scenario: An admin's refused invites do not use up the rate limit
|
||||
Given AUTH_RATE_LIMIT_MAX_ATTEMPTS is 2 for a window covering the whole test
|
||||
When an admin at IP "203.0.113.20" sends 3 POST /v1/user/invite requests
|
||||
Then all 3 responses are 403 and none is 429
|
||||
|
||||
Scenario: The role is read on every request, not from the token
|
||||
Given a verified user id 2 whose stored role is "owner", with a valid jwt cookie
|
||||
And the stored role changes to "admin" without the token changing
|
||||
When the client sends POST /v1/user/invite with a valid body
|
||||
Then the response status is 403
|
||||
|
||||
Scenario: An admin can still list users
|
||||
Given a verified admin id 2 and a valid jwt cookie for user id 2
|
||||
When the client sends GET /v1/user/
|
||||
Then the response status is 200
|
||||
Reference in new issue
Block a user