refactor: full-stack restructure with multi-tenancy, workspace management, and K8s diagnostics

- Add Workspace domain (entity, repository, service, handler, DTO)
- Add multi-tenant K8s client with tenant binding and quota management
- Add K8s diagnostics client (instance diagnostics)
- Add authorization middleware (authz package)
- Restructure frontend to feature-based architecture (features/)
- Add User Management page in configuration
- Add AccessDenied page and route guards
- Refactor shared components (form inputs, layout, UI)
- Update Tailwind config for new design system
- Add comprehensive documentation (docs/, tasks/, plans)
- Improve cluster service with better kubeconfig handling
- Add tests for crypto, config, helm client, tenant binding
This commit is contained in:
Ivan087
2026-05-12 16:15:14 +08:00
parent c5e51ed069
commit 7f238a3168
172 changed files with 15703 additions and 3162 deletions

View File

@ -0,0 +1,144 @@
package authz
import (
"context"
"errors"
)
type contextKey string
const principalKey contextKey = "principal"
const (
RoleAdmin = "admin"
RoleUser = "user"
)
const (
VisibilityPrivate = "private"
VisibilityWorkspaceShared = "workspace_shared"
VisibilityGlobalShared = "global_shared"
)
var (
ErrUnauthenticated = errors.New("authentication required")
ErrForbidden = errors.New("permission denied")
)
type Principal struct {
UserID string
Username string
Role string
WorkspaceID string
WorkspaceName string
Namespace string
DefaultClusterID string
QuotaCPU string
QuotaMemory string
QuotaGPU string
QuotaGPUMem string
Permissions []string
PermissionVersion int
}
func WithPrincipal(ctx context.Context, principal *Principal) context.Context {
return context.WithValue(ctx, principalKey, principal)
}
func PrincipalFromContext(ctx context.Context) (*Principal, bool) {
principal, ok := ctx.Value(principalKey).(*Principal)
return principal, ok && principal != nil
}
func RequirePrincipal(ctx context.Context) (*Principal, error) {
principal, ok := PrincipalFromContext(ctx)
if !ok {
return nil, ErrUnauthenticated
}
return principal, nil
}
func (p *Principal) IsAdmin() bool {
return p != nil && p.Role == RoleAdmin
}
func CanReadResource(p *Principal, workspaceID, ownerID, visibility string) bool {
if p == nil {
return false
}
if p.IsAdmin() {
return true
}
switch visibility {
case VisibilityGlobalShared:
return true
case VisibilityWorkspaceShared:
return workspaceID != "" && workspaceID == p.WorkspaceID
default:
return ownerID != "" && ownerID == p.UserID
}
}
func CanWriteResource(p *Principal, workspaceID, ownerID, visibility string) bool {
if p == nil {
return false
}
if p.IsAdmin() {
return true
}
if visibility == VisibilityGlobalShared {
return false
}
return workspaceID != "" && workspaceID == p.WorkspaceID && ownerID != "" && ownerID == p.UserID
}
func NormalizeVisibility(role, requested string) string {
switch requested {
case VisibilityWorkspaceShared:
if role == RoleAdmin {
return requested
}
return VisibilityPrivate
case VisibilityGlobalShared:
if role == RoleAdmin {
return requested
}
return VisibilityPrivate
case VisibilityPrivate:
return requested
default:
return VisibilityPrivate
}
}
func PermissionsForRole(role string) []string {
if role == RoleAdmin {
return []string{
"*",
"home:view",
"workspaces:manage",
"users:manage",
"configuration:clusters:manage",
"configuration:registries:manage",
"artifact:registries:view",
"artifact:instances:manage",
"monitoring:clusters:view",
"clusters:manage:any",
"registries:manage:any",
"instances:manage:any",
"kubeconfig:issue:any",
}
}
return []string{
"home:view",
"configuration:clusters:manage_own",
"configuration:registries:manage_own",
"artifact:registries:view",
"artifact:instances:manage_own",
"monitoring:clusters:view",
"clusters:manage:own",
"registries:manage:own",
"instances:manage:own",
"kubeconfig:issue:own",
}
}

View File

@ -12,7 +12,7 @@ func TestAESEncryptor(t *testing.T) {
plaintext string
}{
{"simple password", "password123"},
{"harbor password", "BWGDIP@ssw0rd1401#"},
{"registry password", "registry-password-example"},
{"empty string", ""},
{"long certificate", "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pP"},
{"unicode", "密码123!@#"},
@ -121,4 +121,3 @@ func TestEncryptionConsistency(t *testing.T) {
t.Error("Decryption should produce original plaintext")
}
}

