agent-exec transport 5231be3441402d174e41c888e044ef05/78ce1b6b9dc4
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"agent-system-backend/internal/authz/catalog"
|
||||
"agent-system-backend/internal/store"
|
||||
)
|
||||
|
||||
@@ -57,7 +58,12 @@ func TestProjectCreateRequiresSystemOrRolePermission(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectCreateLinksOwnProjectManagementOverHTTP(t *testing.T) {
|
||||
// TestProjectCreateIsTheOnlyRolePolicyOverHTTP pins the narrowed role model
|
||||
// (R2): a role carries the single project.create boolean, a batch naming any
|
||||
// other action is refused outright, no linked rows are expanded on write, and
|
||||
// the /api/me own-project link is a catalog projection of that boolean rather
|
||||
// than something the role stored.
|
||||
func TestProjectCreateIsTheOnlyRolePolicyOverHTTP(t *testing.T) {
|
||||
srv, st := newAuthTestServer(t)
|
||||
h := srv.Handler()
|
||||
admin := bootAdmin(t, h, st)
|
||||
@@ -74,40 +80,76 @@ func TestProjectCreateLinksOwnProjectManagementOverHTTP(t *testing.T) {
|
||||
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())
|
||||
permissions := "/api/admin/roles/" + role.RoleID + "/permissions"
|
||||
roleView := func() *httptest.ResponseRecorder {
|
||||
return do(h, "GET", "/api/admin/roles/"+role.RoleID, "", admin)
|
||||
}
|
||||
stale := do(h, "PUT", "/api/admin/roles/"+role.RoleID+"/permissions",
|
||||
`{"expectedRoleVersion":9,"idempotencyKey":"create-stale","changes":[{"actionId":"project.create","allowed":true,"capAllowed":true}]}`, admin)
|
||||
const enableCreate = `{"actionId":"project.create","allowed":true,"capAllowed":true}`
|
||||
const denyLinked = `{"actionId":"project.members.manage","allowed":false,"capAllowed":false}`
|
||||
|
||||
// A batch that enables project.create and explicitly denies a linked
|
||||
// own-project action is refused as a whole: the linked action is not role
|
||||
// authority any more, so nothing in that batch is written.
|
||||
both := do(h, "PUT", permissions,
|
||||
`{"expectedRoleVersion":1,"idempotencyKey":"create-and-deny-linked","changes":[`+enableCreate+`,`+denyLinked+`]}`, admin)
|
||||
if both.Code != http.StatusBadRequest {
|
||||
t.Fatalf("linked deny accepted beside create: %d %s", both.Code, both.Body.String())
|
||||
}
|
||||
stale := do(h, "PUT", permissions,
|
||||
`{"expectedRoleVersion":9,"idempotencyKey":"create-stale","changes":[`+enableCreate+`]}`, 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"]`) {
|
||||
policy := do(h, "PUT", permissions,
|
||||
`{"expectedRoleVersion":1,"idempotencyKey":"create-only","changes":[`+enableCreate+`]}`, admin)
|
||||
if policy.Code != http.StatusOK {
|
||||
t.Fatalf("create-only policy: %d %s", policy.Code, policy.Body.String())
|
||||
}
|
||||
var receipt store.PolicyReceipt
|
||||
if err := json.Unmarshal(policy.Body.Bytes(), &receipt); err != nil {
|
||||
t.Fatalf("decode receipt: %v body=%s", err, policy.Body.String())
|
||||
}
|
||||
if receipt.ReceiptID == "" || receipt.RoleVersion != 2 || len(receipt.LinkedExpansion) != 0 ||
|
||||
strings.Contains(policy.Body.String(), "linkedExpansion") {
|
||||
t.Fatalf("receipt expanded linked rows: %+v body=%s", receipt, policy.Body.String())
|
||||
}
|
||||
requireRoleProjectAccess(t, roleView(), 2, true)
|
||||
|
||||
// Split across two submissions the linked deny is refused on its own, so the
|
||||
// role keeps the project.create policy the first submission wrote.
|
||||
denyLater := do(h, "PUT", permissions,
|
||||
`{"expectedRoleVersion":2,"idempotencyKey":"deny-linked-later","changes":[`+denyLinked+`]}`, admin)
|
||||
if denyLater.Code != http.StatusBadRequest {
|
||||
t.Fatalf("linked deny accepted in a later batch: %d %s", denyLater.Code, denyLater.Body.String())
|
||||
}
|
||||
requireRoleProjectAccess(t, roleView(), 2, true)
|
||||
|
||||
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 {
|
||||
if me.Code != http.StatusOK {
|
||||
t.Fatalf("me: %d %s", me.Code, me.Body.String())
|
||||
}
|
||||
var identity struct {
|
||||
Linked struct {
|
||||
ProjectCreate bool `json:"projectCreate"`
|
||||
Source string `json:"source"`
|
||||
LinkedActions []string `json:"linkedActions"`
|
||||
Conflicts []string `json:"conflicts"`
|
||||
} `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 err := json.Unmarshal(me.Body.Bytes(), &identity); err != nil {
|
||||
t.Fatalf("decode me: %v body=%s", err, 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)
|
||||
if !identity.Linked.ProjectCreate || identity.Linked.Source != "role" || len(identity.Linked.Conflicts) != 0 ||
|
||||
strings.Join(identity.Linked.LinkedActions, ",") != strings.Join(catalog.ProjectCreateLinkedActions(), ",") {
|
||||
t.Fatalf("linked capabilities=%+v", identity.Linked)
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user