From eb18611dc106f58d6b865451b5a49531e919dee7 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 13:17:08 +0000 Subject: [PATCH] feat(api): snapshot authentik runtime config Co-Authored-By: Virgil --- api.go | 1 + authentik.go | 22 ++++++++++++++++ modernization_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++- options.go | 4 ++- runtime_config.go | 10 +++++--- 5 files changed, 89 insertions(+), 6 deletions(-) diff --git a/api.go b/api.go index 58db596..b1d5fde 100644 --- a/api.go +++ b/api.go @@ -59,6 +59,7 @@ type Engine struct { swaggerSecuritySchemes map[string]any swaggerExternalDocsDescription string swaggerExternalDocsURL string + authentikConfig AuthentikConfig pprofEnabled bool expvarEnabled bool ssePath string diff --git a/authentik.go b/authentik.go index 40b1161..dfd63a9 100644 --- a/authentik.go +++ b/authentik.go @@ -34,6 +34,22 @@ type AuthentikConfig struct { PublicPaths []string } +// AuthentikConfig returns the configured Authentik settings for the engine. +// +// The result snapshots the Engine state at call time and clones slices so +// callers can safely reuse or modify the returned value. +// +// Example: +// +// cfg := engine.AuthentikConfig() +func (e *Engine) AuthentikConfig() AuthentikConfig { + if e == nil { + return AuthentikConfig{} + } + + return cloneAuthentikConfig(e.authentikConfig) +} + // AuthentikUser represents an authenticated user extracted from Authentik // forward-auth headers or a validated JWT. // @@ -217,6 +233,12 @@ func authentikMiddleware(cfg AuthentikConfig, publicPaths func() []string) gin.H } } +func cloneAuthentikConfig(cfg AuthentikConfig) AuthentikConfig { + out := cfg + out.PublicPaths = slices.Clone(cfg.PublicPaths) + return out +} + // RequireAuth is Gin middleware that rejects unauthenticated requests. // It checks for a user set by the Authentik middleware and returns 401 // when none is present. diff --git a/modernization_test.go b/modernization_test.go index 8762bbc..54335e7 100644 --- a/modernization_test.go +++ b/modernization_test.go @@ -127,6 +127,12 @@ func TestEngine_RuntimeConfig_Good_SnapshotsCurrentSettings(t *testing.T) { api.WithWSPath("/socket"), api.WithSSE(broker), api.WithSSEPath("/events"), + api.WithAuthentik(api.AuthentikConfig{ + Issuer: "https://auth.example.com", + ClientID: "runtime-client", + TrustedProxy: true, + PublicPaths: []string{"/public", "/docs"}, + }), ) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -152,17 +158,67 @@ func TestEngine_RuntimeConfig_Good_SnapshotsCurrentSettings(t *testing.T) { if !slices.Equal(cfg.I18n.Supported, []string{"en-GB", "fr"}) { t.Fatalf("expected supported locales [en-GB fr], got %v", cfg.I18n.Supported) } + if cfg.Authentik.Issuer != "https://auth.example.com" { + t.Fatalf("expected Authentik issuer https://auth.example.com, got %q", cfg.Authentik.Issuer) + } + if cfg.Authentik.ClientID != "runtime-client" { + t.Fatalf("expected Authentik client ID runtime-client, got %q", cfg.Authentik.ClientID) + } + if !cfg.Authentik.TrustedProxy { + t.Fatal("expected Authentik trusted proxy to be enabled") + } + if !slices.Equal(cfg.Authentik.PublicPaths, []string{"/public", "/docs"}) { + t.Fatalf("expected Authentik public paths [/public /docs], got %v", cfg.Authentik.PublicPaths) + } } func TestEngine_RuntimeConfig_Good_EmptyOnNilEngine(t *testing.T) { var e *api.Engine cfg := e.RuntimeConfig() - if cfg.Swagger.Enabled || cfg.Transport.SwaggerEnabled || cfg.Cache.Enabled || cfg.I18n.DefaultLocale != "" { + if cfg.Swagger.Enabled || cfg.Transport.SwaggerEnabled || cfg.Cache.Enabled || cfg.I18n.DefaultLocale != "" || cfg.Authentik.Issuer != "" { t.Fatalf("expected zero-value runtime config, got %+v", cfg) } } +func TestEngine_AuthentikConfig_Good_SnapshotsCurrentSettings(t *testing.T) { + e, _ := api.New(api.WithAuthentik(api.AuthentikConfig{ + Issuer: "https://auth.example.com", + ClientID: "client", + TrustedProxy: true, + PublicPaths: []string{"/public", "/docs"}, + })) + + cfg := e.AuthentikConfig() + if cfg.Issuer != "https://auth.example.com" { + t.Fatalf("expected issuer https://auth.example.com, got %q", cfg.Issuer) + } + if cfg.ClientID != "client" { + t.Fatalf("expected client ID client, got %q", cfg.ClientID) + } + if !cfg.TrustedProxy { + t.Fatal("expected trusted proxy to be enabled") + } + if !slices.Equal(cfg.PublicPaths, []string{"/public", "/docs"}) { + t.Fatalf("expected public paths [/public /docs], got %v", cfg.PublicPaths) + } +} + +func TestEngine_AuthentikConfig_Good_ClonesPublicPaths(t *testing.T) { + publicPaths := []string{"/public", "/docs"} + e, _ := api.New(api.WithAuthentik(api.AuthentikConfig{ + Issuer: "https://auth.example.com", + PublicPaths: publicPaths, + })) + + cfg := e.AuthentikConfig() + publicPaths[0] = "/mutated" + + if cfg.PublicPaths[0] != "/public" { + t.Fatalf("expected snapshot to preserve original public paths, got %v", cfg.PublicPaths) + } +} + func TestEngine_Register_Good_IgnoresNilGroups(t *testing.T) { e, _ := api.New() diff --git a/options.go b/options.go index e15f9d9..2887978 100644 --- a/options.go +++ b/options.go @@ -173,7 +173,9 @@ func WithWSPath(path string) Option { // api.New(api.WithAuthentik(api.AuthentikConfig{TrustedProxy: true})) func WithAuthentik(cfg AuthentikConfig) Option { return func(e *Engine) { - e.middlewares = append(e.middlewares, authentikMiddleware(cfg, func() []string { + snapshot := cloneAuthentikConfig(cfg) + e.authentikConfig = snapshot + e.middlewares = append(e.middlewares, authentikMiddleware(snapshot, func() []string { return []string{resolveSwaggerPath(e.swaggerPath)} })) } diff --git a/runtime_config.go b/runtime_config.go index ba468cc..f2ad893 100644 --- a/runtime_config.go +++ b/runtime_config.go @@ -13,10 +13,11 @@ package api // // cfg := engine.RuntimeConfig() type RuntimeConfig struct { - Swagger SwaggerConfig - Transport TransportConfig - Cache CacheConfig - I18n I18nConfig + Swagger SwaggerConfig + Transport TransportConfig + Cache CacheConfig + I18n I18nConfig + Authentik AuthentikConfig } // RuntimeConfig returns a stable snapshot of the engine's current runtime @@ -38,5 +39,6 @@ func (e *Engine) RuntimeConfig() RuntimeConfig { Transport: e.TransportConfig(), Cache: e.CacheConfig(), I18n: e.I18nConfig(), + Authentik: e.AuthentikConfig(), } }