Files
digital-psychology/apps/api/internal/service/admin/rbac.go
T
jackyu66gitandCursor 00a798bd85 feat(ECR-017): AskOperations 问答会话只读并 Closed
新增 admin.ask.read、GET /admin/ask/threads*(AskSessionView)与 admin-h5「问答」页;禁改消息/UGC/真支付。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 18:39:37 +08:00

145 lines
4.6 KiB
Go

package admin
import (
"context"
"encoding/json"
"errors"
"github.com/google/uuid"
)
// Permission catalog frozen in ECR-013A Spec.
const (
PermUsersRead = "admin.users.read"
PermMembershipGrant = "admin.users.membership.grant"
PermAskQuotaGrant = "admin.users.ask_quota.grant"
PermOrdersRead = "admin.orders.read"
PermAuditRead = "admin.audit.read"
PermAnalyticsRead = "admin.analytics.read"
PermContentWrite = "admin.content.write"
PermRolesRead = "admin.roles.read"
PermRolesWrite = "admin.roles.write"
PermUsersStatusWrite = "admin.users.status.write"
PermMembershipPlansRead = "admin.membership.plans.read"
PermMembershipPlansWrite = "admin.membership.plans.write"
PermMembershipCodesRead = "admin.membership.codes.read"
PermMembershipCodesWrite = "admin.membership.codes.write"
PermAskRead = "admin.ask.read"
)
var knownPermissions = map[string]struct{}{
PermUsersRead: {}, PermMembershipGrant: {}, PermAskQuotaGrant: {},
PermOrdersRead: {}, PermAuditRead: {}, PermAnalyticsRead: {},
PermContentWrite: {}, PermRolesRead: {}, PermRolesWrite: {},
PermUsersStatusWrite: {}, PermMembershipPlansRead: {}, PermMembershipPlansWrite: {},
PermMembershipCodesRead: {}, PermMembershipCodesWrite: {},
PermAskRead: {},
}
var (
ErrForbidden = errString("forbidden")
ErrRoleNotFound = errString("role not found")
ErrInvalidPerm = errString("invalid permission code")
ErrProtectSystem = errString("system role protected")
)
// AdminMe is the public admin profile (with RBAC).
type AdminMe struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"`
Role string `json:"role,omitempty"`
Permissions []string `json:"permissions"`
}
// RoleDTO is list/detail payload.
type RoleDTO struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
System bool `json:"system"`
Permissions []string `json:"permissions,omitempty"`
}
// HasPermission reports whether admin holds code.
func (s *Service) HasPermission(ctx context.Context, adminID uuid.UUID, code string) (bool, error) {
perms, err := s.Repo.ListPermissionsForAdmin(ctx, adminID)
if err != nil {
return false, err
}
for _, p := range perms {
if p == code {
return true, nil
}
}
return false, nil
}
// DenyPermission audits a forbidden attempt.
func (s *Service) DenyPermission(ctx context.Context, adminID uuid.UUID, code, path string) {
meta, _ := json.Marshal(map[string]string{"permission": code, "path": path})
_ = s.Repo.InsertAudit(ctx, adminID, "permission.denied", "permission", code, meta)
}
// Me returns the current admin profile with permissions.
func (s *Service) Me(ctx context.Context, adminID uuid.UUID) (*AdminMe, error) {
acc, err := s.Repo.FindAccountByID(ctx, adminID)
if err != nil || acc == nil {
return nil, errors.New("admin not found")
}
perms, err := s.Repo.ListPermissionsForAdmin(ctx, adminID)
if err != nil {
return nil, err
}
if perms == nil {
perms = []string{}
}
_, roleName, _ := s.Repo.GetAdminRoleMeta(ctx, adminID)
return &AdminMe{ID: acc.ID, Username: acc.Username, Role: roleName, Permissions: perms}, nil
}
// ListRoles returns roles without permissions.
func (s *Service) ListRoles(ctx context.Context) ([]RoleDTO, error) {
roles, err := s.Repo.ListAdminRoles(ctx)
if err != nil {
return nil, err
}
out := make([]RoleDTO, 0, len(roles))
for _, r := range roles {
out = append(out, RoleDTO{ID: r.ID, Name: r.Name, System: r.System})
}
return out, nil
}
// GetRole returns role + permissions.
func (s *Service) GetRole(ctx context.Context, id uuid.UUID) (*RoleDTO, error) {
role, err := s.Repo.GetAdminRole(ctx, id)
if err != nil || role == nil {
return nil, ErrRoleNotFound
}
perms, err := s.Repo.ListRolePermissions(ctx, id)
if err != nil {
return nil, err
}
if perms == nil {
perms = []string{}
}
return &RoleDTO{ID: role.ID, Name: role.Name, System: role.System, Permissions: perms}, nil
}
// ReplaceRolePermissions updates permissions with audit.
func (s *Service) ReplaceRolePermissions(ctx context.Context, adminID, roleID uuid.UUID, codes []string) error {
role, err := s.Repo.GetAdminRole(ctx, roleID)
if err != nil || role == nil {
return ErrRoleNotFound
}
for _, c := range codes {
if _, ok := knownPermissions[c]; !ok {
return ErrInvalidPerm
}
}
if err := s.Repo.ReplaceRolePermissions(ctx, roleID, codes); err != nil {
return err
}
meta, _ := json.Marshal(map[string]any{"permissions": codes, "role": role.Name})
return s.Repo.InsertAudit(ctx, adminID, "roles.permissions.update", "admin_role", roleID.String(), meta)
}