Files
ocdp-go/backend/internal/adapter/input/http/middleware/authz.go
Ivan087 985369d40f fix: resolve deployment API errors and enable E2E deployment flow
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
2026-04-16 18:39:23 +08:00

284 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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))
})
}
}