Why: System, role, and project entry permissions need explicit actions instead of a single administrator flag or unrestricted project creation. Evidence: The full Backend test suite passed, including role APIs, action isolation, project.create denial/allow, storage migration coverage, and account write fences. Impact: Adds versioned role and grant endpoints, maps account actions to explicit permissions, and requires project.create authority. Refs: Focused role and permission system task 3
135 lines
5.3 KiB
Go
135 lines
5.3 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type roleHTTPView struct {
|
|
RoleID string `json:"roleId"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Enabled bool `json:"enabled"`
|
|
RoleVersion int64 `json:"roleVersion"`
|
|
BindingCount int `json:"bindingCount"`
|
|
}
|
|
|
|
func decodeRoleHTTP(t *testing.T, rec *httptest.ResponseRecorder) roleHTTPView {
|
|
t.Helper()
|
|
var role roleHTTPView
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &role); err != nil {
|
|
t.Fatalf("decode role: %v body=%s", err, rec.Body.String())
|
|
}
|
|
return role
|
|
}
|
|
|
|
func TestRoleHTTPCreateSearchPatchPermissionAndAssignment(t *testing.T) {
|
|
srv, st := newAuthTestServer(t)
|
|
h := srv.Handler()
|
|
admin := bootAdmin(t, h, st)
|
|
|
|
if rec := do(h, "GET", "/api/admin/permissions/catalog", "", admin); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "system.roles.manage") {
|
|
t.Fatalf("catalog: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
createdRec := do(h, "POST", "/api/admin/roles", `{"name":"软件工程师","description":"负责软件研发"}`, admin)
|
|
if createdRec.Code != http.StatusOK {
|
|
t.Fatalf("create role: %d %s", createdRec.Code, createdRec.Body.String())
|
|
}
|
|
created := decodeRoleHTTP(t, createdRec)
|
|
if created.RoleID == "" || created.Description != "负责软件研发" || created.RoleVersion != 1 {
|
|
t.Fatalf("created=%+v", created)
|
|
}
|
|
|
|
list := do(h, "GET", "/api/admin/roles?query=软件工程师&mode=exact&status=enabled", "", admin)
|
|
if list.Code != http.StatusOK || !strings.Contains(list.Body.String(), created.RoleID) {
|
|
t.Fatalf("list role: %d %s", list.Code, list.Body.String())
|
|
}
|
|
patchedRec := do(h, "PATCH", "/api/admin/roles/"+created.RoleID,
|
|
`{"expectedRoleVersion":1,"description":"负责产品软件研发"}`, admin)
|
|
if patchedRec.Code != http.StatusOK {
|
|
t.Fatalf("patch role: %d %s", patchedRec.Code, patchedRec.Body.String())
|
|
}
|
|
patched := decodeRoleHTTP(t, patchedRec)
|
|
if patched.RoleID != created.RoleID || patched.Description != "负责产品软件研发" || patched.RoleVersion != 2 {
|
|
t.Fatalf("patched=%+v", patched)
|
|
}
|
|
permissions := do(h, "PUT", "/api/admin/roles/"+created.RoleID+"/permissions",
|
|
`{"expectedRoleVersion":2,"idempotencyKey":"role-policy-1","changes":[{"actionId":"project.create","allowed":true,"capAllowed":true}]}`, admin)
|
|
if permissions.Code != http.StatusOK {
|
|
t.Fatalf("permissions: %d %s", permissions.Code, permissions.Body.String())
|
|
}
|
|
|
|
userRec := do(h, "POST", "/api/admin/users", `{"owner":"alice","profile":{"name":"Alice"}}`, admin)
|
|
if userRec.Code != http.StatusOK {
|
|
t.Fatalf("create user: %d %s", userRec.Code, userRec.Body.String())
|
|
}
|
|
var user struct {
|
|
UserID string `json:"userId"`
|
|
StateVersion int64 `json:"stateVersion"`
|
|
}
|
|
if err := json.Unmarshal(userRec.Body.Bytes(), &user); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assign := do(h, "PUT", "/api/admin/users/"+user.UserID+"/role",
|
|
`{"roleId":"`+created.RoleID+`","expectedStateVersion":`+"1"+`,"idempotencyKey":"assign-alice"}`, admin)
|
|
if assign.Code != http.StatusOK || !strings.Contains(assign.Body.String(), created.RoleID) {
|
|
t.Fatalf("assign: %d %s", assign.Code, assign.Body.String())
|
|
}
|
|
get := do(h, "GET", "/api/admin/users/"+user.UserID+"/role", "", admin)
|
|
if get.Code != http.StatusOK || !strings.Contains(get.Body.String(), created.RoleID) {
|
|
t.Fatalf("get binding: %d %s", get.Code, get.Body.String())
|
|
}
|
|
list = do(h, "GET", "/api/admin/roles?query=软件&mode=fuzzy&status=all", "", admin)
|
|
if list.Code != http.StatusOK || !strings.Contains(list.Body.String(), `"bindingCount":1`) {
|
|
t.Fatalf("binding count: %d %s", list.Code, list.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestRoleHTTPUsesSpecificSystemActions(t *testing.T) {
|
|
srv, st := newAuthTestServer(t)
|
|
h := srv.Handler()
|
|
admin := bootAdmin(t, h, st)
|
|
if err := st.CreateAccount("role-manager", "Role Manager", "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager, err := st.GetAccountByOwner("role-manager")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
epoch, err := st.AuthorityEpoch()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
grant := do(h, "PUT", "/api/admin/users/"+manager.ID+"/system-grants",
|
|
`{"expectedAuthorityEpoch":`+strconv.FormatInt(epoch, 10)+`,"idempotencyKey":"grant-role-manager","actions":["system.roles.view","system.roles.manage"]}`, admin)
|
|
if grant.Code != http.StatusOK {
|
|
t.Fatalf("grant: %d %s", grant.Code, grant.Body.String())
|
|
}
|
|
key, err := st.CreateAPIKey("role-manager", "role-manager-test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := httptest.NewRequest("POST", "/api/admin/roles", strings.NewReader(`{"name":"测试职位"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+key)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("role manager create: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
role := decodeRoleHTTP(t, rec)
|
|
req = httptest.NewRequest("PUT", "/api/admin/roles/"+role.RoleID+"/permissions",
|
|
strings.NewReader(`{"expectedRoleVersion":1,"idempotencyKey":"forbidden-policy","changes":[{"actionId":"project.create","allowed":true,"capAllowed":true}]}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+key)
|
|
rec = httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("role manager changed permissions: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|