diff --git a/configuration-schema.json b/configuration-schema.json index b80019e64..4124bec14 100644 --- a/configuration-schema.json +++ b/configuration-schema.json @@ -134,7 +134,7 @@ }, "enable-auth-scopes-on-context": { "type": "boolean", - "description": "Re-enables the legacy emission of security scheme scopes by generated server code: the per-scheme context key types (e.g. `bearerAuthContextKey`), the scope constants (e.g. `BearerAuthScopes`), and the per-operation calls that store the operation's scopes into the request context.\nThis mechanism is deprecated and off by default: it flattens the OpenAPI `security` requirements into a per-scheme list of scopes, and cannot represent alternative schemes (OR), combined schemes (AND), or anonymous (`{}`) alternatives. Authentication and authorization should instead be performed at runtime using the request validation middleware, which evaluates the spec's security requirements directly.\nPlease see https://github.com/oapi-codegen/oapi-codegen/issues/1524" + "description": "DEPRECATED: perform authentication and authorization at runtime using the request validation middleware instead, which evaluates the spec's security requirements directly. Please see https://github.com/oapi-codegen/oapi-codegen/issues/1524\nRe-enables the legacy emission of security scheme scopes by generated server code: the per-scheme context key types (e.g. `bearerAuthContextKey`), the scope constants (e.g. `BearerAuthScopes`), and the per-operation calls that store the operation's scopes into the request context.\nThis mechanism is off by default: it flattens the OpenAPI `security` requirements into a per-scheme list of scopes, and cannot represent alternative schemes (OR), combined schemes (AND), or anonymous (`{}`) alternatives.\nA security scheme that is a $ref into a spec covered by import-mapping does not declare its own context key type; its scopes constant is an alias of the one in the mapped package, so `context.Value` lookups use the same key across the generated packages. This requires the mapped spec's config to also set this flag. Please see https://github.com/oapi-codegen/oapi-codegen/issues/2383" } } }, diff --git a/internal/test/references/multipackage/externalref.cfg.yaml b/internal/test/references/multipackage/externalref.cfg.yaml index 7ccf24f22..afeb46f19 100644 --- a/internal/test/references/multipackage/externalref.cfg.yaml +++ b/internal/test/references/multipackage/externalref.cfg.yaml @@ -10,3 +10,7 @@ import-mapping: output: externalref.gen.go output-options: skip-prune: true +# issue-2383: the BearerAuth scopes constant must alias packageA's instead of +# declaring a distinct context key type. +compatibility: + enable-auth-scopes-on-context: true diff --git a/internal/test/references/multipackage/externalref.gen.go b/internal/test/references/multipackage/externalref.gen.go index 91806f1f3..c91e0df06 100644 --- a/internal/test/references/multipackage/externalref.gen.go +++ b/internal/test/references/multipackage/externalref.gen.go @@ -18,6 +18,10 @@ import ( externalRef1 "github.com/oapi-codegen/oapi-codegen/v2/internal/test/references/multipackage/petstore" ) +const ( + BearerAuthScopes = externalRef0.BearerAuthScopes +) + // Container defines model for Container. type Container struct { ObjectA *externalRef0.ObjectA `json:"object_a,omitempty"` @@ -31,15 +35,16 @@ type Container struct { // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "vFVNb9swDP0rAbej62bYsINvbXdfDtupKAxGZhxt1scouk1R+L8PkmPHjTs0G7JeEkUUn/jeI5UnUM54", - "Z8lKgOIJgtqSwbS8cVZQW+L4w7PzxKIphdz6BykpMa7fM22ggHeXB6DLPcqlR/UTa7oqgydVfk1ZV9Bl", - "A8D6RIDrKcD1BEC9BjCe6zLwJK8dDw9Y18QX6HU5rD1JEMdUMm1CuSWsQmkwCHEZWJUGtS2ZgmtZUSid", - "JxuzVyTQdUeVYlVp0c5is5roKdxSBvLoCYr98VTui+LNvLBoKH7v84OwtvUZNO4OJRyFTi0hIpxP0BsU", - "qh0/zq/XVfykHRrfEBQfMtg4NihQgLby+ROM4morVBNHdYaaxzT44upwODqSOHYmg93FwIRdK8QXxlXU", - "RHiX7yP5wDFPsXwsPoOdaQ6agRoC5xVr1bf6c53URMG3GYKRdpfNXFr+o02Vq2tNc6My8Fsn7js3fVMI", - "mTBvymP9h5yp0ciME6ceGL2nqp/TZJOgtAm7oqBY+zjSEYtk0ccW2i5kS4skGWRAtjVQ3ALeo25w3cQ9", - "T7bqKwquqeDuBUKC9XMub+PaN+xvP0WNLgOmX63muHXbqzp14u4s4xPbeeZcemDPOTSR9x8el7/o1P/y", - "fsTSjgUQTG9sHC27celqLWlEIIN74tB3Zfor6BlCAR/zZb6MDqFsI7+u+x0AAP//", + "vFXBbtswDP2VgNvRdTts2MG3truvwLZTURiMzNjabEmj6LZB4X8fJMeOY7doNmS9tIooPj++R0pPoGzj", + "rCEjHrIn8KqiBuPy2hpBbYjDD8fWEYumGLLrn6Qkx7B+z7SBDN6d74HOdyjnDtUvLOky945U/jVmXUKX", + "DADrIwGupgBXEwD1GsB4rkvAkbx23D9gWRKfodP5sHYkXixTzrTxeUVY+LxBL8S5Z5U3qE3O5G3Linxu", + "HZmQfUMCXTdjikWhRVuD9c1ET+GWEpCtI8h2xyPdZ8VbeGGwofB/l++FtSlPoHG3pzALHUshIJxO0GsU", + "Ki1vl5/XRfhLj9i4miD7kMDGcoMCGWgjnz/BKK42QiVxUGfgPKbBF1v6/dGxiLkzCTyeDZWwbYX4rLEF", + "1QHeprtIOtSYxlg6kk/gsan3moEaAqcV66Zv9UOd1ETBtxmCsewuWbh08Y82FbYsNS2NSsBVVuwPrvum", + "EGr8sinn+g85U6ORGSdOPTA6R0U/p9EmQWkjdkFesXZhpAMWyaqPrbRZSUWrKBkkQKZtILsFvEdd47oO", + "e45M0TPyti7g7pmCBMvDWt7Gte/Yf/0YNboEmH63msPWba/q1Im7k4xPaOeFc/GCPeXQhLpfuFz+olP/", + "y/0RqM0FEBzuWFIta9l+C53Qs74iZOLLVqoX2+YwafZUT/IXL9Ehdmy/QGcdt/fSVCKu56fNxkZltMQJ", + "hgTuiX0/NPGl6g2ADD6mF+lFaCCUKhTSdX8CAAD//w==", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/test/references/multipackage/imports_test.go b/internal/test/references/multipackage/imports_test.go index 9af788c1d..8163764f1 100644 --- a/internal/test/references/multipackage/imports_test.go +++ b/internal/test/references/multipackage/imports_test.go @@ -1,6 +1,7 @@ package referencesmultipackage import ( + "context" "testing" packageA "github.com/oapi-codegen/oapi-codegen/v2/internal/test/references/multipackage/packageA" @@ -30,3 +31,13 @@ func TestGetSwagger(t *testing.T) { _, err = GetSpec() require.Nil(t, err) } + +// TestSecuritySchemeScopesShared verifies that the scopes context key of a +// security scheme $ref'd from an import-mapped spec is shared across the +// generated packages: this package's BearerAuthScopes aliases packageA's, so +// a context value stored under one key is retrievable with the other. +// Reproduces https://github.com/oapi-codegen/oapi-codegen/issues/2383 +func TestSecuritySchemeScopesShared(t *testing.T) { + ctx := context.WithValue(context.Background(), packageA.BearerAuthScopes, []string{"read"}) + require.Equal(t, []string{"read"}, ctx.Value(BearerAuthScopes)) +} diff --git a/internal/test/references/multipackage/packageA/config.yaml b/internal/test/references/multipackage/packageA/config.yaml index baeb61083..fe33fdf90 100644 --- a/internal/test/references/multipackage/packageA/config.yaml +++ b/internal/test/references/multipackage/packageA/config.yaml @@ -3,6 +3,10 @@ package: packagea generate: models: true embedded-spec: true +# issue-2383: emit the BearerAuth scopes context key so that packages +# generated from specs $ref'ing this one can alias it. +compatibility: + enable-auth-scopes-on-context: true output-options: skip-prune: true import-mapping: diff --git a/internal/test/references/multipackage/packageA/externalref.gen.go b/internal/test/references/multipackage/packageA/externalref.gen.go index 79044bd95..3d0047f24 100644 --- a/internal/test/references/multipackage/packageA/externalref.gen.go +++ b/internal/test/references/multipackage/packageA/externalref.gen.go @@ -16,6 +16,10 @@ import ( externalRef0 "github.com/oapi-codegen/oapi-codegen/v2/internal/test/references/multipackage/packageB" ) +const ( + BearerAuthScopes bearerAuthContextKey = "BearerAuth.Scopes" +) + // EnrichedUser defines model for EnrichedUser. type EnrichedUser struct { ExtraField *string `json:"extra_field,omitempty"` @@ -36,17 +40,21 @@ type StatusPostPayload struct { Status externalRef0.StatusEnum `json:"status"` } +// bearerAuthContextKey is the context key for BearerAuth security scheme +type bearerAuthContextKey string + // Base64 encoded, compressed with deflate, json marshaled OpenAPI spec. // Stored as a slice of fixed-width chunks rather than one concatenated // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "nFPNbsIwDH4Xb8dIiGtuQ+IM2jQuCFUmcSFbmmRJOg1VefcpoQgqOqnsZtf21++n7UDYxllDJgbgHQRx", - "pAZLuTReiSPJ90A+96j1qga+7eDZUw0cnmbX21l/OHMoPvFAiyo4ElW5TawD560jHxUVaPqJHqtakZa5", - "jSdHwCFEr8wBUmKXJ3b/QSJC2iUGq1K/5P0hmMGGRlBYf13t83A65fN7FpAyj7eIsQ1rG+IaT9qi/K8R", - "Z6DNfMQMTxismezDONuprqQ7hFeryyKZtgG+BZSNMsCgzdHt2L2tY8KW5fgGRET1TcBAmb6cirSZDy0e", - "qgpl57FAbxhm9Z6+WuVJZpo93G6C0Ze/YMhHydEvz1vdzyM1D9IteVyjR+/xlPucx1+hDlUpCTfr9+Ly", - "vjK1BW5arRlYRwadAg6QVcdjOE/SbwAAAP//", + "nFRNq9swEPwv2x4F4V19e4F3fqGhuYRgFHkdq5UldbUuNUb/vUh2PkxccHrTancmszOKB1Cu9c6i5QDF", + "AEE12Mp8/LCkVYPV94CUamnMZw3FcYCvhDUU8GVzx24m4MZL9VNecFsGj6rM2CgG8OQ8EmvM1PiHSZa1", + "RlOlknuPUEBg0vYCMYrrjTv/QMUQT1HAZz6/p/k5mZUtLrCICV2eU3O95PF3thCTjj1L7sLOBd7J3jhZ", + "/a8RI9HhbcEMQhmcXe3Dstq1rsQnhm/O5EG0XQvFEWTVagsCuhTdSTzburTYRwY/kCjWvxEEaDsd1zId", + "3uYWz7cKeea1QB8Upu0Jf3WasEoyJ7rTCqOv/4K5Hl0tvjxyZuozti/KzXnco5dEsk91yuNfoc630hU8", + "jD8vFwUEVB1p7vdJwih1i5KQ3jtubt+BBDrna7iRNMx+fEba1g4K2xkjwHm00msoAJJx3ISxE/8GAAD/", + "/w==", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/test/references/multipackage/packageA/spec.yaml b/internal/test/references/multipackage/packageA/spec.yaml index 7b790839e..3299fc140 100644 --- a/internal/test/references/multipackage/packageA/spec.yaml +++ b/internal/test/references/multipackage/packageA/spec.yaml @@ -1,4 +1,11 @@ components: + # Reproduces https://github.com/oapi-codegen/oapi-codegen/issues/2383 + # A security scheme shared via $ref must share its scopes context key + # across the packages generated from the referencing specs. + securitySchemes: + BearerAuth: + type: http + scheme: bearer schemas: ObjectA: properties: diff --git a/internal/test/references/multipackage/spec.yaml b/internal/test/references/multipackage/spec.yaml index a0540389f..dd4ca574c 100644 --- a/internal/test/references/multipackage/spec.yaml +++ b/internal/test/references/multipackage/spec.yaml @@ -2,6 +2,12 @@ openapi: "3.0.0" info: { } paths: { } components: + # Reproduces https://github.com/oapi-codegen/oapi-codegen/issues/2383 + # The scopes constant for a $ref'd security scheme must alias the one + # declared by the import-mapped package, sharing the context key. + securitySchemes: + BearerAuth: + $ref: ./packageA/spec.yaml#/components/securitySchemes/BearerAuth schemas: Container: properties: diff --git a/internal/test/servers/middleware/fiber/middleware.gen.go b/internal/test/servers/middleware/fiber/middleware.gen.go index c279fb75e..b6f200db8 100644 --- a/internal/test/servers/middleware/fiber/middleware.gen.go +++ b/internal/test/servers/middleware/fiber/middleware.gen.go @@ -7,6 +7,10 @@ import ( "github.com/gofiber/fiber/v2" ) +const ( + BearerAuthScopes bearerAuthContextKey = "bearerAuth.Scopes" +) + // bearerAuthContextKey is the context key for bearerAuth security scheme type bearerAuthContextKey string diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index ea29b723e..90ced55a3 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -30,7 +30,6 @@ import ( "regexp" "runtime/debug" "slices" - "sort" "strings" "text/template" "time" @@ -317,7 +316,7 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { return "", fmt.Errorf("error generating Go types for operations: %w", err) } - constantDefinitions, err = GenerateConstants(t, allOps) + constantDefinitions, err = GenerateConstants(t, spec) if err != nil { return "", fmt.Errorf("error generating constants: %w", err) } @@ -1036,25 +1035,66 @@ func renderBoilerplate(t *template.Template, allEmitted []TypeDefinition) (enums } // GenerateConstants generates operation ids, context keys, paths, etc. to be exported as constants -func GenerateConstants(t *template.Template, ops []OperationDefinition) (string, error) { - constants := Constants{ - SecuritySchemeProviderNames: []string{}, - } - - providerNameMap := map[string]struct{}{} - for _, op := range ops { - for _, def := range op.SecurityDefinitions { - providerName := SanitizeGoIdentity(def.ProviderName) - providerNameMap[providerName] = struct{}{} +// +// Scopes constants are derived from components/securitySchemes rather than +// from the operations' security requirements (which are filtered to defined +// schemes anyway), so that a spec holding shared definitions with no paths +// still exports the constants other packages alias via import-mapping. +func GenerateConstants(t *template.Template, swagger *openapi3.T) (string, error) { + var constants Constants + + if swagger.Components != nil { + for _, schemeName := range SortedSecuritySchemeKeys(swagger.Components.SecuritySchemes) { + provider := SecuritySchemeProvider{Name: SanitizeGoIdentity(schemeName)} + alias := importedSecuritySchemeScopes(swagger.Components.SecuritySchemes[schemeName].Ref) + if alias == securitySchemeScopesConstant(provider.Name) { + // The scheme $refs a spec that import-mapping assigns to the + // current package under the same name: the sibling config + // generating that spec into this package already declares + // the constant, so re-declaring it here would collide. + continue + } + provider.ImportedScopes = alias + constants.SecuritySchemeProviders = append(constants.SecuritySchemeProviders, provider) } } - providerNames := slices.Collect(maps.Keys(providerNameMap)) - sort.Strings(providerNames) + return GenerateTemplates([]string{"constants.tmpl"}, t, constants) +} - constants.SecuritySchemeProviderNames = append(constants.SecuritySchemeProviderNames, providerNames...) +// securitySchemeScopesConstant returns the name of the generated scopes +// context-key constant for a security scheme name. It must mirror the name +// construction in constants.tmpl (`sanitizeGoIdentity | ucFirst` + "Scopes"). +func securitySchemeScopesConstant(schemeName string) string { + return UppercaseFirstCharacter(SanitizeGoIdentity(schemeName)) + "Scopes" +} - return GenerateTemplates([]string{"constants.tmpl"}, t, constants) +// importedSecuritySchemeScopes resolves a security scheme's $ref through +// import-mapping to the scopes constant declared by the package generated +// from the ref'd document. It returns "" when the scheme must be declared +// locally: it is defined inline, the ref is internal, or the ref'd document +// has no import-mapping entry (each package then keeps its own declaration, +// matching the behavior from before typed context keys existed). For a +// document mapped to the current package ("-") the returned constant is +// unqualified — the sibling config generating that document declares it. +func importedSecuritySchemeScopes(ref string) string { + if ref == "" || strings.HasPrefix(ref, "#") { + return "" + } + pathParts := strings.Split(ref, "#") + if len(pathParts) != 2 { + return "" + } + goPkg, ok := globalState.importMapping[pathParts[0]] + if !ok { + return "" + } + componentParts := strings.Split(pathParts[1], "/") + constName := securitySchemeScopesConstant(componentParts[len(componentParts)-1]) + if goPkg.Path == importMappingCurrentPackage { + return constName + } + return fmt.Sprintf("%s.%s", goPkg.Name, constName) } // GenerateTypesForSchemas generates type definitions for any custom types defined in the @@ -1272,6 +1312,13 @@ func GenerateTypesForSecuritySchemes(t *template.Template, schemes map[string]*o var types []TypeDefinition for _, schemeName := range SortedSecuritySchemeKeys(schemes) { + if importedSecuritySchemeScopes(schemes[schemeName].Ref) != "" { + // The scheme $refs a spec assigned to another package by + // import-mapping. That package declares the context key type; + // the scopes constant alias emitted by GenerateConstants + // carries it over, so no local type is declared. + continue + } // Generate a type to be used as a key in context.WithValue goTypeName := LowercaseFirstCharacter(SchemaNameToTypeName(schemeName)) + "ContextKey" goType := Schema{ diff --git a/pkg/codegen/codegen_test.go b/pkg/codegen/codegen_test.go index 8bce786fb..e468e03d8 100644 --- a/pkg/codegen/codegen_test.go +++ b/pkg/codegen/codegen_test.go @@ -3,6 +3,9 @@ package codegen import ( _ "embed" "go/format" + "os" + "path/filepath" + "strings" "testing" "github.com/getkin/kin-openapi/openapi3" @@ -578,5 +581,163 @@ paths: assert.Contains(t, code, `ctx = context.WithValue(ctx, BearerAuthScopes, []string{"read"})`) } +const securityScopesSharedSpec = ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Common +paths: {} +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer +` + +const securityScopesUserSpec = ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: User API +paths: + /user: + get: + operationId: getUser + security: + - BearerAuth: ["read"] + - LocalAuth: [] + responses: + '200': + description: ok +components: + securitySchemes: + BearerAuth: + $ref: './common.yml#/components/securitySchemes/BearerAuth' + LocalAuth: + type: http + scheme: basic +` + +// loadSecurityScopesUserSpec writes the shared/user spec pair to disk and +// loads the user spec, resolving the cross-file security scheme $ref. +func loadSecurityScopesUserSpec(t *testing.T) *openapi3.T { + t.Helper() + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "common.yml"), []byte(securityScopesSharedSpec), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "user.yml"), []byte(securityScopesUserSpec), 0o600)) + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + swagger, err := loader.LoadFromFile(filepath.Join(dir, "user.yml")) + require.NoError(t, err) + return swagger +} + +// TestSecuritySchemeScopesWithImportMapping verifies that a security scheme +// $ref'd from an import-mapped spec aliases the scopes constant declared by +// the mapped package instead of declaring its own context key type, so +// shared middleware sees a single context key across generated packages. +// Please see https://github.com/oapi-codegen/oapi-codegen/issues/2383 +func TestSecuritySchemeScopesWithImportMapping(t *testing.T) { + swagger := loadSecurityScopesUserSpec(t) + + opts := Configuration{ + PackageName: "user", + Generate: GenerateOptions{ + StdHTTPServer: true, + Models: true, + }, + Compatibility: CompatibilityOptions{EnableAuthScopesOnContext: true}, + ImportMapping: map[string]string{"./common.yml": "example.com/common"}, + } + + code, err := Generate(swagger, opts) + require.NoError(t, err) + + // The const block is column-aligned by gofmt, so collapse runs of + // whitespace before matching the declarations. + normalized := strings.Join(strings.Fields(code), " ") + + // The imported scheme aliases the shared constant — carrying the shared + // package's context key type — and declares no local type; middleware + // still references the constant by its local name. + assert.Contains(t, normalized, "BearerAuthScopes = externalRef0.BearerAuthScopes") + assert.NotContains(t, code, "bearerAuthContextKey") + assert.Contains(t, code, `ctx = context.WithValue(ctx, BearerAuthScopes, []string{"read"})`) + + // The locally declared scheme keeps its own typed constant. + assert.Contains(t, normalized, `LocalAuthScopes localAuthContextKey = "LocalAuth.Scopes"`) +} + +// TestSecuritySchemeScopesWithoutImportMapping verifies that an external +// scheme $ref without an import-mapping entry keeps the historical behavior +// of declaring the context key type and constant locally. +func TestSecuritySchemeScopesWithoutImportMapping(t *testing.T) { + swagger := loadSecurityScopesUserSpec(t) + + opts := Configuration{ + PackageName: "user", + Generate: GenerateOptions{ + StdHTTPServer: true, + Models: true, + }, + Compatibility: CompatibilityOptions{EnableAuthScopesOnContext: true}, + } + + code, err := Generate(swagger, opts) + require.NoError(t, err) + + assert.Contains(t, code, `BearerAuthScopes bearerAuthContextKey = "BearerAuth.Scopes"`) +} + +// TestSecuritySchemeScopesCurrentPackageMapping verifies that a scheme $ref'd +// from a spec mapped to the current package ("-") emits neither the type nor +// the constant: the sibling config generating that spec into the same package +// declares both. +func TestSecuritySchemeScopesCurrentPackageMapping(t *testing.T) { + swagger := loadSecurityScopesUserSpec(t) + + opts := Configuration{ + PackageName: "user", + Generate: GenerateOptions{ + StdHTTPServer: true, + Models: true, + }, + Compatibility: CompatibilityOptions{EnableAuthScopesOnContext: true}, + ImportMapping: map[string]string{"./common.yml": "-"}, + } + + code, err := Generate(swagger, opts) + require.NoError(t, err) + + // No local declarations for the shared scheme... + assert.NotContains(t, code, "bearerAuthContextKey") + assert.NotContains(t, code, `= "BearerAuth.Scopes"`) + // ...but middleware references the sibling-declared constant. + assert.Contains(t, code, `ctx = context.WithValue(ctx, BearerAuthScopes, []string{"read"})`) +} + +// TestSecuritySchemeScopesWithoutOperations verifies that a spec holding only +// shared definitions (no paths) still exports the scopes constants, so that +// other packages can alias them via import-mapping. +func TestSecuritySchemeScopesWithoutOperations(t *testing.T) { + loader := openapi3.NewLoader() + swagger, err := loader.LoadFromData([]byte(securityScopesSharedSpec)) + require.NoError(t, err) + + opts := Configuration{ + PackageName: "common", + Generate: GenerateOptions{ + Models: true, + }, + Compatibility: CompatibilityOptions{EnableAuthScopesOnContext: true}, + } + + code, err := Generate(swagger, opts) + require.NoError(t, err) + + assert.Contains(t, code, `BearerAuthScopes bearerAuthContextKey = "BearerAuth.Scopes"`) +} + //go:embed test_spec.yaml var testOpenAPIDefinition string diff --git a/pkg/codegen/configuration.go b/pkg/codegen/configuration.go index 88c0a39c8..593498462 100644 --- a/pkg/codegen/configuration.go +++ b/pkg/codegen/configuration.go @@ -358,7 +358,16 @@ type CompatibilityOptions struct { // types (e.g. `bearerAuthContextKey`), the scope constants (e.g. // `BearerAuthScopes`), and the per-operation calls that store the // operation's scopes into the request context. - // This mechanism is deprecated and off by default: it flattens the + // + // A security scheme that is a $ref into a spec covered by import-mapping + // does not declare its own context key type; its scopes constant is an + // alias of the one in the mapped package, so `context.Value` lookups use + // the same key across the generated packages. This requires the mapped + // spec's config to also set this flag, so that the referenced constant + // exists. Please see + // https://github.com/oapi-codegen/oapi-codegen/issues/2383 + // + // Deprecated: this mechanism is off by default because it flattens the // OpenAPI `security` requirements into a per-scheme list of scopes, and // cannot represent alternative schemes (OR), combined schemes (AND), or // anonymous (`{}`) alternatives. Authentication and authorization should diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index 0081fa41a..25eee3901 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -229,9 +229,28 @@ func (e *EnumDefinition) GetValues() map[string]string { return newValues } +// SecuritySchemeProvider describes one security scheme from +// components/securitySchemes for which a scopes context-key constant is +// generated. +type SecuritySchemeProvider struct { + // Name is the sanitized scheme name; templates derive the constant name + // (Scopes), the context key type name and the key's string value + // from it. + Name string + // ImportedScopes, when non-empty, is the scopes constant declared by the + // package that import-mapping assigns this scheme's $ref to (e.g. + // "externalRef0.BearerAuthScopes", or unqualified for the current + // package). The local constant is declared as an alias of it, so the + // context key — which context.Value compares by type and value — is + // shared across the generated packages. Empty means the scheme is + // declared locally with its own context key type. + ImportedScopes string +} + type Constants struct { - // SecuritySchemeProviderNames holds all provider names for security schemes. - SecuritySchemeProviderNames []string + // SecuritySchemeProviders holds all security schemes for which scopes + // context-key constants are generated. + SecuritySchemeProviders []SecuritySchemeProvider // EnumDefinitions holds type and value information for all enums EnumDefinitions []EnumDefinition // SkipEnumValidate suppresses generation of the `Valid()` method on diff --git a/pkg/codegen/templates/constants.tmpl b/pkg/codegen/templates/constants.tmpl index 4d451ae33..f09498c7e 100644 --- a/pkg/codegen/templates/constants.tmpl +++ b/pkg/codegen/templates/constants.tmpl @@ -1,7 +1,11 @@ -{{- if and (gt (len .SecuritySchemeProviderNames) 0) opts.Compatibility.EnableAuthScopesOnContext }} +{{- if and (gt (len .SecuritySchemeProviders) 0) opts.Compatibility.EnableAuthScopesOnContext }} const ( -{{range $ProviderName := .SecuritySchemeProviderNames}} - {{- $ProviderName | sanitizeGoIdentity | ucFirst}}Scopes {{$ProviderName | schemaNameToTypeName | lcFirst}}ContextKey = "{{$ProviderName}}.Scopes" +{{range $Provider := .SecuritySchemeProviders}} + {{- if $Provider.ImportedScopes}} + {{- $Provider.Name | sanitizeGoIdentity | ucFirst}}Scopes = {{$Provider.ImportedScopes}} + {{- else}} + {{- $Provider.Name | sanitizeGoIdentity | ucFirst}}Scopes {{$Provider.Name | schemaNameToTypeName | lcFirst}}ContextKey = "{{$Provider.Name}}.Scopes" + {{- end}} {{end}} ) {{end}}