This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_10x in repository https://gitbox.apache.org/repos/asf/solr.git
commit 5479dede64d5988a2add463d7fb4379cbcdf2a7d Author: sarkaramrit1993 <[email protected]> AuthorDate: Tue Nov 3 08:36:19 2026 -0800 SOLR-18579: Authorize container-level JAX-RS v2 APIs once, by their declared permission (#5214) (cherry picked from commit e9a2ecde7fca8e4020604d50ae7879da21a543d7) --- .../unreleased/SOLR-18579-v2-authz-all-rule.yml | 12 +++ .../src/java/org/apache/solr/api/V2HttpCall.java | 7 ++ .../java/org/apache/solr/servlet/HttpSolrCall.java | 2 +- .../security/JaxRsPredefinedPermissionTest.java | 73 +++++++++++++++++ .../solr/security/JaxRsRemoteProxyAuthzTest.java | 94 ++++++++++++++++++++++ 5 files changed, 187 insertions(+), 1 deletion(-) diff --git a/changelog/unreleased/SOLR-18579-v2-authz-all-rule.yml b/changelog/unreleased/SOLR-18579-v2-authz-all-rule.yml new file mode 100644 index 00000000000..54c0f04be4a --- /dev/null +++ b/changelog/unreleased/SOLR-18579-v2-authz-all-rule.yml @@ -0,0 +1,12 @@ +title: > + Container-level v2 APIs implemented in JAX-RS are now authorized once, by their declared + permission. Previously a catch-all "all" rule in security.json could reject users holding the + API's predefined permission. +type: fixed +authors: + - name: Amrit Sarkar + - name: Nick Shanin + nick: nick-boss-tech +links: + - name: SOLR-18579 + url: https://issues.apache.org/jira/browse/SOLR-18579 diff --git a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java index 8aac0682e1c..7643955e79e 100644 --- a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java +++ b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java @@ -528,6 +528,13 @@ public class V2HttpCall extends HttpSolrCall { return api; } + @Override + protected boolean shouldAuthorize() { + // Container-level JAX-RS resources are authorized by SolrRequestAuthorizer, which knows their + // permission + return (api != null || action == PROCESS) && super.shouldAuthorize(); + } + public Map<String, String> getUrlParts() { return parts; } 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 c202ee08dbe..1832f9c6028 100644 --- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java +++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java @@ -606,7 +606,7 @@ public class HttpSolrCall { return coreOrColName; } - private boolean shouldAuthorize() { + protected boolean shouldAuthorize() { if (PublicKeyHandler.PATH.equals(path)) return false; // admin/info/key is the path where public key is exposed . it is always unsecured if (StrUtils.isNullOrEmpty(path) || "/".equals(path) || "/solr/".equals(path)) diff --git a/solr/core/src/test/org/apache/solr/security/JaxRsPredefinedPermissionTest.java b/solr/core/src/test/org/apache/solr/security/JaxRsPredefinedPermissionTest.java new file mode 100644 index 00000000000..14b17c9eeb1 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/security/JaxRsPredefinedPermissionTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security; + +import java.util.List; +import java.util.Map; +import org.apache.solr.client.solrj.RemoteSolrException; +import org.apache.solr.client.solrj.request.LoggingApi; +import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.common.util.Utils; +import org.junit.BeforeClass; +import org.junit.Test; + +public class JaxRsPredefinedPermissionTest extends SolrCloudTestCase { + private static final String PASSWORD = "pwd"; + + @BeforeClass + public static void setupCluster() throws Exception { + String hash = Sha256AuthenticationProvider.getSaltedHashedValue(PASSWORD); + Map<String, Object> security = + Map.of( + "authentication", + Map.of( + "blockUnknown", + true, + "class", + "solr.BasicAuthPlugin", + "credentials", + Map.of("admin", hash, "monitor", hash, "reader", hash)), + "authorization", + Map.of( + "class", + "solr.RuleBasedAuthorizationPlugin", + "permissions", + List.of( + Map.of("name", "config-read", "role", "monitor"), + Map.of("name", "all", "role", "admin")), + "user-role", + Map.of("admin", "admin", "monitor", "monitor", "reader", "reader"))); + configureCluster(1).withSecurityJson(Utils.toJSONString(security)).configure(); + } + + private static void listLoggers(String user) throws Exception { + LoggingApi.ListAllLoggersAndLevels req = new LoggingApi.ListAllLoggersAndLevels(); + req.setBasicAuthCredentials(user, PASSWORD); + req.process(cluster.getJettySolrRunner(0).getSolrClient()); + } + + @Test + public void testPredefinedPermissionAllowedDespiteAllRule() throws Exception { + listLoggers("monitor"); + } + + @Test + public void testUserWithoutPermissionForbidden() { + RemoteSolrException e = expectThrows(RemoteSolrException.class, () -> listLoggers("reader")); + assertEquals(403, e.code()); + } +} diff --git a/solr/core/src/test/org/apache/solr/security/JaxRsRemoteProxyAuthzTest.java b/solr/core/src/test/org/apache/solr/security/JaxRsRemoteProxyAuthzTest.java new file mode 100644 index 00000000000..afd4a6917fa --- /dev/null +++ b/solr/core/src/test/org/apache/solr/security/JaxRsRemoteProxyAuthzTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security; + +import java.util.List; +import java.util.Map; +import org.apache.solr.client.solrj.RemoteSolrException; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.request.GenericV2SolrRequest; +import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.common.util.Utils; +import org.apache.solr.embedded.JettySolrRunner; +import org.junit.BeforeClass; +import org.junit.Test; + +public class JaxRsRemoteProxyAuthzTest extends SolrCloudTestCase { + private static final String PASSWORD = "pwd"; + private static final String COLLECTION = "proxied"; + + @BeforeClass + public static void setupCluster() throws Exception { + String hash = Sha256AuthenticationProvider.getSaltedHashedValue(PASSWORD); + Map<String, Object> security = + Map.of( + "authentication", + Map.of( + "blockUnknown", + true, + "class", + "solr.BasicAuthPlugin", + "credentials", + Map.of("admin", hash, "monitor", hash, "reader", hash)), + "authorization", + Map.of( + "class", + "solr.RuleBasedAuthorizationPlugin", + "permissions", + List.of( + Map.of("name", "config-read", "role", "monitor"), + Map.of("name", "read", "role", "reader"), + Map.of("name", "all", "role", "admin")), + "user-role", + Map.of("admin", "admin", "monitor", "monitor", "reader", "reader"))); + configureCluster(2) + .addConfig("conf", configset("cloud-minimal")) + .withSecurityJson(Utils.toJSONString(security)) + .configure(); + CollectionAdminRequest.Create create = + CollectionAdminRequest.createCollection(COLLECTION, "conf", 1, 1) + .setCreateNodeSet(cluster.getJettySolrRunner(0).getNodeName()); + create.setBasicAuthCredentials("admin", PASSWORD); + create.process(cluster.getSolrClient()); + cluster.waitForActiveCollection(COLLECTION, 1, 1); + } + + private static int selectViaNodeWithoutReplica(String user) throws Exception { + JettySolrRunner node = cluster.getJettySolrRunner(1); + assertTrue(node.getCoreContainer().getAllCoreNames().isEmpty()); + GenericV2SolrRequest req = + new GenericV2SolrRequest(SolrRequest.METHOD.GET, "/collections/" + COLLECTION + "/select"); + req.setBasicAuthCredentials(user, PASSWORD); + try { + req.process(node.getSolrClient()); + return 200; + } catch (RemoteSolrException e) { + return e.code(); + } + } + + @Test + public void testProxiedSelectAllowedForReader() throws Exception { + assertEquals(200, selectViaNodeWithoutReplica("reader")); + } + + @Test + public void testProxiedSelectForbiddenWithoutReadPermission() throws Exception { + assertEquals(403, selectViaNodeWithoutReplica("monitor")); + } +}
