| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- package repository
- import (
- "context"
- "fmt"
- "entgo.io/ent/dialect"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- dbuser "github.com/Wei-Shaw/sub2api/ent/user"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/Wei-Shaw/sub2api/internal/service"
- )
- const userHierarchyLockKey = "user-management:administrator-roles:v1"
- // The supplied client must be transaction-bound. PostgreSQL owns the lock until
- // commit/rollback, including an outer transaction; no process-local mutex can
- // substitute for this across application replicas.
- func lockUserHierarchy(ctx context.Context, client *dbent.Client) (func(), error) {
- if client.Driver().Dialect() == dialect.Postgres {
- if err := client.Driver().Exec(ctx, "SELECT pg_advisory_xact_lock($1)", []any{advisoryLockHash(userHierarchyLockKey)}, nil); err != nil {
- return nil, err
- }
- return func() {}, nil
- }
- return repositoryScopedKeyLocks.lock(userHierarchyLockKey), nil
- }
- func lockHierarchyUsers(ctx context.Context, client *dbent.Client, ids ...int64) (map[int64]*dbent.User, error) {
- query := client.User.Query().Where(dbuser.IDIn(ids...), dbuser.DeletedAtIsNil()).Order(dbent.Asc(dbuser.FieldID))
- if client.Driver().Dialect() == dialect.Postgres {
- query = query.ForUpdate()
- }
- rows, err := query.All(ctx)
- if err != nil {
- return nil, err
- }
- users := make(map[int64]*dbent.User, len(rows))
- for _, user := range rows {
- users[user.ID] = user
- }
- return users, nil
- }
- func verifyHierarchyActor(actor *dbent.User, needsSuper bool) error {
- if actor == nil || actor.Status != service.StatusActive || !service.IsAdminRole(actor.Role) || (needsSuper && actor.Role != service.RoleSuperAdmin) {
- return infraerrors.Forbidden("SUPER_ADMIN_REQUIRED", "操作者权限已变更或不足,请刷新后重试")
- }
- return nil
- }
- func guardUserHierarchyUpdate(ctx context.Context, client *dbent.Client, incoming *service.User, fields service.UserUpdateFields) (func(), error) {
- actorID, managed := service.UserManagementActor(ctx)
- if !fields.Role && !fields.Status && !managed {
- return func() {}, nil
- }
- release, err := lockUserHierarchy(ctx, client)
- if err != nil {
- return nil, err
- }
- check := func() error {
- users, err := lockHierarchyUsers(ctx, client, incoming.ID, actorID)
- if err != nil {
- return err
- }
- current := users[incoming.ID]
- if current == nil {
- return service.ErrUserNotFound
- }
- roleChange := fields.Role && incoming.Role != current.Role
- if managed || roleChange {
- if err := verifyHierarchyActor(users[actorID], roleChange || service.IsAdminRole(current.Role)); err != nil {
- return err
- }
- if roleChange && incoming.ID == actorID {
- return infraerrors.Forbidden("SELF_DEMOTION_FORBIDDEN", "不能修改自己的管理员角色")
- }
- }
- if fields.Role && incoming.Role != service.RoleUser && !service.IsAdminRole(incoming.Role) {
- return infraerrors.BadRequest("INVALID_USER_ROLE", "无效的用户角色")
- }
- if fields.Status && incoming.Status != service.StatusActive && service.IsAdminRole(current.Role) {
- return infraerrors.Forbidden("ADMIN_PROTECTED", "不能禁用管理员账号")
- }
- if !roleChange || !service.IsAdminRole(current.Role) {
- return nil
- }
- remaining := client.User.Query().Where(dbuser.IDNEQ(current.ID), dbuser.StatusEQ(service.StatusActive), dbuser.DeletedAtIsNil())
- if current.Role == service.RoleSuperAdmin {
- remaining = remaining.Where(dbuser.RoleEQ(service.RoleSuperAdmin))
- } else {
- remaining = remaining.Where(dbuser.RoleIn(service.RoleSuperAdmin, service.RoleAdmin))
- }
- exists, err := remaining.Exist(ctx)
- if err != nil {
- return err
- }
- if !exists {
- return infraerrors.Forbidden("LAST_SUPER_ADMIN", "不能降级最后一个有效管理员")
- }
- return nil
- }
- if err := check(); err != nil {
- release()
- return nil, err
- }
- return release, nil
- }
- func guardUserHierarchyCreate(ctx context.Context, client *dbent.Client, user *service.User) (func(), error) {
- if !service.IsAdminRole(user.Role) {
- return func() {}, nil
- }
- release, err := lockUserHierarchy(ctx, client)
- if err != nil {
- return nil, err
- }
- if actorID, managed := service.UserManagementActor(ctx); managed {
- actors, err := lockHierarchyUsers(ctx, client, actorID)
- if err == nil {
- err = verifyHierarchyActor(actors[actorID], true)
- }
- if err != nil {
- release()
- return nil, err
- }
- }
- return release, nil
- }
- func guardUserHierarchyDelete(ctx context.Context, client *dbent.Client, id int64) (func(), error) {
- release, err := lockUserHierarchy(ctx, client)
- if err != nil {
- return nil, err
- }
- actorID, managed := service.UserManagementActor(ctx)
- ids := []int64{id}
- if managed {
- ids = append(ids, actorID)
- }
- users, err := lockHierarchyUsers(ctx, client, ids...)
- if err == nil {
- if users[id] == nil {
- err = service.ErrUserNotFound
- } else if managed {
- err = verifyHierarchyActor(users[actorID], false)
- } else if service.IsAdminRole(users[id].Role) {
- err = infraerrors.Forbidden("ADMIN_PROTECTED", fmt.Sprintf("不能删除管理员账号 %d", id))
- }
- if err == nil && service.IsAdminRole(users[id].Role) {
- err = infraerrors.Forbidden("ADMIN_PROTECTED", fmt.Sprintf("不能删除管理员账号 %d", id))
- }
- }
- if err != nil {
- release()
- return nil, err
- }
- return release, nil
- }
|