Skip to content

Commit

Permalink
Merge pull request #39339 from michalvavrik/feature/simplify-role-map…
Browse files Browse the repository at this point in the history
…pings

Simplify configuration based mapping of token roles to deployment-specific SecurityIdentity
  • Loading branch information
sberyozkin committed Mar 13, 2024
2 parents 284c7dd + e5533d0 commit a899875
Show file tree
Hide file tree
Showing 13 changed files with 296 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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());
}
});
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -25,6 +27,17 @@ public class AuthRuntimeConfig {
@ConfigItem(name = "policy")
public Map<String, PolicyConfig> rolePolicy;

/**
* Map the `SecurityIdentity` roles to deployment specific roles and add the matching roles to `SecurityIdentity`.
* <p>
* 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<String, List<String>> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,4 +26,15 @@ public class ManagementRuntimeAuthConfig {
*/
@ConfigItem(name = "policy")
public Map<String, PolicyConfig> rolePolicy;

/**
* Map the `SecurityIdentity` roles to deployment specific roles and add the matching roles to `SecurityIdentity`.
* <p>
* 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<String, List<String>> rolesMapping;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,18 +42,21 @@ abstract class AbstractHttpAuthorizer {
private final List<HttpSecurityPolicy> policies;
private final SecurityEventHelper<AuthorizationSuccessEvent, AuthorizationFailureEvent> securityEventHelper;
private final HttpSecurityPolicy.AuthorizationRequestContext context;
private final RolesMapping rolesMapping;

AbstractHttpAuthorizer(HttpAuthenticator httpAuthenticator, IdentityProviderManager identityProviderManager,
AuthorizationController controller, List<HttpSecurityPolicy> policies, BeanManager beanManager,
BlockingSecurityExecutor blockingExecutor, Event<AuthorizationFailureEvent> authZFailureEvent,
Event<AuthorizationSuccessEvent> authZSuccessEvent, boolean securityEventsEnabled) {
Event<AuthorizationSuccessEvent> authZSuccessEvent, boolean securityEventsEnabled,
Map<String, List<String>> rolesMapping) {
this.httpAuthenticator = httpAuthenticator;
this.identityProviderManager = identityProviderManager;
this.controller = controller;
this.policies = policies;
this.context = new HttpSecurityPolicy.DefaultAuthorizationRequestContext(blockingExecutor);
this.securityEventHelper = new SecurityEventHelper<>(authZSuccessEvent, authZFailureEvent, AUTHORIZATION_SUCCESS,
AUTHORIZATION_FAILURE, beanManager, securityEventsEnabled);
this.rolesMapping = RolesMapping.of(rolesMapping);
}

/**
Expand All @@ -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<SecurityIdentity> 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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,9 +27,10 @@ public class HttpAuthorizer extends AbstractHttpAuthorizer {
AuthorizationController controller, Instance<HttpSecurityPolicy> installedPolicies,
BlockingSecurityExecutor blockingExecutor, BeanManager beanManager,
Event<AuthorizationFailureEvent> authZFailureEvent, Event<AuthorizationSuccessEvent> 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<HttpSecurityPolicy> toList(Instance<HttpSecurityPolicy> installedPolicies) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,7 +29,8 @@ public ManagementInterfaceHttpAuthorizer(HttpAuthenticator httpAuthenticator,
AuthorizationController controller, ManagementPathMatchingHttpSecurityPolicy installedPolicy,
BlockingSecurityExecutor blockingExecutor, Event<AuthorizationFailureEvent> authZFailureEvent,
Event<AuthorizationSuccessEvent> 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() {

Expand All @@ -38,6 +40,7 @@ public Uni<CheckResult> checkPermission(RoutingContext request, Uni<SecurityIden
return installedPolicy.checkPermission(request, identity, requestContext);
}

}), beanManager, blockingExecutor, authZFailureEvent, authZSuccessEvent, securityEventsEnabled);
}), beanManager, blockingExecutor, authZFailureEvent, authZSuccessEvent, securityEventsEnabled,
runTimeConfig.auth.rolesMapping);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,16 @@ public static Uni<SecurityIdentity> getSecurityIdentity(RoutingContext routingCo
}
return Uni.createFrom().nullItem();
}

static Uni<SecurityIdentity> setIdentity(Uni<SecurityIdentity> 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;
}
}
Loading

0 comments on commit a899875

Please sign in to comment.