You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ja...@apache.org on 2022/09/14 20:26:14 UTC
[solr] branch branch_9x updated: SOLR-16230: JWT nested roles support (#890)
This is an automated email from the ASF dual-hosted git repository.
janhoy pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_9x by this push:
new dd3b35fc00f SOLR-16230: JWT nested roles support (#890)
dd3b35fc00f is described below
commit dd3b35fc00faaf1cafb26350208322dd558a2303
Author: Marco Descher <ma...@descher.at>
AuthorDate: Wed Sep 14 22:10:17 2022 +0200
SOLR-16230: JWT nested roles support (#890)
Co-authored-by: Jan Høydahl <ja...@users.noreply.github.com>
---
solr/CHANGES.txt | 2 +
.../apache/solr/security/jwt/JWTAuthPlugin.java | 37 +++++++++++++++++-
.../solr/security/jwt/JWTAuthPluginTest.java | 44 +++++++++++++++++++++-
.../pages/jwt-authentication-plugin.adoc | 2 +-
4 files changed, 82 insertions(+), 3 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index b50492c26e0..6cde9ec14a9 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -58,6 +58,8 @@ Improvements
* SOLR-16362: Logs: Truncate field values in logs if a doc fails to index. (Nazerke Seidan, David Smiley)
+* SOLR-16230: JWT nested roles support (Marco Descher, janhoy)
+
Optimizations
---------------------
* SOLR-16120: Optimise hl.fl expansion. (Christine Poerschke, David Smiley, Mike Drob)
diff --git a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
index aa13b44e293..6c85d72ba46 100644
--- a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
+++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
@@ -34,6 +34,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -599,11 +600,45 @@ public class JWTAuthPlugin extends AuthenticationPlugin
// Pull roles from separate claim, either as whitespace separated list or as JSON
// array
Object rolesObj = jwtClaims.getClaimValue(rolesClaim);
+ if (rolesObj == null && rolesClaim.indexOf('.') > 0) {
+ // support map resolution of nested values
+ String[] nestedKeys = rolesClaim.split("\\.");
+ rolesObj = jwtClaims.getClaimValue(nestedKeys[0]);
+ for (int i = 1; i < nestedKeys.length; i++) {
+ if (rolesObj instanceof Map) {
+ String key = nestedKeys[i];
+ rolesObj = ((Map<?, ?>) rolesObj).get(key);
+ }
+ }
+ }
+
if (rolesObj != null) {
if (rolesObj instanceof String) {
finalRoles.addAll(Arrays.asList(((String) rolesObj).split("\\s+")));
} else if (rolesObj instanceof List) {
- finalRoles.addAll(jwtClaims.getStringListClaimValue(rolesClaim));
+ ((List<?>) rolesObj)
+ .forEach(
+ entry -> {
+ if (entry instanceof String) {
+ finalRoles.add((String) entry);
+ } else {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ String.format(
+ Locale.ROOT,
+ "Could not parse roles from JWT claim %s; expected array of strings, got array with a value of type %s",
+ rolesClaim,
+ entry.getClass().getSimpleName()));
+ }
+ });
+ } else {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ String.format(
+ Locale.ROOT,
+ "Could not parse roles from JWT claim %s; got %s",
+ rolesClaim,
+ rolesObj.getClass().getSimpleName()));
}
}
}
diff --git a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java
index 1abbdfa4e11..ebbfcd2a114 100644
--- a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java
+++ b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java
@@ -142,6 +142,17 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
List<String> roles = Arrays.asList("group-one", "other-group", "group-three");
claims.setStringListClaim(
"roles", roles); // multi-valued claims work too and will end up as a JSON array
+
+ // Keycloak Style resource_access roles
+ HashMap<String, Object> solrMap = new HashMap<>();
+ solrMap.put("roles", Arrays.asList("user", "admin"));
+ HashMap<String, Object> resourceAccess = new HashMap<>();
+ resourceAccess.put("solr", solrMap);
+ claims.setClaim("resource_access", resourceAccess);
+
+ // Special claim with dots in key, should still be addressable non-nested
+ claims.setClaim("roles.with.dot", Arrays.asList("user", "admin"));
+
return claims;
}
@@ -375,7 +386,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
assertTrue(resp.getErrorMessage(), resp.isAuthenticated());
- // When 'rolesClaim' is defined in config, then roles from that claim are used instead of claims
+ // When 'rolesClaim' is defined in config, then roles from that claim are used instead of scopes
Principal principal = resp.getPrincipal();
assertTrue(principal instanceof VerifiedUserRoles);
Set<String> roles = ((VerifiedUserRoles) principal).getVerifiedRoles();
@@ -385,6 +396,37 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
assertTrue(roles.contains("group-three"));
}
+ @Test
+ public void rolesWithDotInKey() {
+ // Special case where a claim key contains dots without being nested
+ testConfig.put("rolesClaim", "roles.with.dot");
+ plugin.init(testConfig);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
+ assertTrue(resp.getErrorMessage(), resp.isAuthenticated());
+ Principal principal = resp.getPrincipal();
+ assertTrue(principal instanceof VerifiedUserRoles);
+ Set<String> roles = ((VerifiedUserRoles) principal).getVerifiedRoles();
+ assertEquals(2, roles.size());
+ assertTrue(roles.contains("user"));
+ assertTrue(roles.contains("admin"));
+ }
+
+ @Test
+ public void nestedRoles() {
+ testConfig.put("rolesClaim", "resource_access.solr.roles");
+ plugin.init(testConfig);
+ JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader);
+ assertTrue(resp.getErrorMessage(), resp.isAuthenticated());
+
+ // When 'rolesClaim' is defined in config, then roles from that claim are used instead of claims
+ Principal principal = resp.getPrincipal();
+ assertTrue(principal instanceof VerifiedUserRoles);
+ Set<String> roles = ((VerifiedUserRoles) principal).getVerifiedRoles();
+ assertEquals(2, roles.size());
+ assertTrue(roles.contains("user"));
+ assertTrue(roles.contains("admin"));
+ }
+
@Test
public void wrongScope() {
testConfig.put("scope", "wrong");
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc
index 4bef9c62e55..8bd7962ea1d 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc
@@ -59,7 +59,7 @@ requireExp ; Fails requests that lacks an `exp` (expiry time) claim
algAllowlist ; JSON array with algorithms to accept: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, `none ; Default is to allow all algorithms
jwkCacheDur ; Duration of JWK cache in seconds ; `3600` (1 hour)
principalClaim ; What claim id to pull principal from ; `sub`
-rolesClaim ; What claim id to pull user roles from. The claim must then either contain a space separated list of roles or a JSON array. The roles can then be used to define fine-grained access in an Authorization plugin ; By default the scopes from `scope` claim are passed on as user roles
+rolesClaim ; What claim id to pull user roles from. Both top-level claim and nested claim is supported. Use `someClaim.child` syntax to address a claim `child` nested within the `someClaim` object. The claim must then either contain a space separated list of roles or a JSON array. The roles can then be used to define fine-grained access in an Authorization plugin ; By default the scopes from `scope` claim are passed on as user roles
claimsMatch ; JSON object of claims (key) that must match a regular expression (value). Example: `{ "foo" : "A|B" }` will require the `foo` claim to be either "A" or "B". ;
adminUiScope ; Define what scope is requested when logging in from Admin UI ; If not defined, the first scope from `scope` parameter is used
redirectUris ; Valid location(s) for redirect after external authentication. Takes a string or array of strings. Must be the base URL of Solr, e.g., https://solr1.example.com:8983/solr/ and must match the list of redirect URIs registered with the Identity Provider beforehand. ; Defaults to empty list, i.e., any node is assumed to be a valid redirect target.