View File

@ -3,13 +3,13 @@ package jwt
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
const (
AccessTokenDuration = 24 * time.Hour // Access Token 有效期
RefreshTokenDuration = 7 * 24 * time.Hour // Refresh Token 有效期
AccessTokenDuration = 24 * time.Hour // Access Token 有效期
RefreshTokenDuration = 7 * 24 * time.Hour // Refresh Token 有效期
)
// JWTManager JWT 管理器
@ -26,98 +26,133 @@ func NewJWTManager(secretKey string) *JWTManager {
// Claims JWT Claims
type Claims struct {
UserID string `json:"user_id"`
Username string `json:"username"`
UserID string `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
WorkspaceID string `json:"workspace_id"`
TokenType string `json:"token_type"`
jwt.RegisteredClaims
}
// Generate 生成 Access Token 和 Refresh Token
func (m *JWTManager) Generate(userID, username string) (accessToken, refreshToken string, err error) {
func (m *JWTManager) Generate(userID, username, role, workspaceID string) (accessToken, refreshToken string, err error) {
// 生成 Access Token
accessClaims := &Claims{
UserID: userID,
Username: username,
UserID: userID,
Username: username,
Role: role,
WorkspaceID: workspaceID,
TokenType: "access",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessToken, err = accessTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", "", fmt.Errorf("failed to sign access token: %w", err)
}
// 生成 Refresh Token
refreshClaims := &Claims{
UserID: userID,
Username: username,
UserID: userID,
Username: username,
Role: role,
WorkspaceID: workspaceID,
TokenType: "refresh",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(RefreshTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshToken, err = refreshTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", "", fmt.Errorf("failed to sign refresh token: %w", err)
}
return accessToken, refreshToken, nil
}
// Verify 验证 Token
func (m *JWTManager) Verify(tokenString string) (userID, username string, err error) {
userID, username, _, err = m.VerifyWithIssuedAt(tokenString)
return userID, username, err
claims, err := m.VerifyClaims(tokenString, "")
if err != nil {
return "", "", err
}
return claims.UserID, claims.Username, nil
}
func (m *JWTManager) VerifyAccess(tokenString string) (*Claims, error) {
return m.VerifyClaims(tokenString, "access")
}
func (m *JWTManager) VerifyRefresh(tokenString string) (*Claims, error) {
return m.VerifyClaims(tokenString, "refresh")
}
// VerifyWithIssuedAt 验证 Token 并返回签发时间
func (m *JWTManager) VerifyWithIssuedAt(tokenString string) (userID, username string, issuedAt int64, err error) {
claims, err := m.VerifyClaims(tokenString, "access")
if err != nil {
return "", "", 0, err
}
return claims.UserID, claims.Username, claims.IssuedAt.Unix(), nil
}
func (m *JWTManager) VerifyClaims(tokenString, expectedType string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(m.secretKey), nil
})
if err != nil {
return "", "", 0, fmt.Errorf("failed to parse token: %w", err)
return nil, fmt.Errorf("failed to parse token: %w", err)
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims.UserID, claims.Username, claims.IssuedAt.Unix(), nil
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token")
}
return "", "", 0, fmt.Errorf("invalid token")
if expectedType != "" && claims.TokenType != expectedType {
return nil, fmt.Errorf("invalid token type")
}
if claims.IssuedAt == nil {
return nil, fmt.Errorf("token missing issued_at")
}
return claims, nil
}
// Refresh 刷新 Token
func (m *JWTManager) Refresh(refreshToken string) (string, error) {
// 验证 Refresh Token
userID, username, err := m.Verify(refreshToken)
claims, err := m.VerifyRefresh(refreshToken)
if err != nil {
return "", fmt.Errorf("invalid refresh token: %w", err)
}
// 生成新的 Access Token
accessClaims := &Claims{
UserID: userID,
Username: username,
UserID: claims.UserID,
Username: claims.Username,
Role: claims.Role,
WorkspaceID: claims.WorkspaceID,
TokenType: "access",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
newAccessToken, err := accessTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", fmt.Errorf("failed to sign new access token: %w", err)
}
return newAccessToken, nil
}