Skip to content
Open
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
11 changes: 11 additions & 0 deletions bin/configs/spring-boot-pre-authorize.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
generatorName: spring
outputDir: samples/server/others/springboot-pre-authorize
library: spring-boot
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/preauthorize-scopes.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
additionalProperties:
documentationProvider: none
annotationLibrary: none
useSwaggerUI: "false"
useSpringSecurityPreAuthorize: "true"
hideGenerationTimestamp: "true"
2 changes: 2 additions & 0 deletions docs/generators/java-camel.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|sourceFolder|source folder for generated code| |src/main/java|
|springApiVersion|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).| |null|
|springSecurityAuthorityPrefix|Prefix added to OAuth2/OpenID Connect scopes when generating Spring Security authorities.| |SCOPE_|
|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel<T>. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation.| |false|
|testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi|
|title|server title name or client service name| |OpenAPI Spring|
Expand All @@ -127,6 +128,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x. (Use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false|
|useSpringBuiltInValidation|Disable `@Validated` at the class level when using built-in validation.| |false|
|useSpringController|Annotate the generated API as a Spring Controller| |false|
|useSpringSecurityPreAuthorize|Generate Spring Security @PreAuthorize annotations from OAuth2/OpenID Connect security scopes.| |false|
|useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true|
|useTags|use tags for creating interface and controller classnames| |false|
|virtualService|Generates the virtual service. For more details refer - https://github.com/virtualansoftware/virtualan/wiki| |false|
Expand Down
2 changes: 2 additions & 0 deletions docs/generators/spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|sourceFolder|source folder for generated code| |src/main/java|
|springApiVersion|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).| |null|
|springSecurityAuthorityPrefix|Prefix added to OAuth2/OpenID Connect scopes when generating Spring Security authorities.| |SCOPE_|
|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel<T>. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation.| |false|
|testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi|
|title|server title name or client service name| |OpenAPI Spring|
Expand All @@ -120,6 +121,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x. (Use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false|
|useSpringBuiltInValidation|Disable `@Validated` at the class level when using built-in validation.| |false|
|useSpringController|Annotate the generated API as a Spring Controller| |false|
|useSpringSecurityPreAuthorize|Generate Spring Security @PreAuthorize annotations from OAuth2/OpenID Connect security scopes.| |false|
|useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true|
|useTags|use tags for creating interface and controller classnames| |false|
|virtualService|Generates the virtual service. For more details refer - https://github.com/virtualansoftware/virtualan/wiki| |false|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.tags.Tag;
import io.swagger.v3.parser.util.SchemaTypeUtil;
import lombok.Getter;
Expand Down Expand Up @@ -123,6 +125,8 @@ public class SpringCodegen extends AbstractJavaCodegen
public static final String GENERATE_PAGEABLE_CONSTRAINT_VALIDATION = "generatePageableConstraintValidation";
public static final String SUBSTITUTE_GENERIC_PAGED_MODEL = "substituteGenericPagedModel";
public static final String CLIENT_REGISTRATION_ID = "clientRegistrationId";
public static final String USE_SPRING_SECURITY_PRE_AUTHORIZE = "useSpringSecurityPreAuthorize";
public static final String SPRING_SECURITY_AUTHORITY_PREFIX = "springSecurityAuthorityPrefix";

@Getter
public enum RequestMappingMode {
Expand Down Expand Up @@ -199,6 +203,9 @@ public enum RequestMappingMode {
@Setter protected boolean substituteGenericPagedModel = false;
@Getter @Setter
protected String clientRegistrationId = null;
@Setter protected boolean useSpringSecurityPreAuthorize = false;
@Getter @Setter
protected String springSecurityAuthorityPrefix = "SCOPE_";
@Setter protected boolean useEnumValueInterface = false;
private String valuedEnumClassName = "ValuedEnum";

Expand Down Expand Up @@ -347,6 +354,12 @@ public SpringCodegen() {
);
cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use Jspecify for null checks", useJspecify));
cliOptions.add(CliOption.newString(CLIENT_REGISTRATION_ID, "Client registration ID for OAuth2 in Spring HTTP Interface (@ClientRegistrationId annotation). Requires library=spring-http-interface and useSpringBoot4=true (Spring Security 7)."));
cliOptions.add(CliOption.newBoolean(USE_SPRING_SECURITY_PRE_AUTHORIZE,
"Generate Spring Security @PreAuthorize annotations from OAuth2/OpenID Connect security scopes.",
useSpringSecurityPreAuthorize));
cliOptions.add(CliOption.newString(SPRING_SECURITY_AUTHORITY_PREFIX,
"Prefix added to OAuth2/OpenID Connect scopes when generating Spring Security authorities.",
springSecurityAuthorityPrefix));
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
"Spring-Cloud-Feign client with Spring-Boot auto-configured settings.");
Expand Down Expand Up @@ -573,6 +586,8 @@ public void processOpts() {
convertPropertyToBooleanAndWriteBack(USE_SPRING_BUILT_IN_VALIDATION, this::setUseSpringBuiltInValidation);
convertPropertyToBooleanAndWriteBack(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, this::setUseDeductionForOneOfInterfaces);
convertPropertyToStringAndWriteBack(CLIENT_REGISTRATION_ID, this::setClientRegistrationId);
convertPropertyToBooleanAndWriteBack(USE_SPRING_SECURITY_PRE_AUTHORIZE, this::setUseSpringSecurityPreAuthorize);
convertPropertyToStringAndWriteBack(SPRING_SECURITY_AUTHORITY_PREFIX, this::setSpringSecurityAuthorityPrefix);

additionalProperties.put("springHttpStatus", new SpringHttpStatusLambda());

Expand All @@ -592,6 +607,10 @@ public void processOpts() {
throw new IllegalArgumentException(CLIENT_REGISTRATION_ID + " requires " + USE_SPRING_BOOT4 + "=true because @ClientRegistrationId is provided by Spring Security 7");
}
}
if (useSpringSecurityPreAuthorize && !SPRING_BOOT.equals(library)) {
throw new IllegalArgumentException(USE_SPRING_SECURITY_PRE_AUTHORIZE
+ " is only supported with the " + SPRING_BOOT + " library");
}

if (isUseSpringBoot3() || isUseSpringBoot4()) {
if (AnnotationLibrary.SWAGGER1.equals(getAnnotationLibrary())) {
Expand Down Expand Up @@ -1029,6 +1048,10 @@ public void setIsVoid(boolean isVoid) {
normalizeVendorExtensionWithStringList(operation.vendorExtensions, VendorExtension.X_OPERATION_EXTRA_ANNOTATION.getName());
normalizeOperationParameterVendorExtensions(operation, VendorExtension.X_FIELD_EXTRA_ANNOTATION.getName());

if (useSpringSecurityPreAuthorize) {
addSpringSecurityPreAuthorize(operation);
}

if (isLibrary(SPRING_HTTP_INTERFACE) || isLibrary(SPRING_BOOT)) {
if (operation.isArray && "string".equalsIgnoreCase(operation.returnBaseType)) {
operation.vendorExtensions.put(VendorExtension.X_REACTIVE_RETURN_EXCEPT_LIST_OF_STRING.getName(), true);
Expand All @@ -1054,6 +1077,93 @@ public void setIsVoid(boolean isVoid) {
return objs;
}

/**
* Adds a Spring Security expression that preserves the OpenAPI security requirement semantics:
* entries in a {@code security} array are OR alternatives, while schemes and scopes in one
* entry are combined with AND.
*/
private void addSpringSecurityPreAuthorize(CodegenOperation codegenOperation) {
String originalOperationId = codegenOperation.operationIdOriginal != null
? codegenOperation.operationIdOriginal
: codegenOperation.operationId;
Operation rawOperation = findOperation(originalOperationId);
if (rawOperation == null) {
LOGGER.warn("Could not find OpenAPI operation '{}' while generating @PreAuthorize.", originalOperationId);
return;
}

List<SecurityRequirement> requirements = rawOperation.getSecurity();
if (requirements == null) {
requirements = openAPI.getSecurity();
}
if (requirements == null || requirements.isEmpty()) {
return;
}

Map<String, SecurityScheme> schemes = openAPI.getComponents() != null
? openAPI.getComponents().getSecuritySchemes() : null;
if (schemes == null || schemes.isEmpty()) {
return;
}

List<String> alternatives = new ArrayList<>();
for (SecurityRequirement requirement : requirements) {
if (requirement.isEmpty()) {
return;
}
List<String> groupAuthorities = new ArrayList<>();
for (Map.Entry<String, List<String>> entry : requirement.entrySet()) {
SecurityScheme scheme = schemes.get(entry.getKey());
if (scheme == null || (scheme.getType() != SecurityScheme.Type.OAUTH2
&& scheme.getType() != SecurityScheme.Type.OPENIDCONNECT)) {
return;
}
if (entry.getValue() == null || entry.getValue().isEmpty()) {
groupAuthorities.add("isAuthenticated()");
} else {
for (String scope : entry.getValue()) {
groupAuthorities.add("hasAuthority('" + escapeSpelStringLiteral(springSecurityAuthorityPrefix + scope) + "')");
}
}
}
String group = String.join(" and ", groupAuthorities);
alternatives.add(groupAuthorities.size() > 1 && requirements.size() > 1 ? "(" + group + ")" : group);
}

if (!alternatives.isEmpty()) {
codegenOperation.vendorExtensions.put("x-spring-security-pre-authorize",
escapeJavaStringLiteral(String.join(" or ", alternatives)));
}
}

private String escapeSpelStringLiteral(String value) {
return value.replace("'", "''");
}

private String escapeJavaStringLiteral(String value) {
return value.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\r", "\\r")
.replace("\n", "\\n");
}

private Operation findOperation(String operationId) {
if (openAPI.getPaths() == null || operationId == null) {
return null;
}
for (PathItem pathItem : openAPI.getPaths().values()) {
if (pathItem.readOperations() == null) {
continue;
}
for (Operation operation : pathItem.readOperations()) {
if (operationId.equals(operation.getOperationId())) {
return operation;
}
}
}
return null;
}

private interface DataTypeAssigner {
void setReturnType(String returnType);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import org.springframework.http.HttpStatus;
{{#useResponseEntity}}
import org.springframework.http.ResponseEntity;
{{/useResponseEntity}}
{{#useSpringSecurityPreAuthorize}}
import org.springframework.security.access.prepost.PreAuthorize;
{{/useSpringSecurityPreAuthorize}}
{{#useBeanValidation}}{{^useSpringBuiltInValidation}}import org.springframework.validation.annotation.Validated;{{/useSpringBuiltInValidation}}{{/useBeanValidation}}
{{#useSpringController}}
{{#useResponseEntity}}
Expand Down Expand Up @@ -272,6 +275,9 @@ public interface {{classname}} {
{{#vendorExtensions.x-operation-extra-annotation}}
{{{.}}}
{{/vendorExtensions.x-operation-extra-annotation}}
{{#vendorExtensions.x-spring-security-pre-authorize}}
@PreAuthorize("{{{.}}}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: OpenAPI scope names containing Java/SpEL string delimiters generate uncompilable or altered @PreAuthorize expressions because this interpolation bypasses Java-string escaping. Escape the generated expression for a Java annotation literal before rendering it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/JavaSpring/api.mustache, line 279:

<comment>OpenAPI scope names containing Java/SpEL string delimiters generate uncompilable or altered `@PreAuthorize` expressions because this interpolation bypasses Java-string escaping. Escape the generated expression for a Java annotation literal before rendering it.</comment>

<file context>
@@ -272,6 +275,9 @@ public interface {{classname}} {
     {{{.}}}
     {{/vendorExtensions.x-operation-extra-annotation}}
+    {{#vendorExtensions.x-spring-security-pre-authorize}}
+    @PreAuthorize("{{{.}}}")
+    {{/vendorExtensions.x-spring-security-pre-authorize}}
     {{#vendorExtensions.x-sse}}
</file context>

{{/vendorExtensions.x-spring-security-pre-authorize}}
{{#vendorExtensions.x-sse}}
@ResponseBody
{{/vendorExtensions.x-sse}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import org.openapitools.jackson.nullable.JsonNullableModule;
{{/openApiNullable}}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
{{#useSpringSecurityPreAuthorize}}
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
{{/useSpringSecurityPreAuthorize}}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
Expand All @@ -18,8 +25,22 @@ import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGe
basePackages = {"{{basePackage}}", "{{apiPackage}}" , "{{configPackage}}"},
nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class
)
{{#useSpringSecurityPreAuthorize}}
@EnableMethodSecurity
{{/useSpringSecurityPreAuthorize}}
public class OpenApiGeneratorApplication {

{{#useSpringSecurityPreAuthorize}}
@Bean
@ConditionalOnMissingBean(SecurityFilterChain.class)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Reactive generated servers fail to initialize when this option is enabled because WebFlux does not provide the servlet HttpSecurity builder required by this bean. Render a ServerHttpSecurity/SecurityWebFilterChain configuration for reactive, or reject this option combination.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache, line 36:

<comment>Reactive generated servers fail to initialize when this option is enabled because WebFlux does not provide the servlet `HttpSecurity` builder required by this bean. Render a `ServerHttpSecurity`/`SecurityWebFilterChain` configuration for `reactive`, or reject this option combination.</comment>

<file context>
@@ -26,6 +30,17 @@ import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGe
+{{#useSpringSecurityPreAuthorize}}
+    @Bean
+    @ConditionalOnMissingBean(SecurityFilterChain.class)
+    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+        return http
+            .csrf(AbstractHttpConfigurer::disable)
</file context>

return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll())
.build();
}

{{/useSpringSecurityPreAuthorize}}
public static void main(String[] args) {
SpringApplication.run(OpenApiGeneratorApplication.class, args);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@
</plugins>
</build>
<dependencies>
{{#useSpringSecurityPreAuthorize}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
{{/useSpringSecurityPreAuthorize}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web{{#reactive}}flux{{/reactive}}</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@
</plugins>
</build>
<dependencies>
{{#useSpringSecurityPreAuthorize}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
{{/useSpringSecurityPreAuthorize}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web{{#reactive}}flux{{/reactive}}{{^reactive}}mvc{{/reactive}}</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@
</plugins>
</build>
<dependencies>
{{#useSpringSecurityPreAuthorize}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
{{/useSpringSecurityPreAuthorize}}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web{{#reactive}}flux{{/reactive}}</artifactId>
Expand Down
Loading
Loading