Why: Independent review found delegated account actions, stale resource policy, and role migration cutover gaps in the initial U4 implementation. Evidence: Backend full SQLite suite passed; isolated PostgreSQL role and migration suite passed; independent final review found no remaining findings. Impact: Enforces transactional system grants, current role snapshots, staged role import with v36 migration, and fail-closed activation. Refs: feature/iam-u4-role-authority
90 lines
4.1 KiB
Go
90 lines
4.1 KiB
Go
package app
|
||
|
||
// EXT-8b 资源级授权槽位测试(U9 首切片):
|
||
// 1. V2 未注册 → AuthorizeResource 回落 V1(owner×project 兼容现行);
|
||
// 2. V2 注册 → 按查询六要素咨询;受信字段缺失(actor/action/type/id 空)=拒绝;
|
||
// 3. V2 返回 error = 拒绝(fail-closed);
|
||
// 4. 首注册者生效(后来者忽略);
|
||
// 5. compiledRegistry 的 V2 注册被拒(声明式无宿主回调)。
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
|
||
"agent-system-backend/pkg/extension"
|
||
)
|
||
|
||
func TestAuthorizeResourceV2FallbackAndQuery(t *testing.T) {
|
||
var a projectAuthz
|
||
// 未注册任何适配器 → 拒绝。
|
||
if ok, err := a.AuthorizeResource(extension.ResourceAuthzQuery{
|
||
Actor: "u1", Action: "download", ResourceType: "artifact", ResourceID: "a1",
|
||
Owner: "u2", ProjectID: "p1"}); ok || err != nil {
|
||
t.Fatalf("未注册应拒绝, got %v %v", ok, err)
|
||
}
|
||
// 仅 V1:回落 owner×project 布尔。
|
||
a.set("p1", func(owner string, projectID string) (bool, error) { return true, nil })
|
||
if ok, _ := a.AuthorizeResource(extension.ResourceAuthzQuery{
|
||
Actor: "u1", Action: "download", ResourceType: "artifact", ResourceID: "a1",
|
||
Owner: "u2", ProjectID: "p1"}); !ok {
|
||
t.Fatal("V1 允许时回落应放行")
|
||
}
|
||
// V1 路径要求 owner/project 非空。
|
||
if ok, _ := a.AuthorizeResource(extension.ResourceAuthzQuery{
|
||
Actor: "u1", Action: "download", ResourceType: "artifact", ResourceID: "a1"}); ok {
|
||
t.Fatal("V1 回落在无项目私域应拒绝")
|
||
}
|
||
// 注册 V2 → 按六要素咨询。
|
||
a.setV2("p1", func(q extension.ResourceAuthzQuery) (bool, error) {
|
||
return q.Actor == "u1" && q.Action == "download" && q.ResourceType == "artifact" &&
|
||
q.ResourceID == "a1" && q.Owner == "u2" && q.ProjectID == "p1", nil
|
||
})
|
||
if ok, _ := a.AuthorizeResource(extension.ResourceAuthzQuery{
|
||
Actor: "u1", Action: "download", ResourceType: "artifact", ResourceID: "a1",
|
||
Owner: "u2", ProjectID: "p1"}); !ok {
|
||
t.Fatal("V2 六要素齐备应放行")
|
||
}
|
||
// 六要素缺一(action 空)=拒绝(不可判定)。
|
||
if ok, _ := a.AuthorizeResource(extension.ResourceAuthzQuery{
|
||
Actor: "u1", ResourceType: "artifact", ResourceID: "a1", Owner: "u2", ProjectID: "p1"}); ok {
|
||
t.Fatal("受信字段缺失应拒绝")
|
||
}
|
||
// V2 出错=拒绝(fail-closed)。
|
||
a2 := projectAuthz{}
|
||
a2.setV2("p1", func(extension.ResourceAuthzQuery) (bool, error) { return false, errors.New("boom") })
|
||
if ok, err := a2.AuthorizeResource(extension.ResourceAuthzQuery{
|
||
Actor: "u", Action: "read", ResourceType: "x", ResourceID: "1"}); ok || err == nil {
|
||
t.Fatal("V2 error 应拒绝")
|
||
}
|
||
// 首注册者生效。
|
||
a3 := projectAuthz{}
|
||
a3.setV2("first", func(extension.ResourceAuthzQuery) (bool, error) { return true, nil })
|
||
a3.setV2("second", func(extension.ResourceAuthzQuery) (bool, error) { return false, nil })
|
||
if ok, _ := a3.AuthorizeResource(extension.ResourceAuthzQuery{
|
||
Actor: "u", Action: "read", ResourceType: "x", ResourceID: "1"}); !ok {
|
||
t.Fatal("首个 V2 注册者应生效")
|
||
}
|
||
}
|
||
|
||
func TestCompiledRegistryRejectsV2(t *testing.T) {
|
||
r := compiledRegistry{}
|
||
if err := r.RegisterResourceAuthorizerV2(func(extension.ResourceAuthzQuery) (bool, error) { return true, nil }); err == nil {
|
||
t.Fatal("声明式扩展不得注册资源级授权回调(fail-closed)")
|
||
}
|
||
}
|
||
|
||
func TestAuthorizeResourceEnrichesEveryInProcessQueryWithCurrentRolePolicy(t *testing.T) {
|
||
snapshot := extension.RolePolicySnapshot{SchemaVersion: extension.RolePolicySchemaVersion,
|
||
Owner: "alice", AccountEnabled: true, RoleID: "role_1", RoleEnabled: true, AuthorityEpoch: 7}
|
||
a := projectAuthz{enrich: func(q extension.ResourceAuthzQuery) (extension.ResourceAuthzQuery, error) {
|
||
q.AuthorityEpoch = snapshot.AuthorityEpoch
|
||
q.RolePolicy = &snapshot
|
||
return q, nil
|
||
}}
|
||
a.setV2("rdm", func(q extension.ResourceAuthzQuery) (bool, error) {
|
||
return q.AuthorityEpoch == 7 && q.RolePolicy != nil && q.RolePolicy.Owner == q.Actor, nil
|
||
})
|
||
if ok, err := a.AuthorizeResource(extension.ResourceAuthzQuery{Actor: "alice", Action: "search", ResourceType: "knowledge", ResourceID: "k1"}); err != nil || !ok {
|
||
t.Fatalf("enriched query ok=%v err=%v", ok, err)
|
||
}
|
||
}
|