Files
Agent-System-Backend/internal/app/role_provider_test.go
T
agent-exec-transport 612e04402d
governance / governance (pull_request) Successful in 16s
ci / vet-and-test (pull_request) Successful in 1m43s
agent-exec transport be0c4744ff5899017313eb3f4fbd4a3f/43fe057c8332
2026-10-20 11:57:53 +00:00

201 lines
7.8 KiB
Go

package app
import (
"context"
"errors"
"testing"
"agent-system-backend/internal/store"
"agent-system-backend/pkg/extension"
)
func TestRoleProviderReturnsVersionedNonSecretPolicy(t *testing.T) {
st, err := store.OpenSQLite(t.TempDir() + "/role-provider.db")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
if err := st.CreateAccount("admin", "Admin", "hash", true); err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("alice", "Alice", "hash", false); err != nil {
t.Fatal(err)
}
alice, _ := st.GetAccountByOwner("alice")
role, err := st.CreateRole("admin", store.RoleCreate{Name: "软件工程师", Description: "负责软件研发"})
if err != nil {
t.Fatal(err)
}
receipt, err := st.ReplaceRolePermissions("admin", role.RoleID, role.RoleVersion,
[]store.RolePermissionChange{{ActionID: "document.read", Allowed: true, CapAllowed: true}}, "provider-policy")
if err != nil {
t.Fatal(err)
}
if _, err := st.AssignUserRole("admin", alice.ID, role.RoleID, alice.StateVersion, "provider-binding"); err != nil {
t.Fatal(err)
}
h := inprocHost{caps: hostcaps{st: st}, pluginID: "rdm", owner: "admin", actor: "admin"}
provider, ok := any(h).(extension.RolePolicyProvider)
if !ok {
t.Fatal("dispatch host missing RolePolicyProvider")
}
snapshot, err := provider.RolePolicyForUser(context.Background(), alice.ID)
if err != nil {
t.Fatal(err)
}
byOwner, err := provider.RolePolicyForOwner(context.Background(), "alice")
if err != nil || byOwner.UserID != alice.ID || byOwner.RoleID != role.RoleID {
t.Fatalf("owner policy=%+v err=%v", byOwner, err)
}
if snapshot.SchemaVersion != extension.RolePolicySchemaVersion || snapshot.UserID != alice.ID || !snapshot.AccountEnabled ||
snapshot.RoleID != role.RoleID || snapshot.RoleName != "软件工程师" || snapshot.RoleDescription != "负责软件研发" ||
!snapshot.RoleEnabled || snapshot.RoleVersion != receipt.RoleVersion || snapshot.AuthorityEpoch < receipt.AuthorityEpoch {
t.Fatalf("snapshot=%+v", snapshot)
}
permission := snapshot.Permissions["document.read"]
if !permission.Allowed || !permission.CapAllowed {
t.Fatalf("permission=%+v", permission)
}
decision, err := provider.CheckRoleAction(context.Background(), alice.ID, "document.read")
if err != nil || !decision.Allowed || !decision.CapAllowed || decision.RoleID != role.RoleID {
t.Fatalf("decision=%+v err=%v", decision, err)
}
systemProvider, ok := any(h).(extension.SystemActionProvider)
if !ok {
t.Fatal("dispatch host missing SystemActionProvider")
}
systemDecision, err := systemProvider.CheckCurrentSystemAction(context.Background(), "system.permissions.manage")
if err != nil || !systemDecision.Allowed || systemDecision.AuthorityEpoch < 1 {
t.Fatalf("system decision=%+v err=%v", systemDecision, err)
}
updated, err := st.ReplaceRolePermissions("admin", role.RoleID, snapshot.RoleVersion,
[]store.RolePermissionChange{{ActionID: "document.read", Allowed: false, CapAllowed: true}}, "provider-policy-update")
if err != nil {
t.Fatal(err)
}
refreshed, err := provider.RolePolicyForUser(context.Background(), alice.ID)
if err != nil || refreshed.AuthorityEpoch != updated.AuthorityEpoch || refreshed.RoleVersion != updated.RoleVersion {
t.Fatalf("refreshed=%+v receipt=%+v err=%v", refreshed, updated, err)
}
}
func TestRoleProviderRejectsUntrustedScopedHost(t *testing.T) {
st, err := store.OpenSQLite(t.TempDir() + "/role-provider.db")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
if err := st.CreateAccount("alice", "Alice", "hash", false); err != nil {
t.Fatal(err)
}
alice, _ := st.GetAccountByOwner("alice")
h := inprocHost{caps: hostcaps{st: st}, pluginID: "rdm", owner: "alice", actor: ""}
provider := any(h).(extension.RolePolicyProvider)
if _, err := provider.RolePolicyForUser(context.Background(), alice.ID); !errors.Is(err, store.ErrAccountForbidden) {
t.Fatalf("empty actor provider: %v", err)
}
}
func TestRolePolicyAdminUsesBoundActorAndStableOwnerCompatibility(t *testing.T) {
st, err := store.OpenSQLite(t.TempDir() + "/role-admin.db")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
if err := st.CreateAccount("admin", "Admin", "hash", true); err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("alice", "Alice", "hash", false); err != nil {
t.Fatal(err)
}
h := inprocHost{caps: hostcaps{st: st}, pluginID: "rdm", owner: "admin", actor: "admin"}
admin, ok := any(h).(extension.RolePolicyAdmin)
if !ok {
t.Fatal("dispatch host missing RolePolicyAdmin")
}
role, err := admin.CreateRole(context.Background(), "软件工程师", "负责软件研发")
if err != nil {
t.Fatal(err)
}
role, err = admin.SetRolePermission(context.Background(), role.RoleID, "document.read", true, true, "legacy-action")
if err != nil || !role.Permissions["document.read"].Allowed {
t.Fatalf("role=%+v err=%v", role, err)
}
binding, err := admin.AssignRoleByOwner(context.Background(), "alice", role.RoleID, "legacy-assign")
if err != nil || binding.RoleID != role.RoleID {
t.Fatalf("binding=%+v err=%v", binding, err)
}
roles, err := admin.ListRoles(context.Background())
if err != nil || len(roles) != 1 || roles[0].BindingCount != 1 {
t.Fatalf("roles=%+v err=%v", roles, err)
}
}
// 非管理员分发主体(项目创建者)只读已启用职位:roleId+name,按 name→roleId 排序,
// 不含权限/绑定/计数;空 actor(scoped host)与已停用账号一律拒绝。
func TestRoleOptionsExposeEnabledRolesToNonAdminActor(t *testing.T) {
st, err := store.OpenSQLite(t.TempDir() + "/role-options.db")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
if err := st.CreateAccount("admin", "Admin", "hash", true); err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("alice", "Alice", "hash", false); err != nil {
t.Fatal(err)
}
alice, err := st.GetAccountByOwner("alice")
if err != nil {
t.Fatal(err)
}
beta, err := st.CreateRole("admin", store.RoleCreate{Name: "Beta 职位", Description: "停用前"})
if err != nil {
t.Fatal(err)
}
alpha, err := st.CreateRole("admin", store.RoleCreate{Name: "Alpha 职位"})
if err != nil {
t.Fatal(err)
}
retired, err := st.CreateRole("admin", store.RoleCreate{Name: "Gamma 职位"})
if err != nil {
t.Fatal(err)
}
disabled := false
if _, err := st.PatchRole("admin", retired.RoleID, retired.RoleVersion, store.RolePatch{Enabled: &disabled}); err != nil {
t.Fatal(err)
}
h := inprocHost{caps: hostcaps{st: st}, pluginID: "rdm", owner: "alice", actor: "alice"}
options, ok := any(h).(extension.RoleOptionsProvider)
if !ok {
t.Fatal("dispatch host missing RoleOptionsProvider")
}
// 管理面仍然拒绝非管理员:只读目录不放大权限。
if _, err := any(h).(extension.RolePolicyAdmin).ListRoles(context.Background()); !errors.Is(err, store.ErrAccountForbidden) {
t.Fatalf("non-admin ListRoles: %v", err)
}
got, err := options.ListRoleOptions(context.Background())
if err != nil {
t.Fatal(err)
}
if len(got) != 2 || got[0] != (extension.RoleOption{RoleID: alpha.RoleID, Name: "Alpha 职位"}) ||
got[1] != (extension.RoleOption{RoleID: beta.RoleID, Name: "Beta 职位"}) {
t.Fatalf("options=%+v", got)
}
scoped := inprocHost{caps: hostcaps{st: st}, pluginID: "rdm", owner: "alice"}
if _, err := any(scoped).(extension.RoleOptionsProvider).ListRoleOptions(context.Background()); !errors.Is(err, store.ErrAccountForbidden) {
t.Fatalf("empty actor options: %v", err)
}
if _, err := st.SetAccountEnabled("admin", alice.ID, alice.StateVersion, false); err != nil {
t.Fatal(err)
}
if _, err := options.ListRoleOptions(context.Background()); !errors.Is(err, store.ErrAccountForbidden) {
t.Fatalf("disabled actor options: %v", err)
}
cancelled, cancel := context.WithCancel(context.Background())
cancel()
if _, err := options.ListRoleOptions(cancelled); !errors.Is(err, context.Canceled) {
t.Fatalf("cancelled context: %v", err)
}
}