feat(frontend): add Helm chart browser, monitoring, chart-references and values templates pages
Add new frontend pages for the multi-tenant OCDP platform: - Charts page (/charts): Browse Harbor OCI registries to list Helm chart repositories and versions, with deploy modal to launch charts on selected clusters - Monitoring page (/monitoring): Display cluster metrics (CPU/Memory/GPU usage) and per-node details with resource utilization bars - Chart References page (/chart-references): CRUD for chart metadata references - Values Templates page (/templates): CRUD for Helm values templates with version history and rollback support - Sidebar: Add Charts navigation, update Storage and Templates links - api.ts: Add all API client functions (clusterApi, registryApi, instanceApi, monitoringApi, storageApi, chartReferenceApi, valuesTemplateApi, workspaceApi, userApi) with full TypeScript types Note: deploy flow and values template rollback not yet end-to-end tested.
This commit is contained in:
283
backend/internal/adapter/input/http/middleware/authz.go
Normal file
283
backend/internal/adapter/input/http/middleware/authz.go
Normal file
@ -0,0 +1,283 @@
|
||||
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]
|
||||
|
||||
// 这里需要从 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user