refac: reamaining auth surface rewrite, T0
This commit is contained in:
1 parent
6d510d2805
commit
aa865af2e9
50 files changed
+3723
-149
No files matched your search
@@ -0,0 +1,165 @@
|
||||
Feature: Protected article routes and the offline-inclusive listing require a verified account
|
||||
|
||||
As the maintainer of RenewCMS
|
||||
I want every protected article route, and the "see offline articles too" behavior of
|
||||
GET /v1/article/, to require an account that has completed email verification, not merely
|
||||
a valid jwt session
|
||||
So that a user who registered but never confirmed their email cannot create, read, edit,
|
||||
publish, unpublish, delete or attach images to articles, and cannot see unpublished drafts
|
||||
through the listing endpoint, while a verified user's existing workflow is unchanged
|
||||
|
||||
Background:
|
||||
Given before this slice, the protected group inside NewArticleRouter (api/router.go) applies
|
||||
only jwtauth.Verifier then jwtauth.Authenticator to GET /v1/article/{id}, POST /v1/article/,
|
||||
DELETE /v1/article/{id}, PATCH /v1/article/{id}, PATCH /v1/article/{id}/publish,
|
||||
PATCH /v1/article/{id}/unpublish and POST /v1/article/{id}/image, so any caller with a
|
||||
validly signed, unexpired jwt cookie reaches the handler regardless of that account's
|
||||
is_verified status
|
||||
And this slice adds a third middleware, api/middleware.RequireVerifiedUser(getUser
|
||||
*useCases.GetUserUseCase), mounted after jwtauth.Authenticator in that same group, so the
|
||||
order becomes Verifier, Authenticator, RequireVerifiedUser for all seven routes above
|
||||
And RequireVerifiedUser reads user_id from the request's jwt claims using a new exported
|
||||
helper, api/pkg/claims.UserId(r *http.Request) (uint32, bool), which replaces the
|
||||
identical private userIdFromClaims method api/handlers/auth.Handler.Verify and
|
||||
CancelRegistration already used (features/account_verification.feature,
|
||||
features/cancel_pending_registration.feature); those two handlers are updated to call the
|
||||
shared helper, and their observable 401 behavior on a missing or non-numeric user_id claim
|
||||
is unchanged
|
||||
And when claims.UserId reports no usable user_id, RequireVerifiedUser answers 401 with
|
||||
apperr.UnauthorizedErrorMessage without calling GetUserUseCase, the same body and status the
|
||||
protected auth routes already use for the same condition
|
||||
And otherwise RequireVerifiedUser calls GetUserUseCase.GetUser(userId); when that returns
|
||||
user.ErrUserNotFound (a token minted for an account since deleted), it answers 401 with
|
||||
apperr.UnauthorizedErrorMessage; when it returns any other error, it answers 500 with body
|
||||
"Internal server error"; when it returns a user whose IsVerified is false, it answers 403
|
||||
with a new apperr.VerificationRequiredErrorMessage constant, "You must verify your account
|
||||
to access this resource."; when IsVerified is true, it calls next.ServeHTTP unchanged, so
|
||||
the wrapped handler runs exactly as it did before this slice
|
||||
And this slice deletes the dead api/handlers/auth.Handler.IsVerified method: it was never
|
||||
wired to any route, and it performed the bare token.PrivateClaims()["user_id"].(float64)
|
||||
assertion this slice avoids everywhere else; RequireVerifiedUser is the behavior it was
|
||||
meant to provide, so it is removed rather than kept unused
|
||||
And this slice also changes GET /v1/article/, which sits under jwtauth.Verifier only (no
|
||||
Authenticator, per features/public_article_listing.feature) and therefore is never rejected
|
||||
outright: article.Handler gains a GetUserUseCase field, wired from container.GetUserUseCase
|
||||
in api/router.go's NewArticleRouter, and Handler.ListArticles now calls ListAllArticles only
|
||||
when the request carries a resolvable user_id (via the same claims.UserId helper) whose
|
||||
GetUserUseCase.GetUser lookup succeeds and returns IsVerified true; any other case (no jwt,
|
||||
malformed jwt, unresolvable user_id, deleted account, or an account that exists but is not
|
||||
yet verified) falls back to ListOnlineArticles, the same fallback
|
||||
features/public_article_listing.feature already documents for a malformed cookie; this
|
||||
listing route never answers 401 or 403 for an unverified caller, it just narrows the result
|
||||
And a repository failure while ListArticles resolves the caller (GetUserUseCase.GetUser
|
||||
returning an error other than user.ErrUserNotFound) is treated the same as "not verified"
|
||||
for the purpose of choosing which list to show, so the listing still answers 200 with the
|
||||
online-only array rather than failing the request; only a failure inside ListOnlineArticles
|
||||
or ListAllArticles itself still answers 500, unchanged from features/public_article_listing.feature
|
||||
|
||||
Scenario Outline: An authenticated but unverified user is refused on every protected article route
|
||||
Given a user repository containing user id 1, is_verified false
|
||||
And a valid jwt cookie for user id 1
|
||||
And the article repository contains an article with id 1 titled "Backyard Renovation Update"
|
||||
When the client sends <method> <path> with that cookie
|
||||
Then the response status is 403
|
||||
And the response body is the apperr.VerificationRequiredErrorMessage text
|
||||
|
||||
Examples:
|
||||
| method | path |
|
||||
| GET | /v1/article/1 |
|
||||
| POST | /v1/article/ |
|
||||
| DELETE | /v1/article/1 |
|
||||
| PATCH | /v1/article/1 |
|
||||
| PATCH | /v1/article/1/publish |
|
||||
| PATCH | /v1/article/1/unpublish |
|
||||
|
||||
Scenario: An unverified user is also refused when attaching an image
|
||||
Given a user repository containing user id 1, is_verified false
|
||||
And a valid jwt cookie for user id 1
|
||||
And the article repository contains an article with id 1 titled "Backyard Renovation Update"
|
||||
When the client sends POST /v1/article/1/image with that cookie and a multipart form field
|
||||
"file" holding a file named "before.png" with content type "image/png"
|
||||
Then the response status is 403
|
||||
And the response body is the apperr.VerificationRequiredErrorMessage text
|
||||
And the image repository's Create is never called
|
||||
|
||||
Scenario: A verified user still exercises the protected routes exactly as before
|
||||
Given a user repository containing user id 1, is_verified true
|
||||
And a valid jwt cookie for user id 1
|
||||
When the client sends POST /v1/article/ with that cookie and body
|
||||
{"title": "Valid Title", "body": "Some content"}
|
||||
Then the response status is 201
|
||||
And the response body is the created article as JSON
|
||||
|
||||
Scenario Outline: A token that resolves to no usable account still answers 401, never 403
|
||||
Given a user repository containing user id 1, is_verified false
|
||||
And <token description>
|
||||
And the article repository contains an article with id 1 titled "Backyard Renovation Update"
|
||||
When the client sends POST /v1/article/ with that cookie and body
|
||||
{"title": "Valid Title", "body": "Some content"}
|
||||
Then the response status is 401
|
||||
And the response body is the apperr.UnauthorizedErrorMessage text
|
||||
|
||||
Examples:
|
||||
| token description |
|
||||
| a validly signed jwt cookie whose claims carry no "user_id" key at all |
|
||||
| a validly signed jwt cookie whose "user_id" claim is the string "one" |
|
||||
| a validly signed jwt cookie whose "user_id" claim is the number 999, which does not match any user |
|
||||
|
||||
Scenario Outline: A request with no jwt cookie is still refused with 401, never 403
|
||||
Given no jwt cookie is sent
|
||||
And the article repository contains an article with id 1 titled "Backyard Renovation Update"
|
||||
When the client sends <method> <path> without a cookie
|
||||
Then the response status is 401
|
||||
|
||||
Examples:
|
||||
| method | path |
|
||||
| GET | /v1/article/1 |
|
||||
| POST | /v1/article/ |
|
||||
|
||||
Scenario: A repository failure while resolving the caller's verification status answers 500
|
||||
Given a valid jwt cookie for user id 1
|
||||
And the user repository fails on its next call to Get with an error other than
|
||||
user.ErrUserNotFound
|
||||
And the article repository contains an article with id 1 titled "Backyard Renovation Update"
|
||||
When the client sends DELETE /v1/article/1 with that cookie
|
||||
Then the response status is 500
|
||||
And the response body is the text "Internal server error"
|
||||
And the article repository's article with id 1 still exists
|
||||
|
||||
Scenario: Listing falls back to the online-only list for an authenticated but unverified caller
|
||||
Given a user repository containing user id 1, is_verified false
|
||||
And a valid jwt cookie for user id 1
|
||||
And the article repository contains an online article titled "Backyard Renovation Update" with id 1
|
||||
And the same repository contains an offline article titled "Draft: Kitchen Remodel" with id 2
|
||||
When the client sends GET /v1/article/ with that cookie
|
||||
Then the response status is 200
|
||||
And the response body is a JSON array containing exactly the article with id 1
|
||||
And the article with id 2 is absent from the response body
|
||||
|
||||
Scenario: Listing still returns every article for an authenticated and verified caller
|
||||
Given a user repository containing user id 1, is_verified true
|
||||
And a valid jwt cookie for user id 1
|
||||
And the article repository contains an online article titled "Backyard Renovation Update" with id 1
|
||||
And the same repository contains an offline article titled "Draft: Kitchen Remodel" with id 2
|
||||
When the client sends GET /v1/article/ with that cookie
|
||||
Then the response status is 200
|
||||
And the response body is a JSON array containing both the article with id 1 and the article with id 2
|
||||
|
||||
Scenario: Listing falls back to the online-only list when the caller's account no longer exists
|
||||
Given the user repository contains no user with id 999
|
||||
And a valid jwt cookie for user id 999
|
||||
And the article repository contains an online article titled "Backyard Renovation Update" with id 1
|
||||
And the same repository contains an offline article titled "Draft: Kitchen Remodel" with id 2
|
||||
When the client sends GET /v1/article/ with that cookie
|
||||
Then the response status is 200
|
||||
And the response body is a JSON array containing exactly the article with id 1
|
||||
|
||||
Scenario: Listing falls back to the online-only list rather than failing when the user lookup errors
|
||||
Given a valid jwt cookie for user id 1
|
||||
And the user repository fails on its next call to Get with an error other than
|
||||
user.ErrUserNotFound
|
||||
And the article repository contains an online article titled "Backyard Renovation Update" with id 1
|
||||
And the same repository contains an offline article titled "Draft: Kitchen Remodel" with id 2
|
||||
When the client sends GET /v1/article/ with that cookie
|
||||
Then the response status is 200
|
||||
And the response body is a JSON array containing exactly the article with id 1
|
||||
Reference in new issue
Block a user