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 47101d802867d..da574c8b26217 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -332,6 +332,7 @@ tester.group=Software tester.role=Tester %prod.secured=User %dev.secured=** +all-roles=Administrator,Software,Tester,User ---- [source,java] @@ -353,32 +354,38 @@ public class SubjectExposingResource { @Path("admin") @RolesAllowed("${admin}") <1> public String getSubjectSecuredAdmin(@Context SecurityContext sec) { - Principal user = sec.getUserPrincipal(); - String name = user != null ? user.getName() : "anonymous"; - return name; + return getUsername(sec); } @GET @Path("software-tester") @RolesAllowed("${tester.group}-${tester.role}") <2> public String getSubjectSoftwareTester(@Context SecurityContext sec) { - Principal user = sec.getUserPrincipal(); - String name = user != null ? user.getName() : "anonymous"; - return name; + return getUsername(sec); } @GET @Path("user") @RolesAllowed("${customer:User}") <3> public String getSubjectUser(@Context SecurityContext sec) { - Principal user = sec.getUserPrincipal(); - String name = user != null ? user.getName() : "anonymous"; - return name; + return getUsername(sec); } + @GET @Path("secured") @RolesAllowed("${secured}") <4> public String getSubjectSecured(@Context SecurityContext sec) { + return getUsername(sec); + } + + @GET + @Path("list") + @RolesAllowed("${all-roles}") <5> + public String getSubjectList(@Context SecurityContext sec) { + return getUsername(sec); + } + + private String getUsername(SecurityContext sec) { Principal user = sec.getUserPrincipal(); String name = user != null ? user.getName() : "anonymous"; return name; @@ -391,6 +398,7 @@ It is possible to use multiple expressions in the role definition. <3> This `/subject/user` endpoint requires an authenticated user that has been granted the role "User" through the use of the `@RolesAllowed("${customer:User}")` annotation because we did not set the configuration property `customer`. <4> In production, this `/subject/secured` endpoint requires an authenticated user with the `User` role. In development mode, it allows any authenticated user. +<5> Property expression `all-roles` will be treated as a collection type `List`, therefore the endpoint will be accessible for roles `Administrator`, `Software`, `Tester` and `User`. === Permission annotation diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionTest.java index be0c7df88482b..7af69cfa4c78e 100644 --- a/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionTest.java +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/rolesallowed/RolesAllowedExpressionTest.java @@ -39,7 +39,8 @@ public class RolesAllowedExpressionTest { "test-profile-admin=batman\n" + "%test.test-profile-admin=admin\n" + "missing-profile-profile-admin=superman\n" + - "%missing-profile.missing-profile-profile-admin=admin\n"; + "%missing-profile.missing-profile-profile-admin=admin\n" + + "all-roles=Administrator,Software,Tester,User\n"; @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() @@ -76,6 +77,19 @@ public void shouldRestrictAccessToSpecificRole() { // test profile assertSuccess(() -> bean.testProfile(), "accessibleForTestProfileAdmin", ADMIN); assertFailureFor(() -> bean.missingTestProfile(), ForbiddenException.class, ADMIN); + + // property expression with collection separator should be treated as list + assertSuccess(() -> bean.list(), "list", + new AuthData(Set.of("Administrator"), false, "list")); + assertSuccess(() -> bean.list(), "list", + new AuthData(Set.of("Software"), false, "list")); + assertSuccess(() -> bean.list(), "list", + new AuthData(Set.of("Tester"), false, "list")); + assertSuccess(() -> bean.list(), "list", + new AuthData(Set.of("User"), false, "list")); + assertSuccess(() -> bean.list(), "list", + new AuthData(Set.of("Administrator", "Software", "Tester", "User"), false, "list")); + assertFailureFor(() -> bean.list(), ForbiddenException.class, ADMIN); } @Singleton @@ -122,6 +136,11 @@ public final void missingTestProfile() { // should throw exception } + @RolesAllowed("${all-roles}") + public final String list() { + return "list"; + } + } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java index 9a90299b18e6d..8ffc983e7e057 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java @@ -4,6 +4,7 @@ import java.lang.reflect.InvocationTargetException; import java.security.Permission; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -28,6 +29,7 @@ import io.quarkus.security.spi.runtime.SecurityCheck; import io.quarkus.security.spi.runtime.SecurityCheckStorage; import io.smallrye.config.Expressions; +import io.smallrye.config.common.utils.StringUtil; @Recorder public class SecurityCheckRecorder { @@ -69,7 +71,7 @@ public SecurityCheck rolesAllowedSupplier(String[] allowedRoles, int[] configExp private static Supplier resolveRolesAllowedConfigExp(String[] allowedRoles, int[] configExpIndexes, int[] configKeys) { - final String[] roles = Arrays.copyOf(allowedRoles, allowedRoles.length); + final List roles = new ArrayList<>(Arrays.asList(allowedRoles)); return new Supplier() { @Override public String[] get() { @@ -79,10 +81,29 @@ public String[] get() { // property expressions are enabled for (int i = 0; i < configExpIndexes.length; i++) { // resolve configuration expressions specified as value of the @RolesAllowed annotation - roles[configExpIndexes[i]] = config.getValue(transformToKey(configKeys[i]), String.class); + var strVal = config.getValue(transformToKey(configKeys[i]), String.class); + + // treat config value that contains collection separator as a list + // @RolesAllowed({"${my.roles}"}) => my.roles=one,two <=> @RolesAllowed({"one", "two"}) + if (strVal != null && strVal.contains(",")) { + var strArr = StringUtil.split(strVal); + if (strArr.length > 1) { + // role order is irrelevant as logical operator between them is OR + + // first role will go to the original place + strVal = strArr[0]; + + // the rest of the roles will be appended at the end + for (int i1 = 1; i1 < strArr.length; i1++) { + roles.add(strArr[i1]); + } + } + } + + roles.set(configExpIndexes[i], strVal); } } - return roles; + return roles.toArray(String[]::new); } }; }