This is an automated email from the ASF dual-hosted git repository.

dsmiley pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new eaceca8911e SOLR-17789: Fix Internode Authorization not working for 
external roles (#3397)
eaceca8911e is described below

commit eaceca8911ef06c89a293f2437a6a560f34b1a43
Author: bct-timo-crabbe <[email protected]>
AuthorDate: Wed Jul 30 22:58:35 2025 -0400

    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                                   |  3 ++
 .../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(+), 4 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 04099f55806..7889911d4d7 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -220,6 +220,9 @@ Bug Fixes
 
 * 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 4b77790898c..9a45fb2682e 100644
--- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
+++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
@@ -41,6 +41,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.Collection;
 import java.util.Collections;
@@ -72,6 +73,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;
@@ -782,10 +784,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 c2644665e5f..8b1353dff25 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
@@ -64,6 +64,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",

Reply via email to