207 lines
9.1 KiB
Go
207 lines
9.1 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func bearerRequest(h http.Handler, method, path, body, key string) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(method, path, strings.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer "+key)
|
|
if body != "" {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func TestProjectCreateRequiresSystemOrRolePermission(t *testing.T) {
|
|
srv, st := newAuthTestServer(t)
|
|
h := srv.Handler()
|
|
admin := bootAdmin(t, h, st)
|
|
if err := st.CreateAccount("engineer", "Engineer", "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
engineer, _ := st.GetAccountByOwner("engineer")
|
|
key, err := st.CreateAPIKey("engineer", "engineer-test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec := bearerRequest(h, "POST", "/api/projects", `{"name":"未授权项目"}`, key); rec.Code != http.StatusForbidden {
|
|
t.Fatalf("project created without permission: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
roleRec := do(h, "POST", "/api/admin/roles", `{"name":"软件工程师"}`, admin)
|
|
if roleRec.Code != http.StatusOK {
|
|
t.Fatalf("create role: %d %s", roleRec.Code, roleRec.Body.String())
|
|
}
|
|
role := decodeRoleHTTP(t, roleRec)
|
|
policy := do(h, "PUT", "/api/admin/roles/"+role.RoleID+"/permissions",
|
|
`{"expectedRoleVersion":1,"idempotencyKey":"project-create-policy","changes":[{"actionId":"project.create","allowed":true,"capAllowed":true}]}`, admin)
|
|
if policy.Code != http.StatusOK {
|
|
t.Fatalf("set role permission: %d %s", policy.Code, policy.Body.String())
|
|
}
|
|
assign := do(h, "PUT", "/api/admin/users/"+engineer.ID+"/role",
|
|
`{"roleId":"`+role.RoleID+`","expectedStateVersion":`+strconv.FormatInt(engineer.StateVersion, 10)+`,"idempotencyKey":"assign-engineer"}`, admin)
|
|
if assign.Code != http.StatusOK {
|
|
t.Fatalf("assign engineer: %d %s", assign.Code, assign.Body.String())
|
|
}
|
|
if rec := bearerRequest(h, "POST", "/api/projects", `{"name":"授权项目"}`, key); rec.Code != http.StatusOK {
|
|
t.Fatalf("role project create: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestProjectCreateLinksOwnProjectManagementOverHTTP(t *testing.T) {
|
|
srv, st := newAuthTestServer(t)
|
|
h := srv.Handler()
|
|
admin := bootAdmin(t, h, st)
|
|
if err := st.CreateAccount("creator", "Creator", "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
creator, _ := st.GetAccountByOwner("creator")
|
|
key, err := st.CreateAPIKey("creator", "creator-test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
roleRec := do(h, "POST", "/api/admin/roles", `{"name":"项目创建者"}`, admin)
|
|
if roleRec.Code != http.StatusOK {
|
|
t.Fatalf("create role: %d %s", roleRec.Code, roleRec.Body.String())
|
|
}
|
|
role := decodeRoleHTTP(t, roleRec)
|
|
conflict := do(h, "PUT", "/api/admin/roles/"+role.RoleID+"/permissions",
|
|
`{"expectedRoleVersion":1,"idempotencyKey":"create-conflict","changes":[{"actionId":"project.create","allowed":true,"capAllowed":true},{"actionId":"project.members.manage","allowed":false,"capAllowed":false}]}`, admin)
|
|
if conflict.Code != http.StatusBadRequest {
|
|
t.Fatalf("linked conflict accepted: %d %s", conflict.Code, conflict.Body.String())
|
|
}
|
|
stale := do(h, "PUT", "/api/admin/roles/"+role.RoleID+"/permissions",
|
|
`{"expectedRoleVersion":9,"idempotencyKey":"create-stale","changes":[{"actionId":"project.create","allowed":true,"capAllowed":true}]}`, admin)
|
|
if stale.Code != http.StatusConflict {
|
|
t.Fatalf("stale role version: %d %s", stale.Code, stale.Body.String())
|
|
}
|
|
policy := do(h, "PUT", "/api/admin/roles/"+role.RoleID+"/permissions",
|
|
`{"expectedRoleVersion":1,"idempotencyKey":"create-only","changes":[{"actionId":"project.create","allowed":true,"capAllowed":true}]}`, admin)
|
|
if policy.Code != http.StatusOK || !strings.Contains(policy.Body.String(), `"linkedExpansion":["project.settings","project.members.manage","project.permissions.manage","document.duties.assign"]`) {
|
|
t.Fatalf("create-only policy: %d %s", policy.Code, policy.Body.String())
|
|
}
|
|
assign := do(h, "PUT", "/api/admin/users/"+creator.ID+"/role",
|
|
`{"roleId":"`+role.RoleID+`","expectedStateVersion":`+strconv.FormatInt(creator.StateVersion, 10)+`,"idempotencyKey":"assign-creator"}`, admin)
|
|
if assign.Code != http.StatusOK {
|
|
t.Fatalf("assign creator: %d %s", assign.Code, assign.Body.String())
|
|
}
|
|
me := bearerRequest(h, "GET", "/api/me", "", key)
|
|
var body struct {
|
|
Linked struct {
|
|
ProjectCreate bool `json:"projectCreate"`
|
|
Source string `json:"source"`
|
|
LinkedActions []string `json:"linkedActions"`
|
|
} `json:"linkedCapabilities"`
|
|
}
|
|
if me.Code != http.StatusOK || json.Unmarshal(me.Body.Bytes(), &body) != nil {
|
|
t.Fatalf("me: %d %s", me.Code, me.Body.String())
|
|
}
|
|
if !body.Linked.ProjectCreate || body.Linked.Source != "role" ||
|
|
strings.Join(body.Linked.LinkedActions, ",") != "project.settings,project.members.manage,project.permissions.manage,document.duties.assign" {
|
|
t.Fatalf("linked capabilities=%+v", body.Linked)
|
|
}
|
|
}
|
|
|
|
func TestSystemUserActionsDoNotImplyRoleOrPermissionManagement(t *testing.T) {
|
|
srv, st := newAuthTestServer(t)
|
|
h := srv.Handler()
|
|
admin := bootSuperAdmin(t, h, st)
|
|
if err := st.CreateAccount("viewer", "Viewer", "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
viewer, _ := st.GetAccountByOwner("viewer")
|
|
epoch, _ := st.AuthorityEpoch()
|
|
grant := do(h, "PUT", "/api/admin/users/"+viewer.ID+"/system-grants",
|
|
`{"expectedAuthorityEpoch":`+strconv.FormatInt(epoch, 10)+`,"idempotencyKey":"viewer-grant","actions":["system.users.view"]}`, admin)
|
|
if grant.Code != http.StatusOK {
|
|
t.Fatalf("grant viewer: %d %s", grant.Code, grant.Body.String())
|
|
}
|
|
key, err := st.CreateAPIKey("viewer", "viewer-test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec := bearerRequest(h, "GET", "/api/admin/users", "", key); rec.Code != http.StatusOK {
|
|
t.Fatalf("users.view denied: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if rec := bearerRequest(h, "POST", "/api/admin/users", `{"owner":"forbidden-user"}`, key); rec.Code != http.StatusForbidden {
|
|
t.Fatalf("users.view created account: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if rec := bearerRequest(h, "GET", "/api/admin/roles", "", key); rec.Code != http.StatusForbidden {
|
|
t.Fatalf("users.view read roles: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if rec := bearerRequest(h, "GET", "/api/me", "", key); rec.Code != http.StatusOK ||
|
|
!strings.Contains(rec.Body.String(), `"systemActions":["system.users.view"]`) {
|
|
t.Fatalf("me system actions: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if rec := bearerRequest(h, "GET", "/api/authz/explain?action=system.users.view", "", key); rec.Code != http.StatusOK {
|
|
t.Fatalf("explain: %d %s", rec.Code, rec.Body.String())
|
|
} else {
|
|
var decision struct {
|
|
Allowed bool `json:"allowed"`
|
|
Source string `json:"source"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &decision); err != nil || !decision.Allowed || decision.Source != "system-grant" {
|
|
t.Fatalf("decision=%+v err=%v", decision, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDelegatedUserActionsReachTransactionalStoreChecks(t *testing.T) {
|
|
srv, st := newAuthTestServer(t)
|
|
h := srv.Handler()
|
|
admin := bootSuperAdmin(t, h, st)
|
|
if err := st.CreateAccount("user-manager", "User Manager", "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager, err := st.GetAccountByOwner("user-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-user-manager","actions":["system.users.view","system.users.create","system.users.edit","system.users.disable"]}`, admin)
|
|
if grant.Code != http.StatusOK {
|
|
t.Fatalf("grant: %d %s", grant.Code, grant.Body.String())
|
|
}
|
|
key, err := st.CreateAPIKey("user-manager", "user-manager-test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
created := bearerRequest(h, "POST", "/api/admin/users", `{"owner":"managed-user","profile":{"name":"Managed"}}`, key)
|
|
if created.Code != http.StatusOK {
|
|
t.Fatalf("delegated create: %d %s", created.Code, created.Body.String())
|
|
}
|
|
var target struct {
|
|
UserID string `json:"userId"`
|
|
ProfileVersion int64 `json:"profileVersion"`
|
|
StateVersion int64 `json:"stateVersion"`
|
|
}
|
|
if err := json.Unmarshal(created.Body.Bytes(), &target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
search := bearerRequest(h, "GET", "/api/admin/users/search?query=Managed&mode=exact&status=enabled&fields=name&limit=20", "", key)
|
|
if search.Code != http.StatusOK || !strings.Contains(search.Body.String(), target.UserID) {
|
|
t.Fatalf("delegated search: %d %s", search.Code, search.Body.String())
|
|
}
|
|
patched := bearerRequest(h, "PATCH", "/api/admin/users/"+target.UserID+"/profile",
|
|
`{"expectedProfileVersion":`+strconv.FormatInt(target.ProfileVersion, 10)+`,"patch":{"department":"研发"}}`, key)
|
|
if patched.Code != http.StatusOK {
|
|
t.Fatalf("delegated edit: %d %s", patched.Code, patched.Body.String())
|
|
}
|
|
disabled := bearerRequest(h, "PUT", "/api/admin/users/"+target.UserID+"/state",
|
|
`{"expectedStateVersion":`+strconv.FormatInt(target.StateVersion, 10)+`,"enabled":false}`, key)
|
|
if disabled.Code != http.StatusOK {
|
|
t.Fatalf("delegated disable: %d %s", disabled.Code, disabled.Body.String())
|
|
}
|
|
}
|