Skip to content

Commit

Permalink
Allow to map token roles to deployment-specific SecurityIdentity roles
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Nov 24, 2023
1 parent 38ac518 commit ea6f5ea
Show file tree
Hide file tree
Showing 11 changed files with 401 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,19 @@ This configuration only impacts resources served from the fixed or static URL, `

For more information, see link:https://quarkus.io/blog/path-resolution-in-quarkus/[Path Resolution in Quarkus].

[[map-security-identity-roles]]
=== Map `SecurityIdentity` roles

Winning role-based policy can map the `SecurityIdentity` roles to the deployment specific roles.
These roles can later be used for endpoint authorization with the `@RolesAllowed` annotation.

[source,properties]
----
quarkus.http.auth.policy.admin-policy1.roles.admin=Admin1 <1>
quarkus.http.auth.permission.roles1.paths=/*
quarkus.http.auth.permission.roles1.policy=admin-policy1
----
<1> Map the `admin` role to `Admin1` role. The `SecurityIdentity` will newly have both `admin` and `Admin1` roles.

[[standard-security-annotations]]
== Authorization using annotations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ If the token is opaque (binary) then a `scope` property from the remote token in
If UserInfo is the source of the roles then set `quarkus.oidc.authentication.user-info-required=true` and `quarkus.oidc.roles.source=userinfo`, and if needed, `quarkus.oidc.roles.role-claim-path`.

Additionally, a custom `SecurityIdentityAugmentor` can also be used to add the roles as documented in xref:security-customization.adoc#security-identity-customization[Security Identity Customization].
You can also map `SecurityIdentity` roles created from token claims to deployment specific roles with the xref:security-authorize-web-endpoints-reference.adoc#map-security-identity-roles[HTTP Security policy].

[[token-scopes-and-security-identity-permissions]]
=== Token scopes And SecurityIdentity permissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ If the access token contains the roles and this access token is not meant to be
If UserInfo is the source of the roles then set `quarkus.oidc.roles.source=userinfo`, and if needed, `quarkus.oidc.roles.role-claim-path`.

Additionally, a custom `SecurityIdentityAugmentor` can also be used to add the roles. For more information, see xref:security-customization.adoc#security-identity-customization[SecurityIdentity customization].
You can also map `SecurityIdentity` roles created from token claims to deployment specific roles with the xref:security-authorize-web-endpoints-reference.adoc#map-security-identity-roles[HTTP Security policy].

=== Ensuring validity of tokens and authentication data

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ interface AuthenticatedUser {

enum AuthenticatedUserImpl implements AuthenticatedUser {
ADMIN(AuthenticatedUserImpl::useAdminRole),
ROOT(AuthenticatedUserImpl::useRootRole),
USER(AuthenticatedUserImpl::useUserRole),
TEST(AuthenticatedUserImpl::useTestRole),
TEST2(AuthenticatedUserImpl::useTest2Role);
Expand All @@ -331,6 +332,10 @@ private static void useTest2Role() {
TestIdentityController.resetRoles().add("test2", "test2", "test2");
}

private static void useRootRole() {
TestIdentityController.resetRoles().add("root", "root", "root", "Admin1");
}

private static void useAdminRole() {
TestIdentityController.resetRoles().add("admin", "admin", "admin");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package io.quarkus.vertx.http.security.permission;

import static io.quarkus.vertx.http.security.permission.AbstractHttpSecurityPolicyGrantingPermissionsTest.AuthenticatedUserImpl.ADMIN;
import static io.quarkus.vertx.http.security.permission.AbstractHttpSecurityPolicyGrantingPermissionsTest.AuthenticatedUserImpl.ROOT;
import static io.quarkus.vertx.http.security.permission.AbstractHttpSecurityPolicyGrantingPermissionsTest.AuthenticatedUserImpl.USER;

import java.util.function.Supplier;

import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.Arc;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.UnauthorizedException;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.SecurityIdentityAssociation;
import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
import io.quarkus.vertx.http.security.CustomPermission;
import io.quarkus.vertx.http.security.CustomPermissionWithActions;
import io.quarkus.vertx.http.security.permission.AbstractHttpSecurityPolicyGrantingPermissionsTest.AuthenticatedUser;
import io.quarkus.vertx.http.security.permission.AbstractHttpSecurityPolicyGrantingPermissionsTest.AuthenticatedUserImpl;
import io.restassured.RestAssured;
import io.smallrye.mutiny.Uni;
import io.vertx.core.Handler;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;

public class HttpSecPolicyGrantingRolesTest {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(TestIdentityController.class, TestIdentityProvider.class, RolesPathHandler.class,
CDIBean.class, CustomPermission.class, CustomPermissionWithActions.class, AuthenticatedUser.class,
AuthenticatedUserImpl.class)
.addAsResource("conf/http-roles-grant-config.properties", "application.properties"));

@Test
public void mapRolesToRolesSecuredWithRolesAllowed() {
assertSuccess(ADMIN, "/test/new-admin-roles-blocking");
assertSuccess(ADMIN, "/test/new-admin-roles");
assertSuccess(ADMIN, "/test/new-admin-roles2");
assertSuccess(ADMIN, "/test/old-admin-roles-blocking");
assertSuccess(ADMIN, "/test/old-admin-roles");
assertSuccess(ADMIN, "/test/multiple-new-roles-1");
assertSuccess(ADMIN, "/test/multiple-new-roles-2");
assertSuccess(ADMIN, "/test/multiple-new-roles-3");
assertForbidden(USER, "/test/new-admin-roles-blocking");
assertForbidden(USER, "/test/new-admin-roles");
assertForbidden(USER, "/test/new-admin-roles2");
assertForbidden(USER, "/test/old-admin-roles-blocking");
assertForbidden(USER, "/test/old-admin-roles");
assertSuccess(USER, "/test/multiple-new-roles-1");
assertSuccess(USER, "/test/multiple-new-roles-2");
assertSuccess(USER, "/test/multiple-new-roles-3");
}

@Test
public void mapRolesToRolesNoSecurityAnnotation() {
assertSuccess(ADMIN, "/test/roles-allowed-path");
assertForbidden(USER, "/test/roles-allowed-path");
assertForbidden(ROOT, "/test/roles-allowed-path");
assertSuccess(ADMIN, "/test/granted-and-checked-by-policy");
assertForbidden(USER, "/test/granted-and-checked-by-policy");
assertSuccess(ROOT, "/test/granted-and-checked-by-policy");
}

@Test
public void mapRolesToBothPermissionsAndRoles() {
assertSuccess(ADMIN, "/test/roles-and-perms-1");
assertSuccess(ADMIN, "/test/roles-and-perms-2");
assertForbidden(USER, "/test/roles-and-perms-1");
assertForbidden(USER, "/test/roles-and-perms-2");
}

@ApplicationScoped
public static class RolesPathHandler {

@Inject
CDIBean cdiBean;

public void setup(@Observes Router router) {
router.route("/test/new-admin-roles-blocking").blockingHandler(new RouteHandler(() -> {
cdiBean.newRolesBlocking();
return Uni.createFrom().nullItem();
}));
router.route("/test/new-admin-roles").handler(new RouteHandler(cdiBean::newRoles));
router.route("/test/new-admin-roles2").handler(new RouteHandler(cdiBean::newRoles2));
router.route("/test/old-admin-roles-blocking").blockingHandler(new RouteHandler(() -> {
cdiBean.oldRolesBlocking();
return Uni.createFrom().nullItem();
}));
router.route("/test/old-admin-roles").handler(new RouteHandler(cdiBean::oldRoles));
router.route("/test/multiple-new-roles-1").handler(new RouteHandler(cdiBean::multipleNewRoles1));
router.route("/test/multiple-new-roles-2").handler(new RouteHandler(cdiBean::multipleNewRoles2));
router.route("/test/multiple-new-roles-3").handler(new RouteHandler(cdiBean::multipleNewRoles3));
router.route("/test/roles-allowed-path").handler(new RouteHandler(cdiBean::checkAdmin1Role));
router.route("/test/granted-and-checked-by-policy").handler(new RouteHandler(cdiBean::checkAdmin1Role));
router.route("/test/roles-and-perms-1").handler(new RouteHandler(cdiBean::rolesAndPermissions1));
router.route("/test/roles-and-perms-2").handler(new RouteHandler(cdiBean::rolesAndPermissions2));
}
}

private static final class RouteHandler implements Handler<RoutingContext> {

private final Supplier<Uni<Void>> callService;

private RouteHandler(Supplier<Uni<Void>> callService) {
this.callService = callService;
}

@Override
public void handle(RoutingContext event) {
// activate context so that we can use CDI beans
Arc.container().requestContext().activate();
// set identity used by security checks performed by standard security interceptors
QuarkusHttpUser user = (QuarkusHttpUser) event.user();
Arc.container().instance(SecurityIdentityAssociation.class).get().setIdentity(user.getSecurityIdentity());

callService.get().subscribe().with(unused -> {
String ret = user.getSecurityIdentity().getPrincipal().getName() +
":" + event.normalizedPath();
event.response().end(ret);
}, throwable -> {
if (throwable instanceof UnauthorizedException) {
event.response().setStatusCode(401);
} else if (throwable instanceof ForbiddenException) {
event.response().setStatusCode(403);
} else {
event.response().setStatusCode(500);
}
event.end();
});
}
}

private void assertSuccess(AuthenticatedUser user, String... paths) {
user.authenticate();
for (var path : paths) {
RestAssured
.given()
.auth()
.basic(user.role(), user.role())
.get(path)
.then()
.statusCode(200)
.body(Matchers.is(user.role() + ":" + path));
}
}

private void assertForbidden(AuthenticatedUser user, String... paths) {
user.authenticate();
for (var path : paths) {
RestAssured
.given()
.auth()
.basic(user.role(), user.role())
.get(path)
.then()
.statusCode(403);
}
}

@ApplicationScoped
public static class CDIBean {

@Inject
SecurityIdentity identity;

@RolesAllowed("Admin1")
public void newRolesBlocking() {
// NOTHING TO DO
}

@RolesAllowed("Admin1")
public Uni<Void> newRoles() {
return Uni.createFrom().nullItem();
}

@RolesAllowed("Admin2")
public Uni<Void> newRoles2() {
return Uni.createFrom().nullItem();
}

@RolesAllowed("admin")
public void oldRolesBlocking() {
// NOTHING TO DO
}

@RolesAllowed("admin")
public Uni<Void> oldRoles() {
return Uni.createFrom().nullItem();
}

@RolesAllowed("Janet")
public Uni<Void> multipleNewRoles1() {
return Uni.createFrom().nullItem();
}

@RolesAllowed("Monica")
public Uni<Void> multipleNewRoles2() {
return Uni.createFrom().nullItem();
}

@RolesAllowed("Robin")
public Uni<Void> multipleNewRoles3() {
return Uni.createFrom().nullItem();
}

@RolesAllowed("Admin3")
public Uni<Void> rolesAndPermissions1() {
return Uni.createFrom().nullItem();
}

@PermissionsAllowed("jump")
public Uni<Void> rolesAndPermissions2() {
return Uni.createFrom().nullItem();
}

public Uni<Void> checkAdmin1Role() {
if (identity.hasRole("Admin1")) {
if (identity.getPrincipal().getName().equals("root")) {
if (identity.hasRole("sudo")) {
return Uni.createFrom().nullItem();
}
} else {
return Uni.createFrom().nullItem();
}
}
return Uni.createFrom().failure(AuthenticationFailedException::new);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
quarkus.http.auth.basic=true
quarkus.http.auth.policy.t1.roles.admin=Admin1
quarkus.http.auth.permission.t1.paths=/*
quarkus.http.auth.permission.t1.policy=t1
quarkus.http.auth.policy.t2.roles.admin=Admin2
quarkus.http.auth.permission.t2.paths=/*
quarkus.http.auth.permission.t2.policy=t2
quarkus.http.auth.policy.t3.roles.user=Janet,Robin
quarkus.http.auth.policy.t3.roles.admin=Janet,Robin
quarkus.http.auth.permission.t3.paths=/test/multiple-new-roles-1
quarkus.http.auth.permission.t3.policy=t3
quarkus.http.auth.policy.t4.roles.user=Monica,Robin
quarkus.http.auth.policy.t4.roles.admin=Monica,Robin
quarkus.http.auth.permission.t4.paths=/test/multiple-new-roles-2,/test/multiple-new-roles-3
quarkus.http.auth.permission.t4.policy=t4
quarkus.http.auth.policy.t5.roles-allowed=admin
quarkus.http.auth.policy.t5.roles.admin=Admin1
quarkus.http.auth.permission.t5.paths=/test/roles-allowed-path
quarkus.http.auth.permission.t5.policy=t5
quarkus.http.auth.policy.t6.roles-allowed=Admin1
quarkus.http.auth.policy.t6.roles.admin=Admin1
quarkus.http.auth.policy.t6.roles.root=sudo
quarkus.http.auth.permission.t6.paths=/test/granted-and-checked-by-policy
quarkus.http.auth.permission.t6.policy=t6
quarkus.http.auth.policy.t7.roles.admin=Admin3
quarkus.http.auth.policy.t7.permissions.admin=jump
quarkus.http.auth.permission.t7.paths=/test/roles-and-perms-1,/test/roles-and-perms-2
quarkus.http.auth.permission.t7.policy=t7
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ public class PolicyConfig {
@ConvertWith(TrimmedStringConverter.class)
public List<String> rolesAllowed;

/**
* Add roles granted to the `SecurityIdentity` based on the roles that the `SecurityIdentity` already have.
* For example, the Quarkus OIDC extension can map roles from the verified JWT access token, and you may want
* to remap them to a deployment specific roles.
*/
@ConfigDocMapKey("role1")
@ConfigItem
public Map<String, List<String>> roles;

/**
* Permissions granted to the `SecurityIdentity` if this policy is applied successfully
* (the policy allows request to proceed) and the authenticated request has required role.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,12 @@ private static Map<String, HttpSecurityPolicy> toNamedHttpSecPolicies(Map<String
}

for (Map.Entry<String, PolicyConfig> e : rolePolicies.entrySet()) {
PolicyConfig policyConfig = e.getValue();
final PolicyConfig policyConfig = e.getValue();
final Map<String, Set<Permission>> roleToPermissions;
if (policyConfig.permissions.isEmpty()) {
namedPolicies.put(e.getKey(), new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed));
roleToPermissions = null;
} else {
final Map<String, Set<Permission>> roleToPermissions = new HashMap<>();
roleToPermissions = new HashMap<>();
for (Map.Entry<String, List<String>> roleToPermissionStr : policyConfig.permissions.entrySet()) {

// collect permission actions
Expand All @@ -190,9 +191,9 @@ private static Map<String, HttpSecurityPolicy> toNamedHttpSecPolicies(Map<String

roleToPermissions.put(role, Set.copyOf(permissions));
}
namedPolicies.put(e.getKey(),
new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed, Map.copyOf(roleToPermissions)));
}
namedPolicies.put(e.getKey(),
new RolesAllowedHttpSecurityPolicy(policyConfig.rolesAllowed, roleToPermissions, policyConfig.roles));
}
namedPolicies.put("deny", new DenySecurityPolicy());
namedPolicies.put("permit", new PermitSecurityPolicy());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ public ImmutablePathMatcher<T> build() {
if (p.prefixPathHandler != null) {
handler = p.prefixPathHandler;
if (STRING_PATH_SEPARATOR.equals(p.path)) {
defaultHandler = p.prefixPathHandler;
if (defaultHandler == null) {
defaultHandler = p.prefixPathHandler;
} else {
handlerAccumulator.accept(defaultHandler, p.prefixPathHandler);
}
}
}

Expand Down
Loading

0 comments on commit ea6f5ea

Please sign in to comment.