feat(audit): 实现Audit HTTP Handler并提升IAM Middleware覆盖率
1. 新增Audit HTTP Handler (AUD-05, AUD-06完成) - POST /api/v1/audit/events - 创建审计事件(支持幂等) - GET /api/v1/audit/events - 查询事件列表(支持分页和过滤) 2. 提升IAM Middleware测试覆盖率 - 从63.8%提升至83.5% - 新增SetRouteScopePolicy测试 - 新增RequireRole/RequireMinLevel中间件测试 - 新增hasAnyScope测试 TDD完成:33/33任务 (100%)
This commit is contained in:
183
supply-api/internal/audit/handler/audit_handler.go
Normal file
183
supply-api/internal/audit/handler/audit_handler.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"lijiaoqiao/supply-api/internal/audit/model"
|
||||||
|
"lijiaoqiao/supply-api/internal/audit/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuditHandler HTTP处理器
|
||||||
|
type AuditHandler struct {
|
||||||
|
svc *service.AuditService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuditHandler 创建审计处理器
|
||||||
|
func NewAuditHandler(svc *service.AuditService) *AuditHandler {
|
||||||
|
return &AuditHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEventRequest 创建事件请求
|
||||||
|
type CreateEventRequest struct {
|
||||||
|
EventName string `json:"event_name"`
|
||||||
|
EventCategory string `json:"event_category"`
|
||||||
|
EventSubCategory string `json:"event_sub_category"`
|
||||||
|
OperatorID int64 `json:"operator_id"`
|
||||||
|
TenantID int64 `json:"tenant_id"`
|
||||||
|
ObjectType string `json:"object_type"`
|
||||||
|
ObjectID int64 `json:"object_id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||||
|
SourceIP string `json:"source_ip,omitempty"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ResultCode string `json:"result_code,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorResponse 错误响应
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEventsResponse 事件列表响应
|
||||||
|
type ListEventsResponse struct {
|
||||||
|
Events []*model.AuditEvent `json:"events"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEvent 处理POST /api/v1/audit/events
|
||||||
|
// @Summary 创建审计事件
|
||||||
|
// @Description 创建新的审计事件,支持幂等
|
||||||
|
// @Tags audit
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param event body CreateEventRequest true "事件信息"
|
||||||
|
// @Success 201 {object} service.CreateEventResult
|
||||||
|
// @Success 200 {object} service.CreateEventResult "幂等重复"
|
||||||
|
// @Success 409 {object} service.CreateEventResult "幂等冲突"
|
||||||
|
// @Failure 400 {object} ErrorResponse
|
||||||
|
// @Failure 500 {object} ErrorResponse
|
||||||
|
// @Router /api/v1/audit/events [post]
|
||||||
|
func (h *AuditHandler) CreateEvent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req CreateEventRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "INVALID_REQUEST", "invalid request body: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if req.EventName == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "MISSING_FIELD", "event_name is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.EventCategory == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "MISSING_FIELD", "event_category is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event := &model.AuditEvent{
|
||||||
|
EventName: req.EventName,
|
||||||
|
EventCategory: req.EventCategory,
|
||||||
|
EventSubCategory: req.EventSubCategory,
|
||||||
|
OperatorID: req.OperatorID,
|
||||||
|
TenantID: req.TenantID,
|
||||||
|
ObjectType: req.ObjectType,
|
||||||
|
ObjectID: req.ObjectID,
|
||||||
|
Action: req.Action,
|
||||||
|
IdempotencyKey: req.IdempotencyKey,
|
||||||
|
SourceIP: req.SourceIP,
|
||||||
|
Success: req.Success,
|
||||||
|
ResultCode: req.ResultCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.svc.CreateEvent(r.Context(), event)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "CREATE_FAILED", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(result.StatusCode)
|
||||||
|
json.NewEncoder(w).Encode(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEvents 处理GET /api/v1/audit/events
|
||||||
|
// @Summary 查询审计事件
|
||||||
|
// @Description 查询审计事件列表,支持分页和过滤
|
||||||
|
// @Tags audit
|
||||||
|
// @Produce json
|
||||||
|
// @Param tenant_id query int false "租户ID"
|
||||||
|
// @Param category query string false "事件类别"
|
||||||
|
// @Param event_name query string false "事件名称"
|
||||||
|
// @Param offset query int false "偏移量" default(0)
|
||||||
|
// @Param limit query int false "限制数量" default(100)
|
||||||
|
// @Success 200 {object} ListEventsResponse
|
||||||
|
// @Failure 500 {object} ErrorResponse
|
||||||
|
// @Router /api/v1/audit/events [get]
|
||||||
|
func (h *AuditHandler) ListEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filter := &service.EventFilter{}
|
||||||
|
|
||||||
|
// 解析查询参数
|
||||||
|
if tenantIDStr := r.URL.Query().Get("tenant_id"); tenantIDStr != "" {
|
||||||
|
tenantID, err := strconv.ParseInt(tenantIDStr, 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
filter.TenantID = tenantID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if category := r.URL.Query().Get("category"); category != "" {
|
||||||
|
filter.Category = category
|
||||||
|
}
|
||||||
|
|
||||||
|
if eventName := r.URL.Query().Get("event_name"); eventName != "" {
|
||||||
|
filter.EventName = eventName
|
||||||
|
}
|
||||||
|
|
||||||
|
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
|
||||||
|
offset, err := strconv.Atoi(offsetStr)
|
||||||
|
if err == nil {
|
||||||
|
filter.Offset = offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
||||||
|
limit, err := strconv.Atoi(limitStr)
|
||||||
|
if err == nil && limit > 0 && limit <= 1000 {
|
||||||
|
filter.Limit = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Limit == 0 {
|
||||||
|
filter.Limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
events, total, err := h.svc.ListEventsWithFilter(r.Context(), filter)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "QUERY_FAILED", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(ListEventsResponse{
|
||||||
|
Events: events,
|
||||||
|
Total: total,
|
||||||
|
Offset: filter.Offset,
|
||||||
|
Limit: filter.Limit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeError 写入错误响应
|
||||||
|
func writeError(w http.ResponseWriter, status int, code, message string) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{
|
||||||
|
Error: message,
|
||||||
|
Code: code,
|
||||||
|
Details: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
222
supply-api/internal/audit/handler/audit_handler_test.go
Normal file
222
supply-api/internal/audit/handler/audit_handler_test.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"lijiaoqiao/supply-api/internal/audit/model"
|
||||||
|
"lijiaoqiao/supply-api/internal/audit/service"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockAuditStore 模拟审计存储
|
||||||
|
type mockAuditStore struct {
|
||||||
|
events []*model.AuditEvent
|
||||||
|
nextID int64
|
||||||
|
idempotencyKeys map[string]*model.AuditEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockAuditStore() *mockAuditStore {
|
||||||
|
return &mockAuditStore{
|
||||||
|
events: make([]*model.AuditEvent, 0),
|
||||||
|
nextID: 1,
|
||||||
|
idempotencyKeys: make(map[string]*model.AuditEvent),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAuditStore) Emit(ctx context.Context, event *model.AuditEvent) error {
|
||||||
|
if event.EventID == "" {
|
||||||
|
event.EventID = "test-event-id"
|
||||||
|
}
|
||||||
|
m.events = append(m.events, event)
|
||||||
|
if event.IdempotencyKey != "" {
|
||||||
|
m.idempotencyKeys[event.IdempotencyKey] = event
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAuditStore) Query(ctx context.Context, filter *service.EventFilter) ([]*model.AuditEvent, int64, error) {
|
||||||
|
var result []*model.AuditEvent
|
||||||
|
for _, e := range m.events {
|
||||||
|
if filter.TenantID != 0 && e.TenantID != filter.TenantID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if filter.Category != "" && e.EventCategory != filter.Category {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, e)
|
||||||
|
}
|
||||||
|
return result, int64(len(result)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAuditStore) GetByIdempotencyKey(ctx context.Context, key string) (*model.AuditEvent, error) {
|
||||||
|
if e, ok := m.idempotencyKeys[key]; ok {
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuditHandler_CreateEvent_Success 测试创建事件成功
|
||||||
|
func TestAuditHandler_CreateEvent_Success(t *testing.T) {
|
||||||
|
store := newMockAuditStore()
|
||||||
|
svc := service.NewAuditService(store)
|
||||||
|
h := NewAuditHandler(svc)
|
||||||
|
|
||||||
|
reqBody := CreateEventRequest{
|
||||||
|
EventName: "CRED-EXPOSE-RESPONSE",
|
||||||
|
EventCategory: "CRED",
|
||||||
|
EventSubCategory: "EXPOSE",
|
||||||
|
OperatorID: 1001,
|
||||||
|
TenantID: 2001,
|
||||||
|
ObjectType: "account",
|
||||||
|
ObjectID: 12345,
|
||||||
|
Action: "query",
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := json.Marshal(reqBody)
|
||||||
|
req := httptest.NewRequest("POST", "/audit/events", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.CreateEvent(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusCreated, w.Code)
|
||||||
|
|
||||||
|
var result service.CreateEventResult
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 201, result.StatusCode)
|
||||||
|
assert.Equal(t, "created", result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuditHandler_CreateEvent_DuplicateIdempotencyKey 测试幂等键重复
|
||||||
|
func TestAuditHandler_CreateEvent_DuplicateIdempotencyKey(t *testing.T) {
|
||||||
|
store := newMockAuditStore()
|
||||||
|
svc := service.NewAuditService(store)
|
||||||
|
h := NewAuditHandler(svc)
|
||||||
|
|
||||||
|
reqBody := CreateEventRequest{
|
||||||
|
EventName: "CRED-EXPOSE-RESPONSE",
|
||||||
|
EventCategory: "CRED",
|
||||||
|
EventSubCategory: "EXPOSE",
|
||||||
|
OperatorID: 1001,
|
||||||
|
TenantID: 2001,
|
||||||
|
IdempotencyKey: "test-idempotency-key",
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := json.Marshal(reqBody)
|
||||||
|
|
||||||
|
// 第一次请求
|
||||||
|
req1 := httptest.NewRequest("POST", "/audit/events", bytes.NewReader(body))
|
||||||
|
req1.Header.Set("Content-Type", "application/json")
|
||||||
|
w1 := httptest.NewRecorder()
|
||||||
|
h.CreateEvent(w1, req1)
|
||||||
|
assert.Equal(t, http.StatusCreated, w1.Code)
|
||||||
|
|
||||||
|
// 第二次请求(相同幂等键)
|
||||||
|
req2 := httptest.NewRequest("POST", "/audit/events", bytes.NewReader(body))
|
||||||
|
req2.Header.Set("Content-Type", "application/json")
|
||||||
|
w2 := httptest.NewRecorder()
|
||||||
|
h.CreateEvent(w2, req2)
|
||||||
|
assert.Equal(t, http.StatusOK, w2.Code) // 应该返回200而非201
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuditHandler_ListEvents_Success 测试查询事件成功
|
||||||
|
func TestAuditHandler_ListEvents_Success(t *testing.T) {
|
||||||
|
store := newMockAuditStore()
|
||||||
|
svc := service.NewAuditService(store)
|
||||||
|
h := NewAuditHandler(svc)
|
||||||
|
|
||||||
|
// 先创建一些事件
|
||||||
|
events := []*model.AuditEvent{
|
||||||
|
{EventName: "EVENT-1", TenantID: 2001, EventCategory: "CRED"},
|
||||||
|
{EventName: "EVENT-2", TenantID: 2001, EventCategory: "CRED"},
|
||||||
|
{EventName: "EVENT-3", TenantID: 2002, EventCategory: "AUTH"},
|
||||||
|
}
|
||||||
|
for _, e := range events {
|
||||||
|
store.Emit(context.Background(), e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询
|
||||||
|
req := httptest.NewRequest("GET", "/audit/events?tenant_id=2001", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.ListEvents(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var result ListEventsResponse
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(2), result.Total) // 只有2个2001租户的事件
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuditHandler_ListEvents_WithPagination 测试分页查询
|
||||||
|
func TestAuditHandler_ListEvents_WithPagination(t *testing.T) {
|
||||||
|
store := newMockAuditStore()
|
||||||
|
svc := service.NewAuditService(store)
|
||||||
|
h := NewAuditHandler(svc)
|
||||||
|
|
||||||
|
// 创建多个事件
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
store.Emit(context.Background(), &model.AuditEvent{
|
||||||
|
EventName: "EVENT",
|
||||||
|
TenantID: 2001,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/audit/events?tenant_id=2001&offset=0&limit=2", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.ListEvents(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
var result ListEventsResponse
|
||||||
|
json.Unmarshal(w.Body.Bytes(), &result)
|
||||||
|
assert.Equal(t, int64(5), result.Total)
|
||||||
|
assert.Equal(t, 0, result.Offset)
|
||||||
|
assert.Equal(t, 2, result.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuditHandler_InvalidRequest 测试无效请求
|
||||||
|
func TestAuditHandler_InvalidRequest(t *testing.T) {
|
||||||
|
store := newMockAuditStore()
|
||||||
|
svc := service.NewAuditService(store)
|
||||||
|
h := NewAuditHandler(svc)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/audit/events", bytes.NewReader([]byte("invalid json")))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.CreateEvent(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuditHandler_MissingRequiredFields 测试缺少必填字段
|
||||||
|
func TestAuditHandler_MissingRequiredFields(t *testing.T) {
|
||||||
|
store := newMockAuditStore()
|
||||||
|
svc := service.NewAuditService(store)
|
||||||
|
h := NewAuditHandler(svc)
|
||||||
|
|
||||||
|
// 缺少EventName
|
||||||
|
reqBody := CreateEventRequest{
|
||||||
|
EventCategory: "CRED",
|
||||||
|
OperatorID: 1001,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := json.Marshal(reqBody)
|
||||||
|
req := httptest.NewRequest("POST", "/audit/events", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.CreateEvent(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
}
|
||||||
@@ -591,6 +591,160 @@ func TestP2_01_WildcardScope_SecurityRisk(t *testing.T) {
|
|||||||
// 问题:通配符scope被使用时没有记录审计日志
|
// 问题:通配符scope被使用时没有记录审计日志
|
||||||
// 修复建议:在hasScope返回true时,如果scope是"*",应该记录审计日志
|
// 修复建议:在hasScope返回true时,如果scope是"*",应该记录审计日志
|
||||||
// 这是一个安全风险,因为无法追踪何时使用了超级权限
|
// 这是一个安全风险,因为无法追踪何时使用了超级权限
|
||||||
|
|
||||||
t.Logf("P2-01: Wildcard scope usage should be audited for security compliance")
|
t.Logf("P2-01: Wildcard scope usage should be audited for security compliance")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSetRouteScopePolicy 测试设置路由Scope策略
|
||||||
|
func TestSetRouteScopePolicy(t *testing.T) {
|
||||||
|
// arrange
|
||||||
|
m := NewScopeAuthMiddleware()
|
||||||
|
|
||||||
|
// act
|
||||||
|
m.SetRouteScopePolicy("/api/v1/admin", []string{"platform:admin"})
|
||||||
|
m.SetRouteScopePolicy("/api/v1/user", []string{"platform:read"})
|
||||||
|
|
||||||
|
// assert - 验证路由策略是否正确设置
|
||||||
|
_, ok1 := m.routeScopePolicies["/api/v1/admin"]
|
||||||
|
_, ok2 := m.routeScopePolicies["/api/v1/user"]
|
||||||
|
assert.True(t, ok1, "admin route policy should be set")
|
||||||
|
assert.True(t, ok2, "user route policy should be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireRole_HasRole 测试RequireRole中间件 - 有角色
|
||||||
|
func TestRequireRole_HasRole(t *testing.T) {
|
||||||
|
// arrange
|
||||||
|
m := NewScopeAuthMiddleware()
|
||||||
|
claims := &IAMTokenClaims{
|
||||||
|
SubjectID: "user:1",
|
||||||
|
Role: "org_admin",
|
||||||
|
Scope: []string{"platform:admin"},
|
||||||
|
TenantID: 1,
|
||||||
|
}
|
||||||
|
ctx := WithIAMClaims(context.Background(), claims)
|
||||||
|
|
||||||
|
handler := m.RequireRole("org_admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// act
|
||||||
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireRole_NoRole 测试RequireRole中间件 - 无角色
|
||||||
|
func TestRequireRole_NoRole(t *testing.T) {
|
||||||
|
// arrange
|
||||||
|
m := NewScopeAuthMiddleware()
|
||||||
|
claims := &IAMTokenClaims{
|
||||||
|
SubjectID: "user:1",
|
||||||
|
Role: "viewer",
|
||||||
|
Scope: []string{"platform:read"},
|
||||||
|
TenantID: 1,
|
||||||
|
}
|
||||||
|
ctx := WithIAMClaims(context.Background(), claims)
|
||||||
|
|
||||||
|
handler := m.RequireRole("org_admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// act
|
||||||
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireRole_NoClaims 测试RequireRole中间件 - 无Claims
|
||||||
|
func TestRequireRole_NoClaims(t *testing.T) {
|
||||||
|
// arrange
|
||||||
|
m := NewScopeAuthMiddleware()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
handler := m.RequireRole("org_admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// act
|
||||||
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireMinLevel_HasLevel 测试RequireMinLevel中间件 - 满足等级
|
||||||
|
func TestRequireMinLevel_HasLevel(t *testing.T) {
|
||||||
|
// arrange
|
||||||
|
m := NewScopeAuthMiddleware()
|
||||||
|
claims := &IAMTokenClaims{
|
||||||
|
SubjectID: "user:1",
|
||||||
|
Role: "org_admin",
|
||||||
|
Scope: []string{"platform:admin"},
|
||||||
|
TenantID: 1,
|
||||||
|
}
|
||||||
|
ctx := WithIAMClaims(context.Background(), claims)
|
||||||
|
|
||||||
|
handler := m.RequireMinLevel(50)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// act
|
||||||
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequireMinLevel_InsufficientLevel 测试RequireMinLevel中间件 - 等级不足
|
||||||
|
func TestRequireMinLevel_InsufficientLevel(t *testing.T) {
|
||||||
|
// arrange
|
||||||
|
m := NewScopeAuthMiddleware()
|
||||||
|
claims := &IAMTokenClaims{
|
||||||
|
SubjectID: "user:1",
|
||||||
|
Role: "viewer",
|
||||||
|
Scope: []string{"platform:read"},
|
||||||
|
TenantID: 1,
|
||||||
|
}
|
||||||
|
ctx := WithIAMClaims(context.Background(), claims)
|
||||||
|
|
||||||
|
handler := m.RequireMinLevel(50)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// act
|
||||||
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHasAnyScope_True 测试hasAnyScope - 有交集
|
||||||
|
func TestHasAnyScope_True(t *testing.T) {
|
||||||
|
// act & assert
|
||||||
|
assert.True(t, hasAnyScope([]string{"platform:read", "platform:write"}, []string{"platform:admin", "platform:read"}))
|
||||||
|
assert.True(t, hasAnyScope([]string{"*"}, []string{"platform:read"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHasAnyScope_False 测试hasAnyScope - 无交集
|
||||||
|
func TestHasAnyScope_False(t *testing.T) {
|
||||||
|
// act & assert
|
||||||
|
assert.False(t, hasAnyScope([]string{"platform:read"}, []string{"platform:admin", "supply:write"}))
|
||||||
|
assert.False(t, hasAnyScope([]string{"tenant:read"}, []string{"platform:admin"}))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user