Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to map token roles to deployment-specific SecurityIdentity roles #37275

Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,22 @@

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 have both `admin` and `Admin1` roles.

[[standard-security-annotations]]
== Authorization using annotations

Check warning on line 353 in docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc", "range": {"start": {"line": 353, "column": 17}}}, "severity": "INFO"}

{project-name} includes built-in security to allow for link:https://en.wikipedia.org/wiki/Role-based_access_control[Role-Based Access Control (RBAC)]
based on the common security annotations `@RolesAllowed`, `@DenyAll`, `@PermitAll` on REST endpoints and CDI beans.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,11 @@

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].

Check warning on line 116 in docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc", "range": {"start": {"line": 116, "column": 86}}}, "severity": "INFO"}
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

Check warning on line 120 in docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Token scopes And SecurityIdentity permissions'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Token scopes And SecurityIdentity permissions'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc", "range": {"start": {"line": 120, "column": 5}}}, "severity": "INFO"}

SecurityIdentity permissions are mapped in the form of the `io.quarkus.security.StringPermission` from the scope parameter of the <<token-claims-and-security-identity-roles,source of the roles>>, using the same claim separator.

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
michalvavrik marked this conversation as resolved.
Show resolved Hide resolved
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
Loading