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]
