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

+38 -16
View File
@@ -8,6 +8,7 @@ import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/update"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/config"
@@ -116,6 +117,7 @@ func newAPIKeyEnvRecording(t *testing.T, recorder audit.Recorder) *apiKeyEnv {
GetUserUseCase: useCases.NewGetUserUseCase(env.users),
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
GetMyAccessUseCase: useCases.NewGetMyAccessUseCase(env.roles),
CheckPermissionUseCase: useCases.NewCheckPermissionUseCase(env.roles),
ListPermissionsUseCase: useCases.NewListPermissionsUseCase(),
CreateUserUseCase: useCases.NewCreateUserUseCase(env.users, hasher, mail, "https://cms.example.com", recorder),
AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(env.users, hasher, recorder),
@@ -145,15 +147,19 @@ func newAPIKeyEnvRecording(t *testing.T, recorder audit.Recorder) *apiKeyEnv {
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(env.feed, env.updates, apiKeyRunningVersion, clock, recorder),
CreateAPIKeyUseCase: useCases.NewCreateAPIKeyUseCase(env.keys, env.generator, clock, recorder),
ListAPIKeysUseCase: useCases.NewListAPIKeysUseCase(env.keys, env.users, clock),
RevokeAPIKeyUseCase: useCases.NewRevokeAPIKeyUseCase(env.keys, env.users, clock, recorder),
RevokeAPIKeyUseCase: useCases.NewRevokeAPIKeyUseCase(env.keys, clock, recorder),
AuthenticateAPIKeyUseCase: useCases.NewAuthenticateAPIKeyUseCase(env.keys, env.generator, env.users, clock),
}
router := chi.NewRouter()
router.Mount("/v1", api.InitBackendRoutes(env.container, env.settings))
env.router = router
env.rebuildRouter()
return env
}
func (e *apiKeyEnv) rebuildRouter() {
router := chi.NewRouter()
router.Mount("/v1", api.InitBackendRoutes(e.container, e.settings))
e.router = router
}
func (e *apiKeyEnv) issueKey(userId uint32, name string, mutate ...func(*apikey.APIKey)) (string, uint32) {
e.issued++
raw, prefix, hashed := e.generator.Generate()
@@ -361,7 +367,7 @@ func TestAPIKeysHttp(t *testing.T) {
if len(list) != 1 {
t.Fatalf("expected one entry, got %d", len(list))
}
apiKeyAssertExactFields(t, list[0], "id", "user_id", "name", "prefix", "status", "created_at", "last_used_at", "expires_at", "revoked_at")
apiKeyAssertExactFields(t, list[0], "id", "user_id", "name", "prefix", "status", "created_at", "last_used_at", "expires_at", "revoked_at", "created_by_name")
if strings.Contains(rec.Body.String(), raw) || strings.Contains(rec.Body.String(), env.keys.sorted()[0].HashedKey) {
t.Errorf("neither the raw key nor its hash may appear in the body")
}
@@ -431,7 +437,10 @@ func TestAPIKeysHttp(t *testing.T) {
}
raw := apiKeyDecodeObject(t, rec)["key"].(string)
invite := env.withKey(t, http.MethodPost, "/v1/user/invite", `{"email": "new@example.com"}`, raw)
assertAPIKeyMessage(t, invite, http.StatusForbidden, apperr.OwnerRequiredErrorMessage)
assertAPIKeyStatus(t, invite, http.StatusCreated)
if len(env.invites.invites) != 1 || env.invites.invites[1].InvitedBy != apiKeyDevId {
t.Errorf("expected an invite invited by the key's creator, got %+v", env.invites.invites)
}
})
t.Run("Key management needs a verified real session", func(t *testing.T) {
@@ -564,13 +573,14 @@ func TestAPIKeysHttp(t *testing.T) {
}
})
t.Run("An admin's key can not do owner-only things", func(t *testing.T) {
t.Run("A writer's key can not invite", func(t *testing.T) {
env := newAPIKeyEnv(t)
raw, _ := env.issueKey(apiKeyDevId, "ci")
env.addUser(5, "", writerRoleID, true)
raw, _ := env.issueKey(5, "ci")
rec := env.withKey(t, http.MethodPost, "/v1/user/invite", `{"email": "new@example.com"}`, raw)
assertAPIKeyMessage(t, rec, http.StatusForbidden, apperr.OwnerRequiredErrorMessage)
assertAPIKeyMessage(t, rec, http.StatusForbidden, apperr.MissingPermissionErrorMessage)
if len(env.invites.invites) != 0 {
t.Errorf("expected no invite")
}
@@ -647,7 +657,8 @@ func TestAPIKeysHttp(t *testing.T) {
t.Run("A Bearer key wins over the cookie, and a bad key is not rescued by a good cookie", func(t *testing.T) {
env := newAPIKeyEnv(t)
raw, _ := env.issueKey(apiKeyPatId, "ci")
env.addUser(5, "", writerRoleID, true)
raw, _ := env.issueKey(5, "ci")
invite := env.send(t, apiKeyRequest{method: http.MethodPost, path: "/v1/user/invite", body: `{"email": "new@example.com"}`, bearer: raw, cookieUser: apiKeyOwnerId})
assertAPIKeyStatus(t, invite, http.StatusForbidden)
@@ -854,7 +865,7 @@ func TestAPIKeysHttp(t *testing.T) {
}
})
t.Run("Listing as an admin shows only that admin's keys", func(t *testing.T) {
t.Run("Listing as an admin shows every user's keys", func(t *testing.T) {
env := newAPIKeyEnv(t)
env.issueKey(apiKeyDevId, "a")
env.issueKey(apiKeyDevId, "b")
@@ -863,8 +874,8 @@ func TestAPIKeysHttp(t *testing.T) {
rec := env.withCookie(t, http.MethodGet, "/v1/api-key", "", apiKeyDevId)
assertAPIKeyStatus(t, rec, http.StatusOK)
if got := strings.Join(apiKeyEntryNames(t, rec), ","); got != "b,a" {
t.Errorf("expected exactly b and a, got %s", got)
if got := strings.Join(apiKeyEntryNames(t, rec), ","); got != "c,b,a" {
t.Errorf("expected c, b and a, got %s", got)
}
})
@@ -957,16 +968,26 @@ func TestAPIKeysHttp(t *testing.T) {
}
})
t.Run("An admin can't revoke someone else's key, and learns nothing about it", func(t *testing.T) {
t.Run("An own-scope caller can't revoke someone else's key, and learns nothing about it", func(t *testing.T) {
env := newAPIKeyEnv(t)
env.customRoleUser(6, role.Grant{Key: "apikeys.revoke", Scope: role.ScopeOwn})
raw, id := env.issueKey(apiKeyPatId, "ci")
rec := env.withCookie(t, http.MethodDelete, "/v1/api-key/"+strconv.Itoa(int(id)), "", apiKeyDevId)
rec := env.withCookie(t, http.MethodDelete, "/v1/api-key/"+strconv.Itoa(int(id)), "", 6)
assertAPIKeyMessage(t, rec, http.StatusNotFound, apperr.APIKeyNotFoundErrorMessage)
assertAPIKeyStatus(t, env.withKey(t, http.MethodGet, "/v1/settings", "", raw), http.StatusOK)
})
t.Run("An admin revokes someone else's key", func(t *testing.T) {
env := newAPIKeyEnv(t)
raw, id := env.issueKey(apiKeyPatId, "ci")
assertAPIKeyStatus(t, env.withCookie(t, http.MethodDelete, "/v1/api-key/"+strconv.Itoa(int(id)), "", apiKeyDevId), http.StatusNoContent)
assertAPIKeyStatus(t, env.withKey(t, http.MethodGet, "/v1/settings", "", raw), http.StatusUnauthorized)
})
t.Run("The owner can revoke any key", func(t *testing.T) {
env := newAPIKeyEnv(t)
raw, id := env.issueKey(apiKeyPatId, "ci")
@@ -1056,8 +1077,9 @@ func TestAPIKeysHttp(t *testing.T) {
t.Errorf("expected no stored key")
}
env.customRoleUser(6, role.Grant{Key: "apikeys.view", Scope: role.ScopeOwn})
env.keys.fail.failNext("ListByUser")
assertAPIKeyMessage(t, env.withCookie(t, http.MethodGet, "/v1/api-key", "", apiKeyDevId), http.StatusInternalServerError, "Internal server error")
assertAPIKeyMessage(t, env.withCookie(t, http.MethodGet, "/v1/api-key", "", 6), http.StatusInternalServerError, "Internal server error")
env.keys.fail.failNext("ListAll")
assertAPIKeyMessage(t, env.withCookie(t, http.MethodGet, "/v1/api-key", "", apiKeyOwnerId), http.StatusInternalServerError, "Internal server error")