mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-15 23:22:59 +08:00
Merge pull request #4485 from Sub2API-Devs/dev
feat(security-audit): 新增 OpenAI 兼容提示词审计能力与安全审计控制台
This commit is contained in:
@@ -21,6 +21,7 @@ const (
|
||||
auditCtxKeyActorID = "audit_actor_id"
|
||||
auditCtxKeyActorEmail = "audit_actor_email"
|
||||
auditCtxKeySkip = "audit_skip"
|
||||
auditCtxKeyExtra = "audit_extra"
|
||||
// ContextKeyAuthEmail 认证中间件写入的用户邮箱(审计用)。
|
||||
ContextKeyAuthEmail = "auth_email"
|
||||
// ContextKeySessionID 认证中间件写入的会话 ID(refresh token family)。
|
||||
@@ -47,6 +48,65 @@ func SkipAudit(c *gin.Context) {
|
||||
c.Set(auditCtxKeySkip, true)
|
||||
}
|
||||
|
||||
// auditExtraAllowedKeys is deliberately narrow: handlers may only attach
|
||||
// scalar, non-secret operation summaries. Request bodies and arbitrary maps
|
||||
// are never accepted through this channel.
|
||||
var auditExtraAllowedKeys = map[string]struct{}{
|
||||
"result": {}, "error_code": {}, "enabled": {}, "blocking_enabled": {},
|
||||
"config_version": {}, "endpoint_count": {}, "scanner_count": {},
|
||||
"all_groups": {}, "group_count": {}, "guard_endpoint_id": {},
|
||||
"http_status": {}, "latency_ms": {}, "token_applied": {}, "retryable": {},
|
||||
"event_id": {}, "requested_count": {}, "deleted_events": {}, "deleted_jobs": {},
|
||||
"matched_count": {}, "snapshot_max_id": {}, "filter_hash": {}, "confirm": {},
|
||||
}
|
||||
|
||||
// SetAuditExtra adds allowlisted, scalar details to the current audit entry.
|
||||
// It is safe to call more than once; later values replace earlier ones.
|
||||
func SetAuditExtra(c *gin.Context, fields map[string]any) {
|
||||
if c == nil || len(fields) == 0 {
|
||||
return
|
||||
}
|
||||
current := map[string]any{}
|
||||
if value, ok := c.Get(auditCtxKeyExtra); ok {
|
||||
if existing, ok := value.(map[string]any); ok {
|
||||
for key, item := range existing {
|
||||
current[key] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
for key, value := range fields {
|
||||
if _, ok := auditExtraAllowedKeys[key]; !ok || !isAuditExtraScalar(value) {
|
||||
continue
|
||||
}
|
||||
if text, ok := value.(string); ok {
|
||||
value = truncateAuditExtraString(text, 128)
|
||||
}
|
||||
current[key] = value
|
||||
}
|
||||
c.Set(auditCtxKeyExtra, current)
|
||||
}
|
||||
|
||||
func isAuditExtraScalar(value any) bool {
|
||||
switch value.(type) {
|
||||
case string, bool,
|
||||
int, int8, int16, int32, int64,
|
||||
uint, uint8, uint16, uint32, uint64,
|
||||
float32, float64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func truncateAuditExtraString(value string, limit int) string {
|
||||
value = strings.TrimSpace(value)
|
||||
runes := []rune(value)
|
||||
if len(runes) <= limit {
|
||||
return value
|
||||
}
|
||||
return string(runes[:limit])
|
||||
}
|
||||
|
||||
// auditSensitiveReads 需要审计的敏感 GET 读取(method+FullPath → 动作名)。
|
||||
var auditSensitiveReads = map[string]string{
|
||||
"GET /api/v1/admin/accounts/data": "admin.accounts.export",
|
||||
@@ -62,25 +122,37 @@ var auditSensitiveReads = map[string]string{
|
||||
|
||||
// auditActionOverrides 变更类请求的动作名精确映射(未命中时自动推导)。
|
||||
var auditActionOverrides = map[string]string{
|
||||
"POST /api/v1/auth/login": service.AuditActionLogin,
|
||||
"POST /api/v1/auth/login/2fa": service.AuditActionLogin2FA,
|
||||
"POST /api/v1/auth/register": service.AuditActionRegister,
|
||||
"POST /api/v1/auth/refresh": service.AuditActionTokenRefresh,
|
||||
"POST /api/v1/user/totp/step-up": service.AuditActionStepUpVerify,
|
||||
"POST /api/v1/admin/audit-logs/clear": service.AuditActionAuditLogClear,
|
||||
"POST /api/v1/admin/accounts/data": "admin.accounts.import",
|
||||
"POST /api/v1/admin/backups": "admin.backups.create",
|
||||
"POST /api/v1/admin/backups/:id/restore": "admin.backups.restore",
|
||||
"DELETE /api/v1/admin/backups/:id": "admin.backups.delete",
|
||||
"PUT /api/v1/admin/backups/s3-config": "admin.backups.s3_config.update",
|
||||
"POST /api/v1/admin/settings/admin-api-key/regenerate": "admin.admin_api_key.regenerate",
|
||||
"DELETE /api/v1/admin/settings/admin-api-key": "admin.admin_api_key.delete",
|
||||
"POST /api/v1/auth/login": service.AuditActionLogin,
|
||||
"POST /api/v1/auth/login/2fa": service.AuditActionLogin2FA,
|
||||
"POST /api/v1/auth/register": service.AuditActionRegister,
|
||||
"POST /api/v1/auth/refresh": service.AuditActionTokenRefresh,
|
||||
"POST /api/v1/user/totp/step-up": service.AuditActionStepUpVerify,
|
||||
"POST /api/v1/admin/audit-logs/clear": service.AuditActionAuditLogClear,
|
||||
"POST /api/v1/admin/accounts/data": "admin.accounts.import",
|
||||
"POST /api/v1/admin/backups": "admin.backups.create",
|
||||
"POST /api/v1/admin/backups/:id/restore": "admin.backups.restore",
|
||||
"DELETE /api/v1/admin/backups/:id": "admin.backups.delete",
|
||||
"PUT /api/v1/admin/backups/s3-config": "admin.backups.s3_config.update",
|
||||
"POST /api/v1/admin/settings/admin-api-key/regenerate": "admin.admin_api_key.regenerate",
|
||||
"DELETE /api/v1/admin/settings/admin-api-key": "admin.admin_api_key.delete",
|
||||
"PUT /api/v1/admin/prompt-audit/config": "admin.prompt_audit.config.update",
|
||||
"POST /api/v1/admin/prompt-audit/endpoints/probe": "admin.prompt_audit.endpoint.probe",
|
||||
"DELETE /api/v1/admin/prompt-audit/events/:id": "admin.prompt_audit.event.delete",
|
||||
"POST /api/v1/admin/prompt-audit/events/batch-delete": "admin.prompt_audit.events.batch_delete",
|
||||
"POST /api/v1/admin/prompt-audit/events/delete-preview": "admin.prompt_audit.events.delete_preview",
|
||||
"POST /api/v1/admin/prompt-audit/events/delete-by-filter": "admin.prompt_audit.events.filter_delete",
|
||||
}
|
||||
|
||||
// auditBodyOmittedRoutes 请求体几乎整体由凭证构成的路由(如整块粘贴 auth JSON 的导入接口)。
|
||||
// 这类 body 的凭证内嵌在普通字符串值里,键级脱敏无法覆盖,整体不入库。
|
||||
var auditBodyOmittedRoutes = map[string]struct{}{
|
||||
"POST /api/v1/admin/accounts/import/codex-session": {},
|
||||
"POST /api/v1/admin/accounts/import/codex-session": {},
|
||||
"PUT /api/v1/admin/prompt-audit/config": {},
|
||||
"POST /api/v1/admin/prompt-audit/endpoints/probe": {},
|
||||
"DELETE /api/v1/admin/prompt-audit/events/:id": {},
|
||||
"POST /api/v1/admin/prompt-audit/events/batch-delete": {},
|
||||
"POST /api/v1/admin/prompt-audit/events/delete-preview": {},
|
||||
"POST /api/v1/admin/prompt-audit/events/delete-by-filter": {},
|
||||
}
|
||||
|
||||
// NewAuditLogMiddleware 创建审计中间件。
|
||||
@@ -195,6 +267,13 @@ func NewAuditLogMiddleware(auditService *service.AuditLogService) AuditLogMiddle
|
||||
entry.CredentialMasked = MaskedRequestCredential(c)
|
||||
|
||||
extra := map[string]any{}
|
||||
if value, ok := c.Get(auditCtxKeyExtra); ok {
|
||||
if details, ok := value.(map[string]any); ok {
|
||||
for key, item := range details {
|
||||
extra[key] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(c.Params) > 0 {
|
||||
params := make(map[string]string, len(c.Params))
|
||||
for _, p := range c.Params {
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
package middleware
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDeriveAuditAction(t *testing.T) {
|
||||
cases := []struct {
|
||||
@@ -20,3 +32,116 @@ func TestDeriveAuditAction(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type auditCaptureRepository struct {
|
||||
mu sync.Mutex
|
||||
logs []*service.AuditLog
|
||||
}
|
||||
|
||||
func (r *auditCaptureRepository) BatchInsert(_ context.Context, logs []*service.AuditLog) (int64, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.logs = append(r.logs, logs...)
|
||||
return int64(len(logs)), nil
|
||||
}
|
||||
func (r *auditCaptureRepository) Insert(_ context.Context, log *service.AuditLog) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.logs = append(r.logs, log)
|
||||
return nil
|
||||
}
|
||||
func (r *auditCaptureRepository) List(context.Context, *service.AuditLogFilter) (*service.AuditLogList, error) {
|
||||
return &service.AuditLogList{}, nil
|
||||
}
|
||||
func (r *auditCaptureRepository) GetByID(context.Context, int64) (*service.AuditLog, error) {
|
||||
return nil, service.ErrAuditLogNotFound
|
||||
}
|
||||
func (r *auditCaptureRepository) Count(context.Context) (int64, error) { return 0, nil }
|
||||
func (r *auditCaptureRepository) TruncateAll(context.Context) error { return nil }
|
||||
func (r *auditCaptureRepository) DeleteBefore(context.Context, time.Time, int) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func TestPromptAuditAdminOperationsUseOmittedBodiesAndAllowlistedDetails(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
repository := &auditCaptureRepository{}
|
||||
auditService := service.NewAuditLogService(repository, nil)
|
||||
auditService.Start()
|
||||
|
||||
router := gin.New()
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set(string(ContextKeyUser), AuthSubject{UserID: 77})
|
||||
c.Set(string(ContextKeyUserRole), "admin")
|
||||
c.Next()
|
||||
})
|
||||
router.Use(gin.HandlerFunc(NewAuditLogMiddleware(auditService)))
|
||||
router.PUT("/api/v1/admin/prompt-audit/config", func(c *gin.Context) {
|
||||
SetAuditExtra(c, map[string]any{
|
||||
"result": "failed", "error_code": "prompt_audit_config_conflict", "config_version": int64(9),
|
||||
"token": "audit-canary-secret", "raw_prompt": "audit-canary-prompt", "nested": map[string]any{"unsafe": true},
|
||||
})
|
||||
c.JSON(http.StatusConflict, gin.H{"ok": false})
|
||||
})
|
||||
router.POST("/api/v1/admin/prompt-audit/endpoints/probe", func(c *gin.Context) {
|
||||
SetAuditExtra(c, map[string]any{
|
||||
"result": "success", "guard_endpoint_id": "guard-1", "http_status": 200,
|
||||
"latency_ms": 12, "token_applied": true,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
for _, request := range []*http.Request{
|
||||
httptest.NewRequest(http.MethodPut, "/api/v1/admin/prompt-audit/config", bytes.NewBufferString(`{"expected_config_version":8,"token":"audit-canary-secret"}`)),
|
||||
httptest.NewRequest(http.MethodPost, "/api/v1/admin/prompt-audit/endpoints/probe", bytes.NewBufferString(`{"endpoint":{"token":"audit-canary-secret"}}`)),
|
||||
} {
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
}
|
||||
auditService.Stop()
|
||||
|
||||
repository.mu.Lock()
|
||||
logs := append([]*service.AuditLog(nil), repository.logs...)
|
||||
repository.mu.Unlock()
|
||||
require.Len(t, logs, 2)
|
||||
|
||||
byAction := make(map[string]*service.AuditLog, len(logs))
|
||||
for _, entry := range logs {
|
||||
byAction[entry.Action] = entry
|
||||
require.Equal(t, "<credential-bearing body omitted>", entry.RequestBody)
|
||||
require.NotContains(t, entry.RequestBody, "audit-canary")
|
||||
require.NotContains(t, entry.Extra, "token")
|
||||
require.NotContains(t, entry.Extra, "raw_prompt")
|
||||
require.NotContains(t, entry.Extra, "nested")
|
||||
}
|
||||
|
||||
config := byAction["admin.prompt_audit.config.update"]
|
||||
require.NotNil(t, config)
|
||||
require.Equal(t, http.StatusConflict, config.StatusCode)
|
||||
require.Equal(t, "failed", config.Extra["result"])
|
||||
require.Equal(t, "prompt_audit_config_conflict", config.Extra["error_code"])
|
||||
require.EqualValues(t, 9, config.Extra["config_version"])
|
||||
|
||||
probe := byAction["admin.prompt_audit.endpoint.probe"]
|
||||
require.NotNil(t, probe)
|
||||
require.Equal(t, http.StatusOK, probe.StatusCode)
|
||||
require.Equal(t, "success", probe.Extra["result"])
|
||||
require.Equal(t, "guard-1", probe.Extra["guard_endpoint_id"])
|
||||
require.Equal(t, true, probe.Extra["token_applied"])
|
||||
}
|
||||
|
||||
func TestPromptAuditMutationAuditRoutesHaveStableActionsAndOmitBodies(t *testing.T) {
|
||||
expected := map[string]string{
|
||||
"PUT /api/v1/admin/prompt-audit/config": "admin.prompt_audit.config.update",
|
||||
"POST /api/v1/admin/prompt-audit/endpoints/probe": "admin.prompt_audit.endpoint.probe",
|
||||
"DELETE /api/v1/admin/prompt-audit/events/:id": "admin.prompt_audit.event.delete",
|
||||
"POST /api/v1/admin/prompt-audit/events/batch-delete": "admin.prompt_audit.events.batch_delete",
|
||||
"POST /api/v1/admin/prompt-audit/events/delete-preview": "admin.prompt_audit.events.delete_preview",
|
||||
"POST /api/v1/admin/prompt-audit/events/delete-by-filter": "admin.prompt_audit.events.filter_delete",
|
||||
}
|
||||
for route, action := range expected {
|
||||
require.Equal(t, action, auditActionOverrides[route])
|
||||
_, omitted := auditBodyOmittedRoutes[route]
|
||||
require.Truef(t, omitted, "%s must not persist its credential or confirmation-bearing body", route)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,9 @@ func RegisterAdminRoutes(
|
||||
// 风控中心
|
||||
registerContentModerationRoutes(admin, h)
|
||||
|
||||
// 独立提示词输入审计
|
||||
registerPromptAuditRoutes(admin, h)
|
||||
|
||||
// 邀请返利(专属用户管理)
|
||||
registerAffiliateRoutes(admin, h)
|
||||
|
||||
@@ -116,6 +119,22 @@ func RegisterAdminRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
func registerPromptAuditRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
||||
promptAudit := admin.Group("/prompt-audit")
|
||||
{
|
||||
promptAudit.GET("/config", h.Admin.PromptAudit.GetConfig)
|
||||
promptAudit.PUT("/config", h.Admin.PromptAudit.UpdateConfig)
|
||||
promptAudit.POST("/endpoints/probe", h.Admin.PromptAudit.ProbeEndpoint)
|
||||
promptAudit.GET("/runtime", h.Admin.PromptAudit.GetRuntime)
|
||||
promptAudit.GET("/events", h.Admin.PromptAudit.ListEvents)
|
||||
promptAudit.GET("/events/:id", h.Admin.PromptAudit.GetEvent)
|
||||
promptAudit.DELETE("/events/:id", h.Admin.PromptAudit.DeleteEvent)
|
||||
promptAudit.POST("/events/batch-delete", h.Admin.PromptAudit.BatchDelete)
|
||||
promptAudit.POST("/events/delete-preview", h.Admin.PromptAudit.DeletePreview)
|
||||
promptAudit.POST("/events/delete-by-filter", h.Admin.PromptAudit.DeleteByFilter)
|
||||
}
|
||||
}
|
||||
|
||||
func registerAuditLogRoutes(admin *gin.RouterGroup, h *handler.Handlers, _ middleware.StepUpAuthMiddleware) {
|
||||
auditLogs := admin.Group("/audit-logs")
|
||||
{
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/handler"
|
||||
"github.com/Wei-Shaw/sub2api/internal/securityaudit"
|
||||
servermiddleware "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEveryGatewayPOSTRouteIsClassifiedForPromptAuditCoverage(t *testing.T) {
|
||||
routeSource, err := os.ReadFile("gateway.go")
|
||||
require.NoError(t, err)
|
||||
pattern := regexp.MustCompile(`(?:gateway|gemini|r|codexDirect|antigravityV1|antigravityV1Beta)\.POST\("([^"]+)"`)
|
||||
matches := pattern.FindAllStringSubmatch(string(routeSource), -1)
|
||||
actual := map[string]struct{}{}
|
||||
for _, match := range matches {
|
||||
actual[match[1]] = struct{}{}
|
||||
}
|
||||
|
||||
audited := map[string][]string{
|
||||
"/messages": {"gateway_handler.go", "openai_gateway_handler.go"},
|
||||
"/responses": {"gateway_handler_responses.go", "openai_gateway_handler.go"},
|
||||
"/responses/*subpath": {"gateway_handler_responses.go", "openai_gateway_handler.go"},
|
||||
"/chat/completions": {"gateway_handler_chat_completions.go", "openai_chat_completions.go"},
|
||||
"/embeddings": {"openai_embeddings.go"},
|
||||
"/alpha/search": {"openai_alpha_search.go"},
|
||||
"/images/generations": {"openai_images.go", "grok_media.go"},
|
||||
"/images/edits": {"openai_images.go", "grok_media.go"},
|
||||
"/images/generations/async": {"image_task_handler.go"},
|
||||
"/images/edits/async": {"image_task_handler.go"},
|
||||
"/images/batches": {"batch_image_handler.go"},
|
||||
"/videos/generations": {"grok_media.go"},
|
||||
"/videos/edits": {"grok_media.go"},
|
||||
"/videos/extensions": {"grok_media.go"},
|
||||
"/models/*modelAction": {"gemini_v1beta_handler.go"},
|
||||
}
|
||||
excluded := map[string]string{
|
||||
"/messages/count_tokens": "tokenization only; it does not execute a model request",
|
||||
"/images/batches/:id/cancel": "control-plane cancellation with no user prompt",
|
||||
}
|
||||
|
||||
unclassified := make([]string, 0)
|
||||
for route := range actual {
|
||||
if _, ok := audited[route]; ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := excluded[route]; ok {
|
||||
continue
|
||||
}
|
||||
unclassified = append(unclassified, route)
|
||||
}
|
||||
sort.Strings(unclassified)
|
||||
require.Empty(t, unclassified, "new gateway POST routes must be audited or explicitly classified with a no-prompt reason")
|
||||
|
||||
for route, files := range audited {
|
||||
_, exists := actual[route]
|
||||
require.Truef(t, exists, "stale prompt-audit route manifest entry %s", route)
|
||||
for _, filename := range files {
|
||||
source, readErr := os.ReadFile(filepath.Join("..", "..", "handler", filename))
|
||||
require.NoError(t, readErr)
|
||||
require.Containsf(t, string(source), "checkSecurityAudit", "%s route handler %s bypasses Coordinator", route, filename)
|
||||
}
|
||||
}
|
||||
|
||||
for route, reason := range excluded {
|
||||
require.NotEmpty(t, strings.TrimSpace(reason))
|
||||
_, exists := actual[route]
|
||||
require.Truef(t, exists, "stale excluded route %s", route)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsesWebSocketHasFirstAndSubsequentTurnPromptGates(t *testing.T) {
|
||||
routeSource, err := os.ReadFile("gateway.go")
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, strings.Count(string(routeSource), `.GET("/responses"`), 2)
|
||||
handlerSource, err := os.ReadFile(filepath.Join("..", "..", "handler", "openai_gateway_handler.go"))
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(handlerSource), `checkSecurityAuditStage`)
|
||||
require.Contains(t, string(handlerSource), `"first_turn"`)
|
||||
require.Contains(t, string(handlerSource), `"subsequent_turn"`)
|
||||
wsStart := strings.Index(string(handlerSource), `func (h *OpenAIGatewayHandler) ResponsesWebSocket`)
|
||||
require.NotEqual(t, -1, wsStart)
|
||||
wsSource := string(handlerSource)[wsStart:]
|
||||
require.Less(t,
|
||||
strings.Index(wsSource, `"first_turn"`),
|
||||
strings.Index(wsSource, `TryAcquireUserSlotForAPIKey`),
|
||||
"the first response.create gate must precede per-request user/account slots",
|
||||
)
|
||||
}
|
||||
|
||||
func TestPromptAuditAdminRoutesRejectUnauthenticatedAndNonAdminRequests(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
handlers := &handler.Handlers{Admin: &handler.AdminHandlers{
|
||||
PromptAudit: securityaudit.NewPromptAdminHandler(nil),
|
||||
}}
|
||||
adminAuth := servermiddleware.AdminAuthMiddleware(func(c *gin.Context) {
|
||||
if c.GetHeader("Authorization") == "" {
|
||||
servermiddleware.AbortWithError(c, http.StatusUnauthorized, "UNAUTHORIZED", "Authorization required")
|
||||
return
|
||||
}
|
||||
servermiddleware.AbortWithError(c, http.StatusForbidden, "FORBIDDEN", "Admin access required")
|
||||
})
|
||||
auditLog := servermiddleware.AuditLogMiddleware(func(c *gin.Context) { c.Next() })
|
||||
stepUp := servermiddleware.StepUpAuthMiddleware(func(c *gin.Context) { c.Next() })
|
||||
RegisterAdminRoutes(router.Group("/api/v1"), handlers, adminAuth, auditLog, stepUp, nil)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
auth string
|
||||
wantStatus int
|
||||
}{
|
||||
{name: "unauthenticated", wantStatus: http.StatusUnauthorized},
|
||||
{name: "non-admin", auth: "Bearer user-token", wantStatus: http.StatusForbidden},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/admin/prompt-audit/config", nil)
|
||||
if tc.auth != "" {
|
||||
request.Header.Set("Authorization", tc.auth)
|
||||
}
|
||||
router.ServeHTTP(recorder, request)
|
||||
require.Equal(t, tc.wantStatus, recorder.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user