Files
Agent-System-Backend/internal/server/role_authorization_test.go
T
weichen b2a1cd0a60 Expose effective system actions to the client
Why: Non-root managers need the system-management entry and controls to follow their explicit grants.

Evidence: The full Backend test suite passed and /api/me action projection is covered by the grant-isolation test.

Impact: /api/me now returns only the current actor system actions evaluated by the same authorizer.

Refs: Focused role and permission system task 7
2026-10-14 17:37:39 +00:00

101 lines
4.0 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 TestSystemUserActionsDoNotImplyRoleOrPermissionManagement(t *testing.T) {
srv, st := newAuthTestServer(t)
h := srv.Handler()
admin := bootAdmin(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)
}
}
}