security_audit_helper.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. package handler
  2. import (
  3. "crypto/sha256"
  4. "net/http"
  5. "strings"
  6. "github.com/Wei-Shaw/sub2api/internal/securityaudit"
  7. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  8. "github.com/Wei-Shaw/sub2api/internal/service"
  9. "github.com/gin-gonic/gin"
  10. "go.uber.org/zap"
  11. )
  12. const securityAuditCompletedContextKey = "sub2api.security_audit.completed"
  13. const securityAuditWSTurnContextKey = "sub2api.security_audit.ws_turn"
  14. const securityAuditWSDedupeContextKey = "sub2api.security_audit.ws_dedupe"
  15. type securityAuditWSDedupeEntry struct {
  16. stage string
  17. turn int
  18. bodyHash [sha256.Size]byte
  19. decision securityaudit.Decision
  20. }
  21. // cachesSecurityAuditCompletion reports whether a successful audit may be
  22. // reused for the rest of the gin request. WebSocket turns share one Context
  23. // across many response.create frames and must be audited independently.
  24. func cachesSecurityAuditCompletion(stage string) bool {
  25. switch strings.TrimSpace(stage) {
  26. case "", "http":
  27. return true
  28. default:
  29. return false
  30. }
  31. }
  32. func isSecurityAuditWebSocketStage(stage string) bool {
  33. switch strings.TrimSpace(stage) {
  34. case "first_turn", "subsequent_turn":
  35. return true
  36. default:
  37. return false
  38. }
  39. }
  40. func (h *GatewayHandler) checkSecurityAudit(c *gin.Context, reqLog *zap.Logger, apiKey *service.APIKey, subject middleware2.AuthSubject, protocol, model string, body []byte) *securityaudit.Decision {
  41. if h == nil {
  42. return nil
  43. }
  44. return runSecurityAudit(c, reqLog, h.securityAuditCoordinator, h.contentModerationService, apiKey, subject, protocol, model, body, "http", h.securityPolicyService)
  45. }
  46. func (h *OpenAIGatewayHandler) checkSecurityAudit(c *gin.Context, reqLog *zap.Logger, apiKey *service.APIKey, subject middleware2.AuthSubject, protocol, model string, body []byte) *securityaudit.Decision {
  47. if h == nil {
  48. return nil
  49. }
  50. return runSecurityAudit(c, reqLog, h.securityAuditCoordinator, h.contentModerationService, apiKey, subject, protocol, model, body, "http", h.securityPolicyService)
  51. }
  52. func (h *OpenAIGatewayHandler) checkSecurityAuditStage(c *gin.Context, reqLog *zap.Logger, apiKey *service.APIKey, subject middleware2.AuthSubject, protocol, model string, body []byte, stage string) *securityaudit.Decision {
  53. if h == nil {
  54. return nil
  55. }
  56. return runSecurityAudit(c, reqLog, h.securityAuditCoordinator, h.contentModerationService, apiKey, subject, protocol, model, body, stage, h.securityPolicyService)
  57. }
  58. func runSecurityAudit(c *gin.Context, reqLog *zap.Logger, coordinator *securityaudit.Coordinator, legacy *service.ContentModerationService, apiKey *service.APIKey, subject middleware2.AuthSubject, protocol, model string, body []byte, stage string, policies ...*service.SecurityPolicyService) *securityaudit.Decision {
  59. var securityPolicy *service.SecurityPolicyService
  60. if len(policies) > 0 {
  61. securityPolicy = policies[0]
  62. }
  63. if c == nil || c.Request == nil {
  64. return nil
  65. }
  66. // 分组安全策略优先于全局审计:本地关键词前置 + 已终止会话入口检查。
  67. // 开关关闭时为纯 no-op,不影响既有行为。
  68. if decision := checkGroupSecurityPolicy(c, securityPolicy, apiKey, protocol, model, body); decision != nil {
  69. return decision
  70. }
  71. cacheCompletion := cachesSecurityAuditCompletion(stage)
  72. if cacheCompletion {
  73. if completed, exists := c.Get(securityAuditCompletedContextKey); exists && completed == true {
  74. return nil
  75. }
  76. }
  77. if coordinator == nil {
  78. legacyDecision := runContentModeration(c, reqLog, legacy, apiKey, subject, protocol, model, body)
  79. if legacyDecision == nil {
  80. return nil
  81. }
  82. decision := securityaudit.Decision{Kind: securityaudit.DecisionAllow, HTTPStatus: http.StatusOK, AllowNextStage: true}
  83. decision.Legacy = &securityaudit.LegacyDecision{
  84. Allowed: legacyDecision.Allowed, Blocked: legacyDecision.Blocked, Flagged: legacyDecision.Flagged,
  85. Message: legacyDecision.Message, StatusCode: legacyDecision.StatusCode,
  86. ErrorCode: "content_policy_violation", Action: legacyDecision.Action,
  87. }
  88. if legacyDecision.Blocked {
  89. decision.Kind, decision.HTTPStatus, decision.ErrorCode, decision.ClientMessage, decision.AllowNextStage = securityaudit.DecisionBlock, contentModerationStatus(legacyDecision), "content_policy_violation", legacyDecision.Message, false
  90. }
  91. if decision.AllowNextStage && cacheCompletion {
  92. c.Set(securityAuditCompletedContextKey, true)
  93. }
  94. return &decision
  95. }
  96. request := buildSecurityAuditRequest(c, apiKey, subject, protocol, model, body, stage)
  97. if isSecurityAuditWebSocketStage(request.Stage) {
  98. if turnNo, ok := securityAuditWSTurn(c); ok {
  99. bodyHash := sha256.Sum256(body)
  100. if cached, exists := c.Get(securityAuditWSDedupeContextKey); exists {
  101. if entry, ok := cached.(securityAuditWSDedupeEntry); ok &&
  102. entry.stage == request.Stage && entry.turn == turnNo && entry.bodyHash == bodyHash {
  103. decision := entry.decision
  104. logSecurityAuditDone(reqLog, request, decision, true)
  105. return &decision
  106. }
  107. }
  108. logSecurityAuditStart(reqLog, request, len(body), false)
  109. decision := coordinator.Check(c.Request.Context(), request)
  110. if decision.Kind == securityaudit.DecisionAllow {
  111. c.Set(securityAuditWSDedupeContextKey, securityAuditWSDedupeEntry{
  112. stage: request.Stage, turn: turnNo, bodyHash: bodyHash, decision: decision,
  113. })
  114. }
  115. logSecurityAuditDone(reqLog, request, decision, false)
  116. return &decision
  117. }
  118. }
  119. logSecurityAuditStart(reqLog, request, len(body), false)
  120. decision := coordinator.Check(c.Request.Context(), request)
  121. if decision.AllowNextStage && cacheCompletion {
  122. c.Set(securityAuditCompletedContextKey, true)
  123. }
  124. logSecurityAuditDone(reqLog, request, decision, false)
  125. return &decision
  126. }
  127. func logSecurityAuditStart(reqLog *zap.Logger, request securityaudit.Request, bodyBytes int, cached bool) {
  128. if reqLog == nil {
  129. return
  130. }
  131. reqLog.Info("security_audit.gateway_check_start",
  132. zap.String("request_id", request.RequestID), zap.Int64("user_id", request.UserID),
  133. zap.Int64("api_key_id", request.APIKeyID), zap.Int64p("group_id", request.GroupID),
  134. zap.String("endpoint", request.Endpoint), zap.String("provider", request.Provider),
  135. zap.String("protocol", request.Protocol), zap.String("model", request.Model), zap.String("stage", request.Stage),
  136. zap.Int("body_bytes", bodyBytes), zap.Bool("cached", cached))
  137. }
  138. func logSecurityAuditDone(reqLog *zap.Logger, request securityaudit.Request, decision securityaudit.Decision, cached bool) {
  139. if reqLog == nil {
  140. return
  141. }
  142. reqLog.Info("security_audit.gateway_check_done",
  143. zap.String("request_id", request.RequestID), zap.String("decision", string(decision.Kind)),
  144. zap.String("error_code", decision.ErrorCode), zap.Bool("allow_next_stage", decision.AllowNextStage),
  145. zap.String("stage", request.Stage), zap.Bool("cached", cached))
  146. }
  147. func securityAuditWSTurn(c *gin.Context) (int, bool) {
  148. turn, exists := c.Get(securityAuditWSTurnContextKey)
  149. if !exists {
  150. return 0, false
  151. }
  152. turnNo, ok := turn.(int)
  153. return turnNo, ok
  154. }
  155. func buildSecurityAuditRequest(c *gin.Context, apiKey *service.APIKey, subject middleware2.AuthSubject, protocol, model string, body []byte, stage string) securityaudit.Request {
  156. legacy := buildContentModerationInput(c, apiKey, subject, protocol, model, body)
  157. request := securityaudit.Request{
  158. RequestID: legacy.RequestID, UserID: legacy.UserID, UserEmail: legacy.UserEmail,
  159. APIKeyID: legacy.APIKeyID, APIKeyName: legacy.APIKeyName, GroupID: cloneSecurityAuditGroupID(legacy.GroupID),
  160. GroupName: legacy.GroupName, Provider: legacy.Provider, Endpoint: legacy.Endpoint,
  161. Protocol: legacy.Protocol, Model: legacy.Model, Body: body, Stage: strings.TrimSpace(stage),
  162. }
  163. if apiKey != nil && apiKey.User != nil {
  164. request.Username = apiKey.User.Username
  165. if request.UserEmail == "" {
  166. request.UserEmail = apiKey.User.Email
  167. }
  168. }
  169. if request.Stage == "" {
  170. request.Stage = "http"
  171. }
  172. return request
  173. }
  174. func securityAuditStatus(decision *securityaudit.Decision) int {
  175. if decision == nil || decision.HTTPStatus < 400 || decision.HTTPStatus > 599 {
  176. return http.StatusForbidden
  177. }
  178. return decision.HTTPStatus
  179. }
  180. func securityAuditErrorCode(decision *securityaudit.Decision) string {
  181. if decision == nil || strings.TrimSpace(decision.ErrorCode) == "" {
  182. return "content_policy_violation"
  183. }
  184. return decision.ErrorCode
  185. }
  186. func securityAuditMessage(decision *securityaudit.Decision) string {
  187. if decision == nil {
  188. return "Request blocked by content policy"
  189. }
  190. if decision.Legacy != nil && decision.Legacy.Blocked && strings.TrimSpace(decision.Legacy.Message) != "" {
  191. return decision.Legacy.Message
  192. }
  193. if strings.TrimSpace(decision.ClientMessage) != "" {
  194. return decision.ClientMessage
  195. }
  196. return "Request blocked by content policy"
  197. }
  198. func cloneSecurityAuditGroupID(value *int64) *int64 {
  199. if value == nil {
  200. return nil
  201. }
  202. cloned := *value
  203. return &cloned
  204. }