Files

188 lines
8.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())
}
}
type roleProjectAccessHTTPView struct {
RoleVersion int64 `json:"roleVersion"`
ProjectAccess bool `json:"projectAccess"`
Permissions []struct {
ActionID string `json:"actionId"`
Allowed bool `json:"allowed"`
CapAllowed bool `json:"capAllowed"`
} `json:"permissions"`
}
func decodeRoleProjectAccess(t *testing.T, rec *httptest.ResponseRecorder) roleProjectAccessHTTPView {
t.Helper()
var view roleProjectAccessHTTPView
if err := json.Unmarshal(rec.Body.Bytes(), &view); err != nil {
t.Fatalf("decode role access: %v body=%s", err, rec.Body.String())
}
return view
}
// requireRoleProjectAccess asserts the role endpoint exposes exactly the
// normalized project-access boolean and nothing else.
func requireRoleProjectAccess(t *testing.T, rec *httptest.ResponseRecorder, version int64, want bool) {
t.Helper()
if rec.Code != http.StatusOK {
t.Fatalf("read role: %d %s", rec.Code, rec.Body.String())
}
view := decodeRoleProjectAccess(t, rec)
if view.RoleVersion != version || view.ProjectAccess != want || len(view.Permissions) != 1 {
t.Fatalf("role access=%+v want version=%d access=%v", view, version, want)
}
permission := view.Permissions[0]
if permission.ActionID != "project.create" || permission.Allowed != want || permission.CapAllowed != want {
t.Fatalf("role permission=%+v want=%v", permission, want)
}
}
func TestRoleHTTPUsesSpecificSystemActions(t *testing.T) {
srv, st := newAuthTestServer(t)
h := srv.Handler()
admin := bootSuperAdmin(t, h, st)
managerKey := grantSystemActionsFixture(t, h, st, admin, "role-manager", "system.roles.view", "system.roles.manage")
viewerKey := grantSystemActionsFixture(t, h, st, admin, "role-viewer", "system.roles.view")
rec := bearerRequest(h, "POST", "/api/admin/roles", `{"name":"测试职位"}`, managerKey)
if rec.Code != http.StatusOK {
t.Fatalf("role manager create: %d %s", rec.Code, rec.Body.String())
}
role := decodeRoleHTTP(t, rec)
if created := decodeRoleProjectAccess(t, rec); created.ProjectAccess || len(created.Permissions) != 0 {
t.Fatalf("new role has project access: %+v", created)
}
rec = bearerRequest(h, "PUT", "/api/admin/roles/"+role.RoleID+"/permissions",
`{"expectedRoleVersion":1,"idempotencyKey":"manager-policy","changes":[{"actionId":"project.create","allowed":true,"capAllowed":true}]}`, managerKey)
if rec.Code != http.StatusOK {
t.Fatalf("role manager saved project access: %d %s", rec.Code, rec.Body.String())
}
requireRoleProjectAccess(t, bearerRequest(h, "GET", "/api/admin/roles/"+role.RoleID, "", managerKey), 2, true)
rec = bearerRequest(h, "PUT", "/api/admin/roles/"+role.RoleID+"/permissions",
`{"expectedRoleVersion":2,"idempotencyKey":"manager-system-row","changes":[{"actionId":"project.create","allowed":false,"capAllowed":false},{"actionId":"system.settings.write","allowed":true,"capAllowed":true}]}`, managerKey)
if rec.Code != http.StatusBadRequest {
t.Fatalf("role batch with system row: %d %s", rec.Code, rec.Body.String())
}
requireRoleProjectAccess(t, bearerRequest(h, "GET", "/api/admin/roles/"+role.RoleID, "", managerKey), 2, true)
requireRoleProjectAccess(t, bearerRequest(h, "GET", "/api/admin/roles/"+role.RoleID, "", viewerKey), 2, true)
if rec := bearerRequest(h, "GET", "/api/admin/roles", "", viewerKey); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"projectAccess":true`) {
t.Fatalf("role viewer list: %d %s", rec.Code, rec.Body.String())
}
rec = bearerRequest(h, "PUT", "/api/admin/roles/"+role.RoleID+"/permissions",
`{"expectedRoleVersion":2,"idempotencyKey":"viewer-policy","changes":[]}`, viewerKey)
if rec.Code != http.StatusForbidden {
t.Fatalf("role viewer changed project access: %d %s", rec.Code, rec.Body.String())
}
if rec := bearerRequest(h, "POST", "/api/admin/roles", `{"name":"查看者职位"}`, viewerKey); rec.Code != http.StatusForbidden {
t.Fatalf("role viewer created role: %d %s", rec.Code, rec.Body.String())
}
requireRoleProjectAccess(t, bearerRequest(h, "GET", "/api/admin/roles/"+role.RoleID, "", managerKey), 2, true)
viewer, err := st.GetAccountByOwner("role-viewer")
if err != nil {
t.Fatal(err)
}
epoch, err := st.AuthorityEpoch()
if err != nil {
t.Fatal(err)
}
rec = bearerRequest(h, "PUT", "/api/admin/users/"+viewer.ID+"/system-grants",
`{"expectedAuthorityEpoch":`+strconv.FormatInt(epoch, 10)+`,"idempotencyKey":"manager-grants","actions":["project.create"]}`, managerKey)
if rec.Code != http.StatusForbidden {
t.Fatalf("role manager changed system grants: %d %s", rec.Code, rec.Body.String())
}
if actions, _, err := st.SystemGrantsForUser(viewer.ID); err != nil || len(actions) != 1 || actions[0] != "system.roles.view" {
t.Fatalf("viewer grants=%v err=%v", actions, err)
}
}