package usecase import ( "context" "gateway/internal/model/permission/domain" "gateway/internal/model/permission/domain/entity" "gateway/internal/model/permission/domain/enum" domrepo "gateway/internal/model/permission/domain/repository" dom "gateway/internal/model/permission/domain/usecase" "github.com/zeromicro/go-zero/core/logx" ) // UserRoleUseCaseParam injects role + user-role repositories. type UserRoleUseCaseParam struct { Roles domrepo.RoleRepository UserRoles domrepo.UserRoleRepository Reloader PolicyReloader } type userRoleUseCase struct { roles domrepo.RoleRepository userRoles domrepo.UserRoleRepository reload PolicyReloader } // NewUserRoleUseCase returns the assignment manager used by tenant // admins and SyncFromX flows. func NewUserRoleUseCase(param UserRoleUseCaseParam) dom.UserRoleUseCase { return &userRoleUseCase{ roles: param.Roles, userRoles: param.UserRoles, reload: param.Reloader, } } func (uc *userRoleUseCase) Assign(ctx context.Context, param *dom.AssignParam) (*entity.UserRole, error) { if param == nil || param.TenantID == "" || param.UID == "" || param.RoleID == "" { return nil, errb.InputMissingRequired("tenant_id|uid|role_id") } role, err := uc.roles.GetByID(ctx, param.TenantID, param.RoleID) if err != nil { return nil, wrapRepoErr(err) } source := param.Source if source == "" { source = enum.RoleSourceManual } if !source.IsValid() { return nil, errb.InputInvalidFormat("invalid source") } ur := &entity.UserRole{ TenantID: param.TenantID, UID: param.UID, RoleID: role.ID.Hex(), Source: source, } if err := uc.userRoles.Insert(ctx, ur); err != nil { return nil, wrapRepoErr(err, "assign role") } uc.broadcast(ctx, param.TenantID) return ur, nil } func (uc *userRoleUseCase) Revoke(ctx context.Context, tenantID, uid, roleID string) error { if err := uc.userRoles.Delete(ctx, tenantID, uid, roleID); err != nil { return wrapRepoErr(err, "revoke role") } uc.broadcast(ctx, tenantID) return nil } func (uc *userRoleUseCase) List(ctx context.Context, tenantID, uid string) ([]*dom.UserRoleSummary, error) { rows, err := uc.userRoles.ListByUser(ctx, tenantID, uid) if err != nil { return nil, wrapRepoErr(err) } if len(rows) == 0 { return nil, nil } ids := make([]string, 0, len(rows)) for _, ur := range rows { ids = append(ids, ur.RoleID) } roles, err := uc.roles.ListByTenantAndIDs(ctx, tenantID, ids) if err != nil { return nil, wrapRepoErr(err) } roleByID := make(map[string]*entity.Role, len(roles)) for _, role := range roles { roleByID[role.ID.Hex()] = role } out := make([]*dom.UserRoleSummary, 0, len(rows)) for _, ur := range rows { summary := &dom.UserRoleSummary{UserRole: ur} if role, ok := roleByID[ur.RoleID]; ok { summary.RoleKey = role.Key summary.RoleDisplayName = role.DisplayName } out = append(out, summary) } return out, nil } func (uc *userRoleUseCase) ReplaceForSource( ctx context.Context, tenantID, uid string, source enum.RoleSource, roleKeys []string, ) error { if !source.IsValid() { return errb.InputInvalidFormat("invalid source") } if source == enum.RoleSourceManual { // Manual assignments are managed via Assign/Revoke; protect from // accidental wipe by SyncFromX flows (defence in depth). return errb.ResInvalidState("manual source cannot be batch-replaced") } roleIDs := make([]string, 0, len(roleKeys)) for _, key := range roleKeys { role, err := uc.roles.GetByKey(ctx, tenantID, key) if err != nil { // Skip unknown keys silently — keeps SyncFromX resilient when // the IdP exposes groups the tenant has not mapped yet. continue } roleIDs = append(roleIDs, role.ID.Hex()) } if err := uc.userRoles.ReplaceForSource(ctx, tenantID, uid, source, roleIDs); err != nil { return wrapRepoErr(err, "replace user roles") } uc.broadcast(ctx, tenantID) return nil } func (uc *userRoleUseCase) broadcast(ctx context.Context, tenantID string) { if uc.reload == nil { return } if err := uc.reload(ctx, tenantID); err != nil { logx.WithContext(ctx).Errorf("permission user-role: broadcast reload tenant=%s: %v", tenantID, err) } } var _ dom.UserRoleUseCase = (*userRoleUseCase)(nil) var _ = domain.ReservedRoleKeyPrefixes // ensure domain package referenced for go build