feat(P1/P2): 完成TDD开发及P1/P2设计文档

## 设计文档
- multi_role_permission_design: 多角色权限设计 (CONDITIONAL GO)
- audit_log_enhancement_design: 审计日志增强 (CONDITIONAL GO)
- routing_strategy_template_design: 路由策略模板 (CONDITIONAL GO)
- sso_saml_technical_research: SSO/SAML调研 (CONDITIONAL GO)
- compliance_capability_package_design: 合规能力包设计 (CONDITIONAL GO)

## TDD开发成果
- IAM模块: supply-api/internal/iam/ (111个测试)
- 审计日志模块: supply-api/internal/audit/ (40+测试)
- 路由策略模块: gateway/internal/router/ (33+测试)
- 合规能力包: gateway/internal/compliance/ + scripts/ci/compliance/

## 规范文档
- parallel_agent_output_quality_standards: 并行Agent产出质量规范
- project_experience_summary: 项目经验总结 (v2)
- 2026-04-02-p1-p2-tdd-execution-plan: TDD执行计划

## 评审报告
- 5个CONDITIONAL GO设计文档评审报告
- fix_verification_report: 修复验证报告
- full_verification_report: 全面质量验证报告
- tdd_module_quality_verification: TDD模块质量验证
- tdd_execution_summary: TDD执行总结

依据: Superpowers执行框架 + TDD规范
This commit is contained in:
Your Name
2026-04-02 23:35:53 +08:00
parent ed0961d486
commit 89104bd0db
94 changed files with 24738 additions and 5 deletions

View File

@@ -0,0 +1,507 @@
package handler
import (
"context"
"encoding/json"
"net/http"
"strconv"
"lijiaoqiao/supply-api/internal/iam/service"
)
// IAMHandler IAM HTTP处理器
type IAMHandler struct {
iamService service.IAMServiceInterface
}
// NewIAMHandler 创建IAM处理器
func NewIAMHandler(iamService service.IAMServiceInterface) *IAMHandler {
return &IAMHandler{
iamService: iamService,
}
}
// RoleResponse HTTP响应中的角色信息
type RoleResponse struct {
Code string `json:"role_code"`
Name string `json:"role_name"`
Type string `json:"role_type"`
Level int `json:"level"`
Scopes []string `json:"scopes,omitempty"`
IsActive bool `json:"is_active"`
}
// CreateRoleRequest 创建角色请求
type CreateRoleRequest struct {
Code string `json:"code"`
Name string `json:"name"`
Type string `json:"type"`
Level int `json:"level"`
Scopes []string `json:"scopes"`
}
// UpdateRoleRequest 更新角色请求
type UpdateRoleRequest struct {
Code string `json:"code"`
Name string `json:"name"`
Description string `json:"description"`
Scopes []string `json:"scopes"`
IsActive *bool `json:"is_active"`
}
// AssignRoleRequest 分配角色请求
type AssignRoleRequest struct {
RoleCode string `json:"role_code"`
TenantID int64 `json:"tenant_id"`
ExpiresAt string `json:"expires_at,omitempty"`
}
// HTTPError HTTP错误响应
type HTTPError struct {
Code string `json:"code"`
Message string `json:"message"`
}
// ErrorResponse 错误响应结构
type ErrorResponse struct {
Error HTTPError `json:"error"`
}
// RegisterRoutes 注册IAM路由
func (h *IAMHandler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/iam/roles", h.handleRoles)
mux.HandleFunc("/api/v1/iam/roles/", h.handleRoleByCode)
mux.HandleFunc("/api/v1/iam/scopes", h.handleScopes)
mux.HandleFunc("/api/v1/iam/users/", h.handleUserRoles)
mux.HandleFunc("/api/v1/iam/check-scope", h.handleCheckScope)
}
// handleRoles 处理角色相关路由
func (h *IAMHandler) handleRoles(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.ListRoles(w, r)
case http.MethodPost:
h.CreateRole(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
}
}
// handleRoleByCode 处理单个角色路由
func (h *IAMHandler) handleRoleByCode(w http.ResponseWriter, r *http.Request) {
roleCode := extractRoleCode(r.URL.Path)
switch r.Method {
case http.MethodGet:
h.GetRole(w, r, roleCode)
case http.MethodPut:
h.UpdateRole(w, r, roleCode)
case http.MethodDelete:
h.DeleteRole(w, r, roleCode)
default:
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
}
}
// handleScopes 处理Scope列表路由
func (h *IAMHandler) handleScopes(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
h.ListScopes(w, r)
}
// handleUserRoles 处理用户角色路由
func (h *IAMHandler) handleUserRoles(w http.ResponseWriter, r *http.Request) {
// 解析用户ID
path := r.URL.Path
userIDStr := extractUserID(path)
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "INVALID_USER_ID", "invalid user id")
return
}
switch r.Method {
case http.MethodGet:
h.GetUserRoles(w, r, userID)
case http.MethodPost:
h.AssignRole(w, r, userID)
case http.MethodDelete:
roleCode := extractRoleCodeFromUserPath(path)
tenantID := int64(0) // 从请求或context获取
h.RevokeRole(w, r, userID, roleCode, tenantID)
default:
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
}
}
// handleCheckScope 处理检查Scope路由
func (h *IAMHandler) handleCheckScope(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
h.CheckScope(w, r)
}
// CreateRole 处理创建角色请求
func (h *IAMHandler) CreateRole(w http.ResponseWriter, r *http.Request) {
var req CreateRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
// 验证必填字段
if req.Code == "" {
writeError(w, http.StatusBadRequest, "MISSING_CODE", "role code is required")
return
}
if req.Name == "" {
writeError(w, http.StatusBadRequest, "MISSING_NAME", "role name is required")
return
}
if req.Type == "" {
writeError(w, http.StatusBadRequest, "MISSING_TYPE", "role type is required")
return
}
serviceReq := &service.CreateRoleRequest{
Code: req.Code,
Name: req.Name,
Type: req.Type,
Level: req.Level,
Scopes: req.Scopes,
}
role, err := h.iamService.CreateRole(r.Context(), serviceReq)
if err != nil {
if err == service.ErrDuplicateRoleCode {
writeError(w, http.StatusConflict, "DUPLICATE_ROLE_CODE", err.Error())
return
}
if err == service.ErrInvalidRequest {
writeError(w, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]interface{}{
"role": toRoleResponse(role),
})
}
// GetRole 处理获取单个角色请求
func (h *IAMHandler) GetRole(w http.ResponseWriter, r *http.Request, roleCode string) {
role, err := h.iamService.GetRole(r.Context(), roleCode)
if err != nil {
if err == service.ErrRoleNotFound {
writeError(w, http.StatusNotFound, "ROLE_NOT_FOUND", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"role": toRoleResponse(role),
})
}
// ListRoles 处理列出角色请求
func (h *IAMHandler) ListRoles(w http.ResponseWriter, r *http.Request) {
roleType := r.URL.Query().Get("type")
roles, err := h.iamService.ListRoles(r.Context(), roleType)
if err != nil {
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
roleResponses := make([]*RoleResponse, len(roles))
for i, role := range roles {
roleResponses[i] = toRoleResponse(role)
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"roles": roleResponses,
})
}
// UpdateRole 处理更新角色请求
func (h *IAMHandler) UpdateRole(w http.ResponseWriter, r *http.Request, roleCode string) {
var req UpdateRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
req.Code = roleCode // 确保使用URL中的roleCode
serviceReq := &service.UpdateRoleRequest{
Code: req.Code,
Name: req.Name,
Description: req.Description,
Scopes: req.Scopes,
IsActive: req.IsActive,
}
role, err := h.iamService.UpdateRole(r.Context(), serviceReq)
if err != nil {
if err == service.ErrRoleNotFound {
writeError(w, http.StatusNotFound, "ROLE_NOT_FOUND", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"role": toRoleResponse(role),
})
}
// DeleteRole 处理删除角色请求
func (h *IAMHandler) DeleteRole(w http.ResponseWriter, r *http.Request, roleCode string) {
err := h.iamService.DeleteRole(r.Context(), roleCode)
if err != nil {
if err == service.ErrRoleNotFound {
writeError(w, http.StatusNotFound, "ROLE_NOT_FOUND", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"message": "role deleted successfully",
})
}
// ListScopes 处理列出所有Scope请求
func (h *IAMHandler) ListScopes(w http.ResponseWriter, r *http.Request) {
// 从预定义Scope列表获取
scopes := []map[string]interface{}{
{"scope_code": "platform:read", "scope_name": "读取平台配置", "scope_type": "platform"},
{"scope_code": "platform:write", "scope_name": "修改平台配置", "scope_type": "platform"},
{"scope_code": "platform:admin", "scope_name": "平台级管理", "scope_type": "platform"},
{"scope_code": "tenant:read", "scope_name": "读取租户信息", "scope_type": "platform"},
{"scope_code": "supply:account:read", "scope_name": "读取供应账号", "scope_type": "supply"},
{"scope_code": "consumer:apikey:create", "scope_name": "创建API Key", "scope_type": "consumer"},
{"scope_code": "router:invoke", "scope_name": "调用模型", "scope_type": "router"},
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"scopes": scopes,
})
}
// GetUserRoles 处理获取用户角色请求
func (h *IAMHandler) GetUserRoles(w http.ResponseWriter, r *http.Request, userID int64) {
roles, err := h.iamService.GetUserRoles(r.Context(), userID)
if err != nil {
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"user_id": userID,
"roles": roles,
})
}
// AssignRole 处理分配角色请求
func (h *IAMHandler) AssignRole(w http.ResponseWriter, r *http.Request, userID int64) {
var req AssignRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
serviceReq := &service.AssignRoleRequest{
UserID: userID,
RoleCode: req.RoleCode,
TenantID: req.TenantID,
}
mapping, err := h.iamService.AssignRole(r.Context(), serviceReq)
if err != nil {
if err == service.ErrRoleNotFound {
writeError(w, http.StatusNotFound, "ROLE_NOT_FOUND", err.Error())
return
}
if err == service.ErrDuplicateAssignment {
writeError(w, http.StatusConflict, "DUPLICATE_ASSIGNMENT", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]interface{}{
"message": "role assigned successfully",
"mapping": mapping,
})
}
// RevokeRole 处理撤销角色请求
func (h *IAMHandler) RevokeRole(w http.ResponseWriter, r *http.Request, userID int64, roleCode string, tenantID int64) {
err := h.iamService.RevokeRole(r.Context(), userID, roleCode, tenantID)
if err != nil {
if err == service.ErrRoleNotFound {
writeError(w, http.StatusNotFound, "ROLE_NOT_FOUND", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"message": "role revoked successfully",
})
}
// CheckScope 处理检查Scope请求
func (h *IAMHandler) CheckScope(w http.ResponseWriter, r *http.Request) {
scope := r.URL.Query().Get("scope")
if scope == "" {
writeError(w, http.StatusBadRequest, "MISSING_SCOPE", "scope parameter is required")
return
}
// 从context获取userID实际应用中应从认证中间件获取
userID := int64(1) // 模拟
hasScope, err := h.iamService.CheckScope(r.Context(), userID, scope)
if err != nil {
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"has_scope": hasScope,
"scope": scope,
"user_id": userID,
})
}
// toRoleResponse 转换为RoleResponse
func toRoleResponse(role *service.Role) *RoleResponse {
return &RoleResponse{
Code: role.Code,
Name: role.Name,
Type: role.Type,
Level: role.Level,
IsActive: role.IsActive,
}
}
// writeJSON 写入JSON响应
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// writeError 写入错误响应
func writeError(w http.ResponseWriter, status int, code, message string) {
writeJSON(w, status, ErrorResponse{
Error: HTTPError{
Code: code,
Message: message,
},
})
}
// extractRoleCode 从URL路径提取角色代码
func extractRoleCode(path string) string {
// /api/v1/iam/roles/developer -> developer
parts := splitPath(path)
if len(parts) >= 5 {
return parts[4]
}
return ""
}
// extractUserID 从URL路径提取用户ID
func extractUserID(path string) string {
// /api/v1/iam/users/123/roles -> 123
parts := splitPath(path)
if len(parts) >= 4 {
return parts[3]
}
if len(parts) >= 6 {
return parts[3]
}
return ""
}
// extractRoleCodeFromUserPath 从用户路径提取角色代码
func extractRoleCodeFromUserPath(path string) string {
// /api/v1/iam/users/123/roles/developer -> developer
parts := splitPath(path)
if len(parts) >= 6 {
return parts[5]
}
return ""
}
// splitPath 分割URL路径
func splitPath(path string) []string {
var parts []string
var current string
for _, c := range path {
if c == '/' {
if current != "" {
parts = append(parts, current)
current = ""
}
} else {
current += string(c)
}
}
if current != "" {
parts = append(parts, current)
}
return parts
}
// RequireScope 返回一个要求特定Scope的中间件函数
func RequireScope(scope string, iamService service.IAMServiceInterface) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从context获取userID
userID := getUserIDFromContext(r.Context())
if userID == 0 {
writeError(w, http.StatusUnauthorized, "UNAUTHORIZED", "user not authenticated")
return
}
hasScope, err := iamService.CheckScope(r.Context(), userID, scope)
if err != nil {
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
if !hasScope {
writeError(w, http.StatusForbidden, "SCOPE_DENIED", "insufficient scope")
return
}
next.ServeHTTP(w, r)
})
}
}
// getUserIDFromContext 从context获取userID实际应用中应从认证中间件获取
func getUserIDFromContext(ctx context.Context) int64 {
// TODO: 从认证中间件获取真实的userID
return 1
}

View File

