优化结构

This commit is contained in:
Yvan
2026-08-24 13:25:16 +08:00
parent 7a9a738c6a
commit 0b6b8ddf1a
72 changed files with 2475 additions and 1443 deletions
+6 -4
View File
@@ -4,7 +4,7 @@ import (
"context"
"kra/internal/biz/system"
"kra/internal/conf"
"kra/internal/config"
"kra/internal/utils/routepath"
"github.com/gin-gonic/gin"
@@ -15,7 +15,7 @@ type accessController interface {
ContextWithDataScope(context.Context, uint, uint) (context.Context, error)
}
func AccessControl(runtime *conf.Runtime, access accessController) gin.HandlerFunc {
func AccessControl(runtime *config.Store, access accessController) gin.HandlerFunc {
return func(c *gin.Context) {
claims := Claims(c)
if claims == nil {
@@ -29,8 +29,10 @@ func AccessControl(runtime *conf.Runtime, access accessController) gin.HandlerFu
}
path := c.Request.URL.Path
policyPath := path
if config := runtime.Admin(); config != nil {
policyPath = routepath.Normalize(policyPath, config.RouterPrefix)
if runtime != nil {
if snapshot := runtime.Snapshot(); snapshot != nil && snapshot.Admin != nil {
policyPath = routepath.Normalize(policyPath, snapshot.Admin.RouterPrefix)
}
}
allowed, err := access.Authorize(c.Request.Context(), claims.AuthorityID, policyPath, c.Request.Method)
if err != nil || !allowed {
+13 -11
View File
@@ -15,7 +15,7 @@ import (
"strings"
"time"
"kra/internal/conf"
"kra/internal/config"
"kra/internal/routecatalog"
"github.com/gin-gonic/gin"
@@ -25,21 +25,23 @@ const defaultRequestBodyLimit int64 = 8 << 20
// AccessLog is the single global request/response capture point, matching
// the reference middleware ordering and making every HTTP request observable.
func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.HandlerFunc {
func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.HandlerFunc {
return func(c *gin.Context) {
started := time.Now()
var requestBody []byte
multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
mediaUpload := multipart && isMediaUploadRoute(c.FullPath())
var config *conf.AdminBackend
var admin *config.Admin
if runtime != nil {
config = runtime.Admin()
if snapshot := runtime.Snapshot(); snapshot != nil {
admin = snapshot.Admin
}
}
bodyLimit := defaultRequestBodyLimit
if mediaUpload {
bodyLimit = system.DefaultMaxMediaFileSize + (1 << 20)
if config != nil && config.Media != nil && config.Media.MaxFileSize > 0 {
bodyLimit = config.Media.MaxFileSize + (1 << 20)
if admin != nil && admin.Media != nil && admin.Media.MaxFileSize > 0 {
bodyLimit = admin.Media.MaxFileSize + (1 << 20)
}
}
bytesIn := c.Request.ContentLength
@@ -73,8 +75,8 @@ func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.H
bytesIn = 0
}
logLimit := 1024
if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
logLimit = int(config.Zap.AccessLogMaxBytes)
if admin != nil && admin.Zap != nil && admin.Zap.AccessLogMaxBytes > 0 {
logLimit = admin.Zap.AccessLogMaxBytes
}
bodyPolicy := routecatalog.BodyPolicyFor(c.Request.Method, c.Request.URL.Path)
paymentCallback := bodyPolicy == routecatalog.BodyPolicyPaymentCallback
@@ -140,13 +142,13 @@ func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.H
"req_query", redactQuery(c.Request.URL.RawQuery),
}
}
if !paymentCallback && config != nil && config.Zap != nil && config.Zap.AccessReqHeaders {
if !paymentCallback && admin != nil && admin.Zap != nil && admin.Zap.AccessReqHeaders {
attributes = append(attributes, "req_headers", redactHeaders(c.Request.Header))
}
if !paymentCallback && config != nil && config.Zap != nil && config.Zap.AccessReqBody {
if !paymentCallback && admin != nil && admin.Zap != nil && admin.Zap.AccessReqBody {
attributes = append(attributes, "req_body", requestText)
}
if !paymentCallback && config != nil && config.Zap != nil && config.Zap.AccessRespData {
if !paymentCallback && admin != nil && admin.Zap != nil && admin.Zap.AccessRespData {
attributes = append(attributes, "resp_data", responseText)
}
if !paymentCallback && privateErrors != "" {
@@ -9,7 +9,7 @@ import (
"strings"
"testing"
"kra/internal/conf"
"kra/internal/config"
"github.com/gin-gonic/gin"
)
@@ -18,7 +18,7 @@ func TestAccessLogRejectsOversizedRequestBody(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
called := false
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
engine.Use(AccessLog(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, "test"))
engine.POST("/payload", func(c *gin.Context) { called = true })
request := httptest.NewRequest(http.MethodPost, "/payload", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
request.Header.Set("Content-Type", "application/json")
@@ -34,7 +34,7 @@ func TestAccessLogRejectsOversizedMultipartOnOrdinaryRoute(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
called := false
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
engine.Use(AccessLog(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, "test"))
engine.POST("/login", func(c *gin.Context) { called = true })
request := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
request.ContentLength = -1
@@ -51,7 +51,7 @@ func TestAccessLogAllowsMediaLimitOnlyOnUploadRoute(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
called := false
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
engine.Use(AccessLog(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, "test"))
engine.POST("/api/fileUploadAndDownload/upload", func(c *gin.Context) {
called = true
_, _ = io.Copy(io.Discard, c.Request.Body)
@@ -70,7 +70,7 @@ func TestAccessLogRedactsPaymentCallbackPayloadAndHeaders(t *testing.T) {
gin.SetMode(gin.TestMode)
var logs bytes.Buffer
logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelInfo}))
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Zap: &conf.AdminBackend_Zap{AccessReqBody: true, AccessReqHeaders: true, AccessRespData: true}})
runtime := config.NewStore(&config.Config{Admin: &config.Admin{Zap: &config.Zap{AccessReqBody: true, AccessReqHeaders: true, AccessRespData: true}}})
engine := gin.New()
engine.Use(AccessLog(runtime, logger, "test"))
engine.POST("/api/payment/callback/:provider", func(c *gin.Context) {
@@ -101,7 +101,7 @@ func TestAccessLogOmitsPaymentIntegrationConfigBody(t *testing.T) {
gin.SetMode(gin.TestMode)
var logs bytes.Buffer
logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelInfo}))
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Zap: &conf.AdminBackend_Zap{AccessReqBody: true, AccessReqHeaders: true, AccessRespData: true}})
runtime := config.NewStore(&config.Config{Admin: &config.Admin{Zap: &config.Zap{AccessReqBody: true, AccessReqHeaders: true, AccessRespData: true}}})
engine := gin.New()
engine.Use(AccessLog(runtime, logger, "test"))
engine.PUT("/api/integration/configs/:kind/:provider", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0}) })
@@ -127,7 +127,7 @@ func TestAccessLogRedactsWebSocketQueryToken(t *testing.T) {
gin.SetMode(gin.TestMode)
var logs bytes.Buffer
logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelInfo}))
runtime := conf.NewRuntime(nil, &conf.AdminBackend{})
runtime := config.NewStore(&config.Config{Admin: &config.Admin{}})
engine := gin.New()
engine.Use(AccessLog(runtime, logger, "test"))
engine.GET("/ws", func(c *gin.Context) { c.Status(http.StatusSwitchingProtocols) })
+2 -2
View File
@@ -9,7 +9,7 @@ import (
"strings"
"testing"
"kra/internal/conf"
"kra/internal/config"
"github.com/gin-gonic/gin"
)
@@ -36,7 +36,7 @@ func TestAccessControlFailsClosedWhenDataScopeResolutionFails(t *testing.T) {
c.Set(claimsKey, &system.AuthClaims{ID: 7, AuthorityID: 888})
c.Next()
})
engine.Use(AccessControl(conf.NewRuntime(nil, &conf.AdminBackend{}), &accessControllerStub{scopeErr: errors.New("database unavailable")}))
engine.Use(AccessControl(config.NewStore(&config.Config{Admin: &config.Admin{}}), &accessControllerStub{scopeErr: errors.New("database unavailable")}))
called := false
engine.GET("/protected", func(c *gin.Context) { called = true })
+4 -4
View File
@@ -12,7 +12,7 @@ import (
"strings"
"time"
"kra/internal/conf"
"kra/internal/config"
"kra/internal/routecatalog"
"kra/internal/service"
"kra/internal/service/dto"
@@ -22,7 +22,7 @@ import (
const ctxOperationAuditPersistFailedKey = "operation_audit_persist_failed"
func OperationAudit(runtime *conf.Runtime, recorder *service.AuditRecorder) gin.HandlerFunc {
func OperationAudit(runtime *config.Store, recorder *service.AuditRecorder) gin.HandlerFunc {
return func(c *gin.Context) {
if runtime == nil || recorder == nil {
c.Next()
@@ -39,8 +39,8 @@ func OperationAudit(runtime *conf.Runtime, recorder *service.AuditRecorder) gin.
}
var requestBody []byte
maxBytes := 1024
if config := runtime.Admin(); config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
maxBytes = int(config.Zap.AccessLogMaxBytes)
if snapshot := runtime.Snapshot(); snapshot != nil && snapshot.Admin != nil && snapshot.Admin.Zap != nil && snapshot.Admin.Zap.AccessLogMaxBytes > 0 {
maxBytes = snapshot.Admin.Zap.AccessLogMaxBytes
}
if c.Request.Method == http.MethodGet {
requestBody = operationQueryBody(c.Request.URL.RawQuery)
+8 -7
View File
@@ -4,7 +4,7 @@ import (
"net/http"
"strings"
"kra/internal/conf"
"kra/internal/config"
"github.com/gin-gonic/gin"
)
@@ -17,18 +17,19 @@ const (
// CORS applies the current administration CORS rules on every request so a
// configuration reload takes effect without rebuilding the Gin engine.
func CORS(runtime *conf.Runtime) gin.HandlerFunc {
func CORS(runtime *config.Store) gin.HandlerFunc {
return func(c *gin.Context) {
if runtime == nil {
c.Next()
return
}
config := runtime.Admin()
if config == nil || config.Cors == nil {
snapshot := runtime.Snapshot()
if snapshot == nil || snapshot.Admin == nil || snapshot.Admin.CORS == nil {
c.Next()
return
}
mode := strings.TrimSpace(config.Cors.Mode)
cors := snapshot.Admin.CORS
mode := strings.TrimSpace(cors.Mode)
origin := c.GetHeader("Origin")
corsHandled := false
if mode == "allow-all" {
@@ -36,7 +37,7 @@ func CORS(runtime *conf.Runtime) gin.HandlerFunc {
setCORSHeaders(c, origin, defaultCORSHeaders, defaultCORSMethods, defaultCORSExpose, true)
corsHandled = true
}
} else if rule := matchingCORSRule(config.Cors.Whitelist, origin); rule != nil {
} else if rule := matchingCORSRule(cors.Whitelist, origin); rule != nil {
setCORSHeaders(c, rule.AllowOrigin, rule.AllowHeaders, rule.AllowMethods, rule.ExposeHeaders, rule.AllowCredentials)
corsHandled = true
} else if mode == "strict-whitelist" && !(c.Request.Method == http.MethodGet && isHealthPath(c.Request.URL.Path)) {
@@ -56,7 +57,7 @@ func isHealthPath(path string) bool {
return path == "/health" || strings.HasSuffix(path, "/health")
}
func matchingCORSRule(rules []*conf.AdminBackend_CORSRule, origin string) *conf.AdminBackend_CORSRule {
func matchingCORSRule(rules []*config.CORSRule, origin string) *config.CORSRule {
for _, rule := range rules {
if rule != nil && origin == rule.AllowOrigin {
return rule
+5 -5
View File
@@ -5,15 +5,15 @@ import (
"net/http/httptest"
"testing"
"kra/internal/conf"
"kra/internal/config"
"github.com/gin-gonic/gin"
)
func runCORSTest(t *testing.T, admin *conf.AdminBackend, method, origin string) *httptest.ResponseRecorder {
func runCORSTest(t *testing.T, admin *config.Admin, method, origin string) *httptest.ResponseRecorder {
t.Helper()
engine := gin.New()
engine.Use(CORS(conf.NewRuntime(nil, admin)))
engine.Use(CORS(config.NewStore(&config.Config{Admin: admin})))
engine.Any("/test", func(c *gin.Context) { c.Status(http.StatusOK) })
request := httptest.NewRequest(method, "/test", nil)
if origin != "" {
@@ -25,14 +25,14 @@ func runCORSTest(t *testing.T, admin *conf.AdminBackend, method, origin string)
}
func TestCORSDoesNotConsumeUnmatchedWhitelistPreflight(t *testing.T) {
response := runCORSTest(t, &conf.AdminBackend{Cors: &conf.AdminBackend_CORS{Mode: "whitelist"}}, http.MethodOptions, "https://unknown.example")
response := runCORSTest(t, &config.Admin{CORS: &config.CORS{Mode: "whitelist"}}, http.MethodOptions, "https://unknown.example")
if response.Code != http.StatusOK {
t.Fatalf("unmatched whitelist preflight status = %d, want %d", response.Code, http.StatusOK)
}
}
func TestCORSConsumesMatchedWhitelistPreflight(t *testing.T) {
response := runCORSTest(t, &conf.AdminBackend{Cors: &conf.AdminBackend_CORS{Mode: "whitelist", Whitelist: []*conf.AdminBackend_CORSRule{{AllowOrigin: "https://admin.example"}}}}, http.MethodOptions, "https://admin.example")
response := runCORSTest(t, &config.Admin{CORS: &config.CORS{Mode: "whitelist", Whitelist: []*config.CORSRule{{AllowOrigin: "https://admin.example"}}}}, http.MethodOptions, "https://admin.example")
if response.Code != http.StatusNoContent {
t.Fatalf("matched whitelist preflight status = %d, want %d", response.Code, http.StatusNoContent)
}