Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion configuration-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions internal/test/references/multipackage/externalref.cfg.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 14 additions & 9 deletions internal/test/references/multipackage/externalref.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions internal/test/references/multipackage/imports_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package referencesmultipackage

import (
"context"
"testing"

packageA "github.com/oapi-codegen/oapi-codegen/v2/internal/test/references/multipackage/packageA"
Expand Down Expand Up @@ -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))
}
4 changes: 4 additions & 0 deletions internal/test/references/multipackage/packageA/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions internal/test/references/multipackage/packageA/spec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
6 changes: 6 additions & 0 deletions internal/test/references/multipackage/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions internal/test/servers/middleware/fiber/middleware.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 63 additions & 16 deletions pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import (
"regexp"
"runtime/debug"
"slices"
"sort"
"strings"
"text/template"
"time"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Comment thread
mromaszewicz marked this conversation as resolved.

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
Expand Down Expand Up @@ -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{
Expand Down
Loading