[Micronaut] [Server] Add support for security roles with extension (#11995)

* Minor refactor for Micronaut generators

* Add support for security roles in micronaut server generator

* Micronaut Server Generator refactor the x-roles String variable
This commit is contained in:
Andriy Dmytruk
2022-03-28 22:20:25 -04:00
committed by GitHub
parent 21c399f2b8
commit 4cac09324e
5 changed files with 238 additions and 10 deletions

View File

@@ -11,7 +11,7 @@ import org.slf4j.LoggerFactory;
public class JavaMicronautClientCodegen extends JavaMicronautAbstractCodegen {
private final Logger LOGGER = LoggerFactory.getLogger(JavaClientCodegen.class);
private final Logger LOGGER = LoggerFactory.getLogger(JavaMicronautClientCodegen.class);
public static final String OPT_CONFIGURE_AUTH = "configureAuth";

View File

@@ -8,8 +8,10 @@ import org.openapitools.codegen.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class JavaMicronautServerCodegen extends JavaMicronautAbstractCodegen {
@@ -17,7 +19,15 @@ public class JavaMicronautServerCodegen extends JavaMicronautAbstractCodegen {
public static final String OPT_GENERATE_CONTROLLER_FROM_EXAMPLES = "generateControllerFromExamples";
public static final String OPT_GENERATE_CONTROLLER_AS_ABSTRACT = "generateControllerAsAbstract";
private final Logger LOGGER = LoggerFactory.getLogger(JavaClientCodegen.class);
public static final String EXTENSION_ROLES = "x-roles";
public static final String ANONYMOUS_ROLE_KEY = "isAnonymous()";
public static final String ANONYMOUS_ROLE = "SecurityRule.IS_ANONYMOUS";
public static final String AUTHORIZED_ROLE_KEY = "isAuthorized()";
public static final String AUTHORIZED_ROLE = "SecurityRule.IS_AUTHENTICATED";
public static final String DENY_ALL_ROLE_KEY = "denyAll()";
public static final String DENY_ALL_ROLE = "SecurityRule.DENY_ALL";
private final Logger LOGGER = LoggerFactory.getLogger(JavaMicronautServerCodegen.class);
public static final String NAME = "java-micronaut-server";
@@ -30,7 +40,6 @@ public class JavaMicronautServerCodegen extends JavaMicronautAbstractCodegen {
protected String controllerSuffix = "Controller";
protected String apiPrefix = "Abstract";
protected String apiSuffix = "Controller";
private String testOutputDir;
public JavaMicronautServerCodegen() {
super();
@@ -144,6 +153,7 @@ public class JavaMicronautServerCodegen extends JavaMicronautAbstractCodegen {
@Override
public String apiTestFileFolder() {
// Set it to the whole output dir, so that validation always passes
return super.getOutputDir();
}
@@ -154,7 +164,6 @@ public class JavaMicronautServerCodegen extends JavaMicronautAbstractCodegen {
String implementationFolder = outputFolder + File.separator +
sourceFolder + File.separator +
controllerPackage.replace('.', File.separatorChar);
testOutputDir = implementationFolder;
return (implementationFolder + File.separator +
StringUtils.camelize(controllerPrefix + "_" + tag + "_" + controllerSuffix) + ".java"
).replace('/', File.separatorChar);
@@ -184,6 +193,31 @@ public class JavaMicronautServerCodegen extends JavaMicronautAbstractCodegen {
String controllerClassname = StringUtils.camelize(controllerPrefix + "_" + operations.get("pathPrefix") + "_" + controllerSuffix);
objs.put("controllerClassname", controllerClassname);
List<CodegenOperation> allOperations = (List<CodegenOperation>) operations.get("operation");
if (useAuth) {
for (CodegenOperation operation : allOperations) {
if (!operation.vendorExtensions.containsKey(EXTENSION_ROLES)) {
String role = operation.hasAuthMethods ? AUTHORIZED_ROLE : ANONYMOUS_ROLE;
operation.vendorExtensions.put(EXTENSION_ROLES, Collections.singletonList(role));
} else {
List<String> roles = (List<String>) operation.vendorExtensions.get(EXTENSION_ROLES);
roles = roles.stream().map(role -> {
switch (role) {
case ANONYMOUS_ROLE_KEY:
return ANONYMOUS_ROLE;
case AUTHORIZED_ROLE_KEY:
return AUTHORIZED_ROLE;
case DENY_ALL_ROLE_KEY:
return DENY_ALL_ROLE;
default:
return "\"" + escapeText(role) + "\"";
}
}).collect(Collectors.toList());
operation.vendorExtensions.put(EXTENSION_ROLES, roles);
}
}
}
return objs;
}
}

View File

@@ -92,12 +92,7 @@ public {{#generateControllerAsAbstract}}abstract {{/generateControllerAsAbstract
{{/consumes.0}}
{{!security annotations}}
{{#useAuth}}
{{#hasAuthMethods}}
@Secured(SecurityRule.IS_AUTHENTICATED)
{{/hasAuthMethods}}
{{^hasAuthMethods}}
@Secured(SecurityRule.IS_ANONYMOUS)
{{/hasAuthMethods}}
@Secured({{openbrace}}{{#vendorExtensions.x-roles}}{{{.}}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-roles}}{{closebrace}})
{{/useAuth}}
{{!the method definition}}
public {{#returnType}}Mono<{{{returnType}}}>{{/returnType}}{{^returnType}}Mono<Object>{{/returnType}} {{nickname}}{{#generateControllerAsAbstract}}Api{{/generateControllerAsAbstract}}({{#allParams}}

View File

@@ -6,6 +6,7 @@ import io.swagger.v3.oas.models.servers.Server;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.TestUtils;
import org.openapitools.codegen.languages.JavaMicronautAbstractCodegen;
import org.openapitools.codegen.languages.JavaMicronautServerCodegen;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -14,6 +15,8 @@ import static java.util.stream.Collectors.groupingBy;
import static org.testng.Assert.assertEquals;
public class MicronautServerCodegenTest extends AbstractMicronautCodegenTest {
protected static String ROLES_EXTENSION_TEST_PATH = "src/test/resources/3_0/micronaut/roles-extension-test.yaml";
@Test
public void clientOptsUnicity() {
JavaMicronautServerCodegen codegen = new JavaMicronautServerCodegen();
@@ -203,4 +206,35 @@ public class MicronautServerCodegenTest extends AbstractMicronautCodegenTest {
TestUtils.assertExtraAnnotationFiles(outputPath + "/src/main/java/org/openapitools/model");
}
@Test
public void doNotGenerateAuthRolesWithExtensionWhenNotUseAuth() {
JavaMicronautServerCodegen codegen = new JavaMicronautServerCodegen();
codegen.additionalProperties().put(JavaMicronautServerCodegen.OPT_USE_AUTH, false);
String outputPath = generateFiles(codegen, ROLES_EXTENSION_TEST_PATH, CodegenConstants.MODELS, CodegenConstants.APIS);
String controllerPath = outputPath + "src/main/java/org/openapitools/controller/";
assertFileNotContains(controllerPath + "BooksController.java", "@Secured");
assertFileNotContains(controllerPath + "UsersController.java", "@Secured");
assertFileNotContains(controllerPath + "ReviewsController.java", "@Secured");
}
@Test
public void generateAuthRolesWithExtension() {
JavaMicronautServerCodegen codegen = new JavaMicronautServerCodegen();
codegen.additionalProperties().put(JavaMicronautServerCodegen.OPT_USE_AUTH, true);
String outputPath = generateFiles(codegen, ROLES_EXTENSION_TEST_PATH, CodegenConstants.MODELS, CodegenConstants.APIS);
String controllerPath = outputPath + "src/main/java/org/openapitools/controller/";
assertFileContainsRegex(controllerPath + "BooksController.java", "IS_ANONYMOUS[^;]{0,100}bookSearchGet");
assertFileContainsRegex(controllerPath + "BooksController.java", "@Secured\\(\\{\"admin\"\\}\\)[^;]{0,100}createBook");
assertFileContainsRegex(controllerPath + "BooksController.java", "IS_ANONYMOUS[^;]{0,100}getBook");
assertFileContainsRegex(controllerPath + "BooksController.java", "IS_AUTHENTICATED[^;]{0,100}reserveBook");
assertFileContainsRegex(controllerPath + "ReviewsController.java", "IS_AUTHENTICATED[^;]{0,100}bookSendReviewPost");
assertFileContainsRegex(controllerPath + "ReviewsController.java", "IS_ANONYMOUS[^;]{0,100}bookViewReviewsGet");
assertFileContainsRegex(controllerPath + "UsersController.java", "IS_ANONYMOUS[^;]{0,100}getUserProfile");
assertFileContainsRegex(controllerPath + "UsersController.java", "IS_AUTHENTICATED[^;]{0,100}updateProfile");
}
}

View File

@@ -0,0 +1,165 @@
openapi: 3.0.0
info:
description: This is a test api description
version: 1.0.0
title: Library
license:
name: Apache-2.0
url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
tags:
- {name: books, description: Everything about books}
- {name: users, description: Everyting about users}
- {name: reviews, description: Everything related to book reviews}
paths:
/book/{bookName}:
get:
tags: [books]
summary: Get a book by name
operationId: getBook
parameters:
- {name: bookName, in: path, required: true, schema: {type: string}}
responses:
'200':
description: success
content:
application/json:
schema: { $ref: "#/components/schemas/Book" }
x-roles: ["isAnonymous()"]
post:
tags: [books]
summary: Create a new book
operationId: createBook
parameters:
- {name: bookName, in: path, required: true, schema: {type: string}}
requestBody:
content:
application/json: { schema: { $ref: "#/components/schemas/Book" } }
responses:
'200':
description: success
x-roles: ["admin"]
/book/search:
get:
tags: [books]
summary: Search for a book
parameters:
- {name: bookName, in: query, required: false, schema: {type: string, example: "Book 2"}}
- {name: ISBN, in: query, required: false, schema: {type: string, pattern: "[0-9]{13}", example: "0123456789123"}}
- {name: published, in: query, required: false, schema: {type: string, format: date}}
- {name: minNumPages, in: query, required: false, schema: {type: integer, format: int32, minimum: 1, maximum: 1000}}
- {name: minReadTime, in: query, required: false, schema: {type: number, format: float, minimum: 1, example: 5.7}}
- {name: description, in: query, required: false, schema: {type: string, minLength: 4, nullable: true}}
- {name: preferences, in: cookie, required: false, schema: {type: string}}
- {name: geoLocation, in: header, required: false, schema: {type: string}}
responses:
'200':
description: success
content:
application/json:
{ schema: { type: array, items: { $ref: "#/components/schemas/Book" } } }
/book/availability/{bookName}:
get:
tags: [books]
summary: Check book availability
operationId: isBookAvailable
parameters:
- { name: bookName, in: path, required: true, schema: { type: string, example: "Book 1" } }
responses:
'200':
description: success
content:
text/plain:
schema: { $ref: "#/components/schemas/BookAvailability" }
/book/reserve/{bookName}:
get:
tags: [books]
summary: Reserve book for self
operationId: reserveBook
parameters:
- { name: bookName, in: path, required: true, schema: { type: string, example: "Book 2" } }
responses:
'200':
description: success
x-roles: ["isAuthorized()"]
/user/{userName}:
get:
tags: [users]
summary: View user profile
operationId: getUserProfile
parameters:
- {name: userName, in: path, required: true, schema: {type: string, pattern: "[0-9a-zA-Z ]+"}}
responses:
'200':
description: success
content:
application/json: { schema: { $ref: "#/components/schemas/User" } }
/user:
post:
tags: [users]
summary: Update your own profile
operationId: updateProfile
requestBody:
content:
'*/*': { schema: { $ref: "#/components/schemas/User"} }
responses:
'200':
description: success
x-roles: ["isAuthorized()"]
/book/viewReviews:
get:
tags: [reviews]
summary: Get all reviews for a book
parameters:
- { name: bookName, in: query, required: true, schema: { type: string, nullable: false } }
responses:
'200':
description: success
content:
application/json: { schema: { type: array, items: { $ref: "#/content/schemas/Review" } } }
/book/sendReview:
post:
tags: [reviews]
summary: Send a review to a book
parameters:
- {name: bookName, in: query, required: true, schema: { type: string, nullable: false } }
requestBody:
content:
application/x-www-form-urlencoded:
schema: {$ref: "#/components/schemas/Review"}
responses:
'200':
description: success
x-roles: ["isAuthorized()"]
components:
schemas:
Book:
title: Book
description: book instance
type: object
properties:
name: {type: string}
availability: {$ref: "#/components/schemas/BookAvailability"}
pages: {type: integer, format: int32, minimum: 1}
author: {type: string, pattern: "[a-zA-z ]+"}
readTime: {type: number, format: float, minimum: 0, exclusiveMinimum: true}
required: ["name", "availability"]
default:
name: "Bob's Adventures"
availability: "available"
BookAvailability:
type: string
enum: ["available", "not available", "reserved"]
default: "not available"
Review:
type: object
properties:
rating: {type: integer, minimum: 1, maximum: 5, default: 2}
description: {type: string, maxLength: 200}
required: [rating]
User:
type: object
properties:
username: { type: string, minLength: 2, nullable: false }
name: { type: string, minLength: 1 }
description: { type: string, nullable: true }
required: ["username", "name"]