package service import ( "context" "errors" "strings" "time" "github.com/google/uuid" "github.com/ocdp/cluster-service/internal/domain/entity" "github.com/ocdp/cluster-service/internal/domain/repository" "github.com/ocdp/cluster-service/internal/pkg/authz" jwtpkg "github.com/ocdp/cluster-service/internal/pkg/jwt" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/validation" ) // AuthService 认证领域服务 type AuthService struct { userRepo repository.UserRepository workspaceRepo repository.WorkspaceRepository instanceRepo repository.InstanceRepository clusterRepo repository.ClusterRepository bindingRepo repository.WorkspaceClusterBindingRepository tenantClient repository.TenantKubeClient passwordHasher PasswordHasher tokenGenerator TokenGenerator } // PasswordHasher 密码哈希接口 type PasswordHasher interface { Hash(password string) (string, error) Verify(password, hash string) error } // TokenGenerator Token 生成器接口 type TokenGenerator interface { Generate(userID, username, role, workspaceID string) (accessToken, refreshToken string, err error) Verify(token string) (userID, username string, err error) VerifyWithIssuedAt(token string) (userID, username string, issuedAt int64, err error) VerifyAccess(token string) (*jwtpkg.Claims, error) VerifyRefresh(token string) (*jwtpkg.Claims, error) Refresh(refreshToken string) (newAccessToken string, err error) } // NewAuthService 创建认证服务 func NewAuthService( userRepo repository.UserRepository, workspaceRepo repository.WorkspaceRepository, passwordHasher PasswordHasher, tokenGenerator TokenGenerator, ) *AuthService { return &AuthService{ userRepo: userRepo, workspaceRepo: workspaceRepo, passwordHasher: passwordHasher, tokenGenerator: tokenGenerator, } } func (s *AuthService) SetUserLifecycleCleanup( instanceRepo repository.InstanceRepository, clusterRepo repository.ClusterRepository, bindingRepo repository.WorkspaceClusterBindingRepository, tenantClient repository.TenantKubeClient, ) { s.instanceRepo = instanceRepo s.clusterRepo = clusterRepo s.bindingRepo = bindingRepo s.tenantClient = tenantClient } // Register 注册新用户。业务入口只允许 admin 调用;初始 admin 由 bootstrap seeder 创建。 type UserWorkspaceOptions struct { Namespace string DefaultClusterID string QuotaCPU string QuotaMemory string QuotaGPU string QuotaGPUMem string } func defaultEmail(username string) string { return username + "@local.ocdp" } // IsAdminExists checks whether any admin user already exists in the database. func (s *AuthService) IsAdminExists(ctx context.Context) (bool, error) { return s.userRepo.AdminExists(ctx) } // SetupInitialAdmin creates the first admin user and returns access + refresh tokens. // Fails if an admin already exists. func (s *AuthService) SetupInitialAdmin(ctx context.Context, username, password, email string) (*entity.User, string, string, error) { hasAdmin, err := s.IsAdminExists(ctx) if err != nil { return nil, "", "", err } if hasAdmin { return nil, "", "", entity.ErrForbidden } passwordHash, err := s.passwordHasher.Hash(password) if err != nil { return nil, "", "", err } if email == "" { email = defaultEmail(username) } user := entity.NewUser(username, passwordHash, email) user.ID = uuid.New().String() user.Role = authz.RoleAdmin user.WorkspaceID = entity.DefaultWorkspaceID if err := user.Validate(); err != nil { return nil, "", "", err } if err := s.userRepo.Create(ctx, user); err != nil { return nil, "", "", err } // Generate tokens directly — avoid a separate login round-trip accessToken, refreshToken, err := s.tokenGenerator.Generate(user.ID, user.Username, user.Role, user.WorkspaceID) if err != nil { return nil, "", "", err } return user, accessToken, refreshToken, nil } func (s *AuthService) Register(ctx context.Context, username, password, role, workspaceID string, opts UserWorkspaceOptions, isActive, mustChangePassword *bool) (*entity.User, error) { principal, err := authz.RequirePrincipal(ctx) if err != nil { return nil, entity.ErrUnauthorized } if !principal.IsAdmin() { return nil, entity.ErrForbidden } // 检查用户是否已存在 existingUser, _ := s.userRepo.GetByUsername(ctx, username) if existingUser != nil { return nil, entity.ErrUserExists } // 哈希密码 passwordHash, err := s.passwordHasher.Hash(password) if err != nil { return nil, err } normalizedOpts, err := normalizeQuotaOptions(opts) if err != nil { return nil, err } if normalizeUserRole(role) == authz.RoleUser { normalizedOpts = defaultUserQuotaOptions(normalizedOpts) } // 默认生成占位邮箱,避免数据库约束失败 email := defaultEmail(username) // 创建用户 user := entity.NewUser(username, passwordHash, email) user.ID = uuid.New().String() user.Role = normalizeUserRole(role) user.WorkspaceID = workspaceID if user.Role == authz.RoleUser { workspace, err := s.createUserWorkspace(ctx, username, principal.UserID, normalizedOpts) if err != nil { return nil, err } user.WorkspaceID = workspace.ID } if user.WorkspaceID == "" { user.WorkspaceID = entity.DefaultWorkspaceID } if user.Role == authz.RoleAdmin { user.WorkspaceID = entity.DefaultWorkspaceID } if isActive != nil { user.IsActive = *isActive } if mustChangePassword != nil { user.MustChangePassword = *mustChangePassword } if err := user.Validate(); err != nil { return nil, err } if err := s.userRepo.Create(ctx, user); err != nil { return nil, err } return user, nil } func (s *AuthService) createUserWorkspace(ctx context.Context, username, createdBy string, opts UserWorkspaceOptions) (*entity.Workspace, error) { if s.workspaceRepo == nil { return nil, entity.ErrWorkspaceNotFound } name := userWorkspaceName(username) namespace := strings.TrimSpace(opts.Namespace) if namespace == "" { namespace = entity.NamespaceForUser(username) } if namespace != "" { if len(validation.IsDNS1123Label(namespace)) > 0 { return nil, entity.ErrInvalidNamespace } } if existing, err := s.workspaceRepo.GetByName(ctx, name); err == nil && existing != nil { if namespace != "" && existing.K8sNamespace != namespace { if err := s.ensureNamespaceAvailable(ctx, namespace, existing.ID); err != nil { return nil, err } } applyWorkspaceOptions(existing, opts) if namespace != "" { existing.K8sNamespace = namespace existing.K8sSAName = entity.ServiceAccountForNamespace(namespace) } if err := s.workspaceRepo.Update(ctx, existing); err != nil { return nil, err } return existing, nil } else if err != nil && !errors.Is(err, entity.ErrWorkspaceNotFound) { return nil, err } if err := s.ensureNamespaceAvailable(ctx, namespace, ""); err != nil { return nil, err } workspace := entity.NewWorkspace(name, createdBy) workspace.ID = uuid.New().String() workspace.DefaultClusterID = strings.TrimSpace(opts.DefaultClusterID) if namespace != "" { workspace.K8sNamespace = namespace workspace.K8sSAName = entity.ServiceAccountForNamespace(namespace) } workspace.QuotaCPU = strings.TrimSpace(opts.QuotaCPU) workspace.QuotaMemory = strings.TrimSpace(opts.QuotaMemory) workspace.QuotaGPU = strings.TrimSpace(opts.QuotaGPU) workspace.QuotaGPUMem = strings.TrimSpace(opts.QuotaGPUMem) if err := s.workspaceRepo.Create(ctx, workspace); err != nil { if errors.Is(err, entity.ErrWorkspaceExists) { existing, getErr := s.workspaceRepo.GetByName(ctx, name) if getErr != nil { return nil, err } if existing.K8sNamespace != namespace { return nil, entity.ErrWorkspaceNamespaceConflict } return existing, nil } return nil, err } return workspace, nil } func userWorkspaceName(username string) string { return strings.TrimPrefix(entity.NamespaceForUser(username), "ocdp-u-") } func (s *AuthService) ensureNamespaceAvailable(ctx context.Context, namespace, allowedWorkspaceID string) error { if s.workspaceRepo == nil || strings.TrimSpace(namespace) == "" { return nil } workspaces, err := s.workspaceRepo.List(ctx) if err != nil { return err } for _, workspace := range workspaces { if workspace == nil || workspace.K8sNamespace != namespace { continue } if allowedWorkspaceID != "" && workspace.ID == allowedWorkspaceID { continue } return entity.ErrWorkspaceNamespaceConflict } return nil } func normalizeQuotaOptions(opts UserWorkspaceOptions) (UserWorkspaceOptions, error) { opts.Namespace = strings.TrimSpace(opts.Namespace) opts.DefaultClusterID = strings.TrimSpace(opts.DefaultClusterID) opts.QuotaCPU = normalizeStandardQuotaQuantity(opts.QuotaCPU) opts.QuotaMemory = normalizeStandardQuotaQuantity(opts.QuotaMemory) opts.QuotaGPU = normalizeStandardQuotaQuantity(opts.QuotaGPU) gpuMem, err := normalizeGPUMemoryQuota(opts.QuotaGPUMem) if err != nil { return opts, err } opts.QuotaGPUMem = gpuMem for _, value := range []string{opts.QuotaCPU, opts.QuotaMemory, opts.QuotaGPU} { if value == "" { continue } if _, err := resource.ParseQuantity(value); err != nil { return opts, entity.ErrInvalidTenantResourceQuota } } if opts.Namespace != "" && len(validation.IsDNS1123Label(opts.Namespace)) > 0 { return opts, entity.ErrInvalidNamespace } return opts, nil } func defaultUserQuotaOptions(opts UserWorkspaceOptions) UserWorkspaceOptions { if strings.TrimSpace(opts.QuotaGPU) == "" { opts.QuotaGPU = "0" } if strings.TrimSpace(opts.QuotaGPUMem) == "" { opts.QuotaGPUMem = "0" } return opts } func (s *AuthService) ListUsers(ctx context.Context) ([]*entity.User, error) { principal, err := authz.RequirePrincipal(ctx) if err != nil { return nil, entity.ErrUnauthorized } if !principal.IsAdmin() { return nil, entity.ErrForbidden } return s.userRepo.List(ctx) } func (s *AuthService) UpdateUser(ctx context.Context, userID, role, workspaceID string, opts UserWorkspaceOptions, isActive, mustChangePassword *bool) (*entity.User, error) { principal, err := authz.RequirePrincipal(ctx) if err != nil { return nil, entity.ErrUnauthorized } if !principal.IsAdmin() { return nil, entity.ErrForbidden } user, err := s.userRepo.GetByID(ctx, userID) if err != nil { return nil, entity.ErrUserNotFound } previousRole := user.Role if role != "" { user.Role = normalizeUserRole(role) } if workspaceID != "" && user.Role != authz.RoleUser { user.WorkspaceID = workspaceID } workspaceHandled := false if user.Role == authz.RoleAdmin { user.WorkspaceID = entity.DefaultWorkspaceID } if user.Role == authz.RoleUser && (role != "" || workspaceID != "" || hasWorkspaceUpdates(opts)) { normalizedOpts, err := normalizeQuotaOptions(opts) if err != nil { return nil, err } normalizedOpts = defaultUserQuotaOptions(normalizedOpts) currentWorkspace, _ := s.currentUserWorkspace(ctx, user) if currentWorkspace != nil && shouldCreatePrivateWorkspace(user, previousRole, currentWorkspace) { if normalizedOpts.Namespace == "" || normalizedOpts.Namespace == currentWorkspace.K8sNamespace { normalizedOpts.Namespace = "" } } workspace, err := s.ensureUserWorkspaceForUpdate(ctx, user, previousRole, currentWorkspace, opts, normalizedOpts, principal.UserID) if err != nil { return nil, err } user.WorkspaceID = workspace.ID workspaceHandled = true } if isActive != nil { if user.ID == principal.UserID && !*isActive { return nil, entity.ErrForbidden } user.IsActive = *isActive } if mustChangePassword != nil { user.MustChangePassword = *mustChangePassword } if user.Role != authz.RoleAdmin && !workspaceHandled && hasWorkspaceUpdates(opts) { normalizedOpts, err := normalizeQuotaOptions(opts) if err != nil { return nil, err } workspace, err := s.workspaceRepo.GetByID(ctx, user.WorkspaceID) if err != nil { return nil, err } applyWorkspaceOptionsForUpdate(workspace, opts, normalizedOpts) if err := s.workspaceRepo.Update(ctx, workspace); err != nil { return nil, err } if err := s.syncWorkspaceBindings(ctx, workspace); err != nil { return nil, err } } user.RevokedAfter = time.Now() user.UpdatedAt = time.Now() if err := user.Validate(); err != nil { return nil, err } if err := s.userRepo.Update(ctx, user); err != nil { return nil, err } return user, nil } func hasWorkspaceUpdates(opts UserWorkspaceOptions) bool { return strings.TrimSpace(opts.Namespace) != "" || strings.TrimSpace(opts.DefaultClusterID) != "" || strings.TrimSpace(opts.QuotaCPU) != "" || strings.TrimSpace(opts.QuotaMemory) != "" || strings.TrimSpace(opts.QuotaGPU) != "" || strings.TrimSpace(opts.QuotaGPUMem) != "" } func applyWorkspaceOptions(workspace *entity.Workspace, opts UserWorkspaceOptions) { if namespace := strings.TrimSpace(opts.Namespace); namespace != "" { workspace.K8sNamespace = namespace workspace.K8sSAName = entity.ServiceAccountForNamespace(namespace) } if value := strings.TrimSpace(opts.DefaultClusterID); value != "" { workspace.DefaultClusterID = value } if value := strings.TrimSpace(opts.QuotaCPU); value != "" { workspace.QuotaCPU = value } if value := strings.TrimSpace(opts.QuotaMemory); value != "" { workspace.QuotaMemory = value } if value := strings.TrimSpace(opts.QuotaGPU); value != "" { workspace.QuotaGPU = value } if value := strings.TrimSpace(opts.QuotaGPUMem); value != "" { workspace.QuotaGPUMem = value } } func (s *AuthService) currentUserWorkspace(ctx context.Context, user *entity.User) (*entity.Workspace, error) { if s.workspaceRepo == nil || user == nil || user.WorkspaceID == "" { return nil, entity.ErrWorkspaceNotFound } return s.workspaceRepo.GetByID(ctx, user.WorkspaceID) } func shouldCreatePrivateWorkspace(user *entity.User, previousRole string, current *entity.Workspace) bool { if user == nil { return true } if previousRole == authz.RoleAdmin || user.WorkspaceID == "" || user.WorkspaceID == entity.DefaultWorkspaceID { return true } if current == nil { return true } return current.Name != userWorkspaceName(user.Username) } func (s *AuthService) ensureUserWorkspaceForUpdate(ctx context.Context, user *entity.User, previousRole string, current *entity.Workspace, rawOpts, normalizedOpts UserWorkspaceOptions, createdBy string) (*entity.Workspace, error) { if s.workspaceRepo == nil { return nil, entity.ErrWorkspaceNotFound } if shouldCreatePrivateWorkspace(user, previousRole, current) { return s.createUserWorkspace(ctx, user.Username, createdBy, normalizedOpts) } if rawNamespace := strings.TrimSpace(rawOpts.Namespace); rawNamespace != "" && rawNamespace != current.K8sNamespace { if err := s.ensureNamespaceAvailable(ctx, rawNamespace, current.ID); err != nil { return nil, err } } applyWorkspaceOptionsForUpdate(current, rawOpts, normalizedOpts) if err := s.workspaceRepo.Update(ctx, current); err != nil { return nil, err } if err := s.syncWorkspaceBindings(ctx, current); err != nil { return nil, err } return current, nil } func applyWorkspaceOptionsForUpdate(workspace *entity.Workspace, rawOpts, normalizedOpts UserWorkspaceOptions) { if namespace := strings.TrimSpace(rawOpts.Namespace); namespace != "" { workspace.K8sNamespace = namespace workspace.K8sSAName = entity.ServiceAccountForNamespace(namespace) } if strings.TrimSpace(rawOpts.DefaultClusterID) != "" { workspace.DefaultClusterID = normalizedOpts.DefaultClusterID } if strings.TrimSpace(rawOpts.QuotaCPU) != "" { workspace.QuotaCPU = normalizedOpts.QuotaCPU } if strings.TrimSpace(rawOpts.QuotaMemory) != "" { workspace.QuotaMemory = normalizedOpts.QuotaMemory } if strings.TrimSpace(rawOpts.QuotaGPU) != "" { workspace.QuotaGPU = normalizedOpts.QuotaGPU } if strings.TrimSpace(rawOpts.QuotaGPUMem) != "" { workspace.QuotaGPUMem = normalizedOpts.QuotaGPUMem } } func (s *AuthService) syncWorkspaceBindings(ctx context.Context, workspace *entity.Workspace) error { if workspace == nil || s.bindingRepo == nil { return nil } bindings, err := s.bindingRepo.ListByWorkspace(ctx, workspace.ID) if err != nil { return err } for _, binding := range bindings { if binding == nil { continue } binding.QuotaCPU = strings.TrimSpace(workspace.QuotaCPU) binding.QuotaMemory = strings.TrimSpace(workspace.QuotaMemory) binding.QuotaGPU = strings.TrimSpace(workspace.QuotaGPU) if binding.QuotaGPU == "" { binding.QuotaGPU = "0" } binding.QuotaGPUMem = strings.TrimSpace(workspace.QuotaGPUMem) if binding.QuotaGPUMem == "" { binding.QuotaGPUMem = "0" } binding.UpdatedAt = time.Now() if s.tenantClient != nil && s.clusterRepo != nil { cluster, err := s.clusterRepo.GetByID(ctx, binding.ClusterID) if err != nil { if errors.Is(err, entity.ErrClusterNotFound) { continue } return err } tenantBinding := entity.NewTenantBinding(binding.Namespace) tenantBinding.ServiceAccountName = binding.ServiceAccount tenantBinding.ResourceQuotaHard = bindingQuotaHard(binding) if err := s.tenantClient.EnsureTenant(ctx, cluster, tenantBinding); err != nil { return err } } if err := s.bindingRepo.Upsert(ctx, binding); err != nil { return err } } return nil } func (s *AuthService) DeleteUser(ctx context.Context, userID string) error { principal, err := authz.RequirePrincipal(ctx) if err != nil { return entity.ErrUnauthorized } if !principal.IsAdmin() { return entity.ErrForbidden } if userID == principal.UserID { return entity.ErrForbidden } user, err := s.userRepo.GetByID(ctx, userID) if err != nil { return entity.ErrUserNotFound } if err := s.ensureUserHasNoInstances(ctx, user); err != nil { return err } if s.isExclusiveUserWorkspace(ctx, user) { if err := s.cleanupUserWorkspace(ctx, user.WorkspaceID); err != nil { return err } } return s.userRepo.Delete(ctx, userID) } func (s *AuthService) ensureUserHasNoInstances(ctx context.Context, user *entity.User) error { if s.instanceRepo == nil || user == nil { return nil } instances, err := s.instanceRepo.List(ctx) if err != nil { return err } for _, instance := range instances { if instance == nil { continue } if instance.OwnerID == user.ID { return entity.ErrUserHasInstances } if user.WorkspaceID != "" && user.WorkspaceID != entity.DefaultWorkspaceID && instance.WorkspaceID == user.WorkspaceID { return entity.ErrUserHasInstances } } return nil } func (s *AuthService) isExclusiveUserWorkspace(ctx context.Context, user *entity.User) bool { if user == nil || user.Role == authz.RoleAdmin || user.WorkspaceID == "" || user.WorkspaceID == entity.DefaultWorkspaceID { return false } users, err := s.userRepo.List(ctx) if err != nil { return false } for _, other := range users { if other == nil || other.ID == user.ID { continue } if other.WorkspaceID == user.WorkspaceID { return false } } return true } func (s *AuthService) cleanupUserWorkspace(ctx context.Context, workspaceID string) error { if s.workspaceRepo == nil || s.bindingRepo == nil { return nil } workspace, err := s.workspaceRepo.GetByID(ctx, workspaceID) if err != nil { return err } if isProtectedWorkspaceNamespace(workspace.K8sNamespace) { return entity.ErrProtectedNamespace } bindings, err := s.bindingRepo.ListByWorkspace(ctx, workspace.ID) if err != nil { return err } for _, binding := range bindings { if binding == nil { continue } if isProtectedWorkspaceNamespace(binding.Namespace) { return entity.ErrProtectedNamespace } if s.tenantClient != nil && s.clusterRepo != nil { cluster, err := s.clusterRepo.GetByID(ctx, binding.ClusterID) if err != nil && !errors.Is(err, entity.ErrClusterNotFound) { return err } if err == nil { tenantBinding := entity.NewTenantBinding(binding.Namespace) tenantBinding.ServiceAccountName = binding.ServiceAccount tenantBinding.ResourceQuotaHard = resourceQuotaHard(workspace) if err := s.tenantClient.DeleteTenant(ctx, cluster, tenantBinding); err != nil { return err } } } if err := s.bindingRepo.Delete(ctx, binding.WorkspaceID, binding.ClusterID); err != nil { return err } } if err := s.workspaceRepo.Delete(ctx, workspace.ID); err != nil && !errors.Is(err, entity.ErrWorkspaceNotFound) { return err } return nil } func isProtectedWorkspaceNamespace(namespace string) bool { switch strings.TrimSpace(namespace) { case "", "default", "kube-system", "kube-public", "kube-node-lease": return true default: return false } } func normalizeUserRole(role string) string { if role == authz.RoleAdmin { return authz.RoleAdmin } return authz.RoleUser } // Login 用户登录 func (s *AuthService) Login(ctx context.Context, username, password string) (accessToken, refreshToken string, user *entity.User, err error) { // 查找用户 user, err = s.userRepo.GetByUsername(ctx, username) if err != nil { return "", "", nil, entity.ErrUserNotFound } if !user.IsActive { return "", "", nil, entity.ErrUserInactive } if err := s.ensureWorkspaceActive(ctx, user); err != nil { return "", "", nil, err } // 验证密码 if err := s.passwordHasher.Verify(password, user.PasswordHash); err != nil { return "", "", nil, entity.ErrInvalidPassword } // 生成 Token accessToken, refreshToken, err = s.tokenGenerator.Generate(user.ID, user.Username, user.Role, user.WorkspaceID) if err != nil { return "", "", nil, err } return accessToken, refreshToken, user, nil } // RefreshToken 刷新 Token func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (string, *entity.User, error) { claims, err := s.tokenGenerator.VerifyRefresh(refreshToken) if err != nil { return "", nil, err } user, err := s.userRepo.GetByID(ctx, claims.UserID) if err != nil { return "", nil, entity.ErrUserNotFound } if !user.IsActive { return "", nil, entity.ErrUserInactive } if claims.IssuedAt == nil || claims.IssuedAt.Unix() < user.RevokedAfter.Unix() { return "", nil, entity.ErrTokenRevoked } if err := s.ensureWorkspaceActive(ctx, user); err != nil { return "", nil, err } accessToken, _, err := s.tokenGenerator.Generate(user.ID, user.Username, user.Role, user.WorkspaceID) if err != nil { return "", nil, err } return accessToken, user, nil } // GetUserByID 根据 ID 获取用户 func (s *AuthService) GetUserByID(ctx context.Context, id string) (*entity.User, error) { return s.userRepo.GetByID(ctx, id) } // VerifyAccessToken 验证 Access Token(包括 revoked_after 检查) func (s *AuthService) VerifyAccessToken(ctx context.Context, token string) (*authz.Principal, error) { // 1. JWT 自验证 claims, err := s.tokenGenerator.VerifyAccess(token) if err != nil { return nil, err } // 2. 检查用户级别的撤销时间 user, err := s.userRepo.GetByID(ctx, claims.UserID) if err != nil { return nil, entity.ErrUserNotFound } if !user.IsActive { return nil, entity.ErrUserInactive } // 3. 如果 Token 签发时间早于 revoked_after,则失效 if claims.IssuedAt == nil || claims.IssuedAt.Unix() < user.RevokedAfter.Unix() { return nil, entity.ErrTokenRevoked } if err := s.ensureWorkspaceActive(ctx, user); err != nil { return nil, err } workspaceName := "" namespace := "" defaultClusterID := "" quotaCPU := "" quotaMemory := "" quotaGPU := "" quotaGPUMem := "" if s.workspaceRepo != nil && user.WorkspaceID != "" { if workspace, err := s.workspaceRepo.GetByID(ctx, user.WorkspaceID); err == nil && workspace != nil { workspaceName = workspace.Name namespace = workspace.K8sNamespace defaultClusterID = workspace.DefaultClusterID quotaCPU = workspace.QuotaCPU quotaMemory = workspace.QuotaMemory quotaGPU = workspace.QuotaGPU quotaGPUMem = workspace.QuotaGPUMem } } return &authz.Principal{ UserID: user.ID, Username: user.Username, Role: user.Role, WorkspaceID: user.WorkspaceID, WorkspaceName: workspaceName, Namespace: namespace, DefaultClusterID: defaultClusterID, QuotaCPU: quotaCPU, QuotaMemory: quotaMemory, QuotaGPU: quotaGPU, QuotaGPUMem: quotaGPUMem, Permissions: authz.PermissionsForRole(user.Role), PermissionVersion: 1, }, nil } func (s *AuthService) GetWorkspaceByID(ctx context.Context, id string) (*entity.Workspace, error) { if s.workspaceRepo == nil || id == "" { return nil, entity.ErrWorkspaceNotFound } return s.workspaceRepo.GetByID(ctx, id) } func (s *AuthService) ensureWorkspaceActive(ctx context.Context, user *entity.User) error { if user.Role == authz.RoleAdmin || user.WorkspaceID == "" || s.workspaceRepo == nil { return nil } workspace, err := s.workspaceRepo.GetByID(ctx, user.WorkspaceID) if err != nil { return entity.ErrWorkspaceNotFound } if workspace.Status == entity.WorkspaceSuspended { return entity.ErrWorkspaceSuspended } return nil } // ChangePassword 修改密码(会触发全局登出) func (s *AuthService) ChangePassword(ctx context.Context, userID, oldPassword, newPassword string) error { // 1. 获取用户 user, err := s.userRepo.GetByID(ctx, userID) if err != nil { return entity.ErrUserNotFound } // 2. 验证旧密码 if err := s.passwordHasher.Verify(oldPassword, user.PasswordHash); err != nil { return entity.ErrInvalidPassword } // 3. 哈希新密码 newPasswordHash, err := s.passwordHasher.Hash(newPassword) if err != nil { return err } // 4. 更新密码(会自动触发 revoked_after 更新) user.UpdatePassword(newPasswordHash) // 5. 保存到数据库 return s.userRepo.Update(ctx, user) } // ForceLogoutAll 强制全局登出(管理员操作) func (s *AuthService) ForceLogoutAll(ctx context.Context, userID string) error { user, err := s.userRepo.GetByID(ctx, userID) if err != nil { return entity.ErrUserNotFound } user.RevokeAllTokens() return s.userRepo.Update(ctx, user) }