feat(api): surface authentik metadata in specs
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
eb18611dc1
commit
f234fcba5f
7 changed files with 164 additions and 27 deletions
|
|
@ -121,6 +121,10 @@ func sdkSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) {
|
|||
cacheMaxBytes: cfg.cacheMaxBytes,
|
||||
i18nDefaultLocale: cfg.i18nDefaultLocale,
|
||||
i18nSupportedLocales: cfg.i18nSupportedLocales,
|
||||
authentikIssuer: cfg.authentikIssuer,
|
||||
authentikClientID: cfg.authentikClientID,
|
||||
authentikTrustedProxy: cfg.authentikTrustedProxy,
|
||||
authentikPublicPaths: cfg.authentikPublicPaths,
|
||||
termsURL: cfg.termsURL,
|
||||
contactName: cfg.contactName,
|
||||
contactURL: cfg.contactURL,
|
||||
|
|
|
|||
|
|
@ -87,6 +87,10 @@ func registerSpecBuilderFlags(cmd *cli.Command, cfg *specBuilderConfig) {
|
|||
cli.IntFlag(cmd, &cfg.cacheMaxBytes, "cache-max-bytes", "", 0, "Cache max bytes in generated spec")
|
||||
cli.StringFlag(cmd, &cfg.i18nDefaultLocale, "i18n-default-locale", "", "", "Default locale in generated spec")
|
||||
cli.StringFlag(cmd, &cfg.i18nSupportedLocales, "i18n-supported-locales", "", "", "Comma-separated supported locales in generated spec")
|
||||
cli.StringFlag(cmd, &cfg.authentikIssuer, "authentik-issuer", "", "", "Authentik issuer URL in generated spec")
|
||||
cli.StringFlag(cmd, &cfg.authentikClientID, "authentik-client-id", "", "", "Authentik client ID in generated spec")
|
||||
cli.BoolFlag(cmd, &cfg.authentikTrustedProxy, "authentik-trusted-proxy", "", false, "Mark Authentik proxy headers as trusted in generated spec")
|
||||
cli.StringFlag(cmd, &cfg.authentikPublicPaths, "authentik-public-paths", "", "", "Comma-separated public paths in generated spec")
|
||||
cli.StringFlag(cmd, &cfg.termsURL, "terms-of-service", "", "", "OpenAPI terms of service URL in spec")
|
||||
cli.StringFlag(cmd, &cfg.contactName, "contact-name", "", "", "OpenAPI contact name in spec")
|
||||
cli.StringFlag(cmd, &cfg.contactURL, "contact-url", "", "", "OpenAPI contact URL in spec")
|
||||
|
|
|
|||
|
|
@ -790,6 +790,53 @@ func TestAPISpecCmd_Good_RuntimePathsPopulatedSpec(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAPISpecCmd_Good_AuthentikFlagsPopulateSpecMetadata(t *testing.T) {
|
||||
root := &cli.Command{Use: "root"}
|
||||
AddAPICommands(root)
|
||||
|
||||
outputFile := t.TempDir() + "/spec.json"
|
||||
root.SetArgs([]string{
|
||||
"api", "spec",
|
||||
"--authentik-issuer", "https://auth.example.com",
|
||||
"--authentik-client-id", "core-client",
|
||||
"--authentik-trusted-proxy",
|
||||
"--authentik-public-paths", "/public, /docs, /public",
|
||||
"--output", outputFile,
|
||||
})
|
||||
root.SetErr(new(bytes.Buffer))
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(outputFile)
|
||||
if err != nil {
|
||||
t.Fatalf("expected spec file to be written: %v", err)
|
||||
}
|
||||
|
||||
var spec map[string]any
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
t.Fatalf("expected valid JSON spec, got error: %v", err)
|
||||
}
|
||||
|
||||
if got := spec["x-authentik-issuer"]; got != "https://auth.example.com" {
|
||||
t.Fatalf("expected x-authentik-issuer=https://auth.example.com, got %v", got)
|
||||
}
|
||||
if got := spec["x-authentik-client-id"]; got != "core-client" {
|
||||
t.Fatalf("expected x-authentik-client-id=core-client, got %v", got)
|
||||
}
|
||||
if got := spec["x-authentik-trusted-proxy"]; got != true {
|
||||
t.Fatalf("expected x-authentik-trusted-proxy=true, got %v", got)
|
||||
}
|
||||
publicPaths, ok := spec["x-authentik-public-paths"].([]any)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPISDKCmd_Bad_EmptyLanguages(t *testing.T) {
|
||||
root := &cli.Command{Use: "root"}
|
||||
AddAPICommands(root)
|
||||
|
|
@ -892,6 +939,18 @@ func TestAPISDKCmd_Good_ValidatesLanguage(t *testing.T) {
|
|||
if sdkCmd.Flag("i18n-supported-locales") == nil {
|
||||
t.Fatal("expected --i18n-supported-locales flag on sdk command")
|
||||
}
|
||||
if sdkCmd.Flag("authentik-issuer") == nil {
|
||||
t.Fatal("expected --authentik-issuer flag on sdk command")
|
||||
}
|
||||
if sdkCmd.Flag("authentik-client-id") == nil {
|
||||
t.Fatal("expected --authentik-client-id flag on sdk command")
|
||||
}
|
||||
if sdkCmd.Flag("authentik-trusted-proxy") == nil {
|
||||
t.Fatal("expected --authentik-trusted-proxy flag on sdk command")
|
||||
}
|
||||
if sdkCmd.Flag("authentik-public-paths") == nil {
|
||||
t.Fatal("expected --authentik-public-paths flag on sdk command")
|
||||
}
|
||||
if sdkCmd.Flag("terms-of-service") == nil {
|
||||
t.Fatal("expected --terms-of-service flag on sdk command")
|
||||
}
|
||||
|
|
@ -929,31 +988,35 @@ func TestAPISDKCmd_Good_TempSpecUsesMetadataFlags(t *testing.T) {
|
|||
api.RegisterSpecGroups(specCmdStubGroup{})
|
||||
|
||||
builder, err := sdkSpecBuilder(specBuilderConfig{
|
||||
title: "Custom SDK API",
|
||||
summary: "Custom SDK overview",
|
||||
description: "Custom SDK description",
|
||||
version: "9.9.9",
|
||||
swaggerPath: "/docs",
|
||||
graphqlPath: "/gql",
|
||||
graphqlPlayground: true,
|
||||
ssePath: "/events",
|
||||
wsPath: "/ws",
|
||||
pprofEnabled: true,
|
||||
expvarEnabled: true,
|
||||
cacheEnabled: true,
|
||||
cacheTTL: "5m0s",
|
||||
cacheMaxEntries: 42,
|
||||
cacheMaxBytes: 8192,
|
||||
i18nDefaultLocale: "en-GB",
|
||||
i18nSupportedLocales: "en-GB,fr,en-GB",
|
||||
termsURL: "https://example.com/terms",
|
||||
contactName: "SDK Support",
|
||||
contactURL: "https://example.com/support",
|
||||
contactEmail: "support@example.com",
|
||||
licenseName: "EUPL-1.2",
|
||||
licenseURL: "https://eupl.eu/1.2/en/",
|
||||
servers: "https://api.example.com, /, https://api.example.com",
|
||||
securitySchemes: `{"apiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"}}`,
|
||||
title: "Custom SDK API",
|
||||
summary: "Custom SDK overview",
|
||||
description: "Custom SDK description",
|
||||
version: "9.9.9",
|
||||
swaggerPath: "/docs",
|
||||
graphqlPath: "/gql",
|
||||
graphqlPlayground: true,
|
||||
ssePath: "/events",
|
||||
wsPath: "/ws",
|
||||
pprofEnabled: true,
|
||||
expvarEnabled: true,
|
||||
cacheEnabled: true,
|
||||
cacheTTL: "5m0s",
|
||||
cacheMaxEntries: 42,
|
||||
cacheMaxBytes: 8192,
|
||||
i18nDefaultLocale: "en-GB",
|
||||
i18nSupportedLocales: "en-GB,fr,en-GB",
|
||||
authentikIssuer: "https://auth.example.com",
|
||||
authentikClientID: "core-client",
|
||||
authentikTrustedProxy: true,
|
||||
authentikPublicPaths: "/public, /docs, /public",
|
||||
termsURL: "https://example.com/terms",
|
||||
contactName: "SDK Support",
|
||||
contactURL: "https://example.com/support",
|
||||
contactEmail: "support@example.com",
|
||||
licenseName: "EUPL-1.2",
|
||||
licenseURL: "https://eupl.eu/1.2/en/",
|
||||
servers: "https://api.example.com, /, https://api.example.com",
|
||||
securitySchemes: `{"apiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"}}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error building sdk spec: %v", err)
|
||||
|
|
@ -1040,6 +1103,22 @@ func TestAPISDKCmd_Good_TempSpecUsesMetadataFlags(t *testing.T) {
|
|||
if len(locales) != 2 || locales[0] != "en-GB" || locales[1] != "fr" {
|
||||
t.Fatalf("expected supported locales [en-GB fr], got %v", locales)
|
||||
}
|
||||
if got := spec["x-authentik-issuer"]; got != "https://auth.example.com" {
|
||||
t.Fatalf("expected x-authentik-issuer=https://auth.example.com, got %v", got)
|
||||
}
|
||||
if got := spec["x-authentik-client-id"]; got != "core-client" {
|
||||
t.Fatalf("expected x-authentik-client-id=core-client, got %v", got)
|
||||
}
|
||||
if got := spec["x-authentik-trusted-proxy"]; got != true {
|
||||
t.Fatalf("expected x-authentik-trusted-proxy=true, got %v", got)
|
||||
}
|
||||
publicPaths, ok := spec["x-authentik-public-paths"].([]any)
|
||||
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 info["termsOfService"] != "https://example.com/terms" {
|
||||
t.Fatalf("expected termsOfService to be preserved, got %v", info["termsOfService"])
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ type specBuilderConfig struct {
|
|||
cacheMaxBytes int
|
||||
i18nDefaultLocale string
|
||||
i18nSupportedLocales string
|
||||
authentikIssuer string
|
||||
authentikClientID string
|
||||
authentikTrustedProxy bool
|
||||
authentikPublicPaths string
|
||||
termsURL string
|
||||
contactName string
|
||||
contactURL string
|
||||
|
|
@ -75,6 +79,10 @@ func newSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) {
|
|||
LicenseURL: cfg.licenseURL,
|
||||
ExternalDocsDescription: cfg.externalDocsDescription,
|
||||
ExternalDocsURL: cfg.externalDocsURL,
|
||||
AuthentikIssuer: strings.TrimSpace(cfg.authentikIssuer),
|
||||
AuthentikClientID: strings.TrimSpace(cfg.authentikClientID),
|
||||
AuthentikTrustedProxy: cfg.authentikTrustedProxy,
|
||||
AuthentikPublicPaths: splitUniqueCSV(cfg.authentikPublicPaths),
|
||||
}
|
||||
|
||||
builder.I18nSupportedLocales = parseLocales(cfg.i18nSupportedLocales)
|
||||
|
|
|
|||
18
openapi.go
18
openapi.go
|
|
@ -17,7 +17,7 @@ import (
|
|||
// SpecBuilder constructs an OpenAPI 3.1 specification from registered RouteGroups.
|
||||
// Title, Summary, Description, Version, and optional contact/licence/terms metadata populate the
|
||||
// OpenAPI info block. Top-level external documentation metadata is also supported, along with
|
||||
// additive extension fields that describe runtime transport, cache, and i18n settings.
|
||||
// additive extension fields that describe runtime transport, cache, i18n, and Authentik settings.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
|
|
@ -55,6 +55,10 @@ type SpecBuilder struct {
|
|||
CacheMaxBytes int
|
||||
I18nDefaultLocale string
|
||||
I18nSupportedLocales []string
|
||||
AuthentikIssuer string
|
||||
AuthentikClientID string
|
||||
AuthentikTrustedProxy bool
|
||||
AuthentikPublicPaths []string
|
||||
}
|
||||
|
||||
type preparedRouteGroup struct {
|
||||
|
|
@ -156,6 +160,18 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
|
|||
if len(sb.I18nSupportedLocales) > 0 {
|
||||
spec["x-i18n-supported-locales"] = slices.Clone(sb.I18nSupportedLocales)
|
||||
}
|
||||
if issuer := strings.TrimSpace(sb.AuthentikIssuer); issuer != "" {
|
||||
spec["x-authentik-issuer"] = issuer
|
||||
}
|
||||
if clientID := strings.TrimSpace(sb.AuthentikClientID); clientID != "" {
|
||||
spec["x-authentik-client-id"] = clientID
|
||||
}
|
||||
if sb.AuthentikTrustedProxy {
|
||||
spec["x-authentik-trusted-proxy"] = true
|
||||
}
|
||||
if len(sb.AuthentikPublicPaths) > 0 {
|
||||
spec["x-authentik-public-paths"] = slices.Clone(sb.AuthentikPublicPaths)
|
||||
}
|
||||
|
||||
if sb.TermsOfService != "" {
|
||||
spec["info"].(map[string]any)["termsOfService"] = sb.TermsOfService
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ type SwaggerConfig struct {
|
|||
}
|
||||
|
||||
// OpenAPISpecBuilder returns a SpecBuilder populated from the engine's current
|
||||
// Swagger, transport, cache, and i18n metadata.
|
||||
// Swagger, transport, cache, i18n, and Authentik metadata.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
|
|
@ -85,6 +85,10 @@ func (e *Engine) OpenAPISpecBuilder() *SpecBuilder {
|
|||
|
||||
builder.I18nDefaultLocale = runtime.I18n.DefaultLocale
|
||||
builder.I18nSupportedLocales = slices.Clone(runtime.I18n.Supported)
|
||||
builder.AuthentikIssuer = runtime.Authentik.Issuer
|
||||
builder.AuthentikClientID = runtime.Authentik.ClientID
|
||||
builder.AuthentikTrustedProxy = runtime.Authentik.TrustedProxy
|
||||
builder.AuthentikPublicPaths = slices.Clone(runtime.Authentik.PublicPaths)
|
||||
|
||||
return builder
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) {
|
|||
DefaultLocale: "en-GB",
|
||||
Supported: []string{"en-GB", "fr"},
|
||||
}),
|
||||
api.WithAuthentik(api.AuthentikConfig{
|
||||
Issuer: "https://auth.example.com",
|
||||
ClientID: "core-client",
|
||||
TrustedProxy: true,
|
||||
PublicPaths: []string{"/public", "/docs"},
|
||||
}),
|
||||
api.WithWSPath("/socket"),
|
||||
api.WithWSHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})),
|
||||
api.WithGraphQL(newTestSchema(), api.WithPlayground(), api.WithGraphQLPath("/gql")),
|
||||
|
|
@ -133,6 +139,22 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) {
|
|||
if len(locales) != 2 || locales[0] != "en-GB" || locales[1] != "fr" {
|
||||
t.Fatalf("expected supported locales [en-GB fr], got %v", locales)
|
||||
}
|
||||
if got := spec["x-authentik-issuer"]; got != "https://auth.example.com" {
|
||||
t.Fatalf("expected x-authentik-issuer=https://auth.example.com, got %v", got)
|
||||
}
|
||||
if got := spec["x-authentik-client-id"]; got != "core-client" {
|
||||
t.Fatalf("expected x-authentik-client-id=core-client, got %v", got)
|
||||
}
|
||||
if got := spec["x-authentik-trusted-proxy"]; got != true {
|
||||
t.Fatalf("expected x-authentik-trusted-proxy=true, got %v", got)
|
||||
}
|
||||
publicPaths, ok := spec["x-authentik-public-paths"].([]any)
|
||||
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)
|
||||
}
|
||||
|
||||
contact, ok := info["contact"].(map[string]any)
|
||||
if !ok {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue