From 42e546888b61d299d598b2d2c548a53589aea32f Mon Sep 17 00:00:00 2001 From: Harry Chan <38070640+re-thc@users.noreply.github.com> Date: Fri, 11 Oct 2024 08:49:58 +1100 Subject: [PATCH] Add roles support for Helidon (#506) Helidon has security providers e.g. OIDC. These integrate with WebServer Security as `SecurityHandler`s with the shorthand `SecurityFeature.rolesAllowed()` and the authorization provider will then validate the role before delegating to the actual handler. This PR takes Avaje's `@Roles` and populates them and the rest is up to Helidon. --- README.md | 4 ++-- .../helidon/nima/ControllerMethodWriter.java | 9 ++++++++ .../helidon/nima/ControllerWriter.java | 3 +++ .../helidon/nima/NimaPlatformAdapter.java | 4 +++- tests/test-nima/pom.xml | 5 +++++ .../src/main/java/org/example/AppRoles.java | 5 +++++ .../java/org/example/HelloController.java | 8 +++++++ .../src/main/java/org/example/Roles.java | 21 +++++++++++++++++++ 8 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 tests/test-nima/src/main/java/org/example/AppRoles.java create mode 100644 tests/test-nima/src/main/java/org/example/Roles.java diff --git a/README.md b/README.md index a47585153..f0660e3dc 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ public class WidgetController$Route implements HttpFeature { private void _getById(ServerRequest req, ServerResponse res) throws Exception { res.status(OK_200); var pathParams = req.path().pathParameters(); - var id = asInt(pathParams.first("id").get()); + var id = asInt(pathParams.contains("id") ? pathParams.get("id") : null); var result = controller.getById(id); res.send(result); } @@ -263,7 +263,7 @@ public class WidgetController$Route implements HttpFeature { private void _getById(ServerRequest req, ServerResponse res) throws Exception { res.status(OK_200); var pathParams = req.path().pathParameters(); - var id = asInt(pathParams.first("id").get()); + var id = asInt(pathParams.contains("id") ? pathParams.get("id") : null); var result = controller.getById(id); res.headers().contentType(MediaTypes.APPLICATION_JSON); //jsonb has a special accommodation for helidon to improve performance diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java index 5afa98747..993ea6c64 100644 --- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java +++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java @@ -88,6 +88,15 @@ void writeRule() { writer.append(" routing.addFilter(this::_%s);", method.simpleName()).eol(); } else { writer.append(" routing.%s(\"%s\", ", webMethod.name().toLowerCase(), method.fullPath().replace("\\", "\\\\")); + var roles = method.roles(); + if (!roles.isEmpty()) { + writer.append("SecurityFeature.rolesAllowed("); + writer.append("\"%s\"", Util.shortName(roles.getFirst(), true)); + for (var i = 1; i < roles.size(); i++) { + writer.append(", \"%s\"", Util.shortName(roles.get(i), true)); + } + writer.append("), "); + } var hxRequest = method.hxRequest(); if (hxRequest != null) { writer.append("HxHandler.builder(this::_%s)", method.simpleName()); diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java index 78f90f972..1e3d75d5f 100644 --- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java +++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java @@ -50,6 +50,9 @@ class ControllerWriter extends BaseControllerWriter { reader.addImportType("io.helidon.webserver.http.ServerResponse"); reader.addImportType("io.helidon.webserver.http.HttpFeature"); reader.addImportType("io.helidon.http.HeaderNames"); + if (!reader.roles().isEmpty() || reader.methods().stream().anyMatch(m -> !m.roles().isEmpty())) { + reader.addImportType("io.helidon.webserver.security.SecurityFeature"); + } if (reader.isIncludeValidator()) { reader.addImportType("io.helidon.http.HeaderName"); } diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/NimaPlatformAdapter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/NimaPlatformAdapter.java index 03d211896..8f73b0bde 100644 --- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/NimaPlatformAdapter.java +++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/NimaPlatformAdapter.java @@ -61,7 +61,9 @@ public void methodRoles(List roles, ControllerReader controller) { } private void addRoleImports(List roles, ControllerReader controller) { - // nothing here yet + for (final String role : roles) { + controller.addStaticImportType(role); + } } @Override diff --git a/tests/test-nima/pom.xml b/tests/test-nima/pom.xml index bfb9f65fe..c2d0daaf3 100644 --- a/tests/test-nima/pom.xml +++ b/tests/test-nima/pom.xml @@ -33,6 +33,11 @@ helidon-webserver ${nima.version} + + io.helidon.webserver + helidon-webserver-security + ${nima.version} + io.helidon.http.media helidon-http-media-jsonb diff --git a/tests/test-nima/src/main/java/org/example/AppRoles.java b/tests/test-nima/src/main/java/org/example/AppRoles.java new file mode 100644 index 000000000..7e361aa10 --- /dev/null +++ b/tests/test-nima/src/main/java/org/example/AppRoles.java @@ -0,0 +1,5 @@ +package org.example; + +public enum AppRoles { + ANYONE, ADMIN, BASIC_USER +} diff --git a/tests/test-nima/src/main/java/org/example/HelloController.java b/tests/test-nima/src/main/java/org/example/HelloController.java index fbff3ec17..88df46723 100644 --- a/tests/test-nima/src/main/java/org/example/HelloController.java +++ b/tests/test-nima/src/main/java/org/example/HelloController.java @@ -2,6 +2,7 @@ import io.avaje.http.api.Controller; import io.avaje.http.api.Get; +import io.avaje.http.api.Produces; @Controller public class HelloController { @@ -18,4 +19,11 @@ Person person(String name, String sortBy) { p.setName(name + " hello" + " sortBy:" + sortBy); return p; } + + @Roles({AppRoles.ADMIN, AppRoles.BASIC_USER}) + @Produces("text/plain") + @Get("other/{name}") + String name(String name) { + return "hi " + name; + } } diff --git a/tests/test-nima/src/main/java/org/example/Roles.java b/tests/test-nima/src/main/java/org/example/Roles.java new file mode 100644 index 000000000..7e17989e4 --- /dev/null +++ b/tests/test-nima/src/main/java/org/example/Roles.java @@ -0,0 +1,21 @@ +package org.example; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Specify permitted roles. + */ +@Target(value={METHOD, TYPE}) +@Retention(value=RUNTIME) +public @interface Roles { + + /** + * Specify the permitted roles. + */ + AppRoles[] value() default {}; +}