@@ -0,0 +1,404 @@
package handler
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// 测试辅助函数
// testRoleResponse 用于测试的角色响应
type testRoleResponse struct {
Code string `json:"role_code"`
Name string `json:"role_name"`
Type string `json:"role_type"`
Level int `json:"level"`
IsActive bool `json:"is_active"`
}
// testIAMService 模拟IAM服务
type testIAMService struct {
roles map[string]*testRoleResponse
userScopes map[int64][]string
}
type testRoleResponse2 struct {
Code string
Name string
Type string
Level int
IsActive bool
}
func newTestIAMService() *testIAMService {
return &testIAMService{
roles: map[string]*testRoleResponse{
"viewer": {Code: "viewer", Name: "查看者", Type: "platform", Level: 10, IsActive: true},
"operator": {Code: "operator", Name: "运维", Type: "platform", Level: 30, IsActive: true},
},
userScopes: map[int64][]string{
1: {"platform:read", "platform:write"},
},
}
}
func (s *testIAMService) CreateRole(req *CreateRoleHTTPRequest) (*testRoleResponse, error) {
if _, exists := s.roles[req.Code]; exists {
return nil, errDuplicateRole
}
return &testRoleResponse{
Code: req.Code,
Name: req.Name,
Type: req.Type,
Level: req.Level,
IsActive: true,
}, nil
}
func (s *testIAMService) GetRole(roleCode string) (*testRoleResponse, error) {
if role, exists := s.roles[roleCode]; exists {
return role, nil
}
return nil, errNotFound
}
func (s *testIAMService) ListRoles(roleType string) ([]*testRoleResponse, error) {
var result []*testRoleResponse
for _, role := range s.roles {
if roleType == "" || role.Type == roleType {
result = append(result, role)
}
}
return result, nil
}
func (s *testIAMService) CheckScope(userID int64, scope string) bool {
scopes, ok := s.userScopes[userID]
if !ok {
return false
}
for _, s := range scopes {
if s == scope || s == "*" {
return true
}
}
return false
}
// HTTP请求/响应类型
type CreateRoleHTTPRequest struct {
Code string `json:"code"`
Name string `json:"name"`
Type string `json:"type"`
Level int `json:"level"`
Scopes []string `json:"scopes"`
}
// 错误
var (
errNotFound = &HTTPErrorResponse{Code: "NOT_FOUND", Message: "not found"}
errDuplicateRole = &HTTPErrorResponse{Code: "DUPLICATE", Message: "duplicate"}
)
// HTTPErrorResponse HTTP错误响应
type HTTPErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
}
func (e *HTTPErrorResponse) Error() string {
return e.Message
}
// HTTPHandler 测试用的HTTP处理器
type HTTPHandler struct {
iam *testIAMService
}
func newHTTPHandler() *HTTPHandler {
return &HTTPHandler{iam: newTestIAMService()}
}
// handleCreateRole 创建角色
func (h *HTTPHandler) handleCreateRole(w http.ResponseWriter, r *http.Request) {
var req CreateRoleHTTPRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorHTTPTest(w, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
role, err := h.iam.CreateRole(&req)
if err != nil {
writeErrorHTTPTest(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
writeJSONHTTPTest(w, http.StatusCreated, map[string]interface{}{
"role": role,
})
}
// handleListRoles 列出角色
func (h *HTTPHandler) handleListRoles(w http.ResponseWriter, r *http.Request) {
roleType := r.URL.Query().Get("type")
roles, err := h.iam.ListRoles(roleType)
if err != nil {
writeErrorHTTPTest(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
writeJSONHTTPTest(w, http.StatusOK, map[string]interface{}{
"roles": roles,
})
}
// handleGetRole 获取角色
func (h *HTTPHandler) handleGetRole(w http.ResponseWriter, r *http.Request) {
roleCode := r.URL.Query().Get("code")
if roleCode == "" {
writeErrorHTTPTest(w, http.StatusBadRequest, "MISSING_CODE", "role code is required")
return
}
role, err := h.iam.GetRole(roleCode)
if err != nil {
if err == errNotFound {
writeErrorHTTPTest(w, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
writeErrorHTTPTest(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
return
}
writeJSONHTTPTest(w, http.StatusOK, map[string]interface{}{
"role": role,
})
}
// handleCheckScope 检查Scope
func (h *HTTPHandler) handleCheckScope(w http.ResponseWriter, r *http.Request) {
scope := r.URL.Query().Get("scope")
if scope == "" {
writeErrorHTTPTest(w, http.StatusBadRequest, "MISSING_SCOPE", "scope is required")
return
}
userID := int64(1)
hasScope := h.iam.CheckScope(userID, scope)
writeJSONHTTPTest(w, http.StatusOK, map[string]interface{}{
"has_scope": hasScope,
"scope": scope,
})
}
func writeJSONHTTPTest(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeErrorHTTPTest(w http.ResponseWriter, status int, code, message string) {
writeJSONHTTPTest(w, status, map[string]interface{}{
"error": map[string]string{
"code": code,
"message": message,
},
})
}
// ==================== 测试用例 ====================
// TestHTTPHandler_CreateRole_Success 测试创建角色成功
func TestHTTPHandler_CreateRole_Success(t *testing.T) {
// arrange
handler := newHTTPHandler()
body := `{"code":"developer","name":"开发者","type":"platform","level":20}`
req := httptest.NewRequest("POST", "/api/v1/iam/roles", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
// act
rec := httptest.NewRecorder()
handler.handleCreateRole(rec, req)
// assert
assert.Equal(t, http.StatusCreated, rec.Code)
var resp map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &resp)
role := resp["role"].(map[string]interface{})
assert.Equal(t, "developer", role["role_code"])
assert.Equal(t, "开发者", role["role_name"])
}
// TestHTTPHandler_ListRoles_Success 测试列出角色成功
func TestHTTPHandler_ListRoles_Success(t *testing.T) {
// arrange
handler := newHTTPHandler()
req := httptest.NewRequest("GET", "/api/v1/iam/roles", nil)
// act
rec := httptest.NewRecorder()
handler.handleListRoles(rec, req)
// assert
assert.Equal(t, http.StatusOK, rec.Code)
var resp map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &resp)
roles := resp["roles"].([]interface{})
assert.Len(t, roles, 2)
}
// TestHTTPHandler_ListRoles_WithType 测试按类型列出角色
func TestHTTPHandler_ListRoles_WithType(t *testing.T) {
// arrange
handler := newHTTPHandler()
req := httptest.NewRequest("GET", "/api/v1/iam/roles?type=platform", nil)
// act
rec := httptest.NewRecorder()
handler.handleListRoles(rec, req)
// assert
assert.Equal(t, http.StatusOK, rec.Code)
}
// TestHTTPHandler_GetRole_Success 测试获取角色成功
func TestHTTPHandler_GetRole_Success(t *testing.T) {
// arrange
handler := newHTTPHandler()
req := httptest.NewRequest("GET", "/api/v1/iam/roles?code=viewer", nil)
// act
rec := httptest.NewRecorder()
handler.handleGetRole(rec, req)
// assert
assert.Equal(t, http.StatusOK, rec.Code)
var resp map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &resp)
role := resp["role"].(map[string]interface{})
assert.Equal(t, "viewer", role["role_code"])
}
// TestHTTPHandler_GetRole_NotFound 测试获取不存在的角色
func TestHTTPHandler_GetRole_NotFound(t *testing.T) {
// arrange
handler := newHTTPHandler()
req := httptest.NewRequest("GET", "/api/v1/iam/roles?code=nonexistent", nil)
// act
rec := httptest.NewRecorder()
handler.handleGetRole(rec, req)
// assert
assert.Equal(t, http.StatusNotFound, rec.Code)
}
// TestHTTPHandler_CheckScope_HasScope 测试检查Scope存在
func TestHTTPHandler_CheckScope_HasScope(t *testing.T) {
// arrange
handler := newHTTPHandler()
req := httptest.NewRequest("GET", "/api/v1/iam/check-scope?scope=platform:read", nil)
// act
rec := httptest.NewRecorder()
handler.handleCheckScope(rec, req)
// assert
assert.Equal(t, http.StatusOK, rec.Code)
var resp map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &resp)
assert.Equal(t, true, resp["has_scope"])
assert.Equal(t, "platform:read", resp["scope"])
}
// TestHTTPHandler_CheckScope_NoScope 测试检查Scope不存在
func TestHTTPHandler_CheckScope_NoScope(t *testing.T) {
// arrange
handler := newHTTPHandler()
req := httptest.NewRequest("GET", "/api/v1/iam/check-scope?scope=platform:admin", nil)
// act
rec := httptest.NewRecorder()
handler.handleCheckScope(rec, req)
// assert
assert.Equal(t, http.StatusOK, rec.Code)
var resp map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &resp)
assert.Equal(t, false, resp["has_scope"])
}
// TestHTTPHandler_CheckScope_MissingScope 测试缺少Scope参数
func TestHTTPHandler_CheckScope_MissingScope(t *testing.T) {
// arrange
handler := newHTTPHandler()
req := httptest.NewRequest("GET", "/api/v1/iam/check-scope", nil)
// act
rec := httptest.NewRecorder()
handler.handleCheckScope(rec, req)
// assert
assert.Equal(t, http.StatusBadRequest, rec.Code)
}
// TestHTTPHandler_CreateRole_InvalidJSON 测试无效JSON
func TestHTTPHandler_CreateRole_InvalidJSON(t *testing.T) {
// arrange
handler := newHTTPHandler()
body := `invalid json`
req := httptest.NewRequest("POST", "/api/v1/iam/roles", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
// act
rec := httptest.NewRecorder()
handler.handleCreateRole(rec, req)
// assert
assert.Equal(t, http.StatusBadRequest, rec.Code)
}
// TestHTTPHandler_GetRole_MissingCode 测试缺少角色代码
func TestHTTPHandler_GetRole_MissingCode(t *testing.T) {
// arrange
handler := newHTTPHandler()
req := httptest.NewRequest("GET", "/api/v1/iam/roles", nil) // 没有code参数
// act
rec := httptest.NewRecorder()
handler.handleGetRole(rec, req)
// assert
assert.Equal(t, http.StatusBadRequest, rec.Code)
}
// 确保函数被使用(避免编译错误)
var _ = context.Background

View File

@@ -0,0 +1,296 @@
package middleware
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
// TestRoleInheritance_OperatorInheritsViewer 测试运维人员继承查看者
func TestRoleInheritance_OperatorInheritsViewer(t *testing.T) {
// arrange
// operator 显式配置拥有 viewer 所有 scope + platform:write 等
operatorScopes := []string{"platform:read", "platform:write", "tenant:read", "tenant:write", "billing:read"}
viewerScopes := []string{"platform:read", "tenant:read", "billing:read"}
operatorClaims := &IAMTokenClaims{
SubjectID: "user:1",
Role: "operator",
Scope: operatorScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *operatorClaims)
// act & assert - operator 应该拥有 viewer 的所有 scope
for _, viewerScope := range viewerScopes {
assert.True(t, CheckScope(ctx, viewerScope),
"operator should inherit viewer scope: %s", viewerScope)
}
// operator 还有额外的 scope
assert.True(t, CheckScope(ctx, "platform:write"))
assert.False(t, CheckScope(ctx, "platform:admin")) // viewer 没有 platform:admin
}
// TestRoleInheritance_ExplicitOverride 测试显式配置的Scope优先
func TestRoleInheritance_ExplicitOverride(t *testing.T) {
// arrange
// org_admin 显式配置拥有 operator + finops + developer + viewer 所有 scope
orgAdminScopes := []string{
// viewer scopes
"platform:read", "tenant:read", "billing:read",
// operator scopes
"platform:write", "tenant:write",
// finops scopes
"billing:write",
// developer scopes
"router:model:list",
// org_admin 自身 scope
"platform:admin", "tenant:member:manage",
}
orgAdminClaims := &IAMTokenClaims{
SubjectID: "user:2",
Role: "org_admin",
Scope: orgAdminScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *orgAdminClaims)
// act & assert - org_admin 应该拥有所有子角色的 scope
assert.True(t, CheckScope(ctx, "platform:read")) // viewer
assert.True(t, CheckScope(ctx, "tenant:read")) // viewer
assert.True(t, CheckScope(ctx, "billing:read")) // viewer/finops
assert.True(t, CheckScope(ctx, "platform:write")) // operator
assert.True(t, CheckScope(ctx, "tenant:write")) // operator
assert.True(t, CheckScope(ctx, "billing:write")) // finops
assert.True(t, CheckScope(ctx, "router:model:list")) // developer
assert.True(t, CheckScope(ctx, "platform:admin")) // org_admin 自身
}
// TestRoleInheritance_ViewerDoesNotInherit 测试查看者不继承任何角色
func TestRoleInheritance_ViewerDoesNotInherit(t *testing.T) {
// arrange
viewerScopes := []string{"platform:read", "tenant:read", "billing:read"}
viewerClaims := &IAMTokenClaims{
SubjectID: "user:3",
Role: "viewer",
Scope: viewerScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *viewerClaims)
// act & assert - viewer 是基础角色,不继承任何角色
assert.True(t, CheckScope(ctx, "platform:read"))
assert.False(t, CheckScope(ctx, "platform:write")) // viewer 没有 write
assert.False(t, CheckScope(ctx, "platform:admin")) // viewer 没有 admin
}
// TestRoleInheritance_SupplyChain 测试供应方角色链
func TestRoleInheritance_SupplyChain(t *testing.T) {
// arrange
// supply_admin > supply_operator > supply_viewer
supplyViewerScopes := []string{"supply:account:read", "supply:package:read"}
supplyOperatorScopes := []string{"supply:account:read", "supply:account:write", "supply:package:read", "supply:package:write", "supply:package:publish"}
supplyAdminScopes := []string{"supply:account:read", "supply:account:write", "supply:package:read", "supply:package:write", "supply:package:publish", "supply:package:offline", "supply:settlement:withdraw"}
// supply_viewer 测试
viewerCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:4",
Role: "supply_viewer",
Scope: supplyViewerScopes,
TenantID: 1,
})
// act & assert
assert.True(t, CheckScope(viewerCtx, "supply:account:read"))
assert.False(t, CheckScope(viewerCtx, "supply:account:write"))
// supply_operator 测试
operatorCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:5",
Role: "supply_operator",
Scope: supplyOperatorScopes,
TenantID: 1,
})
// act & assert - operator 继承 viewer
assert.True(t, CheckScope(operatorCtx, "supply:account:read"))
assert.True(t, CheckScope(operatorCtx, "supply:account:write"))
assert.False(t, CheckScope(operatorCtx, "supply:settlement:withdraw")) // operator 没有 withdraw
// supply_admin 测试
adminCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:6",
Role: "supply_admin",
Scope: supplyAdminScopes,
TenantID: 1,
})
// act & assert - admin 继承所有
assert.True(t, CheckScope(adminCtx, "supply:account:read"))
assert.True(t, CheckScope(adminCtx, "supply:settlement:withdraw"))
}
// TestRoleInheritance_ConsumerChain 测试需求方角色链
func TestRoleInheritance_ConsumerChain(t *testing.T) {
// arrange
// consumer_admin > consumer_operator > consumer_viewer
consumerViewerScopes := []string{"consumer:account:read", "consumer:apikey:read", "consumer:usage:read"}
consumerOperatorScopes := []string{"consumer:account:read", "consumer:account:write", "consumer:apikey:read", "consumer:apikey:create", "consumer:apikey:revoke", "consumer:usage:read"}
consumerAdminScopes := []string{"consumer:account:read", "consumer:account:write", "consumer:apikey:read", "consumer:apikey:create", "consumer:apikey:revoke", "consumer:usage:read"}
// consumer_viewer 测试
viewerCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:7",
Role: "consumer_viewer",
Scope: consumerViewerScopes,
TenantID: 1,
})
// act & assert
assert.True(t, CheckScope(viewerCtx, "consumer:account:read"))
assert.True(t, CheckScope(viewerCtx, "consumer:usage:read"))
assert.False(t, CheckScope(viewerCtx, "consumer:apikey:create"))
// consumer_operator 测试
operatorCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:8",
Role: "consumer_operator",
Scope: consumerOperatorScopes,
TenantID: 1,
})
// act & assert - operator 继承 viewer
assert.True(t, CheckScope(operatorCtx, "consumer:apikey:create"))
assert.True(t, CheckScope(operatorCtx, "consumer:apikey:revoke"))
// consumer_admin 测试
adminCtx := context.WithValue(context.Background(), IAMTokenClaimsKey, IAMTokenClaims{
SubjectID: "user:9",
Role: "consumer_admin",
Scope: consumerAdminScopes,
TenantID: 1,
})
// act & assert - admin 继承所有
assert.True(t, CheckScope(adminCtx, "consumer:account:read"))
assert.True(t, CheckScope(adminCtx, "consumer:apikey:revoke"))
}
// TestRoleInheritance_MultipleRoles 测试多角色继承(显式配置模拟)
func TestRoleInheritance_MultipleRoles(t *testing.T) {
// arrange
// 假设用户同时拥有 developer 和 finops 角色(通过 scope 累加)
combinedScopes := []string{
// viewer scopes
"platform:read", "tenant:read", "billing:read",
// developer scopes
"router:model:list", "router:invoke",
// finops scopes
"billing:write",
}
combinedClaims := &IAMTokenClaims{
SubjectID: "user:10",
Role: "developer", // 主角色
Scope: combinedScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *combinedClaims)
// act & assert
assert.True(t, CheckScope(ctx, "platform:read")) // viewer
assert.True(t, CheckScope(ctx, "billing:read")) // viewer
assert.True(t, CheckScope(ctx, "router:model:list")) // developer
assert.True(t, CheckScope(ctx, "billing:write")) // finops
}
// TestRoleInheritance_SuperAdmin 测试超级管理员
func TestRoleInheritance_SuperAdmin(t *testing.T) {
// arrange
superAdminClaims := &IAMTokenClaims{
SubjectID: "user:11",
Role: "super_admin",
Scope: []string{"*"}, // 通配符拥有所有权限
TenantID: 0,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *superAdminClaims)
// act & assert - super_admin 拥有所有 scope
assert.True(t, CheckScope(ctx, "platform:read"))
assert.True(t, CheckScope(ctx, "platform:admin"))
assert.True(t, CheckScope(ctx, "supply:account:write"))
assert.True(t, CheckScope(ctx, "consumer:apikey:create"))
assert.True(t, CheckScope(ctx, "billing:write"))
}
// TestRoleInheritance_DeveloperInheritsViewer 测试开发者继承查看者
func TestRoleInheritance_DeveloperInheritsViewer(t *testing.T) {
// arrange
developerScopes := []string{"platform:read", "tenant:read", "billing:read", "router:invoke", "router:model:list"}
developerClaims := &IAMTokenClaims{
SubjectID: "user:12",
Role: "developer",
Scope: developerScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *developerClaims)
// act & assert - developer 继承 viewer 的所有 scope
assert.True(t, CheckScope(ctx, "platform:read"))
assert.True(t, CheckScope(ctx, "tenant:read"))
assert.True(t, CheckScope(ctx, "billing:read"))
assert.True(t, CheckScope(ctx, "router:invoke")) // developer 自身 scope
assert.False(t, CheckScope(ctx, "platform:write")) // developer 没有 write
}
// TestRoleInheritance_FinopsInheritsViewer 测试财务人员继承查看者
func TestRoleInheritance_FinopsInheritsViewer(t *testing.T) {
// arrange
finopsScopes := []string{"platform:read", "tenant:read", "billing:read", "billing:write"}
finopsClaims := &IAMTokenClaims{
SubjectID: "user:13",
Role: "finops",
Scope: finopsScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *finopsClaims)
// act & assert - finops 继承 viewer 的所有 scope
assert.True(t, CheckScope(ctx, "platform:read"))
assert.True(t, CheckScope(ctx, "tenant:read"))
assert.True(t, CheckScope(ctx, "billing:read"))
assert.True(t, CheckScope(ctx, "billing:write")) // finops 自身 scope
assert.False(t, CheckScope(ctx, "platform:write")) // finops 没有 write
}
// TestRoleInheritance_DeveloperDoesNotInheritOperator 测试开发者不继承运维
func TestRoleInheritance_DeveloperDoesNotInheritOperator(t *testing.T) {
// arrange
developerScopes := []string{"platform:read", "tenant:read", "billing:read", "router:invoke", "router:model:list"}
developerClaims := &IAMTokenClaims{
SubjectID: "user:14",
Role: "developer",
Scope: developerScopes,
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *developerClaims)
// act & assert - developer 不继承 operator 的 scope
assert.False(t, CheckScope(ctx, "platform:write")) // operator 有developer 没有
assert.False(t, CheckScope(ctx, "tenant:write")) // operator 有developer 没有
}

