diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc index 5c7fd935985ce..e399e71599013 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -392,10 +392,20 @@ These roles are then applicable for endpoint authorization by using the @RolesAl [source,properties] ---- quarkus.http.auth.policy.admin-policy1.roles.admin=Admin1 <1> -quarkus.http.auth.permission.roles1.paths=/* +quarkus.http.auth.permission.roles1.paths=/* <2> quarkus.http.auth.permission.roles1.policy=admin-policy1 ---- <1> Map the `admin` role to `Admin1` role. The `SecurityIdentity` will have both `admin` and `Admin1` roles. +<2> The `/*` path is secured, only authenticated HTTP requests are granted access. + +If all that you need is to map the `SecurityIdentity` roles to the deployment-specific roles regardless of a path, you can also do this: + +[source,properties] +---- +quarkus.http.auth.roles-mapping.admin=Admin1 <1> <2> +---- +<1> Map the `admin` role to `Admin1` role. The `SecurityIdentity` will have both `admin` and `Admin1` roles. +<2> The `/*` path is not secured. You must secure your endpoints with standard security annotations or define HTTP permissions in addition to this configuration property. === Shared permission checks diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java index 1323e9ff3e2ff..e5ee6f22bf9b5 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java @@ -25,12 +25,14 @@ import io.quarkus.builder.Version; import io.quarkus.maven.dependency.Dependency; +import io.quarkus.security.ForbiddenException; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.test.utils.TestIdentityController; import io.quarkus.security.test.utils.TestIdentityProvider; import io.quarkus.test.QuarkusUnitTest; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.smallrye.mutiny.Uni; import io.vertx.core.Vertx; import io.vertx.ext.web.Router; @@ -47,6 +49,8 @@ public class PathMatchingHttpSecurityPolicyTest { quarkus.http.auth.permission.public.policy=permit quarkus.http.auth.permission.foo.paths=/api/foo/bar quarkus.http.auth.permission.foo.policy=authenticated + quarkus.http.auth.permission.unsecured.paths=/api/public + quarkus.http.auth.permission.unsecured.policy=permit quarkus.http.auth.permission.inner-wildcard.paths=/api/*/bar quarkus.http.auth.permission.inner-wildcard.policy=authenticated quarkus.http.auth.permission.inner-wildcard2.paths=/api/next/*/prev @@ -80,6 +84,9 @@ public class PathMatchingHttpSecurityPolicyTest { quarkus.http.auth.permission.shared2.paths=/* quarkus.http.auth.permission.shared2.shared=true quarkus.http.auth.permission.shared2.policy=custom + quarkus.http.auth.roles-mapping.root1=admin,user + quarkus.http.auth.roles-mapping.admin1=admin + quarkus.http.auth.roles-mapping.public1=public2 """; private static WebClient client; @@ -98,7 +105,10 @@ public static void setup() { .add("test", "test", "test") .add("admin", "admin", "admin") .add("user", "user", "user") - .add("root", "root", "root"); + .add("admin1", "admin1", "admin1") + .add("root1", "root1", "root1") + .add("root", "root", "root") + .add("public1", "public1", "public1"); } @AfterAll @@ -223,15 +233,20 @@ public void testRoleMappingSharedPermission() { assurePath("/secured/all", 401, null, null, null); assurePath("/secured/all", 200, null, "test", null); assurePath("/secured/all", 200, null, "root", null); + assurePath("/secured/all", 200, null, "root1", null); assurePath("/secured/all", 200, null, "admin", null); assurePath("/secured/user", 403, null, "test", null); assurePath("/secured/user", 403, null, "admin", null); + assurePath("/secured/user", 403, null, "admin1", null); assurePath("/secured/user", 200, null, "root", null); + assurePath("/secured/user", 200, null, "root1", null); assurePath("/secured/user", 200, null, "user", null); assurePath("/secured/admin", 403, null, "user", null); assurePath("/secured/admin", 403, null, "test", null); assurePath("/secured/admin", 200, null, "admin", null); + assurePath("/secured/admin", 200, null, "admin1", null); assurePath("/secured/admin", 200, null, "root", null); + assurePath("/secured/admin", 200, null, "root1", null); } @Test @@ -240,10 +255,26 @@ public void testMultipleSharedPermissions() { assurePath("/secured/user", 403, null, "root", "deny-header"); } + @Test + public void testRolesMappingOnPublicPath() { + // here no HTTP Security policy that requires authentication is applied, and we want to check that identity + // is still augmented + assurePath("/api/public", 200, null, "public1", null); + assurePath("/api/public", 403, null, "root1", null); + } + @ApplicationScoped public static class RouteHandler { public void setup(@Observes Router router) { router.route("/api/baz").order(-1).handler(rc -> rc.response().end("/api/baz response")); + router.route("/api/public").order(-1).handler(rc -> { + if (rc.user() instanceof QuarkusHttpUser user && user.getSecurityIdentity() != null + && user.getSecurityIdentity().hasRole("public2")) { + rc.response().end("/api/public"); + } else { + rc.fail(new ForbiddenException()); + } + }); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java index f601117ff07e9..01ee26d04e784 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java @@ -1,9 +1,11 @@ package io.quarkus.vertx.http.runtime; import java.nio.file.Path; +import java.util.List; import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -25,6 +27,17 @@ public class AuthRuntimeConfig { @ConfigItem(name = "policy") public Map rolePolicy; + /** + * Map the `SecurityIdentity` roles to deployment specific roles and add the matching roles to `SecurityIdentity`. + *

+ * For example, if `SecurityIdentity` has a `user` role and the endpoint is secured with a 'UserRole' role, + * use this property to map the `user` role to the `UserRole` role, and have `SecurityIdentity` to have + * both `user` and `UserRole` roles. + */ + @ConfigDocMapKey("role1") + @ConfigItem + public Map> rolesMapping; + /** * Properties file containing the client certificate common name (CN) to role mappings. * Use it only if the mTLS authentication mechanism is enabled with either diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java index f9002d619a081..60504136f8708 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/management/ManagementRuntimeAuthConfig.java @@ -1,7 +1,9 @@ package io.quarkus.vertx.http.runtime.management; +import java.util.List; import java.util.Map; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.vertx.http.runtime.PolicyConfig; @@ -24,4 +26,15 @@ public class ManagementRuntimeAuthConfig { */ @ConfigItem(name = "policy") public Map rolePolicy; + + /** + * Map the `SecurityIdentity` roles to deployment specific roles and add the matching roles to `SecurityIdentity`. + *

+ * For example, if `SecurityIdentity` has a `user` role and the endpoint is secured with a 'UserRole' role, + * use this property to map the `user` role to the `UserRole` role, and have `SecurityIdentity` to have + * both `user` and `UserRole` roles. + */ + @ConfigDocMapKey("role1") + @ConfigItem + public Map> rolesMapping; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java index 1b18431ea3b26..7aac8b705a4b6 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java @@ -2,6 +2,7 @@ import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_FAILURE; import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_SUCCESS; +import static io.quarkus.vertx.http.runtime.security.QuarkusHttpUser.setIdentity; import java.io.IOException; import java.util.List; @@ -41,11 +42,13 @@ abstract class AbstractHttpAuthorizer { private final List policies; private final SecurityEventHelper securityEventHelper; private final HttpSecurityPolicy.AuthorizationRequestContext context; + private final RolesMapping rolesMapping; AbstractHttpAuthorizer(HttpAuthenticator httpAuthenticator, IdentityProviderManager identityProviderManager, AuthorizationController controller, List policies, BeanManager beanManager, BlockingSecurityExecutor blockingExecutor, Event authZFailureEvent, - Event authZSuccessEvent, boolean securityEventsEnabled) { + Event authZSuccessEvent, boolean securityEventsEnabled, + Map> rolesMapping) { this.httpAuthenticator = httpAuthenticator; this.identityProviderManager = identityProviderManager; this.controller = controller; @@ -53,6 +56,7 @@ abstract class AbstractHttpAuthorizer { this.context = new HttpSecurityPolicy.DefaultAuthorizationRequestContext(blockingExecutor); this.securityEventHelper = new SecurityEventHelper<>(authZSuccessEvent, authZFailureEvent, AUTHORIZATION_SUCCESS, AUTHORIZATION_FAILURE, beanManager, securityEventsEnabled); + this.rolesMapping = RolesMapping.of(rolesMapping); } /** @@ -65,8 +69,28 @@ public void checkPermission(RoutingContext routingContext) { return; } //check their permissions - doPermissionCheck(routingContext, QuarkusHttpUser.getSecurityIdentity(routingContext, identityProviderManager), 0, null, - policies); + doPermissionCheck(routingContext, augmentAndGetIdentity(routingContext), 0, null, policies); + } + + private Uni augmentAndGetIdentity(RoutingContext routingContext) { + if (rolesMapping != null) { + var identity = routingContext.user() == null ? null + : ((QuarkusHttpUser) routingContext.user()).getSecurityIdentity(); + if (identity == null) { + // make sure augmented identity is used no matter when the authentication happens + return setIdentity( + QuarkusHttpUser.getSecurityIdentity(routingContext, identityProviderManager) + .onItem() + .ifNotNull() + .transform(rolesMapping.withRoutingContext(routingContext)), + routingContext); + } else { + // augment right now as someone downstream could use user instead of deferred identity + return Uni.createFrom().item(rolesMapping.withRoutingContext(routingContext).apply(identity)); + } + } + + return QuarkusHttpUser.getSecurityIdentity(routingContext, identityProviderManager); } private void doPermissionCheck(RoutingContext routingContext, @@ -78,8 +102,7 @@ private void doPermissionCheck(RoutingContext routingContext, if (augmentedIdentity != null) { if (!augmentedIdentity.isAnonymous() && (currentUser == null || currentUser.getSecurityIdentity() != augmentedIdentity)) { - routingContext.setUser(new QuarkusHttpUser(augmentedIdentity)); - routingContext.put(QuarkusHttpUser.DEFERRED_IDENTITY_KEY, Uni.createFrom().item(augmentedIdentity)); + setIdentity(augmentedIdentity, routingContext); } if (securityEventHelper.fireEventOnSuccess()) { securityEventHelper.fireSuccessEvent(new AuthorizationSuccessEvent(augmentedIdentity, diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java index 9b6ea4461c787..b46351d33dce3 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java @@ -15,6 +15,7 @@ import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; import io.quarkus.security.spi.runtime.BlockingSecurityExecutor; +import io.quarkus.vertx.http.runtime.HttpConfiguration; /** * Class that is responsible for running the HTTP based permission checks @@ -26,9 +27,10 @@ public class HttpAuthorizer extends AbstractHttpAuthorizer { AuthorizationController controller, Instance installedPolicies, BlockingSecurityExecutor blockingExecutor, BeanManager beanManager, Event authZFailureEvent, Event authZSuccessEvent, - @ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled) { + @ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled, + HttpConfiguration httpConfig) { super(httpAuthenticator, identityProviderManager, controller, toList(installedPolicies), beanManager, blockingExecutor, - authZFailureEvent, authZSuccessEvent, securityEventsEnabled); + authZFailureEvent, authZSuccessEvent, securityEventsEnabled, httpConfig.auth.rolesMapping); } private static List toList(Instance installedPolicies) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementInterfaceHttpAuthorizer.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementInterfaceHttpAuthorizer.java index 5cb1016d2ba5e..e068829331e7d 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementInterfaceHttpAuthorizer.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ManagementInterfaceHttpAuthorizer.java @@ -14,6 +14,7 @@ import io.quarkus.security.spi.runtime.AuthorizationFailureEvent; import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent; import io.quarkus.security.spi.runtime.BlockingSecurityExecutor; +import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @@ -28,7 +29,8 @@ public ManagementInterfaceHttpAuthorizer(HttpAuthenticator httpAuthenticator, AuthorizationController controller, ManagementPathMatchingHttpSecurityPolicy installedPolicy, BlockingSecurityExecutor blockingExecutor, Event authZFailureEvent, Event authZSuccessEvent, BeanManager beanManager, - @ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled) { + @ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled, + ManagementInterfaceConfiguration runTimeConfig) { super(httpAuthenticator, identityProviderManager, controller, List.of(new HttpSecurityPolicy() { @@ -38,6 +40,7 @@ public Uni checkPermission(RoutingContext request, Uni getSecurityIdentity(RoutingContext routingCo } return Uni.createFrom().nullItem(); } + + static Uni setIdentity(Uni identityUni, RoutingContext routingContext) { + routingContext.setUser(null); + routingContext.put(QuarkusHttpUser.DEFERRED_IDENTITY_KEY, identityUni); + return identityUni; + } + + static SecurityIdentity setIdentity(SecurityIdentity identity, RoutingContext routingContext) { + routingContext.setUser(new QuarkusHttpUser(identity)); + routingContext.put(QuarkusHttpUser.DEFERRED_IDENTITY_KEY, Uni.createFrom().item(identity)); + return identity; + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java index 52061b6c41184..1fe75e4502f9e 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java @@ -1,14 +1,11 @@ package io.quarkus.vertx.http.runtime.security; import java.security.Permission; -import java.security.Principal; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; -import io.quarkus.security.credential.Credential; import io.quarkus.security.identity.SecurityIdentity; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @@ -16,30 +13,13 @@ /** * permission checker that handles role based permissions */ -public class RolesAllowedHttpSecurityPolicy implements HttpSecurityPolicy { +public class RolesAllowedHttpSecurityPolicy extends RolesMapping implements HttpSecurityPolicy { private static final String AUTHENTICATED = "**"; private final String[] rolesAllowed; - private final boolean grantPermissions; - private final boolean grantRoles; - private final Map> roleToPermissions; - private final Map> roleToRoles; public RolesAllowedHttpSecurityPolicy(List rolesAllowed, Map> roleToPermissions, Map> roleToRoles) { - if (roleToPermissions != null && !roleToPermissions.isEmpty()) { - this.grantPermissions = true; - this.roleToPermissions = Map.copyOf(roleToPermissions); - } else { - this.grantPermissions = false; - this.roleToPermissions = null; - } - if (roleToRoles != null && !roleToRoles.isEmpty()) { - this.grantRoles = true; - this.roleToRoles = Map.copyOf(roleToRoles); - } else { - this.grantRoles = false; - this.roleToRoles = null; - } + super(roleToPermissions, roleToRoles); this.rolesAllowed = rolesAllowed.toArray(String[]::new); } @@ -69,103 +49,4 @@ public CheckResult apply(SecurityIdentity securityIdentity) { } }); } - - private SecurityIdentity augmentIdentity(SecurityIdentity securityIdentity) { - Set roles = securityIdentity.getRoles(); - if (roles != null && !roles.isEmpty()) { - Set permissions = grantPermissions ? new HashSet<>() : null; - Set newRoles = grantRoles ? new HashSet<>() : null; - for (String role : roles) { - if (grantPermissions) { - if (roleToPermissions.containsKey(role)) { - permissions.addAll(roleToPermissions.get(role)); - } - } - if (grantRoles) { - if (roleToRoles.containsKey(role)) { - newRoles.addAll(roleToRoles.get(role)); - } - } - } - boolean addPerms = grantPermissions && !permissions.isEmpty(); - if (grantRoles && !newRoles.isEmpty()) { - newRoles.addAll(roles); - return augmentIdentity(securityIdentity, permissions, Set.copyOf(newRoles), addPerms); - } else if (addPerms) { - return augmentIdentity(securityIdentity, permissions, roles, true); - } - } - return null; - } - - private static SecurityIdentity augmentIdentity(SecurityIdentity securityIdentity, Set permissions, - Set roles, boolean addPerms) { - return new SecurityIdentity() { - @Override - public Principal getPrincipal() { - return securityIdentity.getPrincipal(); - } - - @Override - public boolean isAnonymous() { - return securityIdentity.isAnonymous(); - } - - @Override - public Set getRoles() { - return roles; - } - - @Override - public boolean hasRole(String s) { - return roles.contains(s); - } - - @Override - public T getCredential(Class aClass) { - return securityIdentity.getCredential(aClass); - } - - @Override - public Set getCredentials() { - return securityIdentity.getCredentials(); - } - - @Override - public T getAttribute(String s) { - return securityIdentity.getAttribute(s); - } - - @Override - public Map getAttributes() { - return securityIdentity.getAttributes(); - } - - @Override - public Uni checkPermission(Permission requiredPermission) { - if (addPerms) { - for (Permission possessedPermission : permissions) { - if (possessedPermission.implies(requiredPermission)) { - return Uni.createFrom().item(true); - } - } - } - - return securityIdentity.checkPermission(requiredPermission); - } - - @Override - public boolean checkPermissionBlocking(Permission requiredPermission) { - if (addPerms) { - for (Permission possessedPermission : permissions) { - if (possessedPermission.implies(requiredPermission)) { - return true; - } - } - } - - return securityIdentity.checkPermissionBlocking(requiredPermission); - } - }; - } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesMapping.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesMapping.java new file mode 100644 index 0000000000000..fe512febb78d7 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesMapping.java @@ -0,0 +1,161 @@ +package io.quarkus.vertx.http.runtime.security; + +import static io.quarkus.vertx.http.runtime.security.QuarkusHttpUser.setIdentity; + +import java.security.Permission; +import java.security.Principal; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import io.quarkus.security.credential.Credential; +import io.quarkus.security.identity.SecurityIdentity; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class RolesMapping { + + private final Map> roleToPermissions; + private final Map> roleToRoles; + protected final boolean grantPermissions; + protected final boolean grantRoles; + + RolesMapping(Map> roleToPermissions, + Map> roleToRoles) { + if (roleToPermissions != null && !roleToPermissions.isEmpty()) { + this.grantPermissions = true; + this.roleToPermissions = Map.copyOf(roleToPermissions); + } else { + this.grantPermissions = false; + this.roleToPermissions = null; + } + if (roleToRoles != null && !roleToRoles.isEmpty()) { + this.grantRoles = true; + this.roleToRoles = Map.copyOf(roleToRoles); + } else { + this.grantRoles = false; + this.roleToRoles = null; + } + } + + static RolesMapping of(Map> roleToRoles) { + return roleToRoles.isEmpty() ? null : new RolesMapping(null, roleToRoles); + } + + Function withRoutingContext(RoutingContext context) { + return new Function() { + @Override + public SecurityIdentity apply(SecurityIdentity identity) { + if (identity.isAnonymous()) { + return identity; + } + var newIdentity = augmentIdentity(identity); + if (newIdentity == null) { + return identity; + } + return setIdentity(newIdentity, context); + } + }; + } + + protected SecurityIdentity augmentIdentity(SecurityIdentity securityIdentity) { + Set roles = securityIdentity.getRoles(); + if (roles != null && !roles.isEmpty()) { + Set permissions = grantPermissions ? new HashSet<>() : null; + Set newRoles = grantRoles ? new HashSet<>() : null; + for (String role : roles) { + if (grantPermissions) { + if (roleToPermissions.containsKey(role)) { + permissions.addAll(roleToPermissions.get(role)); + } + } + if (grantRoles) { + if (roleToRoles.containsKey(role)) { + newRoles.addAll(roleToRoles.get(role)); + } + } + } + boolean addPerms = grantPermissions && !permissions.isEmpty(); + if (grantRoles && !newRoles.isEmpty()) { + newRoles.addAll(roles); + return augmentIdentity(securityIdentity, permissions, Set.copyOf(newRoles), addPerms); + } else if (addPerms) { + return augmentIdentity(securityIdentity, permissions, roles, true); + } + } + return null; + } + + private static SecurityIdentity augmentIdentity(SecurityIdentity securityIdentity, Set permissions, + Set roles, boolean addPerms) { + return new SecurityIdentity() { + @Override + public Principal getPrincipal() { + return securityIdentity.getPrincipal(); + } + + @Override + public boolean isAnonymous() { + return securityIdentity.isAnonymous(); + } + + @Override + public Set getRoles() { + return roles; + } + + @Override + public boolean hasRole(String s) { + return roles.contains(s); + } + + @Override + public T getCredential(Class aClass) { + return securityIdentity.getCredential(aClass); + } + + @Override + public Set getCredentials() { + return securityIdentity.getCredentials(); + } + + @Override + public T getAttribute(String s) { + return securityIdentity.getAttribute(s); + } + + @Override + public Map getAttributes() { + return securityIdentity.getAttributes(); + } + + @Override + public Uni checkPermission(Permission requiredPermission) { + if (addPerms) { + for (Permission possessedPermission : permissions) { + if (possessedPermission.implies(requiredPermission)) { + return Uni.createFrom().item(true); + } + } + } + + return securityIdentity.checkPermission(requiredPermission); + } + + @Override + public boolean checkPermissionBlocking(Permission requiredPermission) { + if (addPerms) { + for (Permission possessedPermission : permissions) { + if (possessedPermission.implies(requiredPermission)) { + return true; + } + } + } + + return securityIdentity.checkPermissionBlocking(requiredPermission); + } + }; + } +} diff --git a/integration-tests/oidc-tenancy/src/main/resources/application.properties b/integration-tests/oidc-tenancy/src/main/resources/application.properties index 9cd3b8499e0cb..6f41166b32de7 100644 --- a/integration-tests/oidc-tenancy/src/main/resources/application.properties +++ b/integration-tests/oidc-tenancy/src/main/resources/application.properties @@ -165,3 +165,4 @@ quarkus.http.auth.permission.identity-augmentation.policy=roles3 quarkus.http.auth.permission.identity-augmentation.applies-to=JAXRS quarkus.http.auth.policy.roles3.roles-allowed=role3,role2 quarkus.http.auth.policy.roles3.permissions.role3=get-tenant +quarkus.http.auth.roles-mapping.role4=role3 diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java index 1d53ce36f731d..0fd71a499a895 100644 --- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java +++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java @@ -78,6 +78,13 @@ public void testJaxRsIdentityAugmentation() { .when().get("/api/tenant-echo/hr-identity-augmentation") .then().statusCode(200) .body(Matchers.equalTo(("tenant-id=tenant-public-key, static.tenant.id=tenant-public-key, name=alice"))); + + // test mapped role can be used by a roles policy + token = getTokenWithRole("role4"); + RestAssured.given().auth().oauth2(token) + .when().get("/api/tenant-echo/hr-identity-augmentation") + .then().statusCode(200) + .body(Matchers.equalTo(("tenant-id=tenant-public-key, static.tenant.id=tenant-public-key, name=alice"))); } static String getTokenWithRole(String... roles) { diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java index 6ae6e4b636bd7..e55ba009bb4d0 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java @@ -31,6 +31,7 @@ public Map getConfigOverrides() { Map.entry("quarkus.http.auth.policy.roles2.roles-allowed", "role2"), Map.entry("quarkus.http.auth.policy.roles3.roles-allowed", "role3,role2"), Map.entry("quarkus.http.auth.policy.roles3.permissions.role3", "get-tenant"), + Map.entry("quarkus.http.auth.roles-mapping.role4", "role3"), Map.entry("quarkus.http.auth.permission.jax-rs1.paths", "/api/tenant-echo2/hr-jax-rs-perm-check"), Map.entry("quarkus.http.auth.permission.jax-rs1.policy", "roles1"), Map.entry("quarkus.http.auth.permission.jax-rs1.applies-to", "JAXRS"), @@ -301,6 +302,12 @@ public void testJaxRsIdentityAugmentation() { .when().get("/api/tenant-echo/hr-identity-augmentation") .then().statusCode(200) .body(Matchers.equalTo(("tenant-id=hr, static.tenant.id=hr, name=alice"))); + + token = getTokenWithRole("role4"); + RestAssured.given().auth().oauth2(token) + .when().get("/api/tenant-echo/hr-identity-augmentation") + .then().statusCode(200) + .body(Matchers.equalTo(("tenant-id=hr, static.tenant.id=hr, name=alice"))); } finally { server.stop(); }