216 lines
8.8 KiB
Go
216 lines
8.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: "project.create", 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["project.create"]
|
|
if len(snapshot.Permissions) != 1 || !permission.Allowed || !permission.CapAllowed {
|
|
t.Fatalf("permissions=%+v", snapshot.Permissions)
|
|
}
|
|
decision, err := provider.CheckRoleAction(context.Background(), alice.ID, "project.create")
|
|
if err != nil || !decision.Allowed || !decision.CapAllowed || decision.RoleID != role.RoleID {
|
|
t.Fatalf("decision=%+v err=%v", decision, err)
|
|
}
|
|
legacy, err := provider.CheckRoleAction(context.Background(), alice.ID, "document.read")
|
|
if err != nil || legacy.Allowed || legacy.Reason != "action-unconfigured" {
|
|
t.Fatalf("non-project role action=%+v err=%v", legacy, 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: "project.create", 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)
|
|
}
|
|
if denied := refreshed.Permissions["project.create"]; len(refreshed.Permissions) != 1 || denied.Allowed || denied.CapAllowed {
|
|
t.Fatalf("refreshed permissions=%+v", refreshed.Permissions)
|
|
}
|
|
revoked, err := provider.CheckRoleAction(context.Background(), alice.ID, "project.create")
|
|
if err != nil || revoked.Allowed || revoked.CapAllowed {
|
|
t.Fatalf("revoked decision=%+v err=%v", revoked, 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)
|
|
}
|
|
if _, err := admin.SetRolePermission(context.Background(), role.RoleID, "document.read", true, true, "legacy-action"); !errors.Is(err, store.ErrInvalidRolePolicy) {
|
|
t.Fatalf("non-project role action accepted: %v", err)
|
|
}
|
|
role, err = admin.SetRolePermission(context.Background(), role.RoleID, "project.create", true, true, "project-access")
|
|
access := role.Permissions["project.create"]
|
|
if err != nil || len(role.Permissions) != 1 || !access.Allowed || !access.CapAllowed {
|
|
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)
|
|
}
|
|
}
|