From a6693e16565ba35003ffa396226581c797b3d469 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 13:51:54 +0000 Subject: [PATCH] feat(api): surface effective Authentik public paths in specs Co-Authored-By: Virgil --- cmd/api/cmd_test.go | 12 ++++++------ openapi.go | 29 +++++++++++++++++++++++++++-- spec_builder_helper_test.go | 4 ++-- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/cmd/api/cmd_test.go b/cmd/api/cmd_test.go index 55da580..2b442f2 100644 --- a/cmd/api/cmd_test.go +++ b/cmd/api/cmd_test.go @@ -398,8 +398,8 @@ func TestAPISpecCmd_Good_AuthentikPublicPathsAreNormalised(t *testing.T) { if !ok { t.Fatalf("expected x-authentik-public-paths array, got %T", spec["x-authentik-public-paths"]) } - if len(paths) != 2 || paths[0] != "/public" || paths[1] != "/docs" { - t.Fatalf("expected normalised public paths [/public /docs], got %v", paths) + if len(paths) != 4 || paths[0] != "/health" || paths[1] != "/swagger" || paths[2] != "/public" || paths[3] != "/docs" { + t.Fatalf("expected normalised public paths [/health /swagger /public /docs], got %v", paths) } } @@ -867,8 +867,8 @@ func TestAPISpecCmd_Good_AuthentikFlagsPopulateSpecMetadata(t *testing.T) { if !ok { t.Fatalf("expected x-authentik-public-paths array, got %T", spec["x-authentik-public-paths"]) } - if len(publicPaths) != 2 || publicPaths[0] != "/public" || publicPaths[1] != "/docs" { - t.Fatalf("expected public paths [/public /docs], got %v", publicPaths) + if len(publicPaths) != 4 || publicPaths[0] != "/health" || publicPaths[1] != "/swagger" || publicPaths[2] != "/public" || publicPaths[3] != "/docs" { + t.Fatalf("expected public paths [/health /swagger /public /docs], got %v", publicPaths) } } @@ -1151,8 +1151,8 @@ func TestAPISDKCmd_Good_TempSpecUsesMetadataFlags(t *testing.T) { if !ok { t.Fatalf("expected x-authentik-public-paths array, got %T", spec["x-authentik-public-paths"]) } - if len(publicPaths) != 2 || publicPaths[0] != "/public" || publicPaths[1] != "/docs" { - t.Fatalf("expected public paths [/public /docs], got %v", publicPaths) + if len(publicPaths) != 4 || publicPaths[0] != "/health" || publicPaths[1] != "/swagger" || publicPaths[2] != "/docs" || publicPaths[3] != "/public" { + t.Fatalf("expected public paths [/health /swagger /docs /public], got %v", publicPaths) } if info["termsOfService"] != "https://example.com/terms" { diff --git a/openapi.go b/openapi.go index 854d283..c313354 100644 --- a/openapi.go +++ b/openapi.go @@ -169,8 +169,8 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) { if sb.AuthentikTrustedProxy { spec["x-authentik-trusted-proxy"] = true } - if len(sb.AuthentikPublicPaths) > 0 { - spec["x-authentik-public-paths"] = slices.Clone(sb.AuthentikPublicPaths) + if paths := sb.effectiveAuthentikPublicPaths(); len(paths) > 0 { + spec["x-authentik-public-paths"] = paths } if sb.TermsOfService != "" { @@ -1905,6 +1905,31 @@ func (sb *SpecBuilder) effectiveSSEPath() string { return ssePath } +// effectiveAuthentikPublicPaths returns the public paths that Authentik skips +// in practice, including the always-public health and Swagger endpoints. +func (sb *SpecBuilder) effectiveAuthentikPublicPaths() []string { + if !sb.hasAuthentikMetadata() { + return nil + } + + paths := []string{"/health", "/swagger", resolveSwaggerPath(sb.SwaggerPath)} + paths = append(paths, sb.AuthentikPublicPaths...) + return normalisePublicPaths(paths) +} + +// hasAuthentikMetadata reports whether the spec carries any Authentik-related +// configuration worth surfacing. +func (sb *SpecBuilder) hasAuthentikMetadata() bool { + if sb == nil { + return false + } + + return strings.TrimSpace(sb.AuthentikIssuer) != "" || + strings.TrimSpace(sb.AuthentikClientID) != "" || + sb.AuthentikTrustedProxy || + len(sb.AuthentikPublicPaths) > 0 +} + // documentedResponseHeaders converts route-specific response header metadata // into OpenAPI header objects. func documentedResponseHeaders(headers map[string]string) map[string]any { diff --git a/spec_builder_helper_test.go b/spec_builder_helper_test.go index ab3ee60..6e78b4a 100644 --- a/spec_builder_helper_test.go +++ b/spec_builder_helper_test.go @@ -152,8 +152,8 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) { if !ok { t.Fatalf("expected x-authentik-public-paths array, got %T", spec["x-authentik-public-paths"]) } - if len(publicPaths) != 2 || publicPaths[0] != "/public" || publicPaths[1] != "/docs" { - t.Fatalf("expected public paths [/public /docs], got %v", publicPaths) + if len(publicPaths) != 4 || publicPaths[0] != "/health" || publicPaths[1] != "/swagger" || publicPaths[2] != "/docs" || publicPaths[3] != "/public" { + t.Fatalf("expected public paths [/health /swagger /docs /public], got %v", publicPaths) } contact, ok := info["contact"].(map[string]any)