This is an automated email from the ASF dual-hosted git repository.
dsmiley 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 065dcfd48d8 SOLR-17789: Fix Internode Authorization not working for
external roles (#3397)
065dcfd48d8 is described below
commit 065dcfd48d8a07b67dd38423348397dd38c13d7d
Author: bct-timo-crabbe <[email protected]>
AuthorDate: Thu Jul 31 04:58:35 2025 +0200
SOLR-17789: Fix Internode Authorization not working for external roles
(#3397)
When Solr forwards/proxies requests to another node that can service the
request, it needs to pass authorization headers.
* Allow tests to choose the number of nodes their test cluster has
* Test if solr cluster properly forwards authentication principal
* Add user principal on http context when forwarding query
---
solr/CHANGES.txt | 5 +-
.../java/org/apache/solr/servlet/HttpSolrCall.java | 21 ++++++--
...jwt_plugin_jwk_security_with_authorization.json | 27 +++++++++++
.../security/jwt/JWTAuthPluginIntegrationTest.java | 56 +++++++++++++++++++++-
4 files changed, 103 insertions(+), 6 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index edc6a6cbf02..f0f9b65f403 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -19,12 +19,13 @@ Optimizations
Bug Fixes
---------------------
-(No changes)
-
* SOLR-17824: RecoveryStrategy.pingLeader could NPE when there's no shard
leader (David Smiley)
* SOLR-17721: NPE can occur when doing Atomic Update using Add Distinct on
documents with a null field value. (puneetSharma via Eric Pugh)
+* SOLR-17789: When Solr forwards/proxies requests to another node that can
service the request, it needs to pass authorization headers.
+ (Timo Crabbé)
+
Dependency Upgrades
---------------------
(No changes)
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 fe89248fdd4..1fd374536b5 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.InputStream;
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.HttpOptions;
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 @@ public class HttpSolrCall {
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();
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 3bb08460218..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
@@ -63,6 +63,7 @@ import org.apache.solr.common.SolrException;
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,54 @@ public class JWTAuthPluginIntegrationTest extends
SolrCloudAuthTestCase {
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
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
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 the collection to the cluster
+ String COLLECTION = "jwtColl";
+ createCollection(cluster, COLLECTION);
+
+ // Now update three documents
+ Pair<String, Integer> 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,8 +383,13 @@ public class JWTAuthPluginIntegrationTest extends
SolrCloudAuthTestCase {
*/
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",