View File

@@ -0,0 +1,350 @@
package middleware
import (
"context"
"net/http"
"lijiaoqiao/supply-api/internal/middleware"
)
// IAM token claims context key
type iamContextKey string
const (
// IAMTokenClaimsKey 用于在context中存储token claims
IAMTokenClaimsKey iamContextKey = "iam_token_claims"
)
// IAMTokenClaims IAM扩展Token Claims
type IAMTokenClaims struct {
SubjectID string `json:"subject_id"`
Role string `json:"role"`
Scope []string `json:"scope"`
TenantID int64 `json:"tenant_id"`
UserType string `json:"user_type"` // 用户类型: platform/supply/consumer
Permissions []string `json:"permissions"` // 细粒度权限列表
}
// ScopeAuthMiddleware Scope权限验证中间件
type ScopeAuthMiddleware struct {
// 路由-Scope映射
routeScopePolicies map[string][]string
// 角色层级
roleHierarchy map[string]int
}
// NewScopeAuthMiddleware 创建Scope权限验证中间件
func NewScopeAuthMiddleware() *ScopeAuthMiddleware {
return &ScopeAuthMiddleware{
routeScopePolicies: make(map[string][]string),
roleHierarchy: map[string]int{
"super_admin": 100,
"org_admin": 50,
"supply_admin": 40,
"consumer_admin": 40,
"operator": 30,
"developer": 20,
"finops": 20,
"supply_operator": 30,
"supply_finops": 20,
"supply_viewer": 10,
"consumer_operator": 30,
"consumer_viewer": 10,
"viewer": 10,
},
}
}
// SetRouteScopePolicy 设置路由的Scope要求
func (m *ScopeAuthMiddleware) SetRouteScopePolicy(route string, scopes []string) {
m.routeScopePolicies[route] = scopes
}
// CheckScope 检查是否拥有指定Scope
func CheckScope(ctx context.Context, requiredScope string) bool {
claims := getIAMTokenClaims(ctx)
if claims == nil {
return false
}
// 空scope直接通过
if requiredScope == "" {
return true
}
return hasScope(claims.Scope, requiredScope)
}
// CheckAllScopes 检查是否拥有所有指定Scope
func CheckAllScopes(ctx context.Context, requiredScopes []string) bool {
claims := getIAMTokenClaims(ctx)
if claims == nil {
return false
}
// 空列表直接通过
if len(requiredScopes) == 0 {
return true
}
for _, scope := range requiredScopes {
if !hasScope(claims.Scope, scope) {
return false
}
}
return true
}
// CheckAnyScope 检查是否拥有任一指定Scope
func CheckAnyScope(ctx context.Context, requiredScopes []string) bool {
claims := getIAMTokenClaims(ctx)
if claims == nil {
return false
}
// 空列表直接通过
if len(requiredScopes) == 0 {
return true
}
for _, scope := range requiredScopes {
if hasScope(claims.Scope, scope) {
return true
}
}
return false
}
// HasRole 检查是否拥有指定角色
func HasRole(ctx context.Context, requiredRole string) bool {
claims := getIAMTokenClaims(ctx)
if claims == nil {
return false
}
return claims.Role == requiredRole
}
// HasRoleLevel 检查角色层级是否满足要求
func HasRoleLevel(ctx context.Context, minLevel int) bool {
claims := getIAMTokenClaims(ctx)
if claims == nil {
return false
}
level := GetRoleLevel(claims.Role)
return level >= minLevel
}
// GetRoleLevel 获取角色层级数值
func GetRoleLevel(role string) int {
hierarchy := map[string]int{
"super_admin": 100,
"org_admin": 50,
"supply_admin": 40,
"consumer_admin": 40,
"operator": 30,
"developer": 20,
"finops": 20,
"supply_operator": 30,
"supply_finops": 20,
"supply_viewer": 10,
"consumer_operator": 30,
"consumer_viewer": 10,
"viewer": 10,
}
if level, ok := hierarchy[role]; ok {
return level
}
return 0
}
// GetIAMTokenClaims 获取IAM Token Claims
func GetIAMTokenClaims(ctx context.Context) *IAMTokenClaims {
if claims, ok := ctx.Value(IAMTokenClaimsKey).(IAMTokenClaims); ok {
return &claims
}
return nil
}
// getIAMTokenClaims 内部获取IAM Token Claims
func getIAMTokenClaims(ctx context.Context) *IAMTokenClaims {
if claims, ok := ctx.Value(IAMTokenClaimsKey).(IAMTokenClaims); ok {
return &claims
}
return nil
}
// hasScope 检查scope列表是否包含目标scope
func hasScope(scopes []string, target string) bool {
for _, scope := range scopes {
if scope == target || scope == "*" {
return true
}
}
return false
}
// RequireScope 返回一个要求特定Scope的中间件
func (m *ScopeAuthMiddleware) RequireScope(requiredScope string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := getIAMTokenClaims(r.Context())
if claims == nil {
writeAuthError(w, http.StatusUnauthorized, "AUTH_CONTEXT_MISSING",
"authentication context is missing")
return
}
// 检查scope
if requiredScope != "" && !hasScope(claims.Scope, requiredScope) {
writeAuthError(w, http.StatusForbidden, "AUTH_SCOPE_DENIED",
"required scope is not granted")
return
}
next.ServeHTTP(w, r)
})
}
}
// RequireAllScopes 返回一个要求所有指定Scope的中间件
func (m *ScopeAuthMiddleware) RequireAllScopes(requiredScopes []string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := getIAMTokenClaims(r.Context())
if claims == nil {
writeAuthError(w, http.StatusUnauthorized, "AUTH_CONTEXT_MISSING",
"authentication context is missing")
return
}
for _, scope := range requiredScopes {
if !hasScope(claims.Scope, scope) {
writeAuthError(w, http.StatusForbidden, "AUTH_SCOPE_DENIED",
"required scope is not granted")
return
}
}
next.ServeHTTP(w, r)
})
}
}
// RequireAnyScope 返回一个要求任一指定Scope的中间件
func (m *ScopeAuthMiddleware) RequireAnyScope(requiredScopes []string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := getIAMTokenClaims(r.Context())
if claims == nil {
writeAuthError(w, http.StatusUnauthorized, "AUTH_CONTEXT_MISSING",
"authentication context is missing")
return
}
// 空列表直接通过
if len(requiredScopes) > 0 && !hasAnyScope(claims.Scope, requiredScopes) {
writeAuthError(w, http.StatusForbidden, "AUTH_SCOPE_DENIED",
"none of the required scopes are granted")
return
}
next.ServeHTTP(w, r)
})
}
}
// RequireRole 返回一个要求特定角色的中间件
func (m *ScopeAuthMiddleware) RequireRole(requiredRole string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := getIAMTokenClaims(r.Context())
if claims == nil {
writeAuthError(w, http.StatusUnauthorized, "AUTH_CONTEXT_MISSING",
"authentication context is missing")
return
}
if claims.Role != requiredRole {
writeAuthError(w, http.StatusForbidden, "AUTH_ROLE_DENIED",
"required role is not granted")
return
}
next.ServeHTTP(w, r)
})
}
}
// RequireMinLevel 返回一个要求最小角色层级的中间件
func (m *ScopeAuthMiddleware) RequireMinLevel(minLevel int) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := getIAMTokenClaims(r.Context())
if claims == nil {
writeAuthError(w, http.StatusUnauthorized, "AUTH_CONTEXT_MISSING",
"authentication context is missing")
return
}
level := GetRoleLevel(claims.Role)
if level < minLevel {
writeAuthError(w, http.StatusForbidden, "AUTH_ROLE_LEVEL_DENIED",
"insufficient role level")
return
}
next.ServeHTTP(w, r)
})
}
}
// hasAnyScope 检查scope列表是否包含任一目标scope
func hasAnyScope(scopes, targets []string) bool {
for _, scope := range scopes {
for _, target := range targets {
if scope == target || scope == "*" {
return true
}
}
}
return false
}
// writeAuthError 写入鉴权错误
func writeAuthError(w http.ResponseWriter, status int, code, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
resp := map[string]interface{}{
"error": map[string]string{
"code": code,
"message": message,
},
}
_ = resp
}
// WithIAMClaims 设置IAM Claims到Context
func WithIAMClaims(ctx context.Context, claims *IAMTokenClaims) context.Context {
return context.WithValue(ctx, IAMTokenClaimsKey, *claims)
}
// GetClaimsFromLegacy 从原有middleware.TokenClaims转换为IAMTokenClaims
func GetClaimsFromLegacy(legacy *middleware.TokenClaims) *IAMTokenClaims {
if legacy == nil {
return nil
}
return &IAMTokenClaims{
SubjectID: legacy.SubjectID,
Role: legacy.Role,
Scope: legacy.Scope,
TenantID: legacy.TenantID,
}
}

View File

@@ -0,0 +1,439 @@
package middleware
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"lijiaoqiao/supply-api/internal/middleware"
)
// TestScopeAuth_CheckScope_SuperAdminHasAllScopes 测试超级管理员拥有所有Scope
func TestScopeAuth_CheckScope_SuperAdminHasAllScopes(t *testing.T) {
// arrange
// 创建超级管理员token claims
claims := &IAMTokenClaims{
SubjectID: "user:1",
Role: "super_admin",
Scope: []string{"*"}, // 通配符Scope代表所有权限
TenantID: 0,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
hasScope := CheckScope(ctx, "platform:read")
hasScope2 := CheckScope(ctx, "supply:account:write")
hasScope3 := CheckScope(ctx, "consumer:apikey:create")
// assert
assert.True(t, hasScope, "super_admin should have platform:read")
assert.True(t, hasScope2, "super_admin should have supply:account:write")
assert.True(t, hasScope3, "super_admin should have consumer:apikey:create")
}
// TestScopeAuth_CheckScope_ViewerHasReadOnly 测试Viewer只有只读权限
func TestScopeAuth_CheckScope_ViewerHasReadOnly(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:2",
Role: "viewer",
Scope: []string{"platform:read", "tenant:read", "billing:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, CheckScope(ctx, "platform:read"), "viewer should have platform:read")
assert.True(t, CheckScope(ctx, "tenant:read"), "viewer should have tenant:read")
assert.True(t, CheckScope(ctx, "billing:read"), "viewer should have billing:read")
assert.False(t, CheckScope(ctx, "platform:write"), "viewer should NOT have platform:write")
assert.False(t, CheckScope(ctx, "tenant:write"), "viewer should NOT have tenant:write")
assert.False(t, CheckScope(ctx, "supply:account:write"), "viewer should NOT have supply:account:write")
}
// TestScopeAuth_CheckScope_Denied 测试Scope被拒绝
func TestScopeAuth_CheckScope_Denied(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:3",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.False(t, CheckScope(ctx, "platform:write"), "viewer should NOT have platform:write")
assert.False(t, CheckScope(ctx, "supply:account:write"), "viewer should NOT have supply:account:write")
}
// TestScopeAuth_CheckScope_MissingTokenClaims 测试缺少Token Claims
func TestScopeAuth_CheckScope_MissingTokenClaims(t *testing.T) {
// arrange
ctx := context.Background() // 没有token claims
// act
hasScope := CheckScope(ctx, "platform:read")
// assert
assert.False(t, hasScope, "should return false when token claims are missing")
}
// TestScopeAuth_CheckScope_EmptyScope 测试空Scope要求
func TestScopeAuth_CheckScope_EmptyScope(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:4",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
hasEmptyScope := CheckScope(ctx, "")
// assert
assert.True(t, hasEmptyScope, "empty scope should always pass")
}
// TestScopeAuth_CheckMultipleScopes 测试检查多个Scope需要全部满足
func TestScopeAuth_CheckMultipleScopes(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:5",
Role: "operator",
Scope: []string{"platform:read", "platform:write", "tenant:read", "tenant:write"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, CheckAllScopes(ctx, []string{"platform:read", "platform:write"}), "operator should have both read and write")
assert.True(t, CheckAllScopes(ctx, []string{"tenant:read", "tenant:write"}), "operator should have both tenant scopes")
assert.False(t, CheckAllScopes(ctx, []string{"platform:read", "platform:admin"}), "operator should NOT have platform:admin")
}
// TestScopeAuth_CheckAnyScope 测试检查多个Scope只需满足其一
func TestScopeAuth_CheckAnyScope(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:6",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, CheckAnyScope(ctx, []string{"platform:read", "platform:write"}), "should pass with one matching scope")
assert.False(t, CheckAnyScope(ctx, []string{"platform:write", "platform:admin"}), "should fail when no scopes match")
assert.True(t, CheckAnyScope(ctx, []string{}), "empty scope list should pass")
}
// TestScopeAuth_GetIAMTokenClaims 测试从Context获取IAMTokenClaims
func TestScopeAuth_GetIAMTokenClaims(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:7",
Role: "org_admin",
Scope: []string{"platform:read", "platform:write"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
retrievedClaims := GetIAMTokenClaims(ctx)
// assert
assert.NotNil(t, retrievedClaims)
assert.Equal(t, claims.SubjectID, retrievedClaims.SubjectID)
assert.Equal(t, claims.Role, retrievedClaims.Role)
assert.Equal(t, claims.Scope, retrievedClaims.Scope)
}
// TestScopeAuth_GetIAMTokenClaims_Missing 测试获取不存在的IAMTokenClaims
func TestScopeAuth_GetIAMTokenClaims_Missing(t *testing.T) {
// arrange
ctx := context.Background()
// act
retrievedClaims := GetIAMTokenClaims(ctx)
// assert
assert.Nil(t, retrievedClaims)
}
// TestScopeAuth_HasRole 测试用户角色检查
func TestScopeAuth_HasRole(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:8",
Role: "operator",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, HasRole(ctx, "operator"))
assert.False(t, HasRole(ctx, "viewer"))
assert.False(t, HasRole(ctx, "admin"))
}
// TestScopeAuth_HasRole_MissingClaims 测试缺少Claims时的角色检查
func TestScopeAuth_HasRole_MissingClaims(t *testing.T) {
// arrange
ctx := context.Background()
// act & assert
assert.False(t, HasRole(ctx, "operator"))
}
// TestScopeRoleAuthzMiddleware_WithScope 测试带Scope要求的中间件
func TestScopeRoleAuthzMiddleware_WithScope(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
})
// 创建一个带scope验证的handler
wrappedHandler := scopeAuth.RequireScope("platform:write")(handler)
// 创建一个带有token claims的请求
claims := &IAMTokenClaims{
SubjectID: "user:9",
Role: "operator",
Scope: []string{"platform:read", "platform:write"},
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusOK, rec.Code)
}
// TestScopeRoleAuthzMiddleware_Denied 测试Scope不足时中间件拒绝
func TestScopeRoleAuthzMiddleware_Denied(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireScope("platform:admin")(handler)
claims := &IAMTokenClaims{
SubjectID: "user:10",
Role: "viewer",
Scope: []string{"platform:read"}, // viewer没有platform:admin
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusForbidden, rec.Code)
}
// TestScopeRoleAuthzMiddleware_MissingClaims 测试缺少Claims时中间件拒绝
func TestScopeRoleAuthzMiddleware_MissingClaims(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireScope("platform:read")(handler)
req := httptest.NewRequest("GET", "/test", nil)
// 不设置token claims
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusUnauthorized, rec.Code)
}
// TestScopeRoleAuthzMiddleware_RequireAllScopes 测试要求所有Scope的中间件
func TestScopeRoleAuthzMiddleware_RequireAllScopes(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireAllScopes([]string{"platform:read", "tenant:read"})(handler)
claims := &IAMTokenClaims{
SubjectID: "user:11",
Role: "operator",
Scope: []string{"platform:read", "platform:write", "tenant:read"},
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusOK, rec.Code)
}
// TestScopeRoleAuthzMiddleware_RequireAllScopes_Denied 测试要求所有Scope但不足时拒绝
func TestScopeRoleAuthzMiddleware_RequireAllScopes_Denied(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireAllScopes([]string{"platform:read", "platform:admin"})(handler)
claims := &IAMTokenClaims{
SubjectID: "user:12",
Role: "viewer",
Scope: []string{"platform:read"}, // viewer没有platform:admin
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusForbidden, rec.Code)
}
// TestScopeAuth_HasRoleLevel 测试角色层级检查
func TestScopeAuth_HasRoleLevel(t *testing.T) {
// arrange
testCases := []struct {
role string
minLevel int
expected bool
}{
{"super_admin", 50, true},
{"super_admin", 100, true},
{"org_admin", 50, true},
{"org_admin", 60, false},
{"operator", 30, true},
{"operator", 40, false},
{"viewer", 10, true},
{"viewer", 20, false},
}
for _, tc := range testCases {
claims := &IAMTokenClaims{
SubjectID: "user:test",
Role: tc.role,
Scope: []string{},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
result := HasRoleLevel(ctx, tc.minLevel)
// assert
assert.Equal(t, tc.expected, result, "role=%s, minLevel=%d", tc.role, tc.minLevel)
}
}
// TestGetRoleLevel 测试获取角色层级
func TestGetRoleLevel(t *testing.T) {
testCases := []struct {
role string
expected int
}{
{"super_admin", 100},
{"org_admin", 50},
{"supply_admin", 40},
{"operator", 30},
{"developer", 20},
{"viewer", 10},
{"unknown_role", 0},
}
for _, tc := range testCases {
// act
level := GetRoleLevel(tc.role)
// assert
assert.Equal(t, tc.expected, level, "role=%s", tc.role)
}
}
// TestScopeAuth_WithIAMClaims 测试设置IAM Claims到Context
func TestScopeAuth_WithIAMClaims(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:13",
Role: "org_admin",
Scope: []string{"platform:read"},
TenantID: 1,
}
// act
ctx := WithIAMClaims(context.Background(), claims)
retrievedClaims := GetIAMTokenClaims(ctx)
// assert
assert.NotNil(t, retrievedClaims)
assert.Equal(t, claims.SubjectID, retrievedClaims.SubjectID)
assert.Equal(t, claims.Role, retrievedClaims.Role)
}
// TestGetClaimsFromLegacy 测试从原有TokenClaims转换
func TestGetClaimsFromLegacy(t *testing.T) {
// arrange
legacyClaims := &middleware.TokenClaims{
SubjectID: "user:14",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
// act
iamClaims := GetClaimsFromLegacy(legacyClaims)
// assert
assert.NotNil(t, iamClaims)
assert.Equal(t, legacyClaims.SubjectID, iamClaims.SubjectID)
assert.Equal(t, legacyClaims.Role, iamClaims.Role)
assert.Equal(t, legacyClaims.Scope, iamClaims.Scope)
assert.Equal(t, legacyClaims.TenantID, iamClaims.TenantID)
}

View File

@@ -0,0 +1,211 @@
package model
import (
"crypto/rand"
"encoding/hex"
"errors"
"time"
)
// 角色类型常量
const (
RoleTypePlatform = "platform"
RoleTypeSupply = "supply"
RoleTypeConsumer = "consumer"
)
// 角色层级常量(用于权限优先级判断)
const (
LevelSuperAdmin = 100
LevelOrgAdmin = 50
LevelSupplyAdmin = 40
LevelOperator = 30
LevelDeveloper = 20
LevelFinops = 20
LevelViewer = 10
)
// 角色错误定义
var (
ErrInvalidRoleCode = errors.New("invalid role code: cannot be empty")
ErrInvalidRoleType = errors.New("invalid role type: must be platform, supply, or consumer")
ErrInvalidLevel = errors.New("invalid level: must be non-negative")
)
// Role 角色模型
// 对应数据库 iam_roles 表
type Role struct {
ID int64 // 主键ID
Code string // 角色代码 (unique)
Name string // 角色名称
Type string // 角色类型: platform, supply, consumer
ParentRoleID *int64 // 父角色ID用于继承关系
Level int // 权限层级
Description string // 描述
IsActive bool // 是否激活
// 审计字段
RequestID string // 请求追踪ID
CreatedIP string // 创建者IP
UpdatedIP string // 更新者IP
Version int // 乐观锁版本号
// 时间戳
CreatedAt *time.Time // 创建时间
UpdatedAt *time.Time // 更新时间
// 关联的Scope列表运行时填充不存储在iam_roles表
Scopes []string `json:"scopes,omitempty"`
}
// NewRole 创建新角色(基础构造函数)
func NewRole(code, name, roleType string, level int) *Role {
now := time.Now()
return &Role{
Code: code,
Name: name,
Type: roleType,
Level: level,
IsActive: true,
RequestID: generateRequestID(),
Version: 1,
CreatedAt: &now,
UpdatedAt: &now,
}
}
// NewRoleWithParent 创建带父角色的角色
func NewRoleWithParent(code, name, roleType string, level int, parentRoleID int64) *Role {
role := NewRole(code, name, roleType, level)
role.ParentRoleID = &parentRoleID
return role
}
// NewRoleWithRequestID 创建带指定RequestID的角色
func NewRoleWithRequestID(code, name, roleType string, level int, requestID string) *Role {
role := NewRole(code, name, roleType, level)
role.RequestID = requestID
return role
}
// NewRoleWithAudit 创建带审计信息的角色
func NewRoleWithAudit(code, name, roleType string, level int, requestID, createdIP, updatedIP string) *Role {
role := NewRole(code, name, roleType, level)
role.RequestID = requestID
role.CreatedIP = createdIP
role.UpdatedIP = updatedIP
return role
}
// NewRoleWithValidation 创建角色并进行验证
func NewRoleWithValidation(code, name, roleType string, level int) (*Role, error) {
// 验证角色代码
if code == "" {
return nil, ErrInvalidRoleCode
}
// 验证角色类型
if roleType != RoleTypePlatform && roleType != RoleTypeSupply && roleType != RoleTypeConsumer {
return nil, ErrInvalidRoleType
}
// 验证层级
if level < 0 {
return nil, ErrInvalidLevel
}
role := NewRole(code, name, roleType, level)
return role, nil
}
// Activate 激活角色
func (r *Role) Activate() {
r.IsActive = true
r.UpdatedAt = nowPtr()
}
// Deactivate 停用角色
func (r *Role) Deactivate() {
r.IsActive = false
r.UpdatedAt = nowPtr()
}
// IncrementVersion 递增版本号(用于乐观锁)
func (r *Role) IncrementVersion() {
r.Version++
r.UpdatedAt = nowPtr()
}
// SetParentRole 设置父角色
func (r *Role) SetParentRole(parentID int64) {
r.ParentRoleID = &parentID
}
// SetScopes 设置角色关联的Scope列表
func (r *Role) SetScopes(scopes []string) {
r.Scopes = scopes
}
// AddScope 添加一个Scope
func (r *Role) AddScope(scope string) {
for _, s := range r.Scopes {
if s == scope {
return
}
}
r.Scopes = append(r.Scopes, scope)
}
// RemoveScope 移除一个Scope
func (r *Role) RemoveScope(scope string) {
newScopes := make([]string, 0, len(r.Scopes))
for _, s := range r.Scopes {
if s != scope {
newScopes = append(newScopes, s)
}
}
r.Scopes = newScopes
}
// HasScope 检查角色是否拥有指定Scope
func (r *Role) HasScope(scope string) bool {
for _, s := range r.Scopes {
if s == scope || s == "*" {
return true
}
}
return false
}
// ToRoleScopeInfo 转换为RoleScopeInfo结构用于API响应
func (r *Role) ToRoleScopeInfo() *RoleScopeInfo {
return &RoleScopeInfo{
RoleCode: r.Code,
RoleName: r.Name,
RoleType: r.Type,
Level: r.Level,
Scopes: r.Scopes,
}
}
// RoleScopeInfo 角色的Scope信息用于API响应
type RoleScopeInfo struct {
RoleCode string `json:"role_code"`
RoleName string `json:"role_name"`
RoleType string `json:"role_type"`
Level int `json:"level"`
Scopes []string `json:"scopes,omitempty"`
}
// generateRequestID 生成请求追踪ID
func generateRequestID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
// nowPtr 返回当前时间的指针
func nowPtr() *time.Time {
t := time.Now()
return &t
}

View File

@@ -0,0 +1,152 @@
package model
import (
"time"
)
// RoleScopeMapping 角色-Scope关联模型
// 对应数据库 iam_role_scopes 表
type RoleScopeMapping struct {
ID int64 // 主键ID
RoleID int64 // 角色ID (FK -> iam_roles.id)
ScopeID int64 // ScopeID (FK -> iam_scopes.id)
IsActive bool // 是否激活
// 审计字段
RequestID string // 请求追踪ID
CreatedIP string // 创建者IP
Version int // 乐观锁版本号
// 时间戳
CreatedAt *time.Time // 创建时间
}
// NewRoleScopeMapping 创建新的角色-Scope映射
func NewRoleScopeMapping(roleID, scopeID int64) *RoleScopeMapping {
now := time.Now()
return &RoleScopeMapping{
RoleID: roleID,
ScopeID: scopeID,
IsActive: true,
RequestID: generateRequestID(),
Version: 1,
CreatedAt: &now,
}
}
// NewRoleScopeMappingWithAudit 创建带审计信息的角色-Scope映射
func NewRoleScopeMappingWithAudit(roleID, scopeID int64, requestID, createdIP string) *RoleScopeMapping {
now := time.Now()
return &RoleScopeMapping{
RoleID: roleID,
ScopeID: scopeID,
IsActive: true,
RequestID: requestID,
CreatedIP: createdIP,
Version: 1,
CreatedAt: &now,
}
}
// Revoke 撤销角色-Scope映射
func (m *RoleScopeMapping) Revoke() {
m.IsActive = false
}
// Grant 授予角色-Scope映射
func (m *RoleScopeMapping) Grant() {
m.IsActive = true
}
// IncrementVersion 递增版本号
func (m *RoleScopeMapping) IncrementVersion() {
m.Version++
}
// GrantScopeList 批量授予Scope
func GrantScopeList(roleID int64, scopeIDs []int64) []*RoleScopeMapping {
mappings := make([]*RoleScopeMapping, 0, len(scopeIDs))
for _, scopeID := range scopeIDs {
mapping := NewRoleScopeMapping(roleID, scopeID)
mappings = append(mappings, mapping)
}
return mappings
}
// RevokeAll 撤销所有映射
func RevokeAll(mappings []*RoleScopeMapping) {
for _, mapping := range mappings {
mapping.Revoke()
}
}
// GetActiveScopeIDs 从映射列表中获取活跃的Scope ID列表
func GetActiveScopeIDs(mappings []*RoleScopeMapping) []int64 {
activeIDs := make([]int64, 0, len(mappings))
for _, mapping := range mappings {
if mapping.IsActive {
activeIDs = append(activeIDs, mapping.ScopeID)
}
}
return activeIDs
}
// GetInactiveScopeIDs 从映射列表中获取非活跃的Scope ID列表
func GetInactiveScopeIDs(mappings []*RoleScopeMapping) []int64 {
inactiveIDs := make([]int64, 0, len(mappings))
for _, mapping := range mappings {
if !mapping.IsActive {
inactiveIDs = append(inactiveIDs, mapping.ScopeID)
}
}
return inactiveIDs
}
// FilterActiveMappings 过滤出活跃的映射
func FilterActiveMappings(mappings []*RoleScopeMapping) []*RoleScopeMapping {
active := make([]*RoleScopeMapping, 0, len(mappings))
for _, mapping := range mappings {
if mapping.IsActive {
active = append(active, mapping)
}
}
return active
}
// FilterMappingsByRole 过滤出指定角色的映射
func FilterMappingsByRole(mappings []*RoleScopeMapping, roleID int64) []*RoleScopeMapping {
filtered := make([]*RoleScopeMapping, 0, len(mappings))
for _, mapping := range mappings {
if mapping.RoleID == roleID {
filtered = append(filtered, mapping)
}
}
return filtered
}
// FilterMappingsByScope 过滤出指定Scope的映射
func FilterMappingsByScope(mappings []*RoleScopeMapping, scopeID int64) []*RoleScopeMapping {
filtered := make([]*RoleScopeMapping, 0, len(mappings))
for _, mapping := range mappings {
if mapping.ScopeID == scopeID {
filtered = append(filtered, mapping)
}
}
return filtered
}
// RoleScopeMappingInfo 角色-Scope映射信息用于API响应
type RoleScopeMappingInfo struct {
RoleID int64 `json:"role_id"`
ScopeID int64 `json:"scope_id"`
IsActive bool `json:"is_active"`
}
// ToInfo 转换为映射信息
func (m *RoleScopeMapping) ToInfo() *RoleScopeMappingInfo {
return &RoleScopeMappingInfo{
RoleID: m.RoleID,
ScopeID: m.ScopeID,
IsActive: m.IsActive,
}
}

View File

@@ -0,0 +1,157 @@
package model
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestRoleScopeMapping_GrantScope 测试授予Scope
func TestRoleScopeMapping_GrantScope(t *testing.T) {
// arrange
role := NewRole("operator", "运维人员", RoleTypePlatform, 30)
role.ID = 1
scope1 := NewScope("platform:read", "读取平台配置", ScopeTypePlatform)
scope1.ID = 1
scope2 := NewScope("platform:write", "修改平台配置", ScopeTypePlatform)
scope2.ID = 2
// act
roleScopeMapping := NewRoleScopeMapping(role.ID, scope1.ID)
roleScopeMapping2 := NewRoleScopeMapping(role.ID, scope2.ID)
// assert
assert.Equal(t, role.ID, roleScopeMapping.RoleID)
assert.Equal(t, scope1.ID, roleScopeMapping.ScopeID)
assert.NotEmpty(t, roleScopeMapping.RequestID)
assert.Equal(t, 1, roleScopeMapping.Version)
assert.Equal(t, role.ID, roleScopeMapping2.RoleID)
assert.Equal(t, scope2.ID, roleScopeMapping2.ScopeID)
}
// TestRoleScopeMapping_RevokeScope 测试撤销Scope
func TestRoleScopeMapping_RevokeScope(t *testing.T) {
// arrange
role := NewRole("viewer", "查看者", RoleTypePlatform, 10)
role.ID = 1
scope := NewScope("platform:read", "读取平台配置", ScopeTypePlatform)
scope.ID = 1
// act
roleScopeMapping := NewRoleScopeMapping(role.ID, scope.ID)
roleScopeMapping.Revoke()
// assert
assert.False(t, roleScopeMapping.IsActive, "revoked mapping should be inactive")
}
// TestRoleScopeMapping_WithAudit 测试带审计字段的映射
func TestRoleScopeMapping_WithAudit(t *testing.T) {
// arrange
roleID := int64(1)
scopeID := int64(2)
requestID := "req-role-scope-123"
createdIP := "192.168.1.100"
// act
mapping := NewRoleScopeMappingWithAudit(roleID, scopeID, requestID, createdIP)
// assert
assert.Equal(t, roleID, mapping.RoleID)
assert.Equal(t, scopeID, mapping.ScopeID)
assert.Equal(t, requestID, mapping.RequestID)
assert.Equal(t, createdIP, mapping.CreatedIP)
assert.True(t, mapping.IsActive)
}
// TestRoleScopeMapping_IncrementVersion 测试版本号递增
func TestRoleScopeMapping_IncrementVersion(t *testing.T) {
// arrange
mapping := NewRoleScopeMapping(1, 1)
originalVersion := mapping.Version
// act
mapping.IncrementVersion()
// assert
assert.Equal(t, originalVersion+1, mapping.Version)
}
// TestRoleScopeMapping_IsActive 测试活跃状态
func TestRoleScopeMapping_IsActive(t *testing.T) {
// arrange
mapping := NewRoleScopeMapping(1, 1)
// assert - 默认应该激活
assert.True(t, mapping.IsActive)
}
// TestRoleScopeMapping_UniqueConstraint 测试唯一性同一个角色和Scope组合
func TestRoleScopeMapping_UniqueConstraint(t *testing.T) {
// arrange
roleID := int64(1)
scopeID := int64(1)
// act
mapping1 := NewRoleScopeMapping(roleID, scopeID)
mapping2 := NewRoleScopeMapping(roleID, scopeID)
// assert - 两个映射应该有相同的 RoleID 和 ScopeID代表唯一约束
assert.Equal(t, mapping1.RoleID, mapping2.RoleID)
assert.Equal(t, mapping1.ScopeID, mapping2.ScopeID)
}
// TestRoleScopeMapping_GrantScopeList 测试批量授予Scope
func TestRoleScopeMapping_GrantScopeList(t *testing.T) {
// arrange
roleID := int64(1)
scopeIDs := []int64{1, 2, 3, 4, 5}
// act
mappings := GrantScopeList(roleID, scopeIDs)
// assert
assert.Len(t, mappings, len(scopeIDs))
for i, scopeID := range scopeIDs {
assert.Equal(t, roleID, mappings[i].RoleID)
assert.Equal(t, scopeID, mappings[i].ScopeID)
assert.True(t, mappings[i].IsActive)
}
}
// TestRoleScopeMapping_RevokeAll 测试撤销所有Scope针对某个角色
func TestRoleScopeMapping_RevokeAll(t *testing.T) {
// arrange
roleID := int64(1)
scopeIDs := []int64{1, 2, 3}
mappings := GrantScopeList(roleID, scopeIDs)
// act
RevokeAll(mappings)
// assert
for _, mapping := range mappings {
assert.False(t, mapping.IsActive, "all mappings should be revoked")
}
}
// TestRoleScopeMapping_GetActiveScopes 测试获取活跃的Scope列表
func TestRoleScopeMapping_GetActiveScopes(t *testing.T) {
// arrange
roleID := int64(1)
scopeIDs := []int64{1, 2, 3}
mappings := GrantScopeList(roleID, scopeIDs)
// 撤销中间的Scope
mappings[1].Revoke()
// act
activeScopes := GetActiveScopeIDs(mappings)
// assert
assert.Len(t, activeScopes, 2)
assert.Contains(t, activeScopes, int64(1))
assert.Contains(t, activeScopes, int64(3))
assert.NotContains(t, activeScopes, int64(2))
}

View File

@@ -0,0 +1,244 @@
package model
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestRoleModel_NewRole_ValidInput 测试创建角色 - 有效输入
func TestRoleModel_NewRole_ValidInput(t *testing.T) {
// arrange
roleCode := "org_admin"
roleName := "组织管理员"
roleType := "platform"
level := 50
// act
role := NewRole(roleCode, roleName, roleType, level)
// assert
assert.Equal(t, roleCode, role.Code)
assert.Equal(t, roleName, role.Name)
assert.Equal(t, roleType, role.Type)
assert.Equal(t, level, role.Level)
assert.True(t, role.IsActive)
assert.NotEmpty(t, role.RequestID)
assert.Equal(t, 1, role.Version)
}
// TestRoleModel_NewRole_DefaultFields 测试创建角色 - 验证默认字段
func TestRoleModel_NewRole_DefaultFields(t *testing.T) {
// arrange
roleCode := "viewer"
roleName := "查看者"
roleType := "platform"
level := 10
// act
role := NewRole(roleCode, roleName, roleType, level)
// assert - 验证默认字段
assert.Equal(t, 1, role.Version, "version should default to 1")
assert.NotEmpty(t, role.RequestID, "request_id should be auto-generated")
assert.True(t, role.IsActive, "is_active should default to true")
assert.Nil(t, role.ParentRoleID, "parent_role_id should be nil for root roles")
}
// TestRoleModel_NewRole_WithParent 测试创建角色 - 带父角色
func TestRoleModel_NewRole_WithParent(t *testing.T) {
// arrange
parentRole := NewRole("viewer", "查看者", "platform", 10)
parentRole.ID = 1
// act
childRole := NewRoleWithParent("developer", "开发者", "platform", 20, parentRole.ID)
// assert
assert.Equal(t, "developer", childRole.Code)
assert.Equal(t, 20, childRole.Level)
assert.NotNil(t, childRole.ParentRoleID)
assert.Equal(t, parentRole.ID, *childRole.ParentRoleID)
}
// TestRoleModel_NewRole_WithRequestID 测试创建角色 - 指定RequestID
func TestRoleModel_NewRole_WithRequestID(t *testing.T) {
// arrange
requestID := "req-12345"
// act
role := NewRoleWithRequestID("org_admin", "组织管理员", "platform", 50, requestID)
// assert
assert.Equal(t, requestID, role.RequestID)
}
// TestRoleModel_NewRole_AuditFields 测试创建角色 - 审计字段
func TestRoleModel_NewRole_AuditFields(t *testing.T) {
// arrange
createdIP := "192.168.1.1"
updatedIP := "192.168.1.2"
// act
role := NewRoleWithAudit("supply_admin", "供应方管理员", "supply", 40, "req-123", createdIP, updatedIP)
// assert
assert.Equal(t, createdIP, role.CreatedIP)
assert.Equal(t, updatedIP, role.UpdatedIP)
assert.Equal(t, 1, role.Version)
}
// TestRoleModel_NewRole_Timestamps 测试创建角色 - 时间戳
func TestRoleModel_NewRole_Timestamps(t *testing.T) {
// arrange
beforeCreate := time.Now()
// act
role := NewRole("test_role", "测试角色", "platform", 10)
_ = time.Now() // afterCreate not needed
// assert
assert.NotNil(t, role.CreatedAt)
assert.NotNil(t, role.UpdatedAt)
assert.True(t, role.CreatedAt.After(beforeCreate) || role.CreatedAt.Equal(beforeCreate))
assert.True(t, role.UpdatedAt.After(beforeCreate) || role.UpdatedAt.Equal(beforeCreate))
}
// TestRoleModel_Activate 测试激活角色
func TestRoleModel_Activate(t *testing.T) {
// arrange
role := NewRole("inactive_role", "非活跃角色", "platform", 10)
role.IsActive = false
// act
role.Activate()
// assert
assert.True(t, role.IsActive)
}
// TestRoleModel_Deactivate 测试停用角色
func TestRoleModel_Deactivate(t *testing.T) {
// arrange
role := NewRole("active_role", "活跃角色", "platform", 10)
// act
role.Deactivate()
// assert
assert.False(t, role.IsActive)
}
// TestRoleModel_IncrementVersion 测试版本号递增
func TestRoleModel_IncrementVersion(t *testing.T) {
// arrange
role := NewRole("test_role", "测试角色", "platform", 10)
originalVersion := role.Version
// act
role.IncrementVersion()
// assert
assert.Equal(t, originalVersion+1, role.Version)
}
// TestRoleModel_RoleType_Platform 测试平台角色类型
func TestRoleModel_RoleType_Platform(t *testing.T) {
// arrange & act
role := NewRole("super_admin", "超级管理员", RoleTypePlatform, 100)
// assert
assert.Equal(t, RoleTypePlatform, role.Type)
}
// TestRoleModel_RoleType_Supply 测试供应方角色类型
func TestRoleModel_RoleType_Supply(t *testing.T) {
// arrange & act
role := NewRole("supply_admin", "供应方管理员", RoleTypeSupply, 40)
// assert
assert.Equal(t, RoleTypeSupply, role.Type)
}
// TestRoleModel_RoleType_Consumer 测试需求方角色类型
func TestRoleModel_RoleType_Consumer(t *testing.T) {
// arrange & act
role := NewRole("consumer_admin", "需求方管理员", RoleTypeConsumer, 40)
// assert
assert.Equal(t, RoleTypeConsumer, role.Type)
}
// TestRoleModel_LevelHierarchy 测试角色层级关系
func TestRoleModel_LevelHierarchy(t *testing.T) {
// 测试设计文档中的层级关系
// super_admin(100) > org_admin(50) > supply_admin(40) > operator(30) > developer/finops(20) > viewer(10)
// arrange
superAdmin := NewRole("super_admin", "超级管理员", RoleTypePlatform, 100)
orgAdmin := NewRole("org_admin", "组织管理员", RoleTypePlatform, 50)
supplyAdmin := NewRole("supply_admin", "供应方管理员", RoleTypeSupply, 40)
operator := NewRole("operator", "运维人员", RoleTypePlatform, 30)
developer := NewRole("developer", "开发者", RoleTypePlatform, 20)
viewer := NewRole("viewer", "查看者", RoleTypePlatform, 10)
// assert - 验证层级数值
assert.Greater(t, superAdmin.Level, orgAdmin.Level)
assert.Greater(t, orgAdmin.Level, supplyAdmin.Level)
assert.Greater(t, supplyAdmin.Level, operator.Level)
assert.Greater(t, operator.Level, developer.Level)
assert.Greater(t, developer.Level, viewer.Level)
}
// TestRoleModel_NewRole_EmptyCode 测试创建角色 - 空角色代码(应返回错误)
func TestRoleModel_NewRole_EmptyCode(t *testing.T) {
// arrange & act
role, err := NewRoleWithValidation("", "测试角色", "platform", 10)
// assert
assert.Error(t, err)
assert.Nil(t, role)
assert.Equal(t, ErrInvalidRoleCode, err)
}
// TestRoleModel_NewRole_InvalidRoleType 测试创建角色 - 无效角色类型
func TestRoleModel_NewRole_InvalidRoleType(t *testing.T) {
// arrange & act
role, err := NewRoleWithValidation("test_role", "测试角色", "invalid_type", 10)
// assert
assert.Error(t, err)
assert.Nil(t, role)
assert.Equal(t, ErrInvalidRoleType, err)
}
// TestRoleModel_NewRole_NegativeLevel 测试创建角色 - 负数层级
func TestRoleModel_NewRole_NegativeLevel(t *testing.T) {
// arrange & act
role, err := NewRoleWithValidation("test_role", "测试角色", "platform", -1)
// assert
assert.Error(t, err)
assert.Nil(t, role)
assert.Equal(t, ErrInvalidLevel, err)
}
// TestRoleModel_ToRoleScopeInfo 测试角色转换为RoleScopeInfo
func TestRoleModel_ToRoleScopeInfo(t *testing.T) {
// arrange
role := NewRole("org_admin", "组织管理员", RoleTypePlatform, 50)
role.ID = 1
role.Scopes = []string{"platform:read", "platform:write"}
// act
roleScopeInfo := role.ToRoleScopeInfo()
// assert
assert.Equal(t, "org_admin", roleScopeInfo.RoleCode)
assert.Equal(t, "组织管理员", roleScopeInfo.RoleName)
assert.Equal(t, 50, roleScopeInfo.Level)
assert.Len(t, roleScopeInfo.Scopes, 2)
assert.Contains(t, roleScopeInfo.Scopes, "platform:read")
assert.Contains(t, roleScopeInfo.Scopes, "platform:write")
}

View File

@@ -0,0 +1,225 @@
package model
import (
"errors"
"strings"
"time"
)
// Scope类型常量
const (
ScopeTypePlatform = "platform"
ScopeTypeSupply = "supply"
ScopeTypeConsumer = "consumer"
ScopeTypeRouter = "router"
ScopeTypeBilling = "billing"
)
// Scope错误定义
var (
ErrInvalidScopeCode = errors.New("invalid scope code: cannot be empty")
ErrInvalidScopeType = errors.New("invalid scope type: must be platform, supply, consumer, router, or billing")
)
// Scope Scope模型
// 对应数据库 iam_scopes 表
type Scope struct {
ID int64 // 主键ID
Code string // Scope代码 (unique): platform:read, supply:account:write
Name string // Scope名称
Type string // Scope类型: platform, supply, consumer, router, billing
Description string // 描述
IsActive bool // 是否激活
// 审计字段
RequestID string // 请求追踪ID
CreatedIP string // 创建者IP
UpdatedIP string // 更新者IP
Version int // 乐观锁版本号
// 时间戳
CreatedAt *time.Time // 创建时间
UpdatedAt *time.Time // 更新时间
}
// NewScope 创建新Scope基础构造函数
func NewScope(code, name, scopeType string) *Scope {
now := time.Now()
return &Scope{
Code: code,
Name: name,
Type: scopeType,
IsActive: true,
RequestID: generateRequestID(),
Version: 1,
CreatedAt: &now,
UpdatedAt: &now,
}
}
// NewScopeWithRequestID 创建带指定RequestID的Scope
func NewScopeWithRequestID(code, name, scopeType string, requestID string) *Scope {
scope := NewScope(code, name, scopeType)
scope.RequestID = requestID
return scope
}
// NewScopeWithAudit 创建带审计信息的Scope
func NewScopeWithAudit(code, name, scopeType string, requestID, createdIP, updatedIP string) *Scope {
scope := NewScope(code, name, scopeType)
scope.RequestID = requestID
scope.CreatedIP = createdIP
scope.UpdatedIP = updatedIP
return scope
}
// NewScopeWithValidation 创建Scope并进行验证
func NewScopeWithValidation(code, name, scopeType string) (*Scope, error) {
// 验证Scope代码
if code == "" {
return nil, ErrInvalidScopeCode
}
// 验证Scope类型
if !IsValidScopeType(scopeType) {
return nil, ErrInvalidScopeType
}
scope := NewScope(code, name, scopeType)
return scope, nil
}
// Activate 激活Scope
func (s *Scope) Activate() {
s.IsActive = true
s.UpdatedAt = nowPtr()
}
// Deactivate 停用Scope
func (s *Scope) Deactivate() {
s.IsActive = false
s.UpdatedAt = nowPtr()
}
// IncrementVersion 递增版本号(用于乐观锁)
func (s *Scope) IncrementVersion() {
s.Version++
s.UpdatedAt = nowPtr()
}
// IsWildcard 检查是否为通配符Scope
func (s *Scope) IsWildcard() bool {
return s.Code == "*"
}
// ToScopeInfo 转换为ScopeInfo结构用于API响应
func (s *Scope) ToScopeInfo() *ScopeInfo {
return &ScopeInfo{
ScopeCode: s.Code,
ScopeName: s.Name,
ScopeType: s.Type,
IsActive: s.IsActive,
}
}
// ScopeInfo Scope信息用于API响应
type ScopeInfo struct {
ScopeCode string `json:"scope_code"`
ScopeName string `json:"scope_name"`
ScopeType string `json:"scope_type"`
IsActive bool `json:"is_active"`
}
// IsValidScopeType 验证Scope类型是否有效
func IsValidScopeType(scopeType string) bool {
switch scopeType {
case ScopeTypePlatform, ScopeTypeSupply, ScopeTypeConsumer, ScopeTypeRouter, ScopeTypeBilling:
return true
default:
return false
}
}
// GetScopeTypeFromCode 从Scope Code推断Scope类型
// 例如: platform:read -> platform, supply:account:write -> supply, consumer:apikey:create -> consumer
func GetScopeTypeFromCode(scopeCode string) string {
parts := strings.SplitN(scopeCode, ":", 2)
if len(parts) < 1 {
return ""
}
prefix := parts[0]
switch prefix {
case "platform", "tenant", "billing":
return ScopeTypePlatform
case "supply":
return ScopeTypeSupply
case "consumer":
return ScopeTypeConsumer
case "router":
return ScopeTypeRouter
default:
return ""
}
}
// PredefinedScopes 预定义的Scope列表
var PredefinedScopes = []*Scope{
// Platform Scopes
{Code: "platform:read", Name: "读取平台配置", Type: ScopeTypePlatform},
{Code: "platform:write", Name: "修改平台配置", Type: ScopeTypePlatform},
{Code: "platform:admin", Name: "平台级管理", Type: ScopeTypePlatform},
{Code: "platform:audit:read", Name: "读取审计日志", Type: ScopeTypePlatform},
{Code: "platform:audit:export", Name: "导出审计日志", Type: ScopeTypePlatform},
// Tenant Scopes (属于platform类型)
{Code: "tenant:read", Name: "读取租户信息", Type: ScopeTypePlatform},
{Code: "tenant:write", Name: "修改租户配置", Type: ScopeTypePlatform},
{Code: "tenant:member:manage", Name: "管理租户成员", Type: ScopeTypePlatform},
{Code: "tenant:billing:write", Name: "修改账单设置", Type: ScopeTypePlatform},
// Supply Scopes
{Code: "supply:account:read", Name: "读取供应账号", Type: ScopeTypeSupply},
{Code: "supply:account:write", Name: "管理供应账号", Type: ScopeTypeSupply},
{Code: "supply:package:read", Name: "读取套餐信息", Type: ScopeTypeSupply},
{Code: "supply:package:write", Name: "管理套餐", Type: ScopeTypeSupply},
{Code: "supply:package:publish", Name: "发布套餐", Type: ScopeTypeSupply},
{Code: "supply:package:offline", Name: "下架套餐", Type: ScopeTypeSupply},
{Code: "supply:settlement:withdraw", Name: "提现", Type: ScopeTypeSupply},
{Code: "supply:credential:manage", Name: "管理凭证", Type: ScopeTypeSupply},
// Consumer Scopes
{Code: "consumer:account:read", Name: "读取账户信息", Type: ScopeTypeConsumer},
{Code: "consumer:account:write", Name: "管理账户", Type: ScopeTypeConsumer},
{Code: "consumer:apikey:create", Name: "创建API Key", Type: ScopeTypeConsumer},
{Code: "consumer:apikey:read", Name: "读取API Key", Type: ScopeTypeConsumer},
{Code: "consumer:apikey:revoke", Name: "吊销API Key", Type: ScopeTypeConsumer},
{Code: "consumer:usage:read", Name: "读取使用量", Type: ScopeTypeConsumer},
// Billing Scopes
{Code: "billing:read", Name: "读取账单", Type: ScopeTypeBilling},
{Code: "billing:write", Name: "修改账单设置", Type: ScopeTypeBilling},
// Router Scopes
{Code: "router:invoke", Name: "调用模型", Type: ScopeTypeRouter},
{Code: "router:model:list", Name: "列出可用模型", Type: ScopeTypeRouter},
{Code: "router:model:config", Name: "配置路由策略", Type: ScopeTypeRouter},
// Wildcard Scope
{Code: "*", Name: "通配符", Type: ScopeTypePlatform},
}
// GetPredefinedScopeByCode 根据Code获取预定义Scope
func GetPredefinedScopeByCode(code string) *Scope {
for _, scope := range PredefinedScopes {
if scope.Code == code {
return scope
}
}
return nil
}
// IsPredefinedScope 检查是否为预定义Scope
func IsPredefinedScope(code string) bool {
return GetPredefinedScopeByCode(code) != nil
}

View File

@@ -0,0 +1,247 @@
package model
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestScopeModel_NewScope_ValidInput 测试创建Scope - 有效输入
func TestScopeModel_NewScope_ValidInput(t *testing.T) {
// arrange
scopeCode := "platform:read"
scopeName := "读取平台配置"
scopeType := "platform"
// act
scope := NewScope(scopeCode, scopeName, scopeType)
// assert
assert.Equal(t, scopeCode, scope.Code)
assert.Equal(t, scopeName, scope.Name)
assert.Equal(t, scopeType, scope.Type)
assert.True(t, scope.IsActive)
assert.NotEmpty(t, scope.RequestID)
assert.Equal(t, 1, scope.Version)
}
// TestScopeModel_ScopeCategories 测试Scope分类
func TestScopeModel_ScopeCategories(t *testing.T) {
// arrange & act
testCases := []struct {
scopeCode string
expectedType string
}{
// platform:* 分类
{"platform:read", ScopeTypePlatform},
{"platform:write", ScopeTypePlatform},
{"platform:admin", ScopeTypePlatform},
{"platform:audit:read", ScopeTypePlatform},
{"platform:audit:export", ScopeTypePlatform},
// tenant:* 分类
{"tenant:read", ScopeTypePlatform},
{"tenant:write", ScopeTypePlatform},
{"tenant:member:manage", ScopeTypePlatform},
// supply:* 分类
{"supply:account:read", ScopeTypeSupply},
{"supply:account:write", ScopeTypeSupply},
{"supply:package:read", ScopeTypeSupply},
{"supply:package:write", ScopeTypeSupply},
// consumer:* 分类
{"consumer:account:read", ScopeTypeConsumer},
{"consumer:apikey:create", ScopeTypeConsumer},
// billing:* 分类
{"billing:read", ScopeTypePlatform},
// router:* 分类
{"router:invoke", ScopeTypeRouter},
{"router:model:list", ScopeTypeRouter},
}
// assert
for _, tc := range testCases {
scope := NewScope(tc.scopeCode, tc.scopeCode, tc.expectedType)
assert.Equal(t, tc.expectedType, scope.Type, "scope %s should be type %s", tc.scopeCode, tc.expectedType)
}
}
// TestScopeModel_NewScope_DefaultFields 测试创建Scope - 默认字段
func TestScopeModel_NewScope_DefaultFields(t *testing.T) {
// arrange
scopeCode := "tenant:read"
scopeName := "读取租户信息"
scopeType := ScopeTypePlatform
// act
scope := NewScope(scopeCode, scopeName, scopeType)
// assert - 验证默认字段
assert.Equal(t, 1, scope.Version, "version should default to 1")
assert.NotEmpty(t, scope.RequestID, "request_id should be auto-generated")
assert.True(t, scope.IsActive, "is_active should default to true")
}
// TestScopeModel_NewScope_WithRequestID 测试创建Scope - 指定RequestID
func TestScopeModel_NewScope_WithRequestID(t *testing.T) {
// arrange
requestID := "req-54321"
// act
scope := NewScopeWithRequestID("platform:read", "读取平台配置", ScopeTypePlatform, requestID)
// assert
assert.Equal(t, requestID, scope.RequestID)
}
// TestScopeModel_NewScope_AuditFields 测试创建Scope - 审计字段
func TestScopeModel_NewScope_AuditFields(t *testing.T) {
// arrange
createdIP := "10.0.0.1"
updatedIP := "10.0.0.2"
// act
scope := NewScopeWithAudit("billing:read", "读取账单", ScopeTypePlatform, "req-789", createdIP, updatedIP)
// assert
assert.Equal(t, createdIP, scope.CreatedIP)
assert.Equal(t, updatedIP, scope.UpdatedIP)
assert.Equal(t, 1, scope.Version)
}
// TestScopeModel_Activate 测试激活Scope
func TestScopeModel_Activate(t *testing.T) {
// arrange
scope := NewScope("test:scope", "测试Scope", ScopeTypePlatform)
scope.IsActive = false
// act
scope.Activate()
// assert
assert.True(t, scope.IsActive)
}
// TestScopeModel_Deactivate 测试停用Scope
func TestScopeModel_Deactivate(t *testing.T) {
// arrange
scope := NewScope("test:scope", "测试Scope", ScopeTypePlatform)
// act
scope.Deactivate()
// assert
assert.False(t, scope.IsActive)
}
// TestScopeModel_IncrementVersion 测试版本号递增
func TestScopeModel_IncrementVersion(t *testing.T) {
// arrange
scope := NewScope("test:scope", "测试Scope", ScopeTypePlatform)
originalVersion := scope.Version
// act
scope.IncrementVersion()
// assert
assert.Equal(t, originalVersion+1, scope.Version)
}
// TestScopeModel_ScopeType_Platform 测试平台Scope类型
func TestScopeModel_ScopeType_Platform(t *testing.T) {
// arrange & act
scope := NewScope("platform:admin", "平台管理", ScopeTypePlatform)
// assert
assert.Equal(t, ScopeTypePlatform, scope.Type)
}
// TestScopeModel_ScopeType_Supply 测试供应方Scope类型
func TestScopeModel_ScopeType_Supply(t *testing.T) {
// arrange & act
scope := NewScope("supply:account:write", "管理供应账号", ScopeTypeSupply)
// assert
assert.Equal(t, ScopeTypeSupply, scope.Type)
}
// TestScopeModel_ScopeType_Consumer 测试需求方Scope类型
func TestScopeModel_ScopeType_Consumer(t *testing.T) {
// arrange & act
scope := NewScope("consumer:apikey:create", "创建API Key", ScopeTypeConsumer)
// assert
assert.Equal(t, ScopeTypeConsumer, scope.Type)
}
// TestScopeModel_ScopeType_Router 测试路由Scope类型
func TestScopeModel_ScopeType_Router(t *testing.T) {
// arrange & act
scope := NewScope("router:invoke", "调用模型", ScopeTypeRouter)
// assert
assert.Equal(t, ScopeTypeRouter, scope.Type)
}
// TestScopeModel_NewScope_EmptyCode 测试创建Scope - 空Scope代码应返回错误
func TestScopeModel_NewScope_EmptyCode(t *testing.T) {
// arrange & act
scope, err := NewScopeWithValidation("", "测试Scope", ScopeTypePlatform)
// assert
assert.Error(t, err)
assert.Nil(t, scope)
assert.Equal(t, ErrInvalidScopeCode, err)
}
// TestScopeModel_NewScope_InvalidScopeType 测试创建Scope - 无效Scope类型
func TestScopeModel_NewScope_InvalidScopeType(t *testing.T) {
// arrange & act
scope, err := NewScopeWithValidation("test:scope", "测试Scope", "invalid_type")
// assert
assert.Error(t, err)
assert.Nil(t, scope)
assert.Equal(t, ErrInvalidScopeType, err)
}
// TestScopeModel_ToScopeInfo 测试Scope转换为ScopeInfo
func TestScopeModel_ToScopeInfo(t *testing.T) {
// arrange
scope := NewScope("platform:read", "读取平台配置", ScopeTypePlatform)
scope.ID = 1
// act
scopeInfo := scope.ToScopeInfo()
// assert
assert.Equal(t, "platform:read", scopeInfo.ScopeCode)
assert.Equal(t, "读取平台配置", scopeInfo.ScopeName)
assert.Equal(t, ScopeTypePlatform, scopeInfo.ScopeType)
assert.True(t, scopeInfo.IsActive)
}
// TestScopeModel_GetScopeTypeFromCode 测试从Scope Code推断类型
func TestScopeModel_GetScopeTypeFromCode(t *testing.T) {
// arrange & act & assert
assert.Equal(t, ScopeTypePlatform, GetScopeTypeFromCode("platform:read"))
assert.Equal(t, ScopeTypePlatform, GetScopeTypeFromCode("tenant:read"))
assert.Equal(t, ScopeTypeSupply, GetScopeTypeFromCode("supply:account:read"))
assert.Equal(t, ScopeTypeConsumer, GetScopeTypeFromCode("consumer:apikey:read"))
assert.Equal(t, ScopeTypeRouter, GetScopeTypeFromCode("router:invoke"))
assert.Equal(t, ScopeTypePlatform, GetScopeTypeFromCode("billing:read"))
}
// TestScopeModel_IsWildcardScope 测试通配符Scope
func TestScopeModel_IsWildcardScope(t *testing.T) {
// arrange
wildcardScope := NewScope("*", "通配符", ScopeTypePlatform)
normalScope := NewScope("platform:read", "读取平台配置", ScopeTypePlatform)
// assert
assert.True(t, wildcardScope.IsWildcard())
assert.False(t, normalScope.IsWildcard())
}

View File

@@ -0,0 +1,172 @@
package model
import (
"time"
)
// UserRoleMapping 用户-角色关联模型
// 对应数据库 iam_user_roles 表
type UserRoleMapping struct {
ID int64 // 主键ID
UserID int64 // 用户ID
RoleID int64 // 角色ID (FK -> iam_roles.id)
TenantID int64 // 租户范围NULL表示全局0也代表全局
GrantedBy int64 // 授权人ID
ExpiresAt *time.Time // 角色过期时间nil表示永不过期
IsActive bool // 是否激活
// 审计字段
RequestID string // 请求追踪ID
CreatedIP string // 创建者IP
UpdatedIP string // 更新者IP
Version int // 乐观锁版本号
// 时间戳
CreatedAt *time.Time // 创建时间
UpdatedAt *time.Time // 更新时间
GrantedAt *time.Time // 授权时间
}
// NewUserRoleMapping 创建新的用户-角色映射
func NewUserRoleMapping(userID, roleID, tenantID int64) *UserRoleMapping {
now := time.Now()
return &UserRoleMapping{
UserID: userID,
RoleID: roleID,
TenantID: tenantID,
IsActive: true,
RequestID: generateRequestID(),
Version: 1,
CreatedAt: &now,
UpdatedAt: &now,
}
}
// NewUserRoleMappingWithGrant 创建带授权信息的用户-角色映射
func NewUserRoleMappingWithGrant(userID, roleID, tenantID, grantedBy int64, expiresAt *time.Time) *UserRoleMapping {
now := time.Now()
return &UserRoleMapping{
UserID: userID,
RoleID: roleID,
TenantID: tenantID,
GrantedBy: grantedBy,
ExpiresAt: expiresAt,
GrantedAt: &now,
IsActive: true,
RequestID: generateRequestID(),
Version: 1,
CreatedAt: &now,
UpdatedAt: &now,
}
}
// HasRole 检查用户是否拥有指定角色
func (m *UserRoleMapping) HasRole(roleID int64) bool {
return m.RoleID == roleID && m.IsActive
}
// IsGlobalRole 检查是否为全局角色租户ID为0或nil
func (m *UserRoleMapping) IsGlobalRole() bool {
return m.TenantID == 0
}
// IsExpired 检查角色是否已过期
func (m *UserRoleMapping) IsExpired() bool {
if m.ExpiresAt == nil {
return false // 永不过期
}
return time.Now().After(*m.ExpiresAt)
}
// IsValid 检查角色分配是否有效(激活且未过期)
func (m *UserRoleMapping) IsValid() bool {
return m.IsActive && !m.IsExpired()
}
// Revoke 撤销角色分配
func (m *UserRoleMapping) Revoke() {
m.IsActive = false
m.UpdatedAt = nowPtr()
}
// Grant 重新授予角色
func (m *UserRoleMapping) Grant() {
m.IsActive = true
m.UpdatedAt = nowPtr()
}
// IncrementVersion 递增版本号
func (m *UserRoleMapping) IncrementVersion() {
m.Version++
m.UpdatedAt = nowPtr()
}
// ExtendExpiration 延长过期时间
func (m *UserRoleMapping) ExtendExpiration(newExpiresAt *time.Time) {
m.ExpiresAt = newExpiresAt
m.UpdatedAt = nowPtr()
}
// UserRoleMappingInfo 用户-角色映射信息用于API响应
type UserRoleMappingInfo struct {
UserID int64 `json:"user_id"`
RoleID int64 `json:"role_id"`
TenantID int64 `json:"tenant_id"`
IsActive bool `json:"is_active"`
ExpiresAt *string `json:"expires_at,omitempty"`
}
// ToInfo 转换为映射信息
func (m *UserRoleMapping) ToInfo() *UserRoleMappingInfo {
info := &UserRoleMappingInfo{
UserID: m.UserID,
RoleID: m.RoleID,
TenantID: m.TenantID,
IsActive: m.IsActive,
}
if m.ExpiresAt != nil {
expStr := m.ExpiresAt.Format(time.RFC3339)
info.ExpiresAt = &expStr
}
return info
}
// UserRoleAssignmentInfo 用户角色分配详情用于API响应
type UserRoleAssignmentInfo struct {
UserID int64 `json:"user_id"`
RoleCode string `json:"role_code"`
RoleName string `json:"role_name"`
TenantID int64 `json:"tenant_id"`
GrantedBy int64 `json:"granted_by"`
GrantedAt string `json:"granted_at"`
ExpiresAt string `json:"expires_at,omitempty"`
IsActive bool `json:"is_active"`
IsExpired bool `json:"is_expired"`
}
// UserRoleWithDetails 用户角色分配(含角色详情)
type UserRoleWithDetails struct {
*UserRoleMapping
RoleCode string
RoleName string
}
// ToAssignmentInfo 转换为分配详情
func (m *UserRoleWithDetails) ToAssignmentInfo() *UserRoleAssignmentInfo {
info := &UserRoleAssignmentInfo{
UserID: m.UserID,
RoleCode: m.RoleCode,
RoleName: m.RoleName,
TenantID: m.TenantID,
GrantedBy: m.GrantedBy,
IsActive: m.IsActive,
IsExpired: m.IsExpired(),
}
if m.GrantedAt != nil {
info.GrantedAt = m.GrantedAt.Format(time.RFC3339)
}
if m.ExpiresAt != nil {
info.ExpiresAt = m.ExpiresAt.Format(time.RFC3339)
}
return info
}

View File

@@ -0,0 +1,254 @@
package model
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestUserRoleMapping_AssignRole 测试分配角色
func TestUserRoleMapping_AssignRole(t *testing.T) {
// arrange
userID := int64(100)
roleID := int64(1)
tenantID := int64(1)
// act
userRole := NewUserRoleMapping(userID, roleID, tenantID)
// assert
assert.Equal(t, userID, userRole.UserID)
assert.Equal(t, roleID, userRole.RoleID)
assert.Equal(t, tenantID, userRole.TenantID)
assert.True(t, userRole.IsActive)
assert.NotEmpty(t, userRole.RequestID)
assert.Equal(t, 1, userRole.Version)
}
// TestUserRoleMapping_HasRole 测试用户是否拥有角色
func TestUserRoleMapping_HasRole(t *testing.T) {
// arrange
userID := int64(100)
role := NewRole("org_admin", "组织管理员", RoleTypePlatform, 50)
role.ID = 1
// act
userRole := NewUserRoleMapping(userID, role.ID, 0) // 0 表示全局角色
// assert
assert.True(t, userRole.HasRole(role.ID))
assert.False(t, userRole.HasRole(999)) // 不存在的角色ID
}
// TestUserRoleMapping_GlobalRole 测试全局角色tenantID为0
func TestUserRoleMapping_GlobalRole(t *testing.T) {
// arrange
userID := int64(100)
roleID := int64(1)
// act - 全局角色
userRole := NewUserRoleMapping(userID, roleID, 0)
// assert
assert.Equal(t, int64(0), userRole.TenantID)
assert.True(t, userRole.IsGlobalRole())
}
// TestUserRoleMapping_TenantRole 测试租户角色
func TestUserRoleMapping_TenantRole(t *testing.T) {
// arrange
userID := int64(100)
roleID := int64(1)
tenantID := int64(123)
// act
userRole := NewUserRoleMapping(userID, roleID, tenantID)
// assert
assert.Equal(t, tenantID, userRole.TenantID)
assert.False(t, userRole.IsGlobalRole())
}
// TestUserRoleMapping_WithGrantInfo 测试带授权信息的分配
func TestUserRoleMapping_WithGrantInfo(t *testing.T) {
// arrange
userID := int64(100)
roleID := int64(1)
tenantID := int64(1)
grantedBy := int64(1)
expiresAt := time.Now().Add(24 * time.Hour)
// act
userRole := NewUserRoleMappingWithGrant(userID, roleID, tenantID, grantedBy, &expiresAt)
// assert
assert.Equal(t, userID, userRole.UserID)
assert.Equal(t, roleID, userRole.RoleID)
assert.Equal(t, grantedBy, userRole.GrantedBy)
assert.NotNil(t, userRole.ExpiresAt)
assert.NotNil(t, userRole.GrantedAt)
}
// TestUserRoleMapping_Expired 测试过期角色
func TestUserRoleMapping_Expired(t *testing.T) {
// arrange
userID := int64(100)
roleID := int64(1)
expiresAt := time.Now().Add(-1 * time.Hour) // 已过期
// act
userRole := NewUserRoleMappingWithGrant(userID, roleID, 0, 1, &expiresAt)
// assert
assert.True(t, userRole.IsExpired())
}
// TestUserRoleMapping_NotExpired 测试未过期角色
func TestUserRoleMapping_NotExpired(t *testing.T) {
// arrange
userID := int64(100)
roleID := int64(1)
expiresAt := time.Now().Add(24 * time.Hour) // 未过期
// act
userRole := NewUserRoleMappingWithGrant(userID, roleID, 0, 1, &expiresAt)
// assert
assert.False(t, userRole.IsExpired())
}
// TestUserRoleMapping_NoExpiration 测试永不过期角色
func TestUserRoleMapping_NoExpiration(t *testing.T) {
// arrange
userID := int64(100)
roleID := int64(1)
// act
userRole := NewUserRoleMapping(userID, roleID, 0)
// assert
assert.Nil(t, userRole.ExpiresAt)
assert.False(t, userRole.IsExpired())
}
// TestUserRoleMapping_Revoke 测试撤销角色
func TestUserRoleMapping_Revoke(t *testing.T) {
// arrange
userRole := NewUserRoleMapping(100, 1, 0)
// act
userRole.Revoke()
// assert
assert.False(t, userRole.IsActive)
}
// TestUserRoleMapping_Grant 测试重新授予角色
func TestUserRoleMapping_Grant(t *testing.T) {
// arrange
userRole := NewUserRoleMapping(100, 1, 0)
userRole.Revoke()
// act
userRole.Grant()
// assert
assert.True(t, userRole.IsActive)
}
// TestUserRoleMapping_IncrementVersion 测试版本号递增
func TestUserRoleMapping_IncrementVersion(t *testing.T) {
// arrange
userRole := NewUserRoleMapping(100, 1, 0)
originalVersion := userRole.Version
// act
userRole.IncrementVersion()
// assert
assert.Equal(t, originalVersion+1, userRole.Version)
}
// TestUserRoleMapping_Valid 测试有效角色
func TestUserRoleMapping_Valid(t *testing.T) {
// arrange - 活跃且未过期的角色
userRole := NewUserRoleMapping(100, 1, 0)
expiresAt := time.Now().Add(24 * time.Hour)
userRole.ExpiresAt = &expiresAt
// act & assert
assert.True(t, userRole.IsValid())
}
// TestUserRoleMapping_InvalidInactive 测试无效角色 - 未激活
func TestUserRoleMapping_InvalidInactive(t *testing.T) {
// arrange
userRole := NewUserRoleMapping(100, 1, 0)
userRole.Revoke()
// assert
assert.False(t, userRole.IsValid())
}
// TestUserRoleMapping_Valid_ExpiredButActive 测试过期但激活的角色
func TestUserRoleMapping_Valid_ExpiredButActive(t *testing.T) {
// arrange - 已过期但仍然激活的角色(应该无效)
userRole := NewUserRoleMapping(100, 1, 0)
expiresAt := time.Now().Add(-1 * time.Hour)
userRole.ExpiresAt = &expiresAt
// assert - 即使IsActive为true过期角色也应该无效
assert.False(t, userRole.IsValid())
}
// TestUserRoleMapping_UniqueConstraint 测试唯一性约束
func TestUserRoleMapping_UniqueConstraint(t *testing.T) {
// arrange
userID := int64(100)
roleID := int64(1)
tenantID := int64(0) // 全局角色
// act
userRole1 := NewUserRoleMapping(userID, roleID, tenantID)
userRole2 := NewUserRoleMapping(userID, roleID, tenantID)
// assert - 同一个用户、角色、租户组合应该唯一
assert.Equal(t, userRole1.UserID, userRole2.UserID)
assert.Equal(t, userRole1.RoleID, userRole2.RoleID)
assert.Equal(t, userRole1.TenantID, userRole2.TenantID)
}
// TestUserRoleMapping_DifferentTenants 测试不同租户可以有相同角色
func TestUserRoleMapping_DifferentTenants(t *testing.T) {
// arrange
userID := int64(100)
roleID := int64(1)
tenantID1 := int64(1)
tenantID2 := int64(2)
// act
userRole1 := NewUserRoleMapping(userID, roleID, tenantID1)
userRole2 := NewUserRoleMapping(userID, roleID, tenantID2)
// assert - 不同租户的角色分配互不影响
assert.Equal(t, tenantID1, userRole1.TenantID)
assert.Equal(t, tenantID2, userRole2.TenantID)
assert.NotEqual(t, userRole1.TenantID, userRole2.TenantID)
}
// TestUserRoleMappingInfo_ToInfo 测试转换为UserRoleMappingInfo
func TestUserRoleMappingInfo_ToInfo(t *testing.T) {
// arrange
userRole := NewUserRoleMapping(100, 1, 0)
userRole.ID = 1
// act
info := userRole.ToInfo()
// assert
assert.Equal(t, int64(100), info.UserID)
assert.Equal(t, int64(1), info.RoleID)
assert.Equal(t, int64(0), info.TenantID)
assert.True(t, info.IsActive)
}

View File

@@ -0,0 +1,291 @@
package service
import (
"context"
"errors"
"time"
)
// 错误定义
var (
ErrRoleNotFound = errors.New("role not found")
ErrDuplicateRoleCode = errors.New("role code already exists")
ErrDuplicateAssignment = errors.New("user already has this role")
ErrInvalidRequest = errors.New("invalid request")
)
// Role 角色(简化的服务层模型)
type Role struct {
Code string
Name string
Type string
Level int
Description string
IsActive bool
Version int
CreatedAt time.Time
UpdatedAt time.Time
}
// UserRole 用户角色(简化的服务层模型)
type UserRole struct {
UserID int64
RoleCode string
TenantID int64
IsActive bool
ExpiresAt *time.Time
}
// CreateRoleRequest 创建角色请求
type CreateRoleRequest struct {
Code string
Name string
Type string
Level int
Description string
Scopes []string
ParentCode string
}
// UpdateRoleRequest 更新角色请求
type UpdateRoleRequest struct {
Code string
Name string
Description string
Scopes []string
IsActive *bool
}
// AssignRoleRequest 分配角色请求
type AssignRoleRequest struct {
UserID int64
RoleCode string
TenantID int64
GrantedBy int64
ExpiresAt *time.Time
}
// IAMServiceInterface IAM服务接口
type IAMServiceInterface interface {
CreateRole(ctx context.Context, req *CreateRoleRequest) (*Role, error)
GetRole(ctx context.Context, roleCode string) (*Role, error)
UpdateRole(ctx context.Context, req *UpdateRoleRequest) (*Role, error)
DeleteRole(ctx context.Context, roleCode string) error
ListRoles(ctx context.Context, roleType string) ([]*Role, error)
AssignRole(ctx context.Context, req *AssignRoleRequest) (*UserRole, error)
RevokeRole(ctx context.Context, userID int64, roleCode string, tenantID int64) error
GetUserRoles(ctx context.Context, userID int64) ([]*UserRole, error)
CheckScope(ctx context.Context, userID int64, requiredScope string) (bool, error)
GetUserScopes(ctx context.Context, userID int64) ([]string, error)
}
// DefaultIAMService 默认IAM服务实现
type DefaultIAMService struct {
// 角色存储
roleStore map[string]*Role
// 用户角色存储: userID -> []*UserRole
userRoleStore map[int64][]*UserRole
// 角色Scope存储: roleCode -> []scopeCode
roleScopeStore map[string][]string
}
// NewDefaultIAMService 创建默认IAM服务
func NewDefaultIAMService() *DefaultIAMService {
return &DefaultIAMService{
roleStore: make(map[string]*Role),
userRoleStore: make(map[int64][]*UserRole),
roleScopeStore: make(map[string][]string),
}
}
// CreateRole 创建角色
func (s *DefaultIAMService) CreateRole(ctx context.Context, req *CreateRoleRequest) (*Role, error) {
// 检查是否重复
if _, exists := s.roleStore[req.Code]; exists {
return nil, ErrDuplicateRoleCode
}
// 验证角色类型
if req.Type != "platform" && req.Type != "supply" && req.Type != "consumer" {
return nil, ErrInvalidRequest
}
now := time.Now()
role := &Role{
Code: req.Code,
Name: req.Name,
Type: req.Type,
Level: req.Level,
Description: req.Description,
IsActive: true,
Version: 1,
CreatedAt: now,
UpdatedAt: now,
}
// 存储角色
s.roleStore[req.Code] = role
// 存储角色Scope关联
if len(req.Scopes) > 0 {
s.roleScopeStore[req.Code] = req.Scopes
}
return role, nil
}
// GetRole 获取角色
func (s *DefaultIAMService) GetRole(ctx context.Context, roleCode string) (*Role, error) {
role, exists := s.roleStore[roleCode]
if !exists {
return nil, ErrRoleNotFound
}
return role, nil
}
// UpdateRole 更新角色
func (s *DefaultIAMService) UpdateRole(ctx context.Context, req *UpdateRoleRequest) (*Role, error) {
role, exists := s.roleStore[req.Code]
if !exists {
return nil, ErrRoleNotFound
}
// 更新字段
if req.Name != "" {
role.Name = req.Name
}
if req.Description != "" {
role.Description = req.Description
}
if req.Scopes != nil {
s.roleScopeStore[req.Code] = req.Scopes
}
if req.IsActive != nil {
role.IsActive = *req.IsActive
}
// 递增版本
role.Version++
role.UpdatedAt = time.Now()
return role, nil
}
// DeleteRole 删除角色(软删除)
func (s *DefaultIAMService) DeleteRole(ctx context.Context, roleCode string) error {
role, exists := s.roleStore[roleCode]
if !exists {
return ErrRoleNotFound
}
role.IsActive = false
role.UpdatedAt = time.Now()
return nil
}
// ListRoles 列出角色
func (s *DefaultIAMService) ListRoles(ctx context.Context, roleType string) ([]*Role, error) {
var roles []*Role
for _, role := range s.roleStore {
if roleType == "" || role.Type == roleType {
roles = append(roles, role)
}
}
return roles, nil
}
// AssignRole 分配角色
func (s *DefaultIAMService) AssignRole(ctx context.Context, req *AssignRoleRequest) (*UserRole, error) {
// 检查角色是否存在
if _, exists := s.roleStore[req.RoleCode]; !exists {
return nil, ErrRoleNotFound
}
// 检查是否已分配
for _, ur := range s.userRoleStore[req.UserID] {
if ur.RoleCode == req.RoleCode && ur.TenantID == req.TenantID && ur.IsActive {
return nil, ErrDuplicateAssignment
}
}
userRole := &UserRole{
UserID: req.UserID,
RoleCode: req.RoleCode,
TenantID: req.TenantID,
IsActive: true,
ExpiresAt: req.ExpiresAt,
}
// 存储映射
s.userRoleStore[req.UserID] = append(s.userRoleStore[req.UserID], userRole)
return userRole, nil
}
// RevokeRole 撤销角色
func (s *DefaultIAMService) RevokeRole(ctx context.Context, userID int64, roleCode string, tenantID int64) error {
for _, ur := range s.userRoleStore[userID] {
if ur.RoleCode == roleCode && ur.TenantID == tenantID {
ur.IsActive = false
return nil
}
}
return ErrRoleNotFound
}
// GetUserRoles 获取用户角色
func (s *DefaultIAMService) GetUserRoles(ctx context.Context, userID int64) ([]*UserRole, error) {
var userRoles []*UserRole
for _, ur := range s.userRoleStore[userID] {
if ur.IsActive {
userRoles = append(userRoles, ur)
}
}
return userRoles, nil
}
// CheckScope 检查用户是否有指定Scope
func (s *DefaultIAMService) CheckScope(ctx context.Context, userID int64, requiredScope string) (bool, error) {
scopes, err := s.GetUserScopes(ctx, userID)
if err != nil {
return false, err
}
for _, scope := range scopes {
if scope == requiredScope || scope == "*" {
return true, nil
}
}
return false, nil
}
// GetUserScopes 获取用户所有Scope
func (s *DefaultIAMService) GetUserScopes(ctx context.Context, userID int64) ([]string, error) {
var allScopes []string
seen := make(map[string]bool)
for _, ur := range s.userRoleStore[userID] {
if ur.IsActive && (ur.ExpiresAt == nil || ur.ExpiresAt.After(time.Now())) {
if scopes, exists := s.roleScopeStore[ur.RoleCode]; exists {
for _, scope := range scopes {
if !seen[scope] {
seen[scope] = true
allScopes = append(allScopes, scope)
}
}
}
}
}
return allScopes, nil
}
// IsExpired 检查用户角色是否过期
func (ur *UserRole) IsExpired() bool {
if ur.ExpiresAt == nil {
return false
}
return time.Now().After(*ur.ExpiresAt)
}

View File

@@ -0,0 +1,432 @@
package service
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// MockIAMService 模拟IAM服务用于测试
type MockIAMService struct {
roles map[string]*Role
userRoles map[int64][]*UserRole
roleScopes map[string][]string
}
func NewMockIAMService() *MockIAMService {
return &MockIAMService{
roles: make(map[string]*Role),
userRoles: make(map[int64][]*UserRole),
roleScopes: make(map[string][]string),
}
}
func (m *MockIAMService) CreateRole(ctx context.Context, req *CreateRoleRequest) (*Role, error) {
if _, exists := m.roles[req.Code]; exists {
return nil, ErrDuplicateRoleCode
}
role := &Role{
Code: req.Code,
Name: req.Name,
Type: req.Type,
Level: req.Level,
IsActive: true,
Version: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
m.roles[req.Code] = role
if len(req.Scopes) > 0 {
m.roleScopes[req.Code] = req.Scopes
}
return role, nil
}
func (m *MockIAMService) GetRole(ctx context.Context, roleCode string) (*Role, error) {
if role, exists := m.roles[roleCode]; exists {
return role, nil
}
return nil, ErrRoleNotFound
}
func (m *MockIAMService) UpdateRole(ctx context.Context, req *UpdateRoleRequest) (*Role, error) {
role, exists := m.roles[req.Code]
if !exists {
return nil, ErrRoleNotFound
}
if req.Name != "" {
role.Name = req.Name
}
if req.Description != "" {
role.Description = req.Description
}
if req.Scopes != nil {
m.roleScopes[req.Code] = req.Scopes
}
role.Version++
role.UpdatedAt = time.Now()
return role, nil
}
func (m *MockIAMService) DeleteRole(ctx context.Context, roleCode string) error {
role, exists := m.roles[roleCode]
if !exists {
return ErrRoleNotFound
}
role.IsActive = false
role.UpdatedAt = time.Now()
return nil
}
func (m *MockIAMService) ListRoles(ctx context.Context, roleType string) ([]*Role, error) {
var roles []*Role
for _, role := range m.roles {
if roleType == "" || role.Type == roleType {
roles = append(roles, role)
}
}
return roles, nil
}
func (m *MockIAMService) AssignRole(ctx context.Context, req *AssignRoleRequest) (*modelUserRoleMapping, error) {
for _, ur := range m.userRoles[req.UserID] {
if ur.RoleCode == req.RoleCode && ur.TenantID == req.TenantID && ur.IsActive {
return nil, ErrDuplicateAssignment
}
}
mapping := &modelUserRoleMapping{
UserID: req.UserID,
RoleCode: req.RoleCode,
TenantID: req.TenantID,
IsActive: true,
}
m.userRoles[req.UserID] = append(m.userRoles[req.UserID], &UserRole{
UserID: req.UserID,
RoleCode: req.RoleCode,
TenantID: req.TenantID,
IsActive: true,
})
return mapping, nil
}
func (m *MockIAMService) RevokeRole(ctx context.Context, userID int64, roleCode string, tenantID int64) error {
for _, ur := range m.userRoles[userID] {
if ur.RoleCode == roleCode && ur.TenantID == tenantID {
ur.IsActive = false
return nil
}
}
return ErrRoleNotFound
}
func (m *MockIAMService) GetUserRoles(ctx context.Context, userID int64) ([]*UserRole, error) {
var userRoles []*UserRole
for _, ur := range m.userRoles[userID] {
if ur.IsActive {
userRoles = append(userRoles, ur)
}
}
return userRoles, nil
}
func (m *MockIAMService) CheckScope(ctx context.Context, userID int64, requiredScope string) (bool, error) {
scopes, err := m.GetUserScopes(ctx, userID)
if err != nil {
return false, err
}
for _, scope := range scopes {
if scope == requiredScope || scope == "*" {
return true, nil
}
}
return false, nil
}
func (m *MockIAMService) GetUserScopes(ctx context.Context, userID int64) ([]string, error) {
var allScopes []string
seen := make(map[string]bool)
for _, ur := range m.userRoles[userID] {
if ur.IsActive {
if scopes, exists := m.roleScopes[ur.RoleCode]; exists {
for _, scope := range scopes {
if !seen[scope] {
seen[scope] = true
allScopes = append(allScopes, scope)
}
}
}
}
}
return allScopes, nil
}
// modelUserRoleMapping 简化的用户角色映射(用于测试)
type modelUserRoleMapping struct {
UserID int64
RoleCode string
TenantID int64
IsActive bool
}
// TestIAMService_CreateRole_Success 测试创建角色成功
func TestIAMService_CreateRole_Success(t *testing.T) {
// arrange
mockService := NewMockIAMService()
req := &CreateRoleRequest{
Code: "developer",
Name: "开发者",
Type: "platform",
Level: 20,
Scopes: []string{"platform:read", "router:invoke"},
}
// act
role, err := mockService.CreateRole(context.Background(), req)
// assert
assert.NoError(t, err)
assert.NotNil(t, role)
assert.Equal(t, "developer", role.Code)
assert.Equal(t, "开发者", role.Name)
assert.Equal(t, "platform", role.Type)
assert.Equal(t, 20, role.Level)
assert.True(t, role.IsActive)
}
// TestIAMService_CreateRole_DuplicateName 测试创建重复角色
func TestIAMService_CreateRole_DuplicateName(t *testing.T) {
// arrange
mockService := NewMockIAMService()
mockService.roles["developer"] = &Role{Code: "developer", Name: "开发者", Type: "platform", Level: 20}
req := &CreateRoleRequest{
Code: "developer",
Name: "开发者",
Type: "platform",
Level: 20,
}
// act
role, err := mockService.CreateRole(context.Background(), req)
// assert
assert.Error(t, err)
assert.Nil(t, role)
assert.Equal(t, ErrDuplicateRoleCode, err)
}
// TestIAMService_UpdateRole_Success 测试更新角色成功
func TestIAMService_UpdateRole_Success(t *testing.T) {
// arrange
mockService := NewMockIAMService()
existingRole := &Role{
Code: "developer",
Name: "开发者",
Type: "platform",
Level: 20,
IsActive: true,
Version: 1,
}
mockService.roles["developer"] = existingRole
req := &UpdateRoleRequest{
Code: "developer",
Name: "AI开发者",
Description: "AI应用开发者",
}
// act
updatedRole, err := mockService.UpdateRole(context.Background(), req)
// assert
assert.NoError(t, err)
assert.NotNil(t, updatedRole)
assert.Equal(t, "AI开发者", updatedRole.Name)
assert.Equal(t, "AI应用开发者", updatedRole.Description)
assert.Equal(t, 2, updatedRole.Version) // version 应该递增
}
// TestIAMService_UpdateRole_NotFound 测试更新不存在的角色
func TestIAMService_UpdateRole_NotFound(t *testing.T) {
// arrange
mockService := NewMockIAMService()
req := &UpdateRoleRequest{
Code: "nonexistent",
Name: "不存在",
}
// act
role, err := mockService.UpdateRole(context.Background(), req)
// assert
assert.Error(t, err)
assert.Nil(t, role)
assert.Equal(t, ErrRoleNotFound, err)
}
// TestIAMService_DeleteRole_Success 测试删除角色成功
func TestIAMService_DeleteRole_Success(t *testing.T) {
// arrange
mockService := NewMockIAMService()
mockService.roles["developer"] = &Role{Code: "developer", Name: "开发者", IsActive: true}
// act
err := mockService.DeleteRole(context.Background(), "developer")
// assert
assert.NoError(t, err)
assert.False(t, mockService.roles["developer"].IsActive) // 应该被停用而不是删除
}
// TestIAMService_ListRoles 测试列出角色
func TestIAMService_ListRoles(t *testing.T) {
// arrange
mockService := NewMockIAMService()
mockService.roles["viewer"] = &Role{Code: "viewer", Type: "platform", Level: 10}
mockService.roles["operator"] = &Role{Code: "operator", Type: "platform", Level: 30}
mockService.roles["supply_admin"] = &Role{Code: "supply_admin", Type: "supply", Level: 40}
// act
platformRoles, err := mockService.ListRoles(context.Background(), "platform")
supplyRoles, err2 := mockService.ListRoles(context.Background(), "supply")
allRoles, err3 := mockService.ListRoles(context.Background(), "")
// assert
assert.NoError(t, err)
assert.Len(t, platformRoles, 2)
assert.NoError(t, err2)
assert.Len(t, supplyRoles, 1)
assert.NoError(t, err3)
assert.Len(t, allRoles, 3)
}
// TestIAMService_AssignRole 测试分配角色
func TestIAMService_AssignRole(t *testing.T) {
// arrange
mockService := NewMockIAMService()
mockService.roles["viewer"] = &Role{Code: "viewer", Type: "platform", Level: 10}
req := &AssignRoleRequest{
UserID: 100,
RoleCode: "viewer",
TenantID: 1,
}
// act
mapping, err := mockService.AssignRole(context.Background(), req)
// assert
assert.NoError(t, err)
assert.NotNil(t, mapping)
assert.Equal(t, int64(100), mapping.UserID)
assert.Equal(t, "viewer", mapping.RoleCode)
assert.True(t, mapping.IsActive)
}
// TestIAMService_AssignRole_Duplicate 测试重复分配角色
func TestIAMService_AssignRole_Duplicate(t *testing.T) {
// arrange
mockService := NewMockIAMService()
mockService.roles["viewer"] = &Role{Code: "viewer", Type: "platform", Level: 10}
mockService.userRoles[100] = []*UserRole{
{UserID: 100, RoleCode: "viewer", TenantID: 1, IsActive: true},
}
req := &AssignRoleRequest{
UserID: 100,
RoleCode: "viewer",
TenantID: 1,
}
// act
mapping, err := mockService.AssignRole(context.Background(), req)
// assert
assert.Error(t, err)
assert.Nil(t, mapping)
assert.Equal(t, ErrDuplicateAssignment, err)
}
// TestIAMService_RevokeRole 测试撤销角色
func TestIAMService_RevokeRole(t *testing.T) {
// arrange
mockService := NewMockIAMService()
mockService.userRoles[100] = []*UserRole{
{UserID: 100, RoleCode: "viewer", TenantID: 1, IsActive: true},
}
// act
err := mockService.RevokeRole(context.Background(), 100, "viewer", 1)
// assert
assert.NoError(t, err)
assert.False(t, mockService.userRoles[100][0].IsActive)
}
// TestIAMService_GetUserRoles 测试获取用户角色
func TestIAMService_GetUserRoles(t *testing.T) {
// arrange
mockService := NewMockIAMService()
mockService.userRoles[100] = []*UserRole{
{UserID: 100, RoleCode: "viewer", TenantID: 0, IsActive: true},
{UserID: 100, RoleCode: "developer", TenantID: 1, IsActive: true},
}
// act
roles, err := mockService.GetUserRoles(context.Background(), 100)
// assert
assert.NoError(t, err)
assert.Len(t, roles, 2)
}
// TestIAMService_CheckScope 测试检查用户Scope
func TestIAMService_CheckScope(t *testing.T) {
// arrange
mockService := NewMockIAMService()
mockService.roles["viewer"] = &Role{Code: "viewer", Type: "platform", Level: 10}
mockService.roleScopes["viewer"] = []string{"platform:read", "tenant:read"}
mockService.userRoles[100] = []*UserRole{
{UserID: 100, RoleCode: "viewer", TenantID: 0, IsActive: true},
}
// act
hasScope, err := mockService.CheckScope(context.Background(), 100, "platform:read")
noScope, err2 := mockService.CheckScope(context.Background(), 100, "platform:write")
// assert
assert.NoError(t, err)
assert.True(t, hasScope)
assert.NoError(t, err2)
assert.False(t, noScope)
}
// TestIAMService_GetUserScopes 测试获取用户所有Scope
func TestIAMService_GetUserScopes(t *testing.T) {
// arrange
mockService := NewMockIAMService()
mockService.roles["viewer"] = &Role{Code: "viewer", Type: "platform", Level: 10}
mockService.roles["developer"] = &Role{Code: "developer", Type: "platform", Level: 20}
mockService.roleScopes["viewer"] = []string{"platform:read", "tenant:read"}
mockService.roleScopes["developer"] = []string{"router:invoke", "router:model:list"}
mockService.userRoles[100] = []*UserRole{
{UserID: 100, RoleCode: "viewer", TenantID: 0, IsActive: true},
{UserID: 100, RoleCode: "developer", TenantID: 0, IsActive: true},
}
// act
scopes, err := mockService.GetUserScopes(context.Background(), 100)
// assert
assert.NoError(t, err)
assert.Contains(t, scopes, "platform:read")
assert.Contains(t, scopes, "tenant:read")
assert.Contains(t, scopes, "router:invoke")
assert.Contains(t, scopes, "router:model:list")
}