From 1787276d84d99d2af6ba6b6161fb4596dd7e794a Mon Sep 17 00:00:00 2001 From: Ionel Sirbu Date: Thu, 6 Jun 2024 17:43:15 +0100 Subject: [PATCH] #9843: Properly apply the OAuth scopes for an individual endpoint as a subset of the scopes defined in the `securitySchemes`. --- .../swagger/codegen/v3/DefaultGenerator.java | 50 ++++- .../v3/service/GeneratorServiceTest.java | 42 ++++ .../src/test/resources/3_0_0/issue-9843.yaml | 206 ++++++++++++++++++ 3 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 modules/swagger-codegen/src/test/resources/3_0_0/issue-9843.yaml diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/DefaultGenerator.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/DefaultGenerator.java index 282a28c79b1..51247f75d0c 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/DefaultGenerator.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/DefaultGenerator.java @@ -17,8 +17,7 @@ import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.parameters.Parameter; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.security.*; import io.swagger.v3.oas.models.tags.Tag; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ObjectUtils; @@ -1157,16 +1156,55 @@ private Map getAuthMethods(List sec } final Map authMethods = new HashMap<>(); for (SecurityRequirement requirement : securities) { - for (String key : requirement.keySet()) { - SecurityScheme securityScheme = securitySchemes.get(key); + requirement.forEach((securitySchemeName, endpointScopes) -> { + SecurityScheme securityScheme = securitySchemes.get(securitySchemeName); if (securityScheme != null) { - authMethods.put(key, securityScheme); + SecurityScheme endpointSecurityScheme = selectScopeSubsetForScheme(securityScheme, endpointScopes); + authMethods.put(securitySchemeName, endpointSecurityScheme); } - } + }); } return authMethods; } + private static SecurityScheme selectScopeSubsetForScheme(SecurityScheme originalScheme, List scopesToSelect) { + OAuthFlows originalFlows = originalScheme.getFlows(); + if (originalFlows == null) { + return originalScheme; + } + OAuthFlows flowsWithScopeSubset = new OAuthFlows() + .authorizationCode(selectScopeSubsetForFlow(originalFlows.getAuthorizationCode(), scopesToSelect)) + .clientCredentials(selectScopeSubsetForFlow(originalFlows.getClientCredentials(), scopesToSelect)) + .implicit(selectScopeSubsetForFlow(originalFlows.getImplicit(), scopesToSelect)) + .password(selectScopeSubsetForFlow(originalFlows.getPassword(), scopesToSelect)) + .extensions(originalFlows.getExtensions()); + return new SecurityScheme() + .type(originalScheme.getType()) + .$ref(originalScheme.get$ref()) + .name(originalScheme.getName()) + .description(originalScheme.getDescription()) + .scheme(originalScheme.getScheme()) + .in(originalScheme.getIn()) + .flows(flowsWithScopeSubset) + .bearerFormat(originalScheme.getBearerFormat()) + .openIdConnectUrl(originalScheme.getOpenIdConnectUrl()) + .extensions(originalScheme.getExtensions()); + } + + private static OAuthFlow selectScopeSubsetForFlow(OAuthFlow originalFlow, List scopesToSelect) { + if (originalFlow != null) { + Scopes scopeSubset = new Scopes(); + originalFlow.getScopes().entrySet().stream().filter(e -> scopesToSelect.contains(e.getKey())).forEach(e -> scopeSubset.put(e.getKey(), e.getValue())); + return new OAuthFlow() + .authorizationUrl(originalFlow.getAuthorizationUrl()) + .tokenUrl(originalFlow.getTokenUrl()) + .refreshUrl(originalFlow.getRefreshUrl()) + .scopes(scopeSubset) + .extensions(originalFlow.getExtensions()); + } + return null; + } + private Boolean getCustomOptionBooleanValue(String option) { List languageArguments = config.getLanguageArguments(); if (languageArguments == null || languageArguments.isEmpty()) { diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/v3/service/GeneratorServiceTest.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/v3/service/GeneratorServiceTest.java index 33d611e3444..6e1f48223a9 100644 --- a/modules/swagger-codegen/src/test/java/io/swagger/codegen/v3/service/GeneratorServiceTest.java +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/v3/service/GeneratorServiceTest.java @@ -14,6 +14,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; import java.nio.file.Files; import java.util.List; @@ -1022,6 +1023,47 @@ public void testIssue613_605_612_non_resteasy() throws IOException { Assert.assertFalse(files.isEmpty()); System.out.println("Generated server in:\n" + path); } + + @Test + public void testSecurityScopesAreProperlyApplied() throws IOException { + + String path = getTmpFolder().getAbsolutePath(); + GenerationRequest request = new GenerationRequest(); + request + .codegenVersion(GenerationRequest.CodegenVersion.V3) + .type(GenerationRequest.Type.SERVER) + .lang("spring") + .spec(loadSpecAsNode("3_0_0/issue-9843.yaml", true, false)) + .options( + new Options() + .outputDir(path) + ); + + List files = new GeneratorService().generationRequest(request).generate(); + boolean petFound = false, userFound = false; + Assert.assertFalse(files.isEmpty()); + for (File f: files) { + String relPath = f.getAbsolutePath().substring(path.length()).replace("\\", "/"); + if ("/src/main/java/io/swagger/api/PetApi_.java".equals(relPath)) { + petFound = true; + String actualContents = FileUtils.readFileToString(f, Charset.defaultCharset()); + Assert.assertTrue(actualContents.contains("@SecurityRequirement(name = \"petstore_auth\", scopes = {")); + Assert.assertTrue(actualContents.contains("\"read:pets\"")); + Assert.assertFalse(actualContents.contains("\"write:pets\"")); + Assert.assertFalse(actualContents.contains(":users\"")); + } else if ("/src/main/java/io/swagger/api/UserApi.java".equals(relPath)) { + userFound = true; + String actualContents = FileUtils.readFileToString(f, Charset.defaultCharset()); + Assert.assertTrue(actualContents.contains("@SecurityRequirement(name = \"petstore_auth\", scopes = {")); + Assert.assertTrue(actualContents.contains("\"write:users\"")); + Assert.assertFalse(actualContents.contains(":pets\"")); + } + } + final String fileExpectedFormat = "%s expected to be generated"; + Assert.assertTrue(petFound, String.format(fileExpectedFormat, "PetApi.java")); + Assert.assertTrue(userFound, String.format(fileExpectedFormat, "UserApi.java")); + } + protected static File getTmpFolder() { try { File outputFolder = Files.createTempFile("codegentest-", "-tmp").toFile(); diff --git a/modules/swagger-codegen/src/test/resources/3_0_0/issue-9843.yaml b/modules/swagger-codegen/src/test/resources/3_0_0/issue-9843.yaml new file mode 100644 index 00000000000..d44a097a57f --- /dev/null +++ b/modules/swagger-codegen/src/test/resources/3_0_0/issue-9843.yaml @@ -0,0 +1,206 @@ +openapi: 3.0.1 +servers: [] +info: + description: | + This is a sample Petstore server. You can find + out more about Swagger at + [http://swagger.io](http://swagger.io) or on + [irc.freenode.net, #swagger](http://swagger.io/irc/). + version: "1.0.0" + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +paths: + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'read:pets' + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + security: + - petstore_auth: + - 'write:users' +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + schemas: + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Category + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + 'write:users': modify users