Add trusted role administration compatibility
Why: Existing RDM role commands must reach the Backend authority during UI and migration cutover without dual-writing RDM role tables. Evidence: The full Backend suite passed, including actor-bound role administration, owner compatibility, provider, and public-surface tests. Impact: Adds temporary trusted Host administration methods and completes the project-action catalog used by RDM. Refs: Focused role and permission system task 5
This commit is contained in:
@@ -5,9 +5,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
authzsvc "agent-system-backend/internal/authz"
|
||||
"agent-system-backend/internal/authz/catalog"
|
||||
"agent-system-backend/internal/ingest"
|
||||
"agent-system-backend/internal/rag"
|
||||
@@ -370,6 +372,17 @@ func (h inprocHost) RolePolicyForUser(ctx context.Context, userID string) (exten
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func (h inprocHost) RolePolicyForOwner(ctx context.Context, owner string) (extension.RolePolicySnapshot, error) {
|
||||
if _, err := h.rolePolicyActor(); err != nil {
|
||||
return extension.RolePolicySnapshot{}, err
|
||||
}
|
||||
account, err := h.caps.st.GetAccountByOwner(owner)
|
||||
if err != nil {
|
||||
return extension.RolePolicySnapshot{}, err
|
||||
}
|
||||
return h.RolePolicyForUser(ctx, account.ID)
|
||||
}
|
||||
|
||||
func (h inprocHost) CheckRoleAction(ctx context.Context, userID, actionID string) (extension.RoleActionDecision, error) {
|
||||
decision := extension.RoleActionDecision{ActionID: actionID}
|
||||
if _, ok := catalog.DescriptorByID(actionID); !ok {
|
||||
@@ -409,6 +422,154 @@ func (h inprocHost) CheckRoleAction(ctx context.Context, userID, actionID string
|
||||
return decision, nil
|
||||
}
|
||||
|
||||
func (h inprocHost) requireRoleAdminAction(actionID string) error {
|
||||
if h.actor == "" || h.owner != h.actor {
|
||||
return store.ErrAccountForbidden
|
||||
}
|
||||
decision := authzsvc.NewService(h.caps.st).AuthorizeSystem(h.actor, actionID)
|
||||
if !decision.Allowed {
|
||||
return store.ErrAccountForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func roleAdminRecord(view store.RoleAdminView) extension.RoleAdminRecord {
|
||||
out := extension.RoleAdminRecord{RoleID: view.RoleID, Name: view.Name, Description: view.Description,
|
||||
Enabled: view.Enabled, RoleVersion: view.RoleVersion, BindingCount: view.BindingCount,
|
||||
Permissions: map[string]extension.RolePermission{}}
|
||||
for _, permission := range view.Permissions {
|
||||
out.Permissions[permission.ActionID] = extension.RolePermission{Allowed: permission.Allowed, CapAllowed: permission.CapAllowed}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (h inprocHost) ListRoles(ctx context.Context) ([]extension.RoleAdminRecord, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := h.requireRoleAdminAction("system.roles.view"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := []extension.RoleAdminRecord{}
|
||||
cursor := ""
|
||||
for {
|
||||
roles, next, err := h.caps.st.ListRoles("", "fuzzy", "all", "", cursor, 100)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, role := range roles {
|
||||
result = append(result, roleAdminRecord(role))
|
||||
}
|
||||
if next == "" {
|
||||
return result, nil
|
||||
}
|
||||
cursor = next
|
||||
}
|
||||
}
|
||||
|
||||
func (h inprocHost) CreateRole(ctx context.Context, name, description string) (extension.RoleAdminRecord, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return extension.RoleAdminRecord{}, err
|
||||
}
|
||||
if err := h.requireRoleAdminAction("system.roles.manage"); err != nil {
|
||||
return extension.RoleAdminRecord{}, err
|
||||
}
|
||||
role, err := h.caps.st.CreateRole(h.actor, store.RoleCreate{Name: name, Description: description})
|
||||
if err != nil {
|
||||
return extension.RoleAdminRecord{}, err
|
||||
}
|
||||
view, err := h.caps.st.RoleAdmin(role.RoleID)
|
||||
return roleAdminRecord(view), err
|
||||
}
|
||||
|
||||
func (h inprocHost) SetRolePermission(ctx context.Context, roleID, actionID string, allowed, capAllowed bool, key string) (extension.RoleAdminRecord, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return extension.RoleAdminRecord{}, err
|
||||
}
|
||||
if err := h.requireRoleAdminAction("system.permissions.manage"); err != nil {
|
||||
return extension.RoleAdminRecord{}, err
|
||||
}
|
||||
view, err := h.caps.st.RoleAdmin(roleID)
|
||||
if err != nil {
|
||||
return extension.RoleAdminRecord{}, err
|
||||
}
|
||||
changes := make([]store.RolePermissionChange, 0, len(view.Permissions)+1)
|
||||
found := false
|
||||
for _, permission := range view.Permissions {
|
||||
if permission.ActionID == actionID {
|
||||
found = true
|
||||
if permission.Allowed == allowed && permission.CapAllowed == capAllowed {
|
||||
return roleAdminRecord(view), nil
|
||||
}
|
||||
permission.Allowed, permission.CapAllowed = allowed, capAllowed
|
||||
}
|
||||
changes = append(changes, store.RolePermissionChange{ActionID: permission.ActionID, Allowed: permission.Allowed, CapAllowed: permission.CapAllowed})
|
||||
}
|
||||
if !found {
|
||||
changes = append(changes, store.RolePermissionChange{ActionID: actionID, Allowed: allowed, CapAllowed: capAllowed})
|
||||
}
|
||||
if key == "" {
|
||||
return extension.RoleAdminRecord{}, store.ErrInvalidRolePolicy
|
||||
}
|
||||
if _, err := h.caps.st.ReplaceRolePermissions(h.actor, roleID, view.RoleVersion, changes, key+":"+strconv.FormatInt(view.RoleVersion, 10)); err != nil {
|
||||
return extension.RoleAdminRecord{}, err
|
||||
}
|
||||
view, err = h.caps.st.RoleAdmin(roleID)
|
||||
return roleAdminRecord(view), err
|
||||
}
|
||||
|
||||
func (h inprocHost) AssignRoleByOwner(ctx context.Context, owner, roleID, key string) (extension.RoleBindingResult, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return extension.RoleBindingResult{}, err
|
||||
}
|
||||
if err := h.requireRoleAdminAction("system.roles.assign"); err != nil {
|
||||
return extension.RoleBindingResult{}, err
|
||||
}
|
||||
target, err := h.caps.st.GetAccountByOwner(owner)
|
||||
if err != nil {
|
||||
return extension.RoleBindingResult{}, err
|
||||
}
|
||||
current, err := h.caps.st.RoleForUser(target.ID)
|
||||
if err != nil {
|
||||
return extension.RoleBindingResult{}, err
|
||||
}
|
||||
if current.Binding != nil && current.Binding.RoleID == roleID {
|
||||
return extension.RoleBindingResult{BindingID: current.Binding.BindingID, UserID: target.ID, RoleID: roleID,
|
||||
UserStateVersion: current.Binding.UserStateVersion, AuthorityEpoch: current.Binding.AuthorityEpoch}, nil
|
||||
}
|
||||
if key == "" {
|
||||
return extension.RoleBindingResult{}, store.ErrInvalidRolePolicy
|
||||
}
|
||||
binding, err := h.caps.st.AssignUserRole(h.actor, target.ID, roleID, target.StateVersion, key+":"+strconv.FormatInt(target.StateVersion, 10))
|
||||
if err != nil {
|
||||
return extension.RoleBindingResult{}, err
|
||||
}
|
||||
return extension.RoleBindingResult{BindingID: binding.BindingID, UserID: binding.UserID, RoleID: binding.RoleID,
|
||||
UserStateVersion: binding.UserStateVersion, AuthorityEpoch: binding.AuthorityEpoch}, nil
|
||||
}
|
||||
|
||||
func (h inprocHost) SetRoleEnabled(ctx context.Context, roleID string, enabled bool) (extension.RoleAdminRecord, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return extension.RoleAdminRecord{}, err
|
||||
}
|
||||
if err := h.requireRoleAdminAction("system.roles.manage"); err != nil {
|
||||
return extension.RoleAdminRecord{}, err
|
||||
}
|
||||
view, err := h.caps.st.RoleAdmin(roleID)
|
||||
if err != nil {
|
||||
return extension.RoleAdminRecord{}, err
|
||||
}
|
||||
if view.Enabled == enabled {
|
||||
return roleAdminRecord(view), nil
|
||||
}
|
||||
role, err := h.caps.st.PatchRole(h.actor, roleID, view.RoleVersion, store.RolePatch{Enabled: &enabled})
|
||||
if err != nil {
|
||||
return extension.RoleAdminRecord{}, err
|
||||
}
|
||||
view, err = h.caps.st.RoleAdmin(role.RoleID)
|
||||
return roleAdminRecord(view), err
|
||||
}
|
||||
|
||||
func (h inprocHost) KVPut(key, value string) error {
|
||||
if err := h.requireEffective(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -43,6 +43,10 @@ func TestRoleProviderReturnsVersionedNonSecretPolicy(t *testing.T) {
|
||||
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 {
|
||||
@@ -74,3 +78,38 @@ func TestRoleProviderRejectsUntrustedScopedHost(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ var descriptors = []Descriptor{
|
||||
{ActionID: "system.permissions.manage", Label: "管理系统权限", Description: "配置明确系统授权与平台上限", Group: "权限管理", Scope: ScopeSystem, Protected: true},
|
||||
{ActionID: "project.create", Label: "创建项目", Description: "创建新的项目", Group: "项目入口", Scope: ScopeSystem},
|
||||
{ActionID: "project.settings", Label: "项目设置", Description: "修改本项目设置", Group: "项目管理", Scope: ScopeProject},
|
||||
{ActionID: "project.archive", Label: "归档项目", Description: "归档或恢复本人创建的项目", Group: "项目管理", Scope: ScopeProject},
|
||||
{ActionID: "project.stage.advance", Label: "推进项目阶段", Description: "推进本人创建项目的阶段", Group: "项目管理", Scope: ScopeProject},
|
||||
{ActionID: "project.members.manage", Label: "管理项目成员", Description: "维护本项目成员", Group: "项目管理", Scope: ScopeProject},
|
||||
{ActionID: "project.permissions.manage", Label: "管理项目权限", Description: "维护本项目成员权限覆盖", Group: "项目管理", Scope: ScopeProject},
|
||||
{ActionID: "document.duties.assign", Label: "分配文档三权", Description: "分配本项目文档编制、审核和批准职责", Group: "文档职责", Scope: ScopeProject},
|
||||
@@ -39,6 +41,10 @@ var descriptors = []Descriptor{
|
||||
{ActionID: "document.author", Label: "编制文档", Description: "履行已分配的文档编制职责", Group: "文档动作", Scope: ScopeProject},
|
||||
{ActionID: "document.review", Label: "审核文档", Description: "履行已分配的文档审核职责", Group: "文档动作", Scope: ScopeProject},
|
||||
{ActionID: "document.approve", Label: "批准文档", Description: "履行已分配的文档批准职责", Group: "文档动作", Scope: ScopeProject},
|
||||
{ActionID: "document.publish", Label: "发布文档", Description: "发布已完成审签的项目文档", Group: "文档动作", Scope: ScopeProject},
|
||||
{ActionID: "trace.edit", Label: "编辑追溯网", Description: "编辑本项目追溯关系", Group: "项目内容", Scope: ScopeProject},
|
||||
{ActionID: "knowledge.publish", Label: "发布项目知识", Description: "发布或撤销本项目知识", Group: "项目内容", Scope: ScopeProject},
|
||||
{ActionID: "project.audit.read", Label: "读取业务审计", Description: "读取获准项目的业务审计", Group: "项目内容", Scope: ScopeProject},
|
||||
}
|
||||
|
||||
var descriptorByID = func() map[string]Descriptor {
|
||||
|
||||
@@ -40,5 +40,34 @@ type RoleActionDecision struct {
|
||||
type RolePolicyProvider interface {
|
||||
CurrentRolePolicy(context.Context) (RolePolicySnapshot, error)
|
||||
RolePolicyForUser(context.Context, string) (RolePolicySnapshot, error)
|
||||
RolePolicyForOwner(context.Context, string) (RolePolicySnapshot, error)
|
||||
CheckRoleAction(context.Context, string, string) (RoleActionDecision, error)
|
||||
}
|
||||
|
||||
type RoleAdminRecord struct {
|
||||
RoleID string `json:"roleId"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RoleVersion int64 `json:"roleVersion"`
|
||||
BindingCount int `json:"bindingCount"`
|
||||
Permissions map[string]RolePermission `json:"permissions"`
|
||||
}
|
||||
|
||||
type RoleBindingResult struct {
|
||||
BindingID string `json:"bindingId"`
|
||||
UserID string `json:"userId"`
|
||||
RoleID string `json:"roleId"`
|
||||
UserStateVersion int64 `json:"userStateVersion"`
|
||||
AuthorityEpoch int64 `json:"authorityEpoch"`
|
||||
}
|
||||
|
||||
// RolePolicyAdmin is a temporary compatibility surface for trusted RDM
|
||||
// commands while the UI moves to Backend HTTP APIs. The Host binds the actor.
|
||||
type RolePolicyAdmin interface {
|
||||
ListRoles(context.Context) ([]RoleAdminRecord, error)
|
||||
CreateRole(context.Context, string, string) (RoleAdminRecord, error)
|
||||
SetRolePermission(context.Context, string, string, bool, bool, string) (RoleAdminRecord, error)
|
||||
AssignRoleByOwner(context.Context, string, string, string) (RoleBindingResult, error)
|
||||
SetRoleEnabled(context.Context, string, bool) (RoleAdminRecord, error)
|
||||
}
|
||||
|
||||
@@ -80,10 +80,19 @@ ResourceAuthorizer
|
||||
ResourceAuthorizerV2
|
||||
ResourceAuthzQuery
|
||||
RoleActionDecision
|
||||
RoleAdminRecord
|
||||
RoleBindingResult
|
||||
RolePermission
|
||||
RolePolicyAdmin
|
||||
RolePolicyAdmin.AssignRoleByOwner
|
||||
RolePolicyAdmin.CreateRole
|
||||
RolePolicyAdmin.ListRoles
|
||||
RolePolicyAdmin.SetRoleEnabled
|
||||
RolePolicyAdmin.SetRolePermission
|
||||
RolePolicyProvider
|
||||
RolePolicyProvider.CheckRoleAction
|
||||
RolePolicyProvider.CurrentRolePolicy
|
||||
RolePolicyProvider.RolePolicyForOwner
|
||||
RolePolicyProvider.RolePolicyForUser
|
||||
RolePolicySchemaVersion
|
||||
RolePolicySnapshot
|
||||
|
||||
Reference in New Issue
Block a user