优化结构
This commit is contained in:
+15
-15
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/conf"
|
||||
"kra/internal/config"
|
||||
websocket "kra/internal/integration/websocket"
|
||||
"kra/internal/server/handler"
|
||||
"kra/internal/server/middleware"
|
||||
@@ -20,13 +20,13 @@ import (
|
||||
kratoshttp "github.com/go-kratos/kratos/v3/transport/http"
|
||||
)
|
||||
|
||||
func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, handlers *handler.Set, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string) *gin.Engine {
|
||||
func NewGinEngine(runtime *config.Store, access *service.AccessControlService, handlers *handler.Set, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string) *gin.Engine {
|
||||
return NewGinEngineWithRuntime(runtime, access, auth, security, audit, logger, version, platformmodule.NewRuntime(router.NewRoutes(handlers)), nil)
|
||||
}
|
||||
|
||||
func NewGinEngineWithRuntime(runtime *conf.Runtime, access *service.AccessControlService, auth middleware.TokenAuthenticator, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string, routes *platformmodule.Runtime, ws *websocket.Server) *gin.Engine {
|
||||
func NewGinEngineWithRuntime(runtime *config.Store, access *service.AccessControlService, auth middleware.TokenAuthenticator, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string, routes *platformmodule.Runtime, ws *websocket.Server) *gin.Engine {
|
||||
if runtime == nil {
|
||||
runtime = conf.NewRuntime(nil, nil)
|
||||
runtime = config.NewStore(nil)
|
||||
}
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
@@ -36,9 +36,9 @@ func NewGinEngineWithRuntime(runtime *conf.Runtime, access *service.AccessContro
|
||||
engine.Use(middleware.RequestMeta(), middleware.Recovery(logger), middleware.AccessLog(runtime, logger, version), middleware.CORS(runtime), middleware.ErrorAudit(logger), middleware.SecurityRateLimit(security))
|
||||
|
||||
prefix := ""
|
||||
config := runtime.Admin()
|
||||
if config != nil {
|
||||
prefix = strings.TrimSuffix(config.RouterPrefix, "/")
|
||||
snapshot := runtime.Snapshot()
|
||||
if snapshot != nil && snapshot.Admin != nil {
|
||||
prefix = strings.TrimSuffix(snapshot.Admin.RouterPrefix, "/")
|
||||
}
|
||||
public := engine.Group(prefix)
|
||||
public.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, "ok") })
|
||||
@@ -89,19 +89,19 @@ func NewGinEngineWithRuntime(runtime *conf.Runtime, access *service.AccessContro
|
||||
return engine
|
||||
}
|
||||
|
||||
func NewGinServer(c *conf.Server, engine *gin.Engine) *kratoshttp.Server {
|
||||
func NewGinServer(c *config.Server, engine *gin.Engine) *kratoshttp.Server {
|
||||
network, address := "tcp", ":8000"
|
||||
if c != nil && c.Http != nil {
|
||||
if c.Http.Network != "" {
|
||||
network = c.Http.Network
|
||||
if c != nil && c.HTTP != nil {
|
||||
if c.HTTP.Network != "" {
|
||||
network = c.HTTP.Network
|
||||
}
|
||||
if c.Http.Addr != "" {
|
||||
address = c.Http.Addr
|
||||
if c.HTTP.Addr != "" {
|
||||
address = c.HTTP.Addr
|
||||
}
|
||||
}
|
||||
timeout := 10 * time.Minute
|
||||
if c != nil && c.Http != nil && c.Http.Timeout != nil && c.Http.Timeout.AsDuration() > 0 {
|
||||
timeout = c.Http.Timeout.AsDuration()
|
||||
if c != nil && c.HTTP != nil && c.HTTP.Timeout > 0 {
|
||||
timeout = c.HTTP.Timeout
|
||||
}
|
||||
server := kratoshttp.NewServer(kratoshttp.Network(network), kratoshttp.Address(address), kratoshttp.Timeout(timeout))
|
||||
server.HandlePrefix("/", engine)
|
||||
|
||||
+15
-15
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kra/internal/conf"
|
||||
"kra/internal/config"
|
||||
"kra/internal/routecatalog"
|
||||
"kra/internal/server/handler"
|
||||
|
||||
@@ -32,7 +32,7 @@ func emptyHandlers() *handler.Set {
|
||||
}
|
||||
|
||||
func TestGinRouteContract(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
engine := NewGinEngine(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
routes := engine.Routes()
|
||||
actual := make([]string, 0, len(routes))
|
||||
seen := make(map[string]struct{}, len(routes))
|
||||
@@ -66,7 +66,7 @@ func TestGinRouteContract(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGinRoutesMatchSharedCatalog(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
engine := NewGinEngine(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
registered := make(map[string]struct{}, len(engine.Routes()))
|
||||
for _, route := range engine.Routes() {
|
||||
key := route.Method + " " + route.Path
|
||||
@@ -86,7 +86,7 @@ func TestGinRoutesMatchSharedCatalog(t *testing.T) {
|
||||
func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&output, nil))
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, logger, "test")
|
||||
engine := NewGinEngine(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, logger, "test")
|
||||
text := output.String()
|
||||
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
|
||||
t.Fatalf("registered route log count = %d, want %d", got, want)
|
||||
@@ -97,7 +97,7 @@ func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSwaggerUsesRegisteredGinRoutes(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
||||
engine := NewGinEngine(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
||||
response := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil)
|
||||
engine.ServeHTTP(response, request)
|
||||
@@ -113,7 +113,7 @@ func TestSwaggerUsesRegisteredGinRoutes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSwaggerSupportsRouterPrefix(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{RouterPrefix: "/admin"}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
||||
engine := NewGinEngine(config.NewStore(&config.Config{Admin: &config.Admin{RouterPrefix: "/admin"}}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
||||
for _, path := range []string{"/admin/swagger/index.html", "/admin/swagger/doc.json"} {
|
||||
response := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
@@ -128,7 +128,7 @@ func TestSwaggerSupportsRouterPrefix(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSwaggerUsesRootBasePathWithoutRouterPrefix(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
||||
engine := NewGinEngine(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil))
|
||||
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"basePath":"/"`) {
|
||||
@@ -137,7 +137,7 @@ func TestSwaggerUsesRootBasePathWithoutRouterPrefix(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGinDoesNotTrustForwardedIPByDefault(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
engine := NewGinEngine(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
engine.GET("/client-ip", func(c *gin.Context) { c.String(http.StatusOK, c.ClientIP()) })
|
||||
request := httptest.NewRequest(http.MethodGet, "/client-ip", nil)
|
||||
request.RemoteAddr = "203.0.113.10:4321"
|
||||
@@ -151,7 +151,7 @@ func TestGinDoesNotTrustForwardedIPByDefault(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnnouncementDataSourceRequiresAuthentication(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
engine := NewGinEngine(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/info/getInfoDataSource", nil))
|
||||
if response.Code != http.StatusUnauthorized {
|
||||
@@ -166,7 +166,7 @@ func TestLocalStorageResponseHeaders(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
||||
runtime := config.NewStore(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &config.Storage{Type: "local"}}})
|
||||
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
|
||||
for _, test := range []struct {
|
||||
@@ -190,7 +190,7 @@ func TestLocalStorageResponseHeaders(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLocalStorageRoutesAreRegistered(t *testing.T) {
|
||||
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}})
|
||||
runtime := config.NewStore(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}}})
|
||||
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
seen := map[string]bool{}
|
||||
for _, route := range engine.Routes() {
|
||||
@@ -213,9 +213,9 @@ func TestLocalStorageOldPrefixStopsServingAfterReload(t *testing.T) {
|
||||
if err := os.WriteFile(filepath.Join(root, "image.png"), []byte("png"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
||||
runtime := config.NewStore(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &config.Storage{Type: "local"}}})
|
||||
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
runtime.Replace(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: root, PathPrefix: "files"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
||||
runtime.Replace(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: root, PathPrefix: "files"}, Storage: &config.Storage{Type: "local"}}})
|
||||
|
||||
oldResponse := httptest.NewRecorder()
|
||||
engine.ServeHTTP(oldResponse, httptest.NewRequest(http.MethodGet, "/uploads/file/image.png", nil))
|
||||
@@ -230,7 +230,7 @@ func TestLocalStorageOldPrefixStopsServingAfterReload(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLocalStorageConflictingPrefixFallsBackWithoutStartupPanic(t *testing.T) {
|
||||
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "api"}, Storage: &conf.AdminBackend_Storage{Type: "local"}})
|
||||
runtime := config.NewStore(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: t.TempDir(), PathPrefix: "api"}, Storage: &config.Storage{Type: "local"}}})
|
||||
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
for _, route := range engine.Routes() {
|
||||
if route.Method == http.MethodGet && route.Path == "/api/*filepath" {
|
||||
@@ -240,7 +240,7 @@ func TestLocalStorageConflictingPrefixFallsBackWithoutStartupPanic(t *testing.T)
|
||||
}
|
||||
|
||||
func TestLocalStorageRouteIsNotRegisteredForRemoteStorage(t *testing.T) {
|
||||
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Local: &conf.AdminBackend_Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}, Storage: &conf.AdminBackend_Storage{Type: "s3"}})
|
||||
runtime := config.NewStore(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}, Storage: &config.Storage{Type: "s3"}}})
|
||||
engine := NewGinEngine(runtime, nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
for _, route := range engine.Routes() {
|
||||
if strings.HasSuffix(route.Path, "/*filepath") && strings.HasPrefix(route.Path, "/uploads/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 {
|
||||
|
||||
@@ -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) })
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
@@ -9,14 +9,15 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"kra/internal/conf"
|
||||
"kra/internal/config"
|
||||
"kra/internal/utils/uploadpolicy"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Register adds local upload routes when local storage is enabled. A route is
|
||||
// skipped when its root conflicts with an existing GET/HEAD route.
|
||||
func Register(engine *gin.Engine, runtime *conf.Runtime) {
|
||||
func Register(engine *gin.Engine, runtime *config.Store) {
|
||||
config := localConfig(runtime)
|
||||
if config == nil || strings.Trim(config.PathPrefix, "/") == "" {
|
||||
return
|
||||
@@ -36,7 +37,7 @@ func Register(engine *gin.Engine, runtime *conf.Runtime) {
|
||||
|
||||
// Serve resolves the current runtime configuration on every request so a
|
||||
// configuration reload takes effect without rebuilding the Gin engine.
|
||||
func Serve(c *gin.Context, runtime *conf.Runtime) bool {
|
||||
func Serve(c *gin.Context, runtime *config.Store) bool {
|
||||
config := localConfig(runtime)
|
||||
if config == nil {
|
||||
return false
|
||||
@@ -45,18 +46,18 @@ func Serve(c *gin.Context, runtime *conf.Runtime) bool {
|
||||
return serveAt(c, runtime, prefix)
|
||||
}
|
||||
|
||||
func localConfig(runtime *conf.Runtime) *conf.AdminBackend_Local {
|
||||
func localConfig(runtime *config.Store) *config.Local {
|
||||
if runtime == nil {
|
||||
return nil
|
||||
}
|
||||
config := runtime.Admin()
|
||||
if config == nil || config.Local == nil || config.Local.StorePath == "" {
|
||||
snapshot := runtime.Snapshot()
|
||||
if snapshot == nil || snapshot.Admin == nil || snapshot.Admin.Local == nil || snapshot.Admin.Local.StorePath == "" {
|
||||
return nil
|
||||
}
|
||||
if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
|
||||
if snapshot.Admin.Storage != nil && snapshot.Admin.Storage.Type != "" && snapshot.Admin.Storage.Type != "local" {
|
||||
return nil
|
||||
}
|
||||
return config.Local
|
||||
return snapshot.Admin.Local
|
||||
}
|
||||
|
||||
func localStorageRouteConflicts(routes []gin.RouteInfo, prefix string) bool {
|
||||
@@ -73,7 +74,7 @@ func localStorageRouteConflicts(routes []gin.RouteInfo, prefix string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func serveAt(c *gin.Context, runtime *conf.Runtime, prefix string) bool {
|
||||
func serveAt(c *gin.Context, runtime *config.Store, prefix string) bool {
|
||||
config := localConfig(runtime)
|
||||
if config == nil || "/"+strings.Trim(config.PathPrefix, "/") != prefix {
|
||||
return false
|
||||
@@ -83,24 +84,13 @@ func serveAt(c *gin.Context, runtime *conf.Runtime, prefix string) bool {
|
||||
}
|
||||
c.Header("X-Content-Type-Options", "nosniff")
|
||||
filename := path.Base(c.Request.URL.Path)
|
||||
if !canServeInline(filename) {
|
||||
if !uploadpolicy.CanServeInline(filename) {
|
||||
c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
|
||||
}
|
||||
http.StripPrefix(prefix, http.FileServer(filesOnly{FileSystem: http.Dir(config.StorePath)})).ServeHTTP(c.Writer, c.Request)
|
||||
return true
|
||||
}
|
||||
|
||||
func canServeInline(filename string) bool {
|
||||
switch strings.ToLower(path.Ext(filename)) {
|
||||
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".avif",
|
||||
".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac",
|
||||
".mp4", ".webm", ".mov", ".avi", ".mkv":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type filesOnly struct{ http.FileSystem }
|
||||
|
||||
func (f filesOnly) Open(name string) (http.File, error) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kra/internal/conf"
|
||||
"kra/internal/config"
|
||||
"kra/internal/integration/runtimeconfig"
|
||||
websocketintegration "kra/internal/integration/websocket"
|
||||
"kra/internal/server/middleware"
|
||||
@@ -44,7 +44,7 @@ func TestWebSocketRouteRequiresAndAcceptsLoginToken(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
auth := &websocketAuthStub{claims: &system.AuthClaims{ID: 7}}
|
||||
engine := NewGinEngineWithRuntime(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, auth, nil, nil, nil, "test", nil, ws)
|
||||
engine := NewGinEngineWithRuntime(config.NewStore(&config.Config{Admin: &config.Admin{}}), nil, auth, nil, nil, nil, "test", nil, ws)
|
||||
|
||||
unauthorized := httptest.NewRecorder()
|
||||
engine.ServeHTTP(unauthorized, httptest.NewRequest(http.MethodGet, "/ws", nil))
|
||||
|
||||
Reference in New Issue
Block a user