Backend fixes: - instance_dto: add Version field with Normalize() to support both 'version' and 'tag' field names from frontend - instance_handler: add version empty validation before creating instance - authz.go: fix unused variable compilation error - registry_repository: fix GetByID/GetByName to use correct DB schema (add workspace_id, owner_id, is_shared fields); decrypt password gracefully when encryption key mismatches instead of returning error Frontend: - charts/page: add Template and Storage dropdown selectors to Deploy Modal Testing: - add e2e_test.py: 5-step Playwright E2E test (admin login → create workspace → create user → user login → deploy chart) - add tasks/lesson.md: document 4 bug root causes and fixes - add tasks/todo.md: track implementation progress - add PLAN_E2E_DEPLOYMENT.md: comprehensive implementation plan Verification: confirmed deployment creates instance with status=deployed, chart downloads from Harbor OCI to /tmp/charts/, Helm release deploys to K8s
284 lines
7.9 KiB
Go
284 lines
7.9 KiB
Go
package middleware
|
||
|
||
import (
|
||
"context"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||
"github.com/ocdp/cluster-service/internal/domain/service"
|
||
)
|
||
|
||
// Context keys
|
||
type contextKey string
|
||
|
||
const (
|
||
ContextKeyUserID contextKey = "user_id"
|
||
ContextKeyUsername contextKey = "username"
|
||
ContextKeyUserRole contextKey = "user_role"
|
||
ContextKeyWorkspaceID contextKey = "workspace_id"
|
||
)
|
||
|
||
// UserClaims 用户声明(从 JWT 解析)
|
||
type UserClaims struct {
|
||
UserID string
|
||
Username string
|
||
Role entity.UserRole
|
||
WorkspaceID string
|
||
}
|
||
|
||
// WorkspaceMiddleware 工作空间中间件
|
||
// 从 JWT 获取用户角色和 workspace_id,进行权限检查
|
||
func WorkspaceMiddleware(userRepo repository.UserRepository) func(http.Handler) http.Handler {
|
||
return func(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
// 从 Header 获取 Token
|
||
authHeader := r.Header.Get("Authorization")
|
||
if authHeader == "" {
|
||
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
|
||
return
|
||
}
|
||
|
||
// 解析 Bearer Token
|
||
parts := strings.SplitN(authHeader, " ", 2)
|
||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||
http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
|
||
return
|
||
}
|
||
token := parts[1]
|
||
_ = token
|
||
|
||
// 这里需要从 AuthService 获取验证方法
|
||
// 简化处理:假设 token 包含 user_id 和 username
|
||
// 实际实现需要调用 JWT 验证服务
|
||
|
||
// 从数据库获取用户信息
|
||
// 注意:这里需要通过 token 解析出 userID
|
||
// 实际实现应该在 AuthService 中完成
|
||
_ = userRepo
|
||
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
}
|
||
|
||
// RequireWorkspace 强制要求 workspace 上下文
|
||
// 用于非 Admin 用户的资源操作
|
||
func RequireWorkspace(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
workspaceID := r.Header.Get("X-Workspace-ID")
|
||
userRole := r.Header.Get("X-User-Role")
|
||
|
||
// Admin 可以没有 workspace
|
||
if userRole == string(entity.RoleAdmin) {
|
||
next.ServeHTTP(w, r)
|
||
return
|
||
}
|
||
|
||
// 普通用户必须有 workspace
|
||
if workspaceID == "" {
|
||
http.Error(w, "Workspace context required", http.StatusForbidden)
|
||
return
|
||
}
|
||
|
||
// 将 workspace_id 放入 context
|
||
ctx := context.WithValue(r.Context(), ContextKeyWorkspaceID, workspaceID)
|
||
next.ServeHTTP(w, r.WithContext(ctx))
|
||
})
|
||
}
|
||
|
||
// RequireAdmin 要求 Admin 角色
|
||
func RequireAdmin(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
userRole := r.Header.Get("X-User-Role")
|
||
|
||
if userRole != string(entity.RoleAdmin) {
|
||
http.Error(w, "Admin access required", http.StatusForbidden)
|
||
return
|
||
}
|
||
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
|
||
// GetUserClaims 从 Context 获取用户声明
|
||
func GetUserClaims(ctx context.Context) *UserClaims {
|
||
userID, _ := ctx.Value(ContextKeyUserID).(string)
|
||
username, _ := ctx.Value(ContextKeyUsername).(string)
|
||
roleStr, _ := ctx.Value(ContextKeyUserRole).(string)
|
||
workspaceID, _ := ctx.Value(ContextKeyWorkspaceID).(string)
|
||
|
||
return &UserClaims{
|
||
UserID: userID,
|
||
Username: username,
|
||
Role: entity.UserRole(roleStr),
|
||
WorkspaceID: workspaceID,
|
||
}
|
||
}
|
||
|
||
// GetWorkspaceID 从 Context 获取 workspace ID
|
||
func GetWorkspaceID(ctx context.Context) string {
|
||
workspaceID, _ := ctx.Value(ContextKeyWorkspaceID).(string)
|
||
return workspaceID
|
||
}
|
||
|
||
// GetUserID 从 Context 获取用户 ID
|
||
func GetUserID(ctx context.Context) string {
|
||
userID, _ := ctx.Value(ContextKeyUserID).(string)
|
||
return userID
|
||
}
|
||
|
||
// GetUserRole 从 Context 获取用户角色
|
||
func GetUserRole(ctx context.Context) entity.UserRole {
|
||
roleStr, _ := ctx.Value(ContextKeyUserRole).(string)
|
||
return entity.UserRole(roleStr)
|
||
}
|
||
|
||
// FilterByWorkspace 根据用户角色过滤资源
|
||
// Admin: 返回所有资源(workspaceID 忽略)
|
||
// User: 仅返回属于自己 workspace 的资源
|
||
func FilterByWorkspace(workspaceID, userRole string) (filterWorkspaceID string, isAdmin bool) {
|
||
if userRole == string(entity.RoleAdmin) {
|
||
return "", true
|
||
}
|
||
return workspaceID, false
|
||
}
|
||
|
||
// AuthorizationService 授权服务
|
||
type AuthorizationService struct {
|
||
userRepo repository.UserRepository
|
||
}
|
||
|
||
// NewAuthorizationService 创建授权服务
|
||
func NewAuthorizationService(userRepo repository.UserRepository) *AuthorizationService {
|
||
return &AuthorizationService{
|
||
userRepo: userRepo,
|
||
}
|
||
}
|
||
|
||
// CheckResourceAccess 检查用户是否有权访问指定资源
|
||
func (s *AuthorizationService) CheckResourceAccess(ctx context.Context, userID, resourceWorkspaceID string) error {
|
||
user, err := s.userRepo.GetByID(ctx, userID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Admin 可以访问所有资源
|
||
if user.Role == entity.RoleAdmin {
|
||
return nil
|
||
}
|
||
|
||
// 普通用户只能访问自己 workspace 的资源
|
||
if user.WorkspaceID != resourceWorkspaceID {
|
||
return entity.ErrPermissionDenied
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// CanAccessWorkspace 检查用户是否可以访问指定 workspace
|
||
func (s *AuthorizationService) CanAccessWorkspace(ctx context.Context, userID, targetWorkspaceID string) error {
|
||
user, err := s.userRepo.GetByID(ctx, userID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Admin 可以访问所有 workspace
|
||
if user.Role == entity.RoleAdmin {
|
||
return nil
|
||
}
|
||
|
||
// 普通用户只能访问自己的 workspace
|
||
if user.WorkspaceID != targetWorkspaceID {
|
||
return entity.ErrPermissionDenied
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetAccessibleWorkspaces 获取用户可访问的 workspace 列表
|
||
func (s *AuthorizationService) GetAccessibleWorkspaces(ctx context.Context, userID string) ([]string, error) {
|
||
user, err := s.userRepo.GetByID(ctx, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Admin 可以访问所有 workspace
|
||
if user.Role == entity.RoleAdmin {
|
||
return nil, nil // nil 表示所有
|
||
}
|
||
|
||
// 普通用户只能访问自己的 workspace
|
||
if user.WorkspaceID != "" {
|
||
return []string{user.WorkspaceID}, nil
|
||
}
|
||
|
||
return []string{}, nil
|
||
}
|
||
|
||
// RequireRole 要求特定角色
|
||
func RequireRole(roles ...entity.UserRole) func(http.Handler) http.Handler {
|
||
roleSet := make(map[entity.UserRole]bool)
|
||
for _, r := range roles {
|
||
roleSet[r] = true
|
||
}
|
||
|
||
return func(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
userRole := r.Header.Get("X-User-Role")
|
||
|
||
if !roleSet[entity.UserRole(userRole)] {
|
||
http.Error(w, "Insufficient permissions", http.StatusForbidden)
|
||
return
|
||
}
|
||
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
}
|
||
|
||
// WithUserClaims 将用户声明注入到 Context
|
||
func WithUserClaims(claims *UserClaims) func(http.Handler) http.Handler {
|
||
return func(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
ctx := r.Context()
|
||
ctx = context.WithValue(ctx, ContextKeyUserID, claims.UserID)
|
||
ctx = context.WithValue(ctx, ContextKeyUsername, claims.Username)
|
||
ctx = context.WithValue(ctx, ContextKeyUserRole, string(claims.Role))
|
||
if claims.WorkspaceID != "" {
|
||
ctx = context.WithValue(ctx, ContextKeyWorkspaceID, claims.WorkspaceID)
|
||
}
|
||
next.ServeHTTP(w, r.WithContext(ctx))
|
||
})
|
||
}
|
||
}
|
||
|
||
// LoginRequired 要求登录
|
||
func LoginRequired(authService *service.AuthService) func(http.Handler) http.Handler {
|
||
return func(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
authHeader := r.Header.Get("Authorization")
|
||
if authHeader == "" {
|
||
http.Error(w, "Authorization required", http.StatusUnauthorized)
|
||
return
|
||
}
|
||
|
||
parts := strings.SplitN(authHeader, " ", 2)
|
||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||
http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
|
||
return
|
||
}
|
||
|
||
token := parts[1]
|
||
userID, _, err := authService.VerifyAccessToken(r.Context(), token)
|
||
if err != nil {
|
||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
||
return
|
||
}
|
||
|
||
ctx := context.WithValue(r.Context(), ContextKeyUserID, userID)
|
||
next.ServeHTTP(w, r.WithContext(ctx))
|
||
})
|
||
}
|
||
} |