420 lines
11 KiB
Go
420 lines
11 KiB
Go
package rolestore
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"aijianzhan/platform/internal/authx"
|
|
)
|
|
|
|
type Role struct {
|
|
RoleID int64 `json:"role_id"`
|
|
TenantID int64 `json:"tenant_id"`
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Permissions []string `json:"permissions"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type CreateInput struct {
|
|
Code string
|
|
Name string
|
|
Description string
|
|
Permissions []string
|
|
}
|
|
|
|
type UpdateInput struct {
|
|
Name *string
|
|
Description *string
|
|
Permissions *[]string
|
|
}
|
|
|
|
type Store interface {
|
|
List(ctx context.Context, tenantID int64) ([]Role, error)
|
|
Get(ctx context.Context, tenantID, roleID int64) (*Role, error)
|
|
Create(ctx context.Context, tenantID int64, in CreateInput) (*Role, error)
|
|
Update(ctx context.Context, tenantID, roleID int64, in UpdateInput) (*Role, error)
|
|
Delete(ctx context.Context, tenantID, roleID int64) error
|
|
EnsureDefaults(ctx context.Context, tenantID int64) error
|
|
}
|
|
|
|
var defaultRoles = []CreateInput{
|
|
{
|
|
Code: authx.AgentRole生成发布, Name: "生成发布", Description: "生成蓝图、发布应用并导入业务数据(智能体常用最低权限)",
|
|
Permissions: []string{
|
|
"读取模块", "发布模块",
|
|
"导入数据", "查询数据",
|
|
"上传文件", "下载文件",
|
|
},
|
|
},
|
|
{
|
|
Code: authx.AgentRole只读, Name: "只读", Description: "读应用与数据,可导出,不可改写",
|
|
Permissions: []string{"读取模块", "查询数据", "导出数据", "下载文件", "查看审计"},
|
|
},
|
|
{
|
|
Code: authx.AgentRole读写, Name: "读写", Description: "读写业务数据与导入导出,不可发布模块",
|
|
Permissions: []string{
|
|
"读取模块", "写入模块",
|
|
"新增数据", "查询数据", "更新数据", "导出数据", "导入数据",
|
|
"上传文件", "下载文件",
|
|
},
|
|
},
|
|
{
|
|
Code: authx.AgentRole运维, Name: "运维", Description: "含发布、删除及全量行操作,适合运维类智能体",
|
|
Permissions: []string{
|
|
"读取模块", "写入模块", "发布模块",
|
|
"新增数据", "查询数据", "更新数据", "删除数据", "导出数据", "导入数据",
|
|
"上传文件", "下载文件", "查看审计",
|
|
},
|
|
},
|
|
}
|
|
|
|
type memRole struct{ Role }
|
|
|
|
type MemoryStore struct {
|
|
mu sync.Mutex
|
|
seq int64
|
|
by map[int64]*memRole
|
|
}
|
|
|
|
func NewMemoryStore() *MemoryStore {
|
|
return &MemoryStore{by: map[int64]*memRole{}}
|
|
}
|
|
|
|
func findDefaultRole(byCode map[string]Role, d CreateInput) (Role, bool) {
|
|
if r, ok := byCode[d.Code]; ok {
|
|
return r, true
|
|
}
|
|
legacy := map[string]string{
|
|
"publisher": authx.AgentRole生成发布,
|
|
"viewer": authx.AgentRole只读,
|
|
"editor": authx.AgentRole读写,
|
|
"operator": authx.AgentRole运维,
|
|
}
|
|
for eng, zh := range legacy {
|
|
if zh == d.Code {
|
|
if r, ok := byCode[eng]; ok {
|
|
return r, true
|
|
}
|
|
}
|
|
}
|
|
return Role{}, false
|
|
}
|
|
|
|
func (s *MemoryStore) EnsureDefaults(ctx context.Context, tenantID int64) error {
|
|
list, err := s.List(ctx, tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
byCode := map[string]Role{}
|
|
for _, r := range list {
|
|
byCode[r.Code] = r
|
|
}
|
|
for _, d := range defaultRoles {
|
|
if existing, ok := findDefaultRole(byCode, d); ok {
|
|
if d.Code == authx.AgentRole生成发布 {
|
|
perms := append([]string{}, d.Permissions...)
|
|
if _, err := s.Update(ctx, tenantID, existing.RoleID, UpdateInput{
|
|
Name: &d.Name,
|
|
Description: &d.Description,
|
|
Permissions: &perms,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if _, err := s.Create(ctx, tenantID, d); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *MemoryStore) List(_ context.Context, tenantID int64) ([]Role, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
out := []Role{}
|
|
for _, r := range s.by {
|
|
if r.TenantID == tenantID {
|
|
out = append(out, clone(r.Role))
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *MemoryStore) Get(_ context.Context, tenantID, roleID int64) (*Role, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
r, ok := s.by[roleID]
|
|
if !ok || r.TenantID != tenantID {
|
|
return nil, fmt.Errorf("role not found")
|
|
}
|
|
cp := clone(r.Role)
|
|
return &cp, nil
|
|
}
|
|
|
|
func (s *MemoryStore) Create(_ context.Context, tenantID int64, in CreateInput) (*Role, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
code := strings.TrimSpace(in.Code)
|
|
name := strings.TrimSpace(in.Name)
|
|
if code == "" || name == "" {
|
|
return nil, fmt.Errorf("code and name required")
|
|
}
|
|
for _, r := range s.by {
|
|
if r.TenantID == tenantID && r.Code == code {
|
|
return nil, fmt.Errorf("role code already exists")
|
|
}
|
|
}
|
|
s.seq++
|
|
r := &memRole{Role: Role{
|
|
RoleID: s.seq,
|
|
TenantID: tenantID,
|
|
Code: code,
|
|
Name: name,
|
|
Description: strings.TrimSpace(in.Description),
|
|
Permissions: uniq(in.Permissions),
|
|
CreatedAt: time.Now().UTC(),
|
|
}}
|
|
s.by[r.RoleID] = r
|
|
cp := clone(r.Role)
|
|
return &cp, nil
|
|
}
|
|
|
|
func (s *MemoryStore) Update(_ context.Context, tenantID, roleID int64, in UpdateInput) (*Role, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
r, ok := s.by[roleID]
|
|
if !ok || r.TenantID != tenantID {
|
|
return nil, fmt.Errorf("role not found")
|
|
}
|
|
if in.Name != nil {
|
|
r.Name = strings.TrimSpace(*in.Name)
|
|
}
|
|
if in.Description != nil {
|
|
r.Description = strings.TrimSpace(*in.Description)
|
|
}
|
|
if in.Permissions != nil {
|
|
r.Permissions = uniq(*in.Permissions)
|
|
}
|
|
cp := clone(r.Role)
|
|
return &cp, nil
|
|
}
|
|
|
|
func (s *MemoryStore) Delete(_ context.Context, tenantID, roleID int64) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
r, ok := s.by[roleID]
|
|
if !ok || r.TenantID != tenantID {
|
|
return fmt.Errorf("role not found")
|
|
}
|
|
delete(s.by, roleID)
|
|
return nil
|
|
}
|
|
|
|
type PostgresStore struct{ DB *sql.DB }
|
|
|
|
func NewPostgresStore(db *sql.DB) *PostgresStore { return &PostgresStore{DB: db} }
|
|
|
|
func (s *PostgresStore) EnsureDefaults(ctx context.Context, tenantID int64) error {
|
|
list, err := s.List(ctx, tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
byCode := map[string]Role{}
|
|
for _, r := range list {
|
|
byCode[r.Code] = r
|
|
}
|
|
for _, d := range defaultRoles {
|
|
if existing, ok := findDefaultRole(byCode, d); ok {
|
|
// 英文旧编码升级为中文
|
|
if existing.Code != d.Code {
|
|
if _, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.roles SET code=$1, name=$2 WHERE role_id=$3 AND tenant_id=$4`,
|
|
d.Code, d.Name, existing.RoleID, tenantID); err != nil {
|
|
return err
|
|
}
|
|
existing.Code = d.Code
|
|
existing.Name = d.Name
|
|
byCode[d.Code] = existing
|
|
}
|
|
if d.Code == authx.AgentRole生成发布 {
|
|
perms := append([]string{}, d.Permissions...)
|
|
if _, err := s.Update(ctx, tenantID, existing.RoleID, UpdateInput{
|
|
Name: &d.Name,
|
|
Description: &d.Description,
|
|
Permissions: &perms,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if _, err := s.Create(ctx, tenantID, d); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *PostgresStore) List(ctx context.Context, tenantID int64) ([]Role, error) {
|
|
rows, err := s.DB.QueryContext(ctx, `
|
|
SELECT role_id, tenant_id, code, name, description, created_at
|
|
FROM platform_meta.roles WHERE tenant_id=$1 ORDER BY role_id`, tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Role
|
|
for rows.Next() {
|
|
var r Role
|
|
if err := rows.Scan(&r.RoleID, &r.TenantID, &r.Code, &r.Name, &r.Description, &r.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
perms, err := s.loadPerms(ctx, r.RoleID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.Permissions = perms
|
|
out = append(out, r)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *PostgresStore) Get(ctx context.Context, tenantID, roleID int64) (*Role, error) {
|
|
var r Role
|
|
err := s.DB.QueryRowContext(ctx, `
|
|
SELECT role_id, tenant_id, code, name, description, created_at
|
|
FROM platform_meta.roles WHERE role_id=$1 AND tenant_id=$2`, roleID, tenantID,
|
|
).Scan(&r.RoleID, &r.TenantID, &r.Code, &r.Name, &r.Description, &r.CreatedAt)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, fmt.Errorf("role not found")
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
perms, err := s.loadPerms(ctx, r.RoleID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.Permissions = perms
|
|
return &r, nil
|
|
}
|
|
|
|
func (s *PostgresStore) Create(ctx context.Context, tenantID int64, in CreateInput) (*Role, error) {
|
|
code := strings.TrimSpace(in.Code)
|
|
name := strings.TrimSpace(in.Name)
|
|
if code == "" || name == "" {
|
|
return nil, fmt.Errorf("code and name required")
|
|
}
|
|
var r Role
|
|
err := s.DB.QueryRowContext(ctx, `
|
|
INSERT INTO platform_meta.roles(tenant_id, code, name, description)
|
|
VALUES($1,$2,$3,$4)
|
|
RETURNING role_id, tenant_id, code, name, description, created_at`,
|
|
tenantID, code, name, strings.TrimSpace(in.Description),
|
|
).Scan(&r.RoleID, &r.TenantID, &r.Code, &r.Name, &r.Description, &r.CreatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
perms := uniq(in.Permissions)
|
|
if err := s.replacePerms(ctx, r.RoleID, perms); err != nil {
|
|
return nil, err
|
|
}
|
|
r.Permissions = perms
|
|
return &r, nil
|
|
}
|
|
|
|
func (s *PostgresStore) Update(ctx context.Context, tenantID, roleID int64, in UpdateInput) (*Role, error) {
|
|
r, err := s.Get(ctx, tenantID, roleID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
name, desc := r.Name, r.Description
|
|
if in.Name != nil {
|
|
name = strings.TrimSpace(*in.Name)
|
|
}
|
|
if in.Description != nil {
|
|
desc = strings.TrimSpace(*in.Description)
|
|
}
|
|
if _, err := s.DB.ExecContext(ctx, `
|
|
UPDATE platform_meta.roles SET name=$1, description=$2 WHERE role_id=$3 AND tenant_id=$4`,
|
|
name, desc, roleID, tenantID); err != nil {
|
|
return nil, err
|
|
}
|
|
if in.Permissions != nil {
|
|
if err := s.replacePerms(ctx, roleID, uniq(*in.Permissions)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return s.Get(ctx, tenantID, roleID)
|
|
}
|
|
|
|
func (s *PostgresStore) Delete(ctx context.Context, tenantID, roleID int64) error {
|
|
_, _ = s.DB.ExecContext(ctx, `
|
|
UPDATE platform_meta.agent_accounts SET role_id=NULL WHERE role_id=$1 AND tenant_id=$2`, roleID, tenantID)
|
|
res, err := s.DB.ExecContext(ctx, `
|
|
DELETE FROM platform_meta.roles WHERE role_id=$1 AND tenant_id=$2`, roleID, tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("role not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *PostgresStore) loadPerms(ctx context.Context, roleID int64) ([]string, error) {
|
|
rows, err := s.DB.QueryContext(ctx, `SELECT perm FROM platform_meta.role_permissions WHERE role_id=$1`, roleID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []string
|
|
for rows.Next() {
|
|
var p string
|
|
if err := rows.Scan(&p); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return authx.NormalizePerms(out), nil
|
|
}
|
|
|
|
func (s *PostgresStore) replacePerms(ctx context.Context, roleID int64, perms []string) error {
|
|
tx, err := s.DB.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM platform_meta.role_permissions WHERE role_id=$1`, roleID); err != nil {
|
|
return err
|
|
}
|
|
for _, p := range authx.NormalizePerms(perms) {
|
|
if _, err := tx.ExecContext(ctx, `INSERT INTO platform_meta.role_permissions(role_id, perm) VALUES($1,$2)`, roleID, p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func clone(r Role) Role {
|
|
cp := r
|
|
cp.Permissions = append([]string{}, r.Permissions...)
|
|
return cp
|
|
}
|
|
|
|
func uniq(in []string) []string {
|
|
return authx.NormalizePerms(in)
|
|
}
|