Skip to content

Commit 295f6c2

Browse files
khayashi4337devin-ai-integration[bot]claude
authored
fix(python-flask, python-aiohttp): generate valid security function names for scheme names with '-' or '.' (#25458)
* fix(python-flask, python-aiohttp): generate valid security function names for scheme names with '-' or '.' Security scheme names may contain '-' and '.' (component keys match ^[a-zA-Z0-9.\-_]+$), but the generated security_controller.py used them verbatim in function names, e.g. 'def info_from_api-key(', and the x-apikeyInfoFunc / x-basicInfoFunc / x-bearerInfoFunc / x-tokenInfoFunc / x-scopeValidateFunc extensions pointed at the same invalid names. The module didn't compile, so the app couldn't start. Each scheme name now gets one Python identifier suffix, computed once in preprocessOpenAPI in spec order: characters other than [A-Za-z0-9_] become '_', and names that would collide (e.g. 'api-key' and 'api.key') are numbered (_2, _3, ...). Both addSecurityExtensions and the security_controller templates use it, the latter through a fromSecurity override that sets x-python-connexion-security-function-suffix. Names that are already valid identifiers produce exactly the same output as before, and user-provided x-*InfoFunc extensions are kept. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(python-flask, python-aiohttp): keep unnumbered function names for already-valid security scheme names Reserve the security function name suffix of scheme names that are already valid Python identifiers before numbering the names that need character replacement. Previously suffixes were assigned purely in spec order, so a sanitized name like "api-key" -> "api_key" claimed the suffix first and "api_key" listed after it was bumped to "api_key_2" (and a literal "api_key_2" to "api_key_2_2"), changing the generated output for names that were already valid. Also make testSecuritySchemeNamesProduceValidPythonFunctions parse the generated openapi.yaml and assert, per scheme, that each x-*Func extension points at the expected function defined in security_controller.py, instead of only checking that the reference strings occur somewhere in the file. Add an "api_key" scheme listed after "api-key" to the fixture to cover the ordering regression. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * docs(python-flask, python-aiohttp): correct comment on how security function name suffixes are computed --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4043efc commit 295f6c2

5 files changed

Lines changed: 381 additions & 16 deletions

File tree

‎modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java‎

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ public void serialize(Boolean value, JsonGenerator gen, SerializerProvider seria
8686
protected boolean usePythonSrcRootInImports = Boolean.FALSE;
8787
protected boolean moveTestsUnderPythonSrcRoot = Boolean.FALSE;
8888

89+
// Python function name suffix per security scheme name (e.g. "api-key" -> "api_key").
90+
// Suffixes are assigned by addSecurityExtensions (called from preprocessOpenAPI):
91+
// scheme names that are already valid identifier parts keep their name, the rest are
92+
// numbered in spec order, so that the x-*Func references written into the spec and the
93+
// function definitions emitted by security_controller.mustache always agree, even when
94+
// the scheme name is not a valid Python identifier. fromSecurity only reads the values
95+
// that addSecurityExtensions has already computed.
96+
private final Map<String, String> securityFunctionNameSuffixes = new HashMap<>();
97+
8998
public AbstractPythonConnexionServerCodegen(String templateDirectory, boolean fixBodyNameValue) {
9099
super();
91100

@@ -459,26 +468,38 @@ private void addSecurityExtensions(OpenAPI openAPI) {
459468
Components components = openAPI.getComponents();
460469
if (components != null && components.getSecuritySchemes() != null) {
461470
Map<String, SecurityScheme> securitySchemes = components.getSecuritySchemes();
471+
securityFunctionNameSuffixes.clear();
472+
// Reserve the suffix of names that already are valid identifier parts (no
473+
// character replacement needed) so they keep the same function name as before.
474+
// The remaining names are numbered afterwards, in spec order, avoiding the
475+
// reserved suffixes (e.g. "api-key" listed before "api_key" gets "api_key_2",
476+
// while "api_key" keeps "api_key").
477+
for (String securityName : securitySchemes.keySet()) {
478+
if (toPythonIdentifierSuffix(securityName).equals(securityName)) {
479+
securityFunctionNameSuffixes.put(securityName, securityName);
480+
}
481+
}
462482
for (Map.Entry<String, SecurityScheme> securitySchemesEntry : securitySchemes.entrySet()) {
463483
String securityName = securitySchemesEntry.getKey();
464484
SecurityScheme securityScheme = securitySchemesEntry.getValue();
485+
String functionSuffix = securityFunctionNameSuffix(securityName);
465486
String baseFunctionName = controllerPackage + ".security_controller.";
466487
switch (securityScheme.getType()) {
467488
case APIKEY:
468-
addSecurityExtension(securityScheme, "x-apikeyInfoFunc", baseFunctionName + "info_from_" + securityName);
489+
addSecurityExtension(securityScheme, "x-apikeyInfoFunc", baseFunctionName + "info_from_" + functionSuffix);
469490
break;
470491
case HTTP:
471492
if ("basic".equals(securityScheme.getScheme())) {
472-
addSecurityExtension(securityScheme, "x-basicInfoFunc", baseFunctionName + "info_from_" + securityName);
493+
addSecurityExtension(securityScheme, "x-basicInfoFunc", baseFunctionName + "info_from_" + functionSuffix);
473494
} else if ("bearer".equals(securityScheme.getScheme())) {
474-
addSecurityExtension(securityScheme, "x-bearerInfoFunc", baseFunctionName + "info_from_" + securityName);
495+
addSecurityExtension(securityScheme, "x-bearerInfoFunc", baseFunctionName + "info_from_" + functionSuffix);
475496
}
476497
break;
477498
case OPENIDCONNECT:
478499
LOGGER.warn("Security type {} is not supported by connexion yet", securityScheme.getType().toString());
479500
case OAUTH2:
480-
addSecurityExtension(securityScheme, "x-tokenInfoFunc", baseFunctionName + "info_from_" + securityName);
481-
addSecurityExtension(securityScheme, "x-scopeValidateFunc", baseFunctionName + "validate_scope_" + securityName);
501+
addSecurityExtension(securityScheme, "x-tokenInfoFunc", baseFunctionName + "info_from_" + functionSuffix);
502+
addSecurityExtension(securityScheme, "x-scopeValidateFunc", baseFunctionName + "validate_scope_" + functionSuffix);
482503
break;
483504
default:
484505
LOGGER.warn("Unknown security type {}", securityScheme.getType().toString());
@@ -487,6 +508,52 @@ private void addSecurityExtensions(OpenAPI openAPI) {
487508
}
488509
}
489510

511+
@Override
512+
public List<CodegenSecurity> fromSecurity(Map<String, SecurityScheme> schemes) {
513+
List<CodegenSecurity> codegenSecurities = super.fromSecurity(schemes);
514+
for (CodegenSecurity codegenSecurity : codegenSecurities) {
515+
codegenSecurity.vendorExtensions.put("x-python-connexion-security-function-suffix",
516+
securityFunctionNameSuffix(codegenSecurity.name));
517+
}
518+
return codegenSecurities;
519+
}
520+
521+
/**
522+
* Returns the Python identifier suffix used for the info_from_/validate_scope_ functions
523+
* of the given security scheme name. The result is stable per scheme name and unique across
524+
* schemes of the same spec. Scheme names that already are valid identifier parts are
525+
* reserved by addSecurityExtensions and keep their name; for the others, if the sanitized
526+
* suffix is already taken by another scheme name (e.g. "api-key" vs "api.key"), "_2",
527+
* "_3", ... is appended in spec order.
528+
*/
529+
private String securityFunctionNameSuffix(String schemeName) {
530+
return securityFunctionNameSuffixes.computeIfAbsent(schemeName, name -> {
531+
String base = toPythonIdentifierSuffix(name);
532+
String suffix = base;
533+
for (int i = 2; securityFunctionNameSuffixes.containsValue(suffix); i++) {
534+
suffix = base + "_" + i;
535+
}
536+
return suffix;
537+
});
538+
}
539+
540+
/**
541+
* Maps a security scheme name to a string usable as part of a Python identifier:
542+
* every character that is not ASCII alphanumeric or '_' becomes '_'. The result is
543+
* only used after the "info_from_"/"validate_scope_" prefixes, so a leading digit
544+
* needs no further fix-up.
545+
*/
546+
private static String toPythonIdentifierSuffix(String name) {
547+
StringBuilder suffix = new StringBuilder(name.length());
548+
for (int i = 0; i < name.length(); i++) {
549+
char c = name.charAt(i);
550+
boolean isIdentifierChar = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
551+
|| (c >= '0' && c <= '9') || c == '_';
552+
suffix.append(isIdentifierChar ? c : '_');
553+
}
554+
return suffix.toString();
555+
}
556+
490557
private static List<OperationMap> getOperations(Map<String, Object> objs) {
491558
List<OperationMap> result = new ArrayList<>();
492559
ApiInfoMap apiInfo = (ApiInfoMap) objs.get("apiInfo");

‎modules/openapi-generator/src/main/resources/python-aiohttp/security_controller.mustache‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ from typing import List
33
{{#authMethods}}
44
{{#isOAuth}}
55

6-
def info_from_{{name}}(token: str) -> dict:
6+
def info_from_{{{vendorExtensions.x-python-connexion-security-function-suffix}}}(token: str) -> dict:
77
"""
88
Validate and decode token.
99
Returned value will be passed in 'token_info' parameter of your operation function, if there is one.
@@ -14,14 +14,14 @@ def info_from_{{name}}(token: str) -> dict:
1414
return {'scopes': ['read:pets', 'write:pets'], 'uid': 'user_id'}
1515

1616

17-
def validate_scope_{{name}}(required_scopes: List[str], token_scopes: List[str]) -> bool:
17+
def validate_scope_{{{vendorExtensions.x-python-connexion-security-function-suffix}}}(required_scopes: List[str], token_scopes: List[str]) -> bool:
1818
""" Validate required scopes are included in token scope """
1919
return set(required_scopes).issubset(set(token_scopes))
2020

2121
{{/isOAuth}}
2222
{{#isApiKey}}
2323

24-
def info_from_{{name}}(api_key: str, required_scopes: None) -> dict:
24+
def info_from_{{{vendorExtensions.x-python-connexion-security-function-suffix}}}(api_key: str, required_scopes: None) -> dict:
2525
"""
2626
Check and retrieve authentication information from api_key.
2727
Returned value will be passed in 'token_info' parameter of your operation function, if there is one.
@@ -33,7 +33,7 @@ def info_from_{{name}}(api_key: str, required_scopes: None) -> dict:
3333
{{/isApiKey}}
3434
{{#isBasicBasic}}
3535

36-
def info_from_{{name}}(username: str, password: str, required_scopes: None) -> dict:
36+
def info_from_{{{vendorExtensions.x-python-connexion-security-function-suffix}}}(username: str, password: str, required_scopes: None) -> dict:
3737
"""
3838
Check and retrieve authentication information from basic auth.
3939
Returned value will be passed in 'token_info' parameter of your operation function, if there is one.
@@ -45,7 +45,7 @@ def info_from_{{name}}(username: str, password: str, required_scopes: None) -> d
4545
{{/isBasicBasic}}
4646
{{#isBasicBearer}}
4747

48-
def info_from_{{name}}(token: str) -> dict:
48+
def info_from_{{{vendorExtensions.x-python-connexion-security-function-suffix}}}(token: str) -> dict:
4949
"""
5050
Check and retrieve authentication information from custom bearer token.
5151
Returned value will be passed in 'token_info' parameter of your operation function, if there is one.

‎modules/openapi-generator/src/main/resources/python-flask/security_controller.mustache‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ from typing import List
33
{{#authMethods}}
44
{{#isOAuth}}
55

6-
def info_from_{{name}}(token):
6+
def info_from_{{{vendorExtensions.x-python-connexion-security-function-suffix}}}(token):
77
"""
88
Validate and decode token.
99
Returned value will be passed in 'token_info' parameter of your operation function, if there is one.
@@ -18,7 +18,7 @@ def info_from_{{name}}(token):
1818
return {'scopes': ['read:pets', 'write:pets'], 'uid': 'user_id'}
1919

2020

21-
def validate_scope_{{name}}(required_scopes, token_scopes):
21+
def validate_scope_{{{vendorExtensions.x-python-connexion-security-function-suffix}}}(required_scopes, token_scopes):
2222
"""
2323
Validate required scopes are included in token scope
2424

@@ -34,7 +34,7 @@ def validate_scope_{{name}}(required_scopes, token_scopes):
3434
{{/isOAuth}}
3535
{{#isApiKey}}
3636

37-
def info_from_{{name}}(api_key, required_scopes):
37+
def info_from_{{{vendorExtensions.x-python-connexion-security-function-suffix}}}(api_key, required_scopes):
3838
"""
3939
Check and retrieve authentication information from api_key.
4040
Returned value will be passed in 'token_info' parameter of your operation function, if there is one.
@@ -52,7 +52,7 @@ def info_from_{{name}}(api_key, required_scopes):
5252
{{/isApiKey}}
5353
{{#isBasicBasic}}
5454

55-
def info_from_{{name}}(username, password, required_scopes):
55+
def info_from_{{{vendorExtensions.x-python-connexion-security-function-suffix}}}(username, password, required_scopes):
5656
"""
5757
Check and retrieve authentication information from basic auth.
5858
Returned value will be passed in 'token_info' parameter of your operation function, if there is one.
@@ -72,7 +72,7 @@ def info_from_{{name}}(username, password, required_scopes):
7272
{{/isBasicBasic}}
7373
{{#isBasicBearer}}
7474

75-
def info_from_{{name}}(token):
75+
def info_from_{{{vendorExtensions.x-python-connexion-security-function-suffix}}}(token):
7676
"""
7777
Check and retrieve authentication information from custom bearer token.
7878
Returned value will be passed in 'token_info' parameter of your operation function, if there is one.

0 commit comments

Comments
 (0)