package service import ( "context" "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 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, } } // Register 注册新用户。业务入口只允许 admin 调用;初始 admin 由 bootstrap seeder 创建。 type UserWorkspaceOptions struct { Namespace string DefaultClusterID string QuotaCPU string QuotaMemory string QuotaGPU string QuotaGPUMem string } 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 } // 默认生成占位邮箱,避免数据库约束失败 email := username + "@local.ocdp" // 创建用户 user := entity.NewUser(username, passwordHash, email) user.ID = uuid.New().String() user.Role = normalizeUserRole(role) user.WorkspaceID = workspaceID if user.Role == authz.RoleUser && (user.WorkspaceID == "" || user.WorkspaceID == entity.DefaultWorkspaceID) { 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 := strings.TrimPrefix(entity.NamespaceForUser(username), "ocdp-u-") workspace := entity.NewWorkspace(name, createdBy) workspace.ID = uuid.New().String() workspace.DefaultClusterID = strings.TrimSpace(opts.DefaultClusterID) namespace := strings.TrimSpace(opts.Namespace) if namespace == "" { namespace = entity.NamespaceForUser(username) } if namespace != "" { if len(validation.IsDNS1123Label(namespace)) > 0 { return nil, entity.ErrInvalidNamespace } 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 { return nil, err } return workspace, 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 (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 } if role != "" { user.Role = normalizeUserRole(role) } if workspaceID != "" { user.WorkspaceID = workspaceID } if user.Role == authz.RoleAdmin { user.WorkspaceID = entity.DefaultWorkspaceID } if user.Role == authz.RoleUser && (user.WorkspaceID == "" || user.WorkspaceID == entity.DefaultWorkspaceID) { normalizedOpts, err := normalizeQuotaOptions(opts) if err != nil { return nil, err } workspace, err := s.createUserWorkspace(ctx, user.Username, principal.UserID, normalizedOpts) if err != nil { return nil, err } user.WorkspaceID = workspace.ID } 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 && 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 } applyWorkspaceOptions(workspace, normalizedOpts) if err := s.workspaceRepo.Update(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) 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 } return s.userRepo.Delete(ctx, userID) } 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) }