From 88a7f53f3ca30b943cec5c39cf48e39d500d1cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Crabb=C3=A9?= Date: Mon, 16 Jun 2025 10:51:27 +0200 Subject: [PATCH 1/4] Allow tests to choose the number of nodes their test cluster has --- .../solr/security/jwt/JWTAuthPluginIntegrationTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java index 3bb08460218..429092d670d 100644 --- a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java +++ b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java @@ -333,9 +333,14 @@ private MiniSolrCloudCluster configureClusterMockOauth( * @return an instance of the created cluster that the test can talk to */ private MiniSolrCloudCluster configureClusterStaticKeys(String securityJsonFilename) + throws Exception { + return configureClusterStaticKeys(securityJsonFilename, 2); + } + + private MiniSolrCloudCluster configureClusterStaticKeys(String securityJsonFilename, int numberOfNodes) throws Exception { MiniSolrCloudCluster myCluster = - configureCluster(2) // nodes + configureCluster(numberOfNodes) .withSecurityJson(JWT_TEST_PATH().resolve("security").resolve(securityJsonFilename)) .addConfig( "conf1", From b9a1525d80b3c3c95813b9095cd49c2e3fe91bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Crabb=C3=A9?= Date: Tue, 17 Jun 2025 07:41:05 +0200 Subject: [PATCH 2/4] Test if solr cluster properly forwards authentication principal --- ...lugin_jwk_security_with_authorization.json | 27 ++++++++++ .../jwt/JWTAuthPluginIntegrationTest.java | 54 +++++++++++++++++-- 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_with_authorization.json diff --git a/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_with_authorization.json b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_with_authorization.json new file mode 100644 index 00000000000..a6595a3e079 --- /dev/null +++ b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_with_authorization.json @@ -0,0 +1,27 @@ +{ + "authentication": { + "class": "solr.JWTAuthPlugin", + "blockUnknown": true, + "jwk": { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "test", + "alg": "RS256", + "n": "jeyrvOaZrmKWjyNXt0myAc_pJ1hNt3aRupExJEx1ewPaL9J9HFgSCjMrYxCB1ETO1NDyZ3nSgjZis-jHHDqBxBjRdq_t1E2rkGFaYbxAyKt220Pwgme_SFTB9MXVrFQGkKyjmQeVmOmV6zM3KK8uMdKQJ4aoKmwBcF5Zg7EZdDcKOFgpgva1Jq-FlEsaJ2xrYDYo3KnGcOHIt9_0NQeLsqZbeWYLxYni7uROFncXYV5FhSJCeR4A_rrbwlaCydGxE0ToC_9HNYibUHlkJjqyUhAgORCbNS8JLCJH8NUi5sDdIawK9GTSyvsJXZ-QHqo4cMUuxWV5AJtaRGghuMUfqQ" + }, + "realm": "my-solr-jwt", + "adminUiScope": "solr:admin", + "authorizationEndpoint": "http://acmepaymentscorp/oauth/auz/authorize", + "tokenEndpoint": "http://acmepaymentscorp/oauth/oauth20/token", + "authorizationFlow": "code_pkce", + "clientId": "solr-cluster", + "rolesClaim": "roles" + }, + "authorization": { + "class": "solr.ExternalRoleRuleBasedAuthorizationPlugin", + "permissions": [ + { "name": "private-jwt-collection", "collection": "jwtColl", "role": "group-one", "path":"/*"} + ] + } +} diff --git a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java index 429092d670d..c850258761c 100644 --- a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java +++ b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java @@ -63,6 +63,7 @@ import org.apache.solr.common.util.Pair; import org.apache.solr.common.util.TimeSource; import org.apache.solr.common.util.Utils; +import org.apache.solr.embedded.JettySolrRunner; import org.apache.solr.util.CryptoKeys; import org.apache.solr.util.RTimer; import org.apache.solr.util.TimeOut; @@ -290,6 +291,53 @@ public void testMetrics() throws Exception { HttpClientUtil.close(cl); } + /** + * Test if JWTPrincipal is passed correctly on internode communication. Setup a cluster with more + * nodes using jwtAuth for both authentication and authorization.Add a private collection with + * less replicas and shards then the number of nodes. Test if we can query the collection on every + * node. + */ + @Test + public void testInternodeAuthorization() throws Exception { + // Start cluster with security.json that contains permissions for a private collection + cluster = configureClusterStaticKeys("jwt_plugin_jwk_security_with_authorization.json", 3); + // Get a random url to use for general requests to the cluster + String randomBaseUrl = cluster.getRandomJetty(random()).getBaseUrl().toString(); + + // Add our private collection to the cluster with only one shard and replica + String COLLECTION = "jwtColl"; + createCollection(cluster, COLLECTION); + + // Now update three documents + Pair result = + post( + randomBaseUrl + "/" + COLLECTION + "/update?commit=true", + "[{\"id\" : \"1\"}, {\"id\": \"2\"}, {\"id\": \"3\"}]", + jwtStaticTestToken); + assertEquals(Integer.valueOf(200), result.second()); + + // Run query on every node. + // This will force the nodes to transfer the query to another node when they do not have the + // collection themselves. + for (JettySolrRunner node : cluster.getJettySolrRunners()) { + // Get the base url for this node + String nodeBaseUrl = node.getBaseUrl().toString(); + + // Do a query, using JWTAuth for inter-node + result = get(nodeBaseUrl + "/" + COLLECTION + "/query?q=*:*", jwtStaticTestToken); + assertEquals(Integer.valueOf(200), result.second()); + } + + // Delete + assertEquals( + 200, + get( + randomBaseUrl + "/admin/collections?action=DELETE&name=" + COLLECTION, + jwtStaticTestToken) + .second() + .intValue()); + } + static String getBearerAuthHeader(JsonWebSignature jws) throws JoseException { return "Bearer " + jws.getCompactSerialization(); } @@ -334,11 +382,11 @@ private MiniSolrCloudCluster configureClusterMockOauth( */ private MiniSolrCloudCluster configureClusterStaticKeys(String securityJsonFilename) throws Exception { - return configureClusterStaticKeys(securityJsonFilename, 2); + return configureClusterStaticKeys(securityJsonFilename, 2); } - private MiniSolrCloudCluster configureClusterStaticKeys(String securityJsonFilename, int numberOfNodes) - throws Exception { + private MiniSolrCloudCluster configureClusterStaticKeys( + String securityJsonFilename, int numberOfNodes) throws Exception { MiniSolrCloudCluster myCluster = configureCluster(numberOfNodes) .withSecurityJson(JWT_TEST_PATH().resolve("security").resolve(securityJsonFilename)) From 495fd7f5dad37ee6e01aa76031491e863515dda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Crabb=C3=A9?= Date: Tue, 17 Jun 2025 09:39:27 +0200 Subject: [PATCH 3/4] Add user principal on http context when forwarding query --- .../org/apache/solr/servlet/HttpSolrCall.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java index 9c847663907..e0aa1cff749 100644 --- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java +++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java @@ -40,6 +40,7 @@ import java.io.OutputStream; import java.lang.invoke.MethodHandles; import java.nio.charset.StandardCharsets; +import java.security.Principal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -74,6 +75,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.entity.InputStreamEntity; import org.apache.solr.api.ApiBag; import org.apache.solr.api.V2HttpCall; @@ -774,10 +776,23 @@ private void remoteQuery(String coreUrl, HttpServletResponse resp) throws IOExce method.removeHeaders(CONTENT_LENGTH_HEADER); } + // Make sure the user principal is forwarded when its exist + HttpClientContext httpClientRequestContext = + HttpClientUtil.createNewHttpClientRequestContext(); + Principal userPrincipal = req.getUserPrincipal(); + if (userPrincipal != null) { + // Normally the context contains a static userToken to enable reuse resources. However, if a + // personal Principal object exists, we use that instead, also as a means to transfer + // authentication information to Auth plugins that wish to intercept the request later + if (log.isDebugEnabled()) { + log.debug("Forwarding principal {}", userPrincipal); + } + httpClientRequestContext.setUserToken(userPrincipal); + } + + // Execute the method. final HttpResponse response = - solrDispatchFilter - .getHttpClient() - .execute(method, HttpClientUtil.createNewHttpClientRequestContext()); + solrDispatchFilter.getHttpClient().execute(method, httpClientRequestContext); int httpStatus = response.getStatusLine().getStatusCode(); httpEntity = response.getEntity(); From 485314da9b0d6b145104e25d697096c6fbdb22d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Crabb=C3=A9?= Date: Wed, 9 Jul 2025 15:23:39 +0200 Subject: [PATCH 4/4] Updated test description for clarity --- .../security/jwt/JWTAuthPluginIntegrationTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java index c850258761c..e5e3223fdf2 100644 --- a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java +++ b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java @@ -293,18 +293,19 @@ public void testMetrics() throws Exception { /** * Test if JWTPrincipal is passed correctly on internode communication. Setup a cluster with more - * nodes using jwtAuth for both authentication and authorization.Add a private collection with - * less replicas and shards then the number of nodes. Test if we can query the collection on every - * node. + * nodes using jwtAuth for both authentication and authorization. Add a collection with restricted + * access and with less replicas and shards then the number of nodes. Test if we can query the + * collection on every node. */ @Test public void testInternodeAuthorization() throws Exception { - // Start cluster with security.json that contains permissions for a private collection + // Start cluster with security.json that contains permissions for a collection with restricted + // access cluster = configureClusterStaticKeys("jwt_plugin_jwk_security_with_authorization.json", 3); // Get a random url to use for general requests to the cluster String randomBaseUrl = cluster.getRandomJetty(random()).getBaseUrl().toString(); - // Add our private collection to the cluster with only one shard and replica + // Add the collection to the cluster String COLLECTION = "jwtColl"; createCollection(cluster, COLLECTION);