epugh commented on code in PR #4916:
URL: https://github.com/apache/solr/pull/4916#discussion_r4028779197


##########
solr/api/src/java/org/apache/solr/client/api/endpoint/AuthenticationUsersApi.java:
##########
@@ -0,0 +1,82 @@
+/*
+ * 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.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.ListUsersResponse;
+import org.apache.solr.client.api.model.SetUserRequestBody;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/**
+ * Definitions for v2 JAX-RS APIs managing Basic Authentication users.
+ *
+ * <p>These APIs are a resource-oriented alternative to the 
"set-user"/"delete-user" commands
+ * accepted by the {@code /cluster/security/authentication} API - both operate 
on the same
+ * underlying plugin configuration.
+ *
+ * <p>The {@code scheme} path segment names the authentication scheme these 
users belong to (e.g.
+ * "basic"), as configured under {@code MultiAuthPlugin}'s "schemes" list. It 
is ignored when {@code
+ * MultiAuthPlugin} isn't in use - a plain {@code BasicAuthPlugin} setup has 
only one set of users,
+ * and any value may be supplied (conventionally "basic").
+ */
+@Path("/cluster/security/authentication/{scheme}/users")

Review Comment:
   I was thinking that `scheme` is `basic` or `jwt` or `certificate`...     But 
then below it says "LIst the usernames configured for Basic autnetication...   
Need to rethink the summary.   (originaly all of this was just for basic with 
no `scheme`.



##########
solr/api/src/java/org/apache/solr/client/api/endpoint/AuthenticationUsersApi.java:
##########
@@ -0,0 +1,82 @@
+/*
+ * 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.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.ListUsersResponse;
+import org.apache.solr.client.api.model.SetUserRequestBody;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/**
+ * Definitions for v2 JAX-RS APIs managing Basic Authentication users.

Review Comment:
   Need to rethinkg Basic everyhwere in the comments!



##########
solr/api/src/java/org/apache/solr/client/api/endpoint/AuthorizationRolesApi.java:
##########
@@ -0,0 +1,100 @@
+/*
+ * 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.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.GetUserRolesResponse;
+import org.apache.solr.client.api.model.ListUserRolesResponse;
+import org.apache.solr.client.api.model.SetUserRolesRequestBody;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/**
+ * Definitions for v2 JAX-RS APIs mapping roles to users under Rule-Based 
Authorization.
+ *
+ * <p>Resource-oriented alternative to the {@code set-user-role} command 
accepted by the {@code

Review Comment:
   same as mentioned before, we are removing the set-user-role thing



##########
solr/api/src/java/org/apache/solr/client/api/endpoint/AuthorizationPermissionsApi.java:
##########
@@ -0,0 +1,81 @@
+/*
+ * 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.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.CreatePermissionResponse;
+import org.apache.solr.client.api.model.ListPermissionsResponse;
+import org.apache.solr.client.api.model.PermissionDefinition;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/**
+ * Definitions for v2 JAX-RS APIs managing Rule-Based Authorization 
permissions.

Review Comment:
   will this work for `ExternalRoleRuleBasedAuthorizationPlugin` and 
`MultiAuthRuleBasedAuthorizationPlugin` ???   Or do we need to nest here?



##########
solr/api/src/java/org/apache/solr/client/api/model/PermissionDefinition.java:
##########
@@ -0,0 +1,61 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import java.util.Map;
+
+/** The fields of a Rule-Based Authorization permission, as created or updated 
by a caller. */
+public class PermissionDefinition {
+  @Schema(description = "The name of a predefined permission, e.g. 'read', 
'update', 'all'.")
+  @JsonProperty("name")
+  public String name;
+
+  @Schema(description = "The role(s) this permission is granted to.")
+  @JsonProperty("role")
+  public List<String> role;
+
+  @Schema(
+      description =
+          "The collection(s) this permission applies to. Omit for 
collection-agnostic requests"
+              + " (e.g. the Collections API); use an explicit null element to 
mean 'no"
+              + " collection'.")
+  @JsonProperty("collection")
+  public List<String> collection;
+
+  @Schema(description = "The request path(s) this permission applies to.")
+  @JsonProperty("path")
+  public List<String> path;
+
+  @Schema(description = "The HTTP method(s) this permission applies to.")
+  @JsonProperty("method")
+  public List<String> method;

Review Comment:
   ENUM?



##########
solr/core/src/java/org/apache/solr/handler/admin/api/Permissions.java:
##########
@@ -0,0 +1,234 @@
+/*
+ * 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.handler.admin.api;
+
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_READ_PERM;
+
+import jakarta.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.client.api.endpoint.AuthorizationPermissionsApi;
+import org.apache.solr.client.api.model.CreatePermissionResponse;
+import org.apache.solr.client.api.model.ListPermissionsResponse;
+import org.apache.solr.client.api.model.PermissionDefinition;
+import org.apache.solr.client.api.model.PermissionDetails;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.SecurityConfHandler;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for managing Rule-Based Authorization permissions.
+ *
+ * <p>A resource-oriented alternative to the {@code set-permission}/{@code 
update-permission}/
+ * {@code delete-permission} commands accepted by {@link 
ModifyRuleBasedAuthConfigAPI}, via {@link
+ * SecurityConfHandler#editSecurityConfig}. A permission's {@code index} - its 
position in the
+ * evaluated-top-down list - moves from a body field to a path parameter.
+ */
+public class Permissions extends AdminAPIBase implements 
AuthorizationPermissionsApi {
+  private static final String AUTHORIZATION_KEY = "authorization";
+
+  private final SecurityConfHandler securityConfHandler;
+
+  @Inject
+  public Permissions(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+    this.securityConfHandler = coreContainer.getSecurityConfHandler();
+  }
+
+  @Override
+  @PermissionName(SECURITY_READ_PERM)
+  public ListPermissionsResponse listPermissions() {
+    final var response = 
instantiateJerseyResponse(ListPermissionsResponse.class);
+    List<PermissionDetails> permissions = new ArrayList<>();
+    for (Map<String, Object> raw : fetchPermissions()) {
+      permissions.add(toPermissionDetails(raw));
+    }
+    response.permissions = permissions;
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public CreatePermissionResponse createPermission(PermissionDefinition 
requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    // Computed before the edit below, rather than by re-reading and matching 
content afterwards:
+    // a fresh permissions list can contain more than one entry with identical 
fields, so a
+    // straight positional count avoids the ambiguity that would come from 
trying to find "the one
+    // we just added" by content.
+    int existingCount = fetchPermissions().size();
+
+    Map<String, Object> dataMap = toDataMap(requestBody, /* includeBefore= */ 
true);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("set-permission", dataMap)));
+
+    final var response = 
instantiateJerseyResponse(CreatePermissionResponse.class);
+    // A create with no "before" is always appended at the end of the (freshly 
re-numbered)
+    // list, so it ends up one past the pre-edit count; a create with "before: 
N" always takes
+    // over index N directly, since renumbering starts fresh at 1 and 
preserves relative order.
+    response.index = requestBody.before != null ? requestBody.before : 
existingCount + 1;
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public SolrJerseyResponse updatePermission(int index, PermissionDefinition 
requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    ensurePermissionExists(index);
+
+    Map<String, Object> dataMap = toDataMap(requestBody, /* includeBefore= */ 
true);
+    dataMap.put("index", index);
+
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("update-permission", dataMap)));
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public SolrJerseyResponse deletePermission(int index) throws Exception {
+    ensurePermissionExists(index);
+
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    securityConfHandler.editSecurityConfig(

Review Comment:
   editSecurityConfig should be here, not there!



##########
solr/core/src/test/org/apache/solr/handler/admin/api/MultiAuthUsersAndRolesApiCloudTest.java:
##########
@@ -0,0 +1,182 @@
+/*
+ * 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.handler.admin.api;
+
+import static 
org.apache.solr.security.Sha256AuthenticationProvider.getSaltedHashedValue;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.client.api.model.GetUserRolesResponse;
+import org.apache.solr.client.api.model.ListUserRolesResponse;
+import org.apache.solr.client.api.model.ListUsersResponse;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.request.AuthenticationApi;
+import org.apache.solr.client.solrj.request.AuthorizationApi;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.security.MultiAuthPlugin;
+import org.apache.solr.security.MultiAuthRuleBasedAuthorizationPlugin;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Proves the {@code scheme} path segment on {@link Users}/{@link Roles} 
actually reaches the right
+ * sub-plugin under {@link MultiAuthPlugin}/{@link 
MultiAuthRuleBasedAuthorizationPlugin} - two
+ * configured schemes ("basic" and "other", both real {@code 
BasicAuthPlugin}/{@code
+ * RuleBasedAuthorizationPlugin} instances) must stay fully isolated from each 
other: writing to one
+ * scheme's users/roles must not appear under the other.
+ */
+public class MultiAuthUsersAndRolesApiCloudTest extends SolrCloudTestCase {
+
+  private static final String ADMIN_USER = "solr";
+  private static final String ADMIN_PASS = "SolrRocks";
+  private static final String SEED_USER = "seed";
+  private static final String SEED_PASS = "SeedPass123";
+
+  private static final String SECURITY_JSON =

Review Comment:
   should this just be a multiline string?   



##########
solr/core/src/test/org/apache/solr/handler/admin/api/SecurityV2ApiCloudTest.java:
##########
@@ -0,0 +1,132 @@
+/*
+ * 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.handler.admin.api;
+
+import java.util.List;
+import org.apache.solr.client.api.model.CreatePermissionResponse;
+import org.apache.solr.client.api.model.GetUserRolesResponse;
+import org.apache.solr.client.api.model.ListPermissionsResponse;
+import org.apache.solr.client.api.model.ListUserRolesResponse;
+import org.apache.solr.client.solrj.request.AuthorizationApi;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.util.SecurityJson;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * SolrCloud-mode coverage for {@link Permissions}/{@link Roles}. Both read 
via {@code
+ * SecurityConfHandler#getSecurityConfig(true)} (fresh, bypassing {@code 
SecurityConfHandlerZk}'s
+ * cached ZK snapshot) specifically so a GET immediately following one of 
their own writes is
+ * guaranteed to observe it, without any client-side polling for propagation - 
see {@code
+ * SecurityConfHandler#getSecurityConfig}'s javadoc for why a cached ({@code 
getFresh=false}) read
+ * can otherwise lag a write briefly. Standalone mode ({@code 
SecurityConfHandlerLocal}) always
+ * reads security.json fresh from disk regardless of this flag, so this 
behavior needs cloud
+ * coverage specifically to mean anything.
+ *
+ * <p>This also incidentally guards against a real bug this suite caught 
during development: the

Review Comment:
   too much commentry on the changes



##########
solr/solr-ref-guide/modules/deployment-guide/pages/rule-based-authorization-plugin.adoc:
##########
@@ -511,43 +514,128 @@ The following creates a new permission named 
"collection-mgr" that is allowed to
 The permission will be placed before the "read" permission.
 Note also that we have defined `collection` as `null` because requests to the 
Collections API are never collection-specific.
 
+[tabs#set-permission-collection-mgr]
+======
+V1 API::
++
+====
 [source,bash]
-curl --user solr:SolrRocks -H 'Content-type:application/json' -d '{
+----
+curl --user solr:SolrRocks http://localhost:8983/solr/admin/authorization -H 
'Content-type:application/json' -d '{
   "set-permission": {"collection": null,
                      "path":"/admin/collections",
                      "params":{"action":["LIST", "CREATE"]},
                      "before": 3,
                      "role": "admin"}
-}' http://localhost:8983/solr/admin/authorization
+}'
+----
+====
+
+V2 API::
++
+====
+[source,bash]
+----
+curl --user solr:SolrRocks 
http://localhost:8983/api/cluster/security/authorization -H 
'Content-type:application/json' -d '{
+  "set-permission": {"collection": null,
+                     "path":"/admin/collections",
+                     "params":{"action":["LIST", "CREATE"]},
+                     "before": 3,
+                     "role": "admin"}
+}'
+----
+====
+======
 
 Apply an update permission on all collections to a role called `dev` and read 
permissions to a role called `guest`:
 
+[tabs#set-permission-dev-guest]
+======
+V1 API::
++
+====
+[source,bash]
+----
+curl --user solr:SolrRocks http://localhost:8983/solr/admin/authorization -H 
'Content-type:application/json' -d '{
+  "set-permission": {"name": "update", "role":"dev"},
+  "set-permission": {"name": "read", "role":"guest"}
+}'
+----
+====
+
+V2 API::
++
+====
 [source,bash]
-curl --user solr:SolrRocks -H 'Content-type:application/json' -d '{
+----
+curl --user solr:SolrRocks 
http://localhost:8983/api/cluster/security/authorization -H 
'Content-type:application/json' -d '{
   "set-permission": {"name": "update", "role":"dev"},
   "set-permission": {"name": "read", "role":"guest"}
-}' http://localhost:8983/solr/admin/authorization
+}'
+----
+====
+======
 
 === Update or Delete Permissions
 
 Permissions can be accessed using their index in the list.
-Use the `/admin/authorization` API to see the existing permissions and their 
indices.
+Use the Authorization API to see the existing permissions and their indices.
 
 The following example updates the `'role'` attribute of permission at index 
`3`:
 
+[tabs#update-permission]
+======
+V1 API::
++
+====
 [source,bash]
-curl --user solr:SolrRocks -H 'Content-type:application/json' -d '{
+----
+curl --user solr:SolrRocks http://localhost:8983/solr/admin/authorization -H 
'Content-type:application/json' -d '{
   "update-permission": {"index": 3,
                        "role": ["admin", "dev"]}
-}' http://localhost:8983/solr/admin/authorization
+}'
+----
+====
+
+V2 API::
++
+====
+[source,bash]
+----
+curl --user solr:SolrRocks 
http://localhost:8983/api/cluster/security/authorization -H 
'Content-type:application/json' -d '{
+  "update-permission": {"index": 3,

Review Comment:
   old style v2



##########
solr/webapp/web/js/angular/controllers/security.js:
##########
@@ -15,14 +15,20 @@
  limitations under the License.
 */
 
-solrAdminApp.controller('SecurityController', function ($scope, $timeout, 
$cookies, $window, Constants, SystemV2, Security, ApiErrorHandler) {
+solrAdminApp.controller('SecurityController', function ($scope, $timeout, 
$cookies, $window, Constants, SystemV2, Security, AuthenticationV2, 
AuthorizationV2, ApiErrorHandler) {
   $scope.resetMenu("security", Constants.IS_ROOT_PAGE);
 
   $scope.params = [];
   $scope.filteredPredefinedPermissions = [];
 
   var strongPasswordRegex = 
/^(?=.*[0-9])(?=.*[!@#$%^&*\-_()[\]])[a-zA-Z0-9!@#$%^&*\-_()[\]]{8,30}$/;
 
+  // The Users/Roles v2 APIs address the authentication/authorization scheme 
they operate on via
+  // this path segment - this panel only ever manages the "basic" scheme (see 
multiAuthWithBasic

Review Comment:
   interesting, I ddin't know that it only handles BASIC



##########
solr/core/src/java/org/apache/solr/handler/admin/api/Permissions.java:
##########
@@ -0,0 +1,234 @@
+/*
+ * 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.handler.admin.api;
+
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_READ_PERM;
+
+import jakarta.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.client.api.endpoint.AuthorizationPermissionsApi;
+import org.apache.solr.client.api.model.CreatePermissionResponse;
+import org.apache.solr.client.api.model.ListPermissionsResponse;
+import org.apache.solr.client.api.model.PermissionDefinition;
+import org.apache.solr.client.api.model.PermissionDetails;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.SecurityConfHandler;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for managing Rule-Based Authorization permissions.
+ *
+ * <p>A resource-oriented alternative to the {@code set-permission}/{@code 
update-permission}/
+ * {@code delete-permission} commands accepted by {@link 
ModifyRuleBasedAuthConfigAPI}, via {@link
+ * SecurityConfHandler#editSecurityConfig}. A permission's {@code index} - its 
position in the
+ * evaluated-top-down list - moves from a body field to a path parameter.
+ */
+public class Permissions extends AdminAPIBase implements 
AuthorizationPermissionsApi {
+  private static final String AUTHORIZATION_KEY = "authorization";
+
+  private final SecurityConfHandler securityConfHandler;
+
+  @Inject
+  public Permissions(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+    this.securityConfHandler = coreContainer.getSecurityConfHandler();
+  }
+
+  @Override
+  @PermissionName(SECURITY_READ_PERM)
+  public ListPermissionsResponse listPermissions() {
+    final var response = 
instantiateJerseyResponse(ListPermissionsResponse.class);
+    List<PermissionDetails> permissions = new ArrayList<>();
+    for (Map<String, Object> raw : fetchPermissions()) {
+      permissions.add(toPermissionDetails(raw));
+    }
+    response.permissions = permissions;
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public CreatePermissionResponse createPermission(PermissionDefinition 
requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    // Computed before the edit below, rather than by re-reading and matching 
content afterwards:
+    // a fresh permissions list can contain more than one entry with identical 
fields, so a
+    // straight positional count avoids the ambiguity that would come from 
trying to find "the one
+    // we just added" by content.
+    int existingCount = fetchPermissions().size();
+
+    Map<String, Object> dataMap = toDataMap(requestBody, /* includeBefore= */ 
true);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("set-permission", dataMap)));
+
+    final var response = 
instantiateJerseyResponse(CreatePermissionResponse.class);
+    // A create with no "before" is always appended at the end of the (freshly 
re-numbered)
+    // list, so it ends up one past the pre-edit count; a create with "before: 
N" always takes
+    // over index N directly, since renumbering starts fresh at 1 and 
preserves relative order.
+    response.index = requestBody.before != null ? requestBody.before : 
existingCount + 1;
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public SolrJerseyResponse updatePermission(int index, PermissionDefinition 
requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    ensurePermissionExists(index);
+
+    Map<String, Object> dataMap = toDataMap(requestBody, /* includeBefore= */ 
true);
+    dataMap.put("index", index);
+
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("update-permission", dataMap)));
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public SolrJerseyResponse deletePermission(int index) throws Exception {
+    ensurePermissionExists(index);
+
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("delete-permission", index)));
+    return response;
+  }
+
+  private void ensurePermissionExists(int index) {
+    boolean found =
+        fetchPermissions().stream()
+            .anyMatch(p -> p.get("index") instanceof Number n && n.intValue() 
== index);
+    if (!found) {
+      throw new SolrException(
+          SolrException.ErrorCode.NOT_FOUND, "No permission exists with index 
[" + index + "]");
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private List<Map<String, Object>> fetchPermissions() {
+    // Read fresh (bypassing SecurityConfHandler's cached ZK snapshot) so a 
GET immediately
+    // following one of this class's own writes is guaranteed to observe it - 
see
+    // SecurityConfHandler#getSecurityConfig's javadoc for why the cache can 
otherwise lag a write
+    // briefly.
+    Map<String, Object> authorizationConf =
+        (Map<String, Object>)
+            
securityConfHandler.getSecurityConfig(true).getData().get(AUTHORIZATION_KEY);
+    if (authorizationConf == null) {
+      return List.of();
+    }
+    // The "permissions" value is always list-shaped in security.json, but it 
isn't guaranteed to
+    // arrive as a java.util.List: Utils.getDeepCopy(..., mutable=false) - 
used when building
+    // read-only snapshots of a cached security config - wraps it in
+    // Collections.unmodifiableCollection(), which only implements Collection, 
not List. Kept as a
+    // defensive fallback even though this method now always reads fresh.
+    Object rawPermissions = authorizationConf.get("permissions");
+    if (!(rawPermissions instanceof Collection)) {
+      return List.of();
+    }
+    List<Map<String, Object>> permissions = new ArrayList<>();
+    for (Object p : (Collection<?>) rawPermissions) {
+      permissions.add((Map<String, Object>) p);
+    }
+    return permissions;
+  }
+
+  private static PermissionDetails toPermissionDetails(Map<String, Object> 
raw) {
+    PermissionDetails details = new PermissionDetails();
+    populateDefinitionFields(details, raw);
+    Object index = raw.get("index");
+    details.index = index instanceof Number ? ((Number) index).intValue() : 
null;
+    return details;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static void populateDefinitionFields(

Review Comment:
   weird



##########
solr/api/src/java/org/apache/solr/client/api/endpoint/AuthorizationPermissionsApi.java:
##########
@@ -0,0 +1,81 @@
+/*
+ * 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.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.CreatePermissionResponse;
+import org.apache.solr.client.api.model.ListPermissionsResponse;
+import org.apache.solr.client.api.model.PermissionDefinition;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/**
+ * Definitions for v2 JAX-RS APIs managing Rule-Based Authorization 
permissions.
+ *
+ * <p>Resource-oriented alternative to the {@code set-permission}/{@code 
update-permission}/{@code
+ * delete-permission} commands accepted by the {@code 
/cluster/security/authorization} API. A
+ * permission's {@code index} - its position in the evaluated-top-down list - 
moves from a body

Review Comment:
   I don't get the "index", why isn't it just the name?  or a ENUM?



##########
solr/api/src/java/org/apache/solr/client/api/model/CreatePermissionResponse.java:
##########
@@ -0,0 +1,27 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class CreatePermissionResponse extends SolrJerseyResponse {
+  @Schema(description = "The index assigned to the newly created permission.")
+  @JsonProperty("index")

Review Comment:
   index is werid!



##########
solr/api/src/java/org/apache/solr/client/api/endpoint/AuthenticationUsersApi.java:
##########
@@ -0,0 +1,82 @@
+/*
+ * 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.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.ListUsersResponse;
+import org.apache.solr.client.api.model.SetUserRequestBody;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/**
+ * Definitions for v2 JAX-RS APIs managing Basic Authentication users.
+ *
+ * <p>These APIs are a resource-oriented alternative to the 
"set-user"/"delete-user" commands
+ * accepted by the {@code /cluster/security/authentication} API - both operate 
on the same
+ * underlying plugin configuration.
+ *
+ * <p>The {@code scheme} path segment names the authentication scheme these 
users belong to (e.g.
+ * "basic"), as configured under {@code MultiAuthPlugin}'s "schemes" list. It 
is ignored when {@code
+ * MultiAuthPlugin} isn't in use - a plain {@code BasicAuthPlugin} setup has 
only one set of users,
+ * and any value may be supplied (conventionally "basic").
+ */
+@Path("/cluster/security/authentication/{scheme}/users")
+public interface AuthenticationUsersApi {
+  @GET
+  @Operation(
+      summary = "List the usernames configured for Basic Authentication.",
+      tags = {"authentication"})
+  ListUsersResponse listUsers(
+      @Parameter(description = "The authentication scheme these users belong 
to.", required = true)
+          @PathParam("scheme")
+          String scheme);
+
+  @PUT
+  @Path("/{username}")
+  @Operation(
+      summary = "Create a new user, or change an existing user's password.",

Review Comment:
   why are se not having POST for the create and PUT for the chagne?  We do 
later on for permissions!



##########
solr/api/src/java/org/apache/solr/client/api/model/PermissionDefinition.java:
##########
@@ -0,0 +1,61 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import java.util.Map;
+
+/** The fields of a Rule-Based Authorization permission, as created or updated 
by a caller. */
+public class PermissionDefinition {

Review Comment:
   should this be RuleBasedAuthorizationPermission?   What about other tupes of 
Authorization?



##########
solr/api/src/java/org/apache/solr/client/api/endpoint/AuthorizationPermissionsApi.java:
##########
@@ -0,0 +1,81 @@
+/*
+ * 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.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import org.apache.solr.client.api.model.CreatePermissionResponse;
+import org.apache.solr.client.api.model.ListPermissionsResponse;
+import org.apache.solr.client.api.model.PermissionDefinition;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/**
+ * Definitions for v2 JAX-RS APIs managing Rule-Based Authorization 
permissions.
+ *
+ * <p>Resource-oriented alternative to the {@code set-permission}/{@code 
update-permission}/{@code

Review Comment:
   we are ripping out `set-permission` and `update-permission` in v2, so don't 
need that.



##########
solr/api/src/java/org/apache/solr/client/api/model/PermissionDefinition.java:
##########
@@ -0,0 +1,61 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import java.util.Map;
+
+/** The fields of a Rule-Based Authorization permission, as created or updated 
by a caller. */
+public class PermissionDefinition {
+  @Schema(description = "The name of a predefined permission, e.g. 'read', 
'update', 'all'.")
+  @JsonProperty("name")
+  public String name;

Review Comment:
   should this be an ENUM?



##########
solr/api/src/java/org/apache/solr/client/api/model/PermissionDefinition.java:
##########
@@ -0,0 +1,61 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import java.util.Map;
+
+/** The fields of a Rule-Based Authorization permission, as created or updated 
by a caller. */
+public class PermissionDefinition {
+  @Schema(description = "The name of a predefined permission, e.g. 'read', 
'update', 'all'.")
+  @JsonProperty("name")
+  public String name;
+
+  @Schema(description = "The role(s) this permission is granted to.")
+  @JsonProperty("role")
+  public List<String> role;
+
+  @Schema(
+      description =
+          "The collection(s) this permission applies to. Omit for 
collection-agnostic requests"
+              + " (e.g. the Collections API); use an explicit null element to 
mean 'no"
+              + " collection'.")
+  @JsonProperty("collection")
+  public List<String> collection;
+
+  @Schema(description = "The request path(s) this permission applies to.")
+  @JsonProperty("path")
+  public List<String> path;
+
+  @Schema(description = "The HTTP method(s) this permission applies to.")
+  @JsonProperty("method")
+  public List<String> method;
+
+  @Schema(description = "Request parameter values this permission is 
restricted to matching.")
+  @JsonProperty("params")
+  public Map<String, Object> params;
+
+  @Schema(
+      description =
+          "On creation only: place the new permission immediately before the 
permission "
+              + "currently at this index, instead of appending it at the end.")
+  @JsonProperty("before")

Review Comment:
   before?  Priority?



##########
solr/core/src/java/org/apache/solr/handler/admin/api/Permissions.java:
##########
@@ -0,0 +1,234 @@
+/*
+ * 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.handler.admin.api;
+
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_READ_PERM;
+
+import jakarta.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.client.api.endpoint.AuthorizationPermissionsApi;
+import org.apache.solr.client.api.model.CreatePermissionResponse;
+import org.apache.solr.client.api.model.ListPermissionsResponse;
+import org.apache.solr.client.api.model.PermissionDefinition;
+import org.apache.solr.client.api.model.PermissionDetails;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.SecurityConfHandler;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for managing Rule-Based Authorization permissions.
+ *
+ * <p>A resource-oriented alternative to the {@code set-permission}/{@code 
update-permission}/
+ * {@code delete-permission} commands accepted by {@link 
ModifyRuleBasedAuthConfigAPI}, via {@link
+ * SecurityConfHandler#editSecurityConfig}. A permission's {@code index} - its 
position in the
+ * evaluated-top-down list - moves from a body field to a path parameter.
+ */
+public class Permissions extends AdminAPIBase implements 
AuthorizationPermissionsApi {
+  private static final String AUTHORIZATION_KEY = "authorization";
+
+  private final SecurityConfHandler securityConfHandler;
+
+  @Inject
+  public Permissions(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+    this.securityConfHandler = coreContainer.getSecurityConfHandler();
+  }
+
+  @Override
+  @PermissionName(SECURITY_READ_PERM)
+  public ListPermissionsResponse listPermissions() {
+    final var response = 
instantiateJerseyResponse(ListPermissionsResponse.class);
+    List<PermissionDetails> permissions = new ArrayList<>();
+    for (Map<String, Object> raw : fetchPermissions()) {
+      permissions.add(toPermissionDetails(raw));
+    }
+    response.permissions = permissions;
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public CreatePermissionResponse createPermission(PermissionDefinition 
requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    // Computed before the edit below, rather than by re-reading and matching 
content afterwards:
+    // a fresh permissions list can contain more than one entry with identical 
fields, so a
+    // straight positional count avoids the ambiguity that would come from 
trying to find "the one
+    // we just added" by content.
+    int existingCount = fetchPermissions().size();
+
+    Map<String, Object> dataMap = toDataMap(requestBody, /* includeBefore= */ 
true);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("set-permission", dataMap)));
+
+    final var response = 
instantiateJerseyResponse(CreatePermissionResponse.class);
+    // A create with no "before" is always appended at the end of the (freshly 
re-numbered)
+    // list, so it ends up one past the pre-edit count; a create with "before: 
N" always takes
+    // over index N directly, since renumbering starts fresh at 1 and 
preserves relative order.
+    response.index = requestBody.before != null ? requestBody.before : 
existingCount + 1;
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public SolrJerseyResponse updatePermission(int index, PermissionDefinition 
requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    ensurePermissionExists(index);
+
+    Map<String, Object> dataMap = toDataMap(requestBody, /* includeBefore= */ 
true);
+    dataMap.put("index", index);
+
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("update-permission", dataMap)));
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public SolrJerseyResponse deletePermission(int index) throws Exception {
+    ensurePermissionExists(index);
+
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("delete-permission", index)));
+    return response;
+  }
+
+  private void ensurePermissionExists(int index) {
+    boolean found =
+        fetchPermissions().stream()
+            .anyMatch(p -> p.get("index") instanceof Number n && n.intValue() 
== index);
+    if (!found) {
+      throw new SolrException(
+          SolrException.ErrorCode.NOT_FOUND, "No permission exists with index 
[" + index + "]");
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private List<Map<String, Object>> fetchPermissions() {
+    // Read fresh (bypassing SecurityConfHandler's cached ZK snapshot) so a 
GET immediately
+    // following one of this class's own writes is guaranteed to observe it - 
see
+    // SecurityConfHandler#getSecurityConfig's javadoc for why the cache can 
otherwise lag a write
+    // briefly.
+    Map<String, Object> authorizationConf =
+        (Map<String, Object>)
+            
securityConfHandler.getSecurityConfig(true).getData().get(AUTHORIZATION_KEY);
+    if (authorizationConf == null) {
+      return List.of();
+    }
+    // The "permissions" value is always list-shaped in security.json, but it 
isn't guaranteed to
+    // arrive as a java.util.List: Utils.getDeepCopy(..., mutable=false) - 
used when building
+    // read-only snapshots of a cached security config - wraps it in
+    // Collections.unmodifiableCollection(), which only implements Collection, 
not List. Kept as a
+    // defensive fallback even though this method now always reads fresh.

Review Comment:
   so then let's remove this defensive fall back!



##########
solr/core/src/java/org/apache/solr/handler/admin/api/Permissions.java:
##########
@@ -0,0 +1,234 @@
+/*
+ * 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.handler.admin.api;
+
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_READ_PERM;
+
+import jakarta.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.client.api.endpoint.AuthorizationPermissionsApi;
+import org.apache.solr.client.api.model.CreatePermissionResponse;
+import org.apache.solr.client.api.model.ListPermissionsResponse;
+import org.apache.solr.client.api.model.PermissionDefinition;
+import org.apache.solr.client.api.model.PermissionDetails;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.SecurityConfHandler;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for managing Rule-Based Authorization permissions.
+ *
+ * <p>A resource-oriented alternative to the {@code set-permission}/{@code 
update-permission}/
+ * {@code delete-permission} commands accepted by {@link 
ModifyRuleBasedAuthConfigAPI}, via {@link
+ * SecurityConfHandler#editSecurityConfig}. A permission's {@code index} - its 
position in the
+ * evaluated-top-down list - moves from a body field to a path parameter.
+ */
+public class Permissions extends AdminAPIBase implements 
AuthorizationPermissionsApi {
+  private static final String AUTHORIZATION_KEY = "authorization";
+
+  private final SecurityConfHandler securityConfHandler;
+
+  @Inject
+  public Permissions(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+    this.securityConfHandler = coreContainer.getSecurityConfHandler();
+  }
+
+  @Override
+  @PermissionName(SECURITY_READ_PERM)
+  public ListPermissionsResponse listPermissions() {
+    final var response = 
instantiateJerseyResponse(ListPermissionsResponse.class);
+    List<PermissionDetails> permissions = new ArrayList<>();
+    for (Map<String, Object> raw : fetchPermissions()) {
+      permissions.add(toPermissionDetails(raw));
+    }
+    response.permissions = permissions;
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public CreatePermissionResponse createPermission(PermissionDefinition 
requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    // Computed before the edit below, rather than by re-reading and matching 
content afterwards:
+    // a fresh permissions list can contain more than one entry with identical 
fields, so a
+    // straight positional count avoids the ambiguity that would come from 
trying to find "the one
+    // we just added" by content.
+    int existingCount = fetchPermissions().size();
+
+    Map<String, Object> dataMap = toDataMap(requestBody, /* includeBefore= */ 
true);
+    securityConfHandler.editSecurityConfig(

Review Comment:
   we are supposed to move the business logic into here, so securityConfHnadler 
should call this class instead...



##########
solr/api/src/java/org/apache/solr/client/api/model/PermissionDetails.java:
##########
@@ -0,0 +1,28 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/** A permission as it exists in {@code security.json}, including its current 
list position. */
+public class PermissionDetails extends PermissionDefinition {
+  @Schema(description = "This permission's current position in the 
evaluated-top-down list.")
+  @JsonProperty("index")

Review Comment:
   "priority" and a. more specific version of "index"?



##########
solr/core/src/java/org/apache/solr/handler/admin/api/Permissions.java:
##########
@@ -0,0 +1,234 @@
+/*
+ * 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.handler.admin.api;
+
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_READ_PERM;
+
+import jakarta.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.client.api.endpoint.AuthorizationPermissionsApi;
+import org.apache.solr.client.api.model.CreatePermissionResponse;
+import org.apache.solr.client.api.model.ListPermissionsResponse;
+import org.apache.solr.client.api.model.PermissionDefinition;
+import org.apache.solr.client.api.model.PermissionDetails;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.SecurityConfHandler;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for managing Rule-Based Authorization permissions.
+ *
+ * <p>A resource-oriented alternative to the {@code set-permission}/{@code 
update-permission}/
+ * {@code delete-permission} commands accepted by {@link 
ModifyRuleBasedAuthConfigAPI}, via {@link
+ * SecurityConfHandler#editSecurityConfig}. A permission's {@code index} - its 
position in the
+ * evaluated-top-down list - moves from a body field to a path parameter.
+ */
+public class Permissions extends AdminAPIBase implements 
AuthorizationPermissionsApi {
+  private static final String AUTHORIZATION_KEY = "authorization";
+
+  private final SecurityConfHandler securityConfHandler;
+
+  @Inject
+  public Permissions(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+    this.securityConfHandler = coreContainer.getSecurityConfHandler();
+  }
+
+  @Override
+  @PermissionName(SECURITY_READ_PERM)
+  public ListPermissionsResponse listPermissions() {
+    final var response = 
instantiateJerseyResponse(ListPermissionsResponse.class);
+    List<PermissionDetails> permissions = new ArrayList<>();
+    for (Map<String, Object> raw : fetchPermissions()) {
+      permissions.add(toPermissionDetails(raw));
+    }
+    response.permissions = permissions;
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public CreatePermissionResponse createPermission(PermissionDefinition 
requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    // Computed before the edit below, rather than by re-reading and matching 
content afterwards:
+    // a fresh permissions list can contain more than one entry with identical 
fields, so a
+    // straight positional count avoids the ambiguity that would come from 
trying to find "the one
+    // we just added" by content.
+    int existingCount = fetchPermissions().size();
+
+    Map<String, Object> dataMap = toDataMap(requestBody, /* includeBefore= */ 
true);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("set-permission", dataMap)));
+
+    final var response = 
instantiateJerseyResponse(CreatePermissionResponse.class);
+    // A create with no "before" is always appended at the end of the (freshly 
re-numbered)
+    // list, so it ends up one past the pre-edit count; a create with "before: 
N" always takes
+    // over index N directly, since renumbering starts fresh at 1 and 
preserves relative order.
+    response.index = requestBody.before != null ? requestBody.before : 
existingCount + 1;
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public SolrJerseyResponse updatePermission(int index, PermissionDefinition 
requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    ensurePermissionExists(index);
+
+    Map<String, Object> dataMap = toDataMap(requestBody, /* includeBefore= */ 
true);
+    dataMap.put("index", index);
+
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("update-permission", dataMap)));
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public SolrJerseyResponse deletePermission(int index) throws Exception {
+    ensurePermissionExists(index);
+
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("delete-permission", index)));
+    return response;
+  }
+
+  private void ensurePermissionExists(int index) {
+    boolean found =
+        fetchPermissions().stream()
+            .anyMatch(p -> p.get("index") instanceof Number n && n.intValue() 
== index);
+    if (!found) {
+      throw new SolrException(
+          SolrException.ErrorCode.NOT_FOUND, "No permission exists with index 
[" + index + "]");
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private List<Map<String, Object>> fetchPermissions() {
+    // Read fresh (bypassing SecurityConfHandler's cached ZK snapshot) so a 
GET immediately
+    // following one of this class's own writes is guaranteed to observe it - 
see
+    // SecurityConfHandler#getSecurityConfig's javadoc for why the cache can 
otherwise lag a write
+    // briefly.
+    Map<String, Object> authorizationConf =
+        (Map<String, Object>)
+            
securityConfHandler.getSecurityConfig(true).getData().get(AUTHORIZATION_KEY);
+    if (authorizationConf == null) {
+      return List.of();
+    }
+    // The "permissions" value is always list-shaped in security.json, but it 
isn't guaranteed to
+    // arrive as a java.util.List: Utils.getDeepCopy(..., mutable=false) - 
used when building
+    // read-only snapshots of a cached security config - wraps it in
+    // Collections.unmodifiableCollection(), which only implements Collection, 
not List. Kept as a
+    // defensive fallback even though this method now always reads fresh.
+    Object rawPermissions = authorizationConf.get("permissions");
+    if (!(rawPermissions instanceof Collection)) {
+      return List.of();
+    }
+    List<Map<String, Object>> permissions = new ArrayList<>();
+    for (Object p : (Collection<?>) rawPermissions) {
+      permissions.add((Map<String, Object>) p);
+    }
+    return permissions;
+  }
+
+  private static PermissionDetails toPermissionDetails(Map<String, Object> 
raw) {

Review Comment:
   weird



##########
solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java:
##########


Review Comment:
   I could see an argument to NOT moving the business logic out of 
`SecurityConfHandler` and just having V2 call V1...  It's jsut so much churn.



##########
solr/core/src/java/org/apache/solr/handler/admin/api/Permissions.java:
##########
@@ -0,0 +1,234 @@
+/*
+ * 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.handler.admin.api;
+
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.SECURITY_READ_PERM;
+
+import jakarta.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.client.api.endpoint.AuthorizationPermissionsApi;
+import org.apache.solr.client.api.model.CreatePermissionResponse;
+import org.apache.solr.client.api.model.ListPermissionsResponse;
+import org.apache.solr.client.api.model.PermissionDefinition;
+import org.apache.solr.client.api.model.PermissionDetails;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.SecurityConfHandler;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for managing Rule-Based Authorization permissions.
+ *
+ * <p>A resource-oriented alternative to the {@code set-permission}/{@code 
update-permission}/
+ * {@code delete-permission} commands accepted by {@link 
ModifyRuleBasedAuthConfigAPI}, via {@link
+ * SecurityConfHandler#editSecurityConfig}. A permission's {@code index} - its 
position in the
+ * evaluated-top-down list - moves from a body field to a path parameter.
+ */
+public class Permissions extends AdminAPIBase implements 
AuthorizationPermissionsApi {
+  private static final String AUTHORIZATION_KEY = "authorization";
+
+  private final SecurityConfHandler securityConfHandler;
+
+  @Inject
+  public Permissions(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+    this.securityConfHandler = coreContainer.getSecurityConfHandler();
+  }
+
+  @Override
+  @PermissionName(SECURITY_READ_PERM)
+  public ListPermissionsResponse listPermissions() {
+    final var response = 
instantiateJerseyResponse(ListPermissionsResponse.class);
+    List<PermissionDetails> permissions = new ArrayList<>();
+    for (Map<String, Object> raw : fetchPermissions()) {
+      permissions.add(toPermissionDetails(raw));
+    }
+    response.permissions = permissions;
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public CreatePermissionResponse createPermission(PermissionDefinition 
requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    // Computed before the edit below, rather than by re-reading and matching 
content afterwards:
+    // a fresh permissions list can contain more than one entry with identical 
fields, so a
+    // straight positional count avoids the ambiguity that would come from 
trying to find "the one
+    // we just added" by content.
+    int existingCount = fetchPermissions().size();
+
+    Map<String, Object> dataMap = toDataMap(requestBody, /* includeBefore= */ 
true);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("set-permission", dataMap)));
+
+    final var response = 
instantiateJerseyResponse(CreatePermissionResponse.class);
+    // A create with no "before" is always appended at the end of the (freshly 
re-numbered)
+    // list, so it ends up one past the pre-edit count; a create with "before: 
N" always takes
+    // over index N directly, since renumbering starts fresh at 1 and 
preserves relative order.
+    response.index = requestBody.before != null ? requestBody.before : 
existingCount + 1;
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public SolrJerseyResponse updatePermission(int index, PermissionDefinition 
requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    ensurePermissionExists(index);
+
+    Map<String, Object> dataMap = toDataMap(requestBody, /* includeBefore= */ 
true);
+    dataMap.put("index", index);
+
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("update-permission", dataMap)));
+    return response;
+  }
+
+  @Override
+  @PermissionName(SECURITY_EDIT_PERM)
+  public SolrJerseyResponse deletePermission(int index) throws Exception {
+    ensurePermissionExists(index);
+
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    securityConfHandler.editSecurityConfig(
+        solrQueryRequest,
+        AUTHORIZATION_KEY,
+        List.of(new CommandOperation("delete-permission", index)));
+    return response;
+  }
+
+  private void ensurePermissionExists(int index) {
+    boolean found =
+        fetchPermissions().stream()
+            .anyMatch(p -> p.get("index") instanceof Number n && n.intValue() 
== index);
+    if (!found) {
+      throw new SolrException(
+          SolrException.ErrorCode.NOT_FOUND, "No permission exists with index 
[" + index + "]");
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private List<Map<String, Object>> fetchPermissions() {
+    // Read fresh (bypassing SecurityConfHandler's cached ZK snapshot) so a 
GET immediately
+    // following one of this class's own writes is guaranteed to observe it - 
see
+    // SecurityConfHandler#getSecurityConfig's javadoc for why the cache can 
otherwise lag a write
+    // briefly.
+    Map<String, Object> authorizationConf =
+        (Map<String, Object>)
+            
securityConfHandler.getSecurityConfig(true).getData().get(AUTHORIZATION_KEY);
+    if (authorizationConf == null) {
+      return List.of();
+    }
+    // The "permissions" value is always list-shaped in security.json, but it 
isn't guaranteed to
+    // arrive as a java.util.List: Utils.getDeepCopy(..., mutable=false) - 
used when building
+    // read-only snapshots of a cached security config - wraps it in
+    // Collections.unmodifiableCollection(), which only implements Collection, 
not List. Kept as a
+    // defensive fallback even though this method now always reads fresh.
+    Object rawPermissions = authorizationConf.get("permissions");
+    if (!(rawPermissions instanceof Collection)) {
+      return List.of();
+    }
+    List<Map<String, Object>> permissions = new ArrayList<>();
+    for (Object p : (Collection<?>) rawPermissions) {
+      permissions.add((Map<String, Object>) p);
+    }
+    return permissions;
+  }
+
+  private static PermissionDetails toPermissionDetails(Map<String, Object> 
raw) {
+    PermissionDetails details = new PermissionDetails();
+    populateDefinitionFields(details, raw);
+    Object index = raw.get("index");
+    details.index = index instanceof Number ? ((Number) index).intValue() : 
null;
+    return details;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static void populateDefinitionFields(
+      PermissionDefinition definition, Map<String, Object> raw) {
+    definition.name = (String) raw.get("name");
+    definition.role = asList(raw.get("role"));
+    definition.collection = asList(raw.get("collection"));
+    definition.path = asList(raw.get("path"));
+    definition.method = asList(raw.get("method"));
+    Object params = raw.get("params");
+    definition.params = params instanceof Map ? (Map<String, Object>) params : 
null;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static List<String> asList(Object value) {
+    if (value == null) {
+      return null;
+    }
+    if (value instanceof List) {
+      return (List<String>) value;
+    }
+    return List.of(String.valueOf(value));
+  }
+
+  /**
+   * Converts the non-null fields of a {@link PermissionDefinition} request 
body into the {@code
+   * Map} shape the legacy {@code set-permission}/{@code update-permission} 
commands expect.
+   */
+  private static Map<String, Object> toDataMap(PermissionDefinition def, 
boolean includeBefore) {

Review Comment:
   yeah, this shold be in the securityConfHandler so the new stuff wins and the 
v1 has to adapt



##########
solr/solr-ref-guide/modules/deployment-guide/pages/rule-based-authorization-plugin.adoc:
##########
@@ -511,43 +514,128 @@ The following creates a new permission named 
"collection-mgr" that is allowed to
 The permission will be placed before the "read" permission.
 Note also that we have defined `collection` as `null` because requests to the 
Collections API are never collection-specific.
 
+[tabs#set-permission-collection-mgr]
+======
+V1 API::
++
+====
 [source,bash]
-curl --user solr:SolrRocks -H 'Content-type:application/json' -d '{
+----
+curl --user solr:SolrRocks http://localhost:8983/solr/admin/authorization -H 
'Content-type:application/json' -d '{
   "set-permission": {"collection": null,
                      "path":"/admin/collections",
                      "params":{"action":["LIST", "CREATE"]},
                      "before": 3,
                      "role": "admin"}
-}' http://localhost:8983/solr/admin/authorization
+}'
+----
+====
+
+V2 API::
++
+====
+[source,bash]
+----
+curl --user solr:SolrRocks 
http://localhost:8983/api/cluster/security/authorization -H 
'Content-type:application/json' -d '{
+  "set-permission": {"collection": null,

Review Comment:
   umm, why do we have `set-permission` here?  In v2 we use the HTTP verb and 
the end point, no commands liek this.



##########
solr/solr-ref-guide/modules/deployment-guide/pages/rule-based-authorization-plugin.adoc:
##########
@@ -511,43 +514,128 @@ The following creates a new permission named 
"collection-mgr" that is allowed to
 The permission will be placed before the "read" permission.
 Note also that we have defined `collection` as `null` because requests to the 
Collections API are never collection-specific.
 
+[tabs#set-permission-collection-mgr]
+======
+V1 API::
++
+====
 [source,bash]
-curl --user solr:SolrRocks -H 'Content-type:application/json' -d '{
+----
+curl --user solr:SolrRocks http://localhost:8983/solr/admin/authorization -H 
'Content-type:application/json' -d '{
   "set-permission": {"collection": null,
                      "path":"/admin/collections",
                      "params":{"action":["LIST", "CREATE"]},
                      "before": 3,
                      "role": "admin"}
-}' http://localhost:8983/solr/admin/authorization
+}'
+----
+====
+
+V2 API::
++
+====
+[source,bash]
+----
+curl --user solr:SolrRocks 
http://localhost:8983/api/cluster/security/authorization -H 
'Content-type:application/json' -d '{
+  "set-permission": {"collection": null,
+                     "path":"/admin/collections",
+                     "params":{"action":["LIST", "CREATE"]},
+                     "before": 3,
+                     "role": "admin"}
+}'
+----
+====
+======
 
 Apply an update permission on all collections to a role called `dev` and read 
permissions to a role called `guest`:
 
+[tabs#set-permission-dev-guest]
+======
+V1 API::
++
+====
+[source,bash]
+----
+curl --user solr:SolrRocks http://localhost:8983/solr/admin/authorization -H 
'Content-type:application/json' -d '{
+  "set-permission": {"name": "update", "role":"dev"},
+  "set-permission": {"name": "read", "role":"guest"}
+}'
+----
+====
+
+V2 API::
++
+====
 [source,bash]
-curl --user solr:SolrRocks -H 'Content-type:application/json' -d '{
+----
+curl --user solr:SolrRocks 
http://localhost:8983/api/cluster/security/authorization -H 
'Content-type:application/json' -d '{
   "set-permission": {"name": "update", "role":"dev"},
   "set-permission": {"name": "read", "role":"guest"}
-}' http://localhost:8983/solr/admin/authorization
+}'
+----
+====
+======
 
 === Update or Delete Permissions
 
 Permissions can be accessed using their index in the list.
-Use the `/admin/authorization` API to see the existing permissions and their 
indices.
+Use the Authorization API to see the existing permissions and their indices.
 
 The following example updates the `'role'` attribute of permission at index 
`3`:
 
+[tabs#update-permission]
+======
+V1 API::
++
+====
 [source,bash]
-curl --user solr:SolrRocks -H 'Content-type:application/json' -d '{
+----
+curl --user solr:SolrRocks http://localhost:8983/solr/admin/authorization -H 
'Content-type:application/json' -d '{
   "update-permission": {"index": 3,
                        "role": ["admin", "dev"]}
-}' http://localhost:8983/solr/admin/authorization
+}'
+----
+====
+
+V2 API::
++
+====
+[source,bash]
+----
+curl --user solr:SolrRocks 
http://localhost:8983/api/cluster/security/authorization -H 
'Content-type:application/json' -d '{
+  "update-permission": {"index": 3,
+                       "role": ["admin", "dev"]}
+}'
+----
+====
+======
 
 The following example deletes permission at index `3`:
 
+[tabs#delete-permission]
+======
+V1 API::
++
+====
 [source,bash]
-curl --user solr:SolrRocks -H 'Content-type:application/json' -d '{
+----
+curl --user solr:SolrRocks http://localhost:8983/solr/admin/authorization -H 
'Content-type:application/json' -d '{
   "delete-permission": 3
-}' http://localhost:8983/solr/admin/authorization
+}'
+----
+====
 
+V2 API::
++
+====
+[source,bash]
+----
+curl --user solr:SolrRocks 
http://localhost:8983/api/cluster/security/authorization -H 
'Content-type:application/json' -d '{

Review Comment:
   old style v2



##########
solr/solr-ref-guide/modules/deployment-guide/pages/rule-based-authorization-plugin.adoc:
##########
@@ -511,43 +514,128 @@ The following creates a new permission named 
"collection-mgr" that is allowed to
 The permission will be placed before the "read" permission.
 Note also that we have defined `collection` as `null` because requests to the 
Collections API are never collection-specific.
 
+[tabs#set-permission-collection-mgr]
+======
+V1 API::
++
+====
 [source,bash]
-curl --user solr:SolrRocks -H 'Content-type:application/json' -d '{
+----
+curl --user solr:SolrRocks http://localhost:8983/solr/admin/authorization -H 
'Content-type:application/json' -d '{
   "set-permission": {"collection": null,
                      "path":"/admin/collections",
                      "params":{"action":["LIST", "CREATE"]},
                      "before": 3,
                      "role": "admin"}
-}' http://localhost:8983/solr/admin/authorization
+}'
+----
+====
+
+V2 API::
++
+====
+[source,bash]
+----
+curl --user solr:SolrRocks 
http://localhost:8983/api/cluster/security/authorization -H 
'Content-type:application/json' -d '{
+  "set-permission": {"collection": null,

Review Comment:
   is this maybe left over from the first pass where we asked Claude to fill in 
documentation gaps and whe had old style V2?



##########
solr/api/src/java/org/apache/solr/client/api/model/CreatePermissionResponse.java:
##########
@@ -0,0 +1,27 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class CreatePermissionResponse extends SolrJerseyResponse {
+  @Schema(description = "The index assigned to the newly created permission.")
+  @JsonProperty("index")

Review Comment:
   or a "yes"?



##########
solr/core/src/test/org/apache/solr/handler/admin/api/MultiAuthUsersAndRolesApiCloudTest.java:
##########
@@ -0,0 +1,182 @@
+/*
+ * 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.handler.admin.api;
+
+import static 
org.apache.solr.security.Sha256AuthenticationProvider.getSaltedHashedValue;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.client.api.model.GetUserRolesResponse;
+import org.apache.solr.client.api.model.ListUserRolesResponse;
+import org.apache.solr.client.api.model.ListUsersResponse;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.request.AuthenticationApi;
+import org.apache.solr.client.solrj.request.AuthorizationApi;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.security.MultiAuthPlugin;
+import org.apache.solr.security.MultiAuthRuleBasedAuthorizationPlugin;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Proves the {@code scheme} path segment on {@link Users}/{@link Roles} 
actually reaches the right
+ * sub-plugin under {@link MultiAuthPlugin}/{@link 
MultiAuthRuleBasedAuthorizationPlugin} - two
+ * configured schemes ("basic" and "other", both real {@code 
BasicAuthPlugin}/{@code
+ * RuleBasedAuthorizationPlugin} instances) must stay fully isolated from each 
other: writing to one
+ * scheme's users/roles must not appear under the other.
+ */
+public class MultiAuthUsersAndRolesApiCloudTest extends SolrCloudTestCase {
+
+  private static final String ADMIN_USER = "solr";
+  private static final String ADMIN_PASS = "SolrRocks";
+  private static final String SEED_USER = "seed";
+  private static final String SEED_PASS = "SeedPass123";
+
+  private static final String SECURITY_JSON =
+      Utils.toJSONString(
+          Map.of(
+              "authentication",
+              Map.of(
+                  "class",
+                  MultiAuthPlugin.class.getName(),
+                  "schemes",
+                  List.of(
+                      Map.of(
+                          "scheme",
+                          "basic",
+                          "class",
+                          "solr.BasicAuthPlugin",
+                          "blockUnknown",
+                          true,
+                          "credentials",
+                          Map.of(ADMIN_USER, 
getSaltedHashedValue(ADMIN_PASS))),
+                      Map.of(
+                          "scheme",
+                          "other",

Review Comment:
   can we use another real type?  Instead of a solr.basicAuthPlugin?  Be mroe 
real!



##########
solr/api/src/java/org/apache/solr/client/api/model/CreatePermissionResponse.java:
##########
@@ -0,0 +1,27 @@
+/*
+ * 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.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class CreatePermissionResponse extends SolrJerseyResponse {
+  @Schema(description = "The index assigned to the newly created permission.")
+  @JsonProperty("index")

Review Comment:
   maybe we should just retunr all the permissions in an order?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to