Files
Agent-System/internal/authz/roles.go
T
weichenandCursor 6219caf690 feat(authz): complete Phase 1 admin roles, sessions, and unified admin gate
Add roles CRUD backed by system_roles tables, session list/revoke APIs,
capabilities fallback for memory mode, and route all admin checks through
AuthorizationService instead of hardcoded role-admin checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:22 +00:00

292 lines
8.8 KiB
Go

package authz
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
)
var (
ErrRoleNotFound = errors.New("authz: role not found")
ErrRoleIsSystem = errors.New("authz: system role cannot be deleted")
ErrRoleInUse = errors.New("authz: role still assigned to users")
ErrRoleInvalid = errors.New("authz: invalid role payload")
ErrRolesNotAvailable = errors.New("authz: roles API requires database IAM")
)
// SystemRole 系统角色与权限矩阵视图。
type SystemRole struct {
ID string `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
Description string `json:"description"`
IsSystem bool `json:"is_system"`
Status string `json:"status"`
Permissions []string `json:"permissions"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// CreateRoleInput 新建自定义角色。
type CreateRoleInput struct {
Code string `json:"code"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []string `json:"permissions"`
}
// UpdateRoleInput 更新角色元数据与权限矩阵。
type UpdateRoleInput struct {
Name string `json:"name"`
Description string `json:"description"`
Permissions []string `json:"permissions"`
}
func defaultSystemRoles() []SystemRole {
now := "2026-09-08 00:00:00"
return []SystemRole{
{ID: "role-admin", Code: "ADMIN", Name: "系统管理员", Description: "拥有系统所有功能与数据的全量管理权限(防死锁兜底)。", IsSystem: true, Status: "active", Permissions: FallbackCapabilitiesByRoleID("role-admin"), CreatedAt: now, UpdatedAt: now},
{ID: "role-director", Code: "DIRECTOR", Name: "研发总监", Description: "负责研发中心整体项目决策、立项审查、权限监督与终审批准。", IsSystem: false, Status: "active", Permissions: FallbackCapabilitiesByRoleID("role-director"), CreatedAt: now, UpdatedAt: now},
{ID: "role-tech-lead", Code: "TECH_LEAD", Name: "研发技术主管", Description: "负责具体研发管线的进度把控、文档责任人分配与技术审核。", IsSystem: false, Status: "active", Permissions: FallbackCapabilitiesByRoleID("role-tech-lead"), CreatedAt: now, UpdatedAt: now},
{ID: "role-engineer", Code: "ENGINEER", Name: "方案工程师", Description: "负责医疗器械规格设计、受控正文编制、技术测算与推演。", IsSystem: false, Status: "active", Permissions: FallbackCapabilitiesByRoleID("role-engineer"), CreatedAt: now, UpdatedAt: now},
{ID: "role-guest", Code: "GUEST", Name: "外部审计/访客", Description: "默认最小权限,仅在被授权范围内查阅。", IsSystem: false, Status: "active", Permissions: FallbackCapabilitiesByRoleID("role-guest"), CreatedAt: now, UpdatedAt: now},
}
}
func normalizePermKeys(perms []string) []string {
seen := make(map[string]struct{}, len(perms))
out := make([]string, 0, len(perms))
for _, p := range perms {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if _, ok := seen[p]; ok {
continue
}
seen[p] = struct{}{}
out = append(out, p)
}
return out
}
func (s *Service) scanRole(ctx context.Context, roleID string) (*SystemRole, error) {
row := s.pool.QueryRow(ctx, `
SELECT id, code, name, description, is_system, status, created_at, updated_at
FROM system_roles WHERE id = $1
`, roleID)
var role SystemRole
var createdAt, updatedAt time.Time
if err := row.Scan(&role.ID, &role.Code, &role.Name, &role.Description, &role.IsSystem, &role.Status, &createdAt, &updatedAt); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrRoleNotFound
}
return nil, err
}
role.CreatedAt = createdAt.Format("2006-01-02 15:04:05")
role.UpdatedAt = updatedAt.Format("2006-01-02 15:04:05")
rows, err := s.pool.Query(ctx, `
SELECT p.perm_key
FROM system_role_permissions rp
JOIN system_permissions p ON p.id = rp.permission_id
WHERE rp.role_id = $1 AND rp.effect = 'allow'
ORDER BY p.perm_key
`, roleID)
if err != nil {
return nil, err
}
defer rows.Close()
role.Permissions = make([]string, 0)
for rows.Next() {
var key string
if err := rows.Scan(&key); err != nil {
return nil, err
}
role.Permissions = append(role.Permissions, key)
}
return &role, rows.Err()
}
// ListRoles 列出全部系统角色及权限矩阵。
func (s *Service) ListRoles(ctx context.Context) ([]SystemRole, error) {
if !s.enabled() {
return defaultSystemRoles(), nil
}
rows, err := s.pool.Query(ctx, `
SELECT id FROM system_roles WHERE status = 'active' ORDER BY is_system DESC, id ASC
`)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]SystemRole, 0)
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
role, err := s.scanRole(ctx, id)
if err != nil {
return nil, err
}
out = append(out, *role)
}
return out, rows.Err()
}
// CreateRole 创建自定义角色。
func (s *Service) CreateRole(ctx context.Context, in CreateRoleInput) (*SystemRole, error) {
if !s.enabled() {
return nil, ErrRolesNotAvailable
}
code := strings.TrimSpace(strings.ToUpper(in.Code))
name := strings.TrimSpace(in.Name)
if code == "" || name == "" {
return nil, ErrRoleInvalid
}
id := "role-" + strings.ToLower(strings.ReplaceAll(code, "_", "-"))
perms := normalizePermKeys(in.Permissions)
if len(perms) == 0 {
perms = []string{"doc:export", "workflow:view_all"}
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx, `
INSERT INTO system_roles (id, code, name, description, is_system, status)
VALUES ($1, $2, $3, $4, false, 'active')
`, id, code, name, strings.TrimSpace(in.Description))
if err != nil {
return nil, fmt.Errorf("create role: %w", err)
}
if err := s.replaceRolePermissions(ctx, tx, id, perms); err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return s.scanRole(ctx, id)
}
// UpdateRole 更新非系统角色或系统角色的权限矩阵(系统角色不可改名/删)。
func (s *Service) UpdateRole(ctx context.Context, roleID string, in UpdateRoleInput) (*SystemRole, error) {
if !s.enabled() {
return nil, ErrRolesNotAvailable
}
roleID = strings.TrimSpace(roleID)
if roleID == "" {
return nil, ErrRoleInvalid
}
existing, err := s.scanRole(ctx, roleID)
if err != nil {
return nil, err
}
perms := normalizePermKeys(in.Permissions)
if len(perms) == 0 {
return nil, ErrRoleInvalid
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
if existing.IsSystem {
if strings.TrimSpace(in.Name) != "" && in.Name != existing.Name {
return nil, fmt.Errorf("%w: cannot rename system role", ErrRoleIsSystem)
}
} else {
name := strings.TrimSpace(in.Name)
if name == "" {
name = existing.Name
}
_, err = tx.Exec(ctx, `
UPDATE system_roles SET name = $2, description = $3, updated_at = now() WHERE id = $1
`, roleID, name, strings.TrimSpace(in.Description))
if err != nil {
return nil, err
}
}
if err := s.replaceRolePermissions(ctx, tx, roleID, perms); err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return s.scanRole(ctx, roleID)
}
// DeleteRole 删除自定义角色(系统内置角色不可删)。
func (s *Service) DeleteRole(ctx context.Context, roleID string) error {
if !s.enabled() {
return ErrRolesNotAvailable
}
roleID = strings.TrimSpace(roleID)
if roleID == "" {
return ErrRoleInvalid
}
existing, err := s.scanRole(ctx, roleID)
if err != nil {
return err
}
if existing.IsSystem {
return ErrRoleIsSystem
}
var inUse bool
if err := s.pool.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1 FROM user_role_bindings
WHERE role_id = $1 AND (valid_until IS NULL OR valid_until > now())
) OR EXISTS (
SELECT 1 FROM users WHERE role_id = $1
)
`, roleID).Scan(&inUse); err != nil {
return err
}
if inUse {
return ErrRoleInUse
}
tag, err := s.pool.Exec(ctx, `DELETE FROM system_roles WHERE id = $1 AND is_system = false`, roleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrRoleNotFound
}
return nil
}
func (s *Service) replaceRolePermissions(ctx context.Context, tx pgx.Tx, roleID string, perms []string) error {
if _, err := tx.Exec(ctx, `DELETE FROM system_role_permissions WHERE role_id = $1`, roleID); err != nil {
return err
}
for _, permKey := range perms {
var permID int64
err := tx.QueryRow(ctx, `SELECT id FROM system_permissions WHERE perm_key = $1`, permKey).Scan(&permID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return fmt.Errorf("unknown permission %q", permKey)
}
return err
}
if _, err := tx.Exec(ctx, `
INSERT INTO system_role_permissions (role_id, permission_id, effect)
VALUES ($1, $2, 'allow')
ON CONFLICT DO NOTHING
`, roleID, permID); err != nil {
return err
}
}
return nil
}