Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add support for security roles in micronaut server generator
  • Loading branch information
andriy-dmytruk committed Mar 28, 2022
commit 8fc18182c2ec12c06884c08308e249abe57af0b9
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,25 @@
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 {
public static final String OPT_CONTROLLER_PACKAGE = "controllerPackage";
public static final String OPT_GENERATE_CONTROLLER_FROM_EXAMPLES = "generateControllerFromExamples";
public static final String OPT_GENERATE_CONTROLLER_AS_ABSTRACT = "generateControllerAsAbstract";

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";
Expand Down Expand Up @@ -183,6 +193,31 @@ public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> o
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("x-roles")) {
String role = operation.hasAuthMethods ? AUTHORIZED_ROLE : ANONYMOUS_ROLE;
operation.vendorExtensions.put("x-roles", Collections.singletonList(role));
} else {
List<String> roles = (List<String>) operation.vendorExtensions.get("x-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("x-roles", roles);
}
}
}

return objs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;
Expand All @@ -14,6 +15,8 @@
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();
Expand Down Expand Up @@ -203,4 +206,35 @@ public void testExtraAnnotations() throws Exception {
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");
}
}
Original file line number Diff line number Diff line change
@@ -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"]