Skip to content

Commit

Permalink
Support @RolesAllowed property expression treated as list
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Jul 21, 2023
1 parent e701cb9 commit 82a844f
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ tester.group=Software
tester.role=Tester
%prod.secured=User
%dev.secured=**
all-roles=Administrator,Software,Tester,User
----

[source,java]
Expand All @@ -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;
Expand All @@ -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`.

Check warning on line 398 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 '' - ' (range)', 'by using', 'finished', or 'completed' rather than 'through'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using '' - ' (range)', 'by using', 'finished', or 'completed' rather than 'through'.", "location": {"path": "docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc", "range": {"start": {"line": 398, "column": 104}}}, "severity": "INFO"}
<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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -122,6 +136,11 @@ public final void missingTestProfile() {
// should throw exception
}

@RolesAllowed("${all-roles}")
public final String list() {
return "list";
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -69,7 +71,7 @@ public SecurityCheck rolesAllowedSupplier(String[] allowedRoles, int[] configExp
private static Supplier<String[]> resolveRolesAllowedConfigExp(String[] allowedRoles, int[] configExpIndexes,
int[] configKeys) {

final String[] roles = Arrays.copyOf(allowedRoles, allowedRoles.length);
final List<String> roles = new ArrayList<>(Arrays.asList(allowedRoles));
return new Supplier<String[]>() {
@Override
public String[] get() {
Expand All @@ -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);
}
};
}
Expand Down

0 comments on commit 82a844f

Please sign in to comment.