You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2022/06/21 15:42:56 UTC

[solr] branch main updated: SOLR-15738: Convert v2 security APIs to annotations (#897)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new bf0b2b0d30c SOLR-15738: Convert v2 security APIs to annotations (#897)
bf0b2b0d30c is described below

commit bf0b2b0d30c69526efdffa7010214e6f7ad3e46d
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Tue Jun 21 11:42:51 2022 -0400

    SOLR-15738: Convert v2 security APIs to annotations (#897)
    
    Solr's been in the slow process of moving its v2 APIs away from the
    existing apispec/mapping framework towards one that relies on more
    explicit annotations to specify API properties.
    
    This commit converts the APIs from the cluster.security.authentication
    and cluster.security.authorization apispec files to the "new" framework.
---
 .../src/java/org/apache/solr/api/AnnotatedApi.java |  29 ++++
 solr/core/src/java/org/apache/solr/api/ApiBag.java |  19 ++-
 .../solr/handler/admin/SecurityConfHandler.java    |  28 ++--
 .../admin/api/GetAuthenticationConfigAPI.java      |  50 +++++++
 .../admin/api/GetAuthorizationConfigAPI.java       |  49 +++++++
 .../admin/api/ModifyBasicAuthConfigAPI.java        |  47 +++++++
 .../admin/api/ModifyMultiPluginAuthConfigAPI.java  |  57 ++++++++
 .../api/ModifyNoAuthPluginSecurityConfigAPI.java   |  50 +++++++
 .../api/ModifyNoAuthzPluginSecurityConfigAPI.java  |  50 +++++++
 .../admin/api/ModifyRuleBasedAuthConfigAPI.java    |  64 +++++++++
 .../org/apache/solr/security/MultiAuthPlugin.java  |   7 +-
 .../security/RuleBasedAuthorizationPluginBase.java |   7 +-
 .../security/Sha256AuthenticationProvider.java     |   7 +-
 .../org/apache/solr/cloud/TestConfigSetsAPI.java   |   4 +-
 .../handler/admin/V2SecurityAPIMappingTest.java    | 154 +++++++++++++++++++++
 .../admin/api/ModifyJWTAuthPluginConfigAPI.java    |  39 ++++++
 .../solr/handler/admin/api/package-info.java       |  18 +++
 .../apache/solr/security/jwt/JWTAuthPlugin.java    |   6 +-
 .../admin/api/V2JWTSecurityApiMappingTest.java     |  65 +++++++++
 .../solr/handler/admin/api/package-info.java       |  19 +++
 .../request/beans/DeleteBasicAuthUserPayload.java  |  22 +++
 .../beans/SetRuleBasedAuthPermissionPayload.java   |  43 ++++++
 .../UpdateRuleBasedAuthPermissionPayload.java      |  44 ++++++
 .../handler/admin/api/JWTConfigurationPayload.java |  87 ++++++++++++
 .../solr/handler/admin/api/package-info.java       |  19 +++
 .../cluster.security.BasicAuth.Commands.json       |  23 ---
 .../apispec/cluster.security.JwtAuth.Commands.json |  18 ---
 .../cluster.security.MultiPluginAuth.Commands.json |  27 ----
 .../cluster.security.RuleBasedAuthorization.json   | 129 -----------------
 .../cluster.security.authentication.Commands.json  |  12 --
 .../apispec/cluster.security.authentication.json   |  12 --
 .../cluster.security.authorization.Commands.json   |  13 --
 .../apispec/cluster.security.authorization.json    |  13 --
 .../apache/solr/common/util/JsonValidatorTest.java |  21 ---
 34 files changed, 965 insertions(+), 287 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java b/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
index 905157cf71a..468e5cbec95 100644
--- a/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
+++ b/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
@@ -34,6 +34,8 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SpecProvider;
@@ -335,6 +337,33 @@ public class AnnotatedApi extends Api implements PermissionNameProvider, Closeab
       }
     }
 
+    public int hashCode() {
+      return new HashCodeBuilder()
+          .append(command)
+          .append(method)
+          .append(obj)
+          .append(paramsCount)
+          .append(parameterClass)
+          .append(isWrappedInPayloadObj)
+          .toHashCode();
+    }
+
+    public boolean equals(Object rhs) {
+      if (null == rhs) return false;
+      if (this == rhs) return true;
+      if (getClass() != rhs.getClass()) return false;
+
+      final Cmd rhsCast = (Cmd) rhs;
+      return new EqualsBuilder()
+          .append(command, rhsCast.command)
+          .append(method, rhsCast.method)
+          .append(obj, rhsCast.obj)
+          .append(paramsCount, rhsCast.paramsCount)
+          .append(parameterClass, rhsCast.parameterClass)
+          .append(isWrappedInPayloadObj, rhsCast.isWrappedInPayloadObj)
+          .isEquals();
+    }
+
     private void checkForErrorInPayload(CommandOperation cmd) {
       if (cmd.hasError()) {
         throw new ApiBag.ExceptionWithErrObject(
diff --git a/solr/core/src/java/org/apache/solr/api/ApiBag.java b/solr/core/src/java/org/apache/solr/api/ApiBag.java
index 15ed546add7..3d0337670d1 100644
--- a/solr/core/src/java/org/apache/solr/api/ApiBag.java
+++ b/solr/core/src/java/org/apache/solr/api/ApiBag.java
@@ -25,6 +25,7 @@ import static org.apache.solr.common.util.ValidatingJsonMap.NOT_NULL;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
@@ -148,16 +149,28 @@ public class ApiBag {
     private Collection<AnnotatedApi> combinedApis;
 
     protected CommandAggregatingAnnotatedApi(AnnotatedApi api) {
-      super(api.spec, api.getEndPoint(), api.getCommands(), null);
+      super(api.spec, api.getEndPoint(), Maps.newHashMap(api.getCommands()), null);
       combinedApis = Lists.newArrayList();
     }
 
     public void combineWith(AnnotatedApi api) {
       // Merge in new 'command' entries
-      getCommands().putAll(api.getCommands());
+      boolean newCommandsAdded = false;
+      for (Map.Entry<String, AnnotatedApi.Cmd> entry : api.getCommands().entrySet()) {
+        // Skip registering command if it's identical to an already registered command.
+        if (getCommands().containsKey(entry.getKey())
+            && getCommands().get(entry.getKey()).equals(entry.getValue())) {
+          continue;
+        }
+
+        newCommandsAdded = true;
+        getCommands().put(entry.getKey(), entry.getValue());
+      }
 
       // Reference to Api must be saved to to merge uncached values (i.e. 'spec') lazily
-      combinedApis.add(api);
+      if (newCommandsAdded) {
+        combinedApis.add(api);
+      }
     }
 
     @Override
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java
index 3108a2132ef..eda280a98c8 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java
@@ -29,6 +29,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import org.apache.solr.api.AnnotatedApi;
 import org.apache.solr.api.Api;
 import org.apache.solr.api.ApiBag;
 import org.apache.solr.api.ApiBag.ReqHandlerToApi;
@@ -41,6 +42,10 @@ import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.handler.RequestHandlerUtils;
+import org.apache.solr.handler.admin.api.GetAuthenticationConfigAPI;
+import org.apache.solr.handler.admin.api.GetAuthorizationConfigAPI;
+import org.apache.solr.handler.admin.api.ModifyNoAuthPluginSecurityConfigAPI;
+import org.apache.solr.handler.admin.api.ModifyNoAuthzPluginSecurityConfigAPI;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.security.AuthenticationPlugin;
@@ -277,20 +282,27 @@ public abstract class SecurityConfHandler extends RequestHandlerBase
       synchronized (this) {
         if (apis == null) {
           Collection<Api> apis = new ArrayList<>();
-          final SpecProvider authcCommands =
-              Utils.getSpec("cluster.security.authentication.Commands");
-          final SpecProvider authzCommands =
-              Utils.getSpec("cluster.security.authorization.Commands");
-          apis.add(new ReqHandlerToApi(this, Utils.getSpec("cluster.security.authentication")));
-          apis.add(new ReqHandlerToApi(this, Utils.getSpec("cluster.security.authorization")));
+          // GET Apis are the same regardless of which plugins are registered
+          apis.addAll(AnnotatedApi.getApis(new GetAuthenticationConfigAPI(this)));
+          apis.addAll(AnnotatedApi.getApis(new GetAuthorizationConfigAPI(this)));
+
+          // POST Apis come from the specific authc/z plugin registered (with a fallback used if
+          // the plugin isn't a SpecProvider).
+          final Api defaultAuthcApi =
+              AnnotatedApi.getApis(new ModifyNoAuthPluginSecurityConfigAPI(this)).get(0);
+          final Api defaultAuthzApi =
+              AnnotatedApi.getApis(new ModifyNoAuthzPluginSecurityConfigAPI(this)).get(0);
+
           SpecProvider authcSpecProvider =
               () -> {
                 AuthenticationPlugin authcPlugin = cores.getAuthenticationPlugin();
                 return authcPlugin != null && authcPlugin instanceof SpecProvider
                     ? ((SpecProvider) authcPlugin).getSpec()
-                    : authcCommands.getSpec();
+                    : defaultAuthcApi.getSpec();
               };
 
+          // TODO Can we remove this extra ReqHandlerToApi wrapping - nothing but the schema from
+          // the POST authc/authz is getting used.
           apis.add(
               new ReqHandlerToApi(this, authcSpecProvider) {
                 @Override
@@ -309,7 +321,7 @@ public abstract class SecurityConfHandler extends RequestHandlerBase
                 AuthorizationPlugin authzPlugin = cores.getAuthorizationPlugin();
                 return authzPlugin != null && authzPlugin instanceof SpecProvider
                     ? ((SpecProvider) authzPlugin).getSpec()
-                    : authzCommands.getSpec();
+                    : defaultAuthzApi.getSpec();
               };
           apis.add(
               new ApiBag.ReqHandlerToApi(this, authzSpecProvider) {
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/GetAuthenticationConfigAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/GetAuthenticationConfigAPI.java
new file mode 100644
index 00000000000..9a7b7947689
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetAuthenticationConfigAPI.java
@@ -0,0 +1,50 @@
+/*
+ * 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.client.solrj.SolrRequest.METHOD.GET;
+import static org.apache.solr.security.PermissionNameProvider.Name.SECURITY_READ_PERM;
+
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.handler.admin.SecurityConfHandler;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for fetching the authentication section from Solr's security.json configuration.
+ *
+ * <p>This API (GET /v2/cluster/security/authentication) is analogous to the v1 `GET
+ * /solr/admin/authentication` API.
+ */
+public class GetAuthenticationConfigAPI {
+
+  private final SecurityConfHandler securityConfHandler;
+
+  public GetAuthenticationConfigAPI(SecurityConfHandler securityConfHandler) {
+    this.securityConfHandler = securityConfHandler;
+  }
+
+  @EndPoint(
+      path = {"/cluster/security/authentication"},
+      method = GET,
+      permission = SECURITY_READ_PERM)
+  public void fetchAuthenticationConfig(SolrQueryRequest req, SolrQueryResponse rsp)
+      throws Exception {
+    securityConfHandler.handleRequestBody(req, rsp);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/GetAuthorizationConfigAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/GetAuthorizationConfigAPI.java
new file mode 100644
index 00000000000..aa4edbdf1e2
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/GetAuthorizationConfigAPI.java
@@ -0,0 +1,49 @@
+/*
+ * 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.client.solrj.SolrRequest.METHOD.GET;
+import static org.apache.solr.security.PermissionNameProvider.Name.SECURITY_READ_PERM;
+
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.handler.admin.SecurityConfHandler;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for fetching the authorization section of Solr's security.json configuration.
+ *
+ * <p>This API (GET /v2/cluster/security/authorization) is analogous to the v1 `GET
+ * /solr/admin/authorization` API.
+ */
+public class GetAuthorizationConfigAPI {
+  private final SecurityConfHandler securityConfHandler;
+
+  public GetAuthorizationConfigAPI(SecurityConfHandler securityConfHandler) {
+    this.securityConfHandler = securityConfHandler;
+  }
+
+  @EndPoint(
+      path = {"/cluster/security/authorization"},
+      method = GET,
+      permission = SECURITY_READ_PERM)
+  public void fetchAuthorizationConfig(SolrQueryRequest req, SolrQueryResponse rsp)
+      throws Exception {
+    securityConfHandler.handleRequestBody(req, rsp);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyBasicAuthConfigAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyBasicAuthConfigAPI.java
new file mode 100644
index 00000000000..dd7e488b43d
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyBasicAuthConfigAPI.java
@@ -0,0 +1,47 @@
+/*
+ * 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.client.solrj.SolrRequest.METHOD.POST;
+import static org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.api.Command;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.api.PayloadObj;
+
+/** V2 API to modify configuration for Solr's {@link org.apache.solr.security.BasicAuthPlugin} */
+@EndPoint(
+    path = {"/cluster/security/authentication"},
+    method = POST,
+    permission = SECURITY_EDIT_PERM)
+public class ModifyBasicAuthConfigAPI {
+
+  @Command(name = "set-user")
+  public void createUsers(PayloadObj<Map<String, String>> usersToCreatePayload) {
+    // Method stub used only to produce v2 API spec; implementation/body empty.
+    throw new IllegalStateException();
+  }
+
+  @Command(name = "delete-user")
+  public void deleteUsers(PayloadObj<List<String>> usersToDeletePayload) {
+    // Method stub used only to produce v2 API spec; implementation/body empty.
+    throw new IllegalStateException();
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyMultiPluginAuthConfigAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyMultiPluginAuthConfigAPI.java
new file mode 100644
index 00000000000..b9a19eb1ce1
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyMultiPluginAuthConfigAPI.java
@@ -0,0 +1,57 @@
+/*
+ * 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.client.solrj.SolrRequest.METHOD.POST;
+import static org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.api.Command;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.api.PayloadObj;
+import org.apache.solr.security.MultiAuthPlugin;
+
+/** V2 API to modify configuration options for the {@link MultiAuthPlugin}. */
+@EndPoint(
+    path = {"/cluster/security/authentication"},
+    method = POST,
+    permission = SECURITY_EDIT_PERM)
+public class ModifyMultiPluginAuthConfigAPI {
+
+  @Command(name = "set-user")
+  public void createUsers(PayloadObj<Map<String, String>> usersToCreatePayload) {
+    // Method stub used only to produce v2 API spec; implementation/body empty.
+    throw new IllegalStateException();
+  }
+
+  @Command(name = "delete-user")
+  public void deleteUsers(PayloadObj<List<String>> usersToDeletePayload) {
+    // Method stub used only to produce v2 API spec; implementation/body empty.
+    throw new IllegalStateException();
+  }
+
+  // TODO This is a good candidate to create an actual Payload class instead of relying on the
+  // generic Map, if someone understands what syntax this API actually takes.  The apispec
+  // this code was mirrored from is vague, and the ref-guide has no examples afaict.
+  @Command(name = "set-property")
+  public void setProperties(PayloadObj<Map<String, Object>> propertyPayload) {
+    // Method stub used only to produce v2 API spec; implementation/body empty.
+    throw new IllegalStateException();
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyNoAuthPluginSecurityConfigAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyNoAuthPluginSecurityConfigAPI.java
new file mode 100644
index 00000000000..031410e2a46
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyNoAuthPluginSecurityConfigAPI.java
@@ -0,0 +1,50 @@
+/*
+ * 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.client.solrj.SolrRequest.METHOD.POST;
+import static org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.handler.admin.SecurityConfHandler;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for POST requests received when no authentication plugin is active.
+ *
+ * <p>Solr's security APIs only supports authc config modifications once an Authentication plugin is
+ * in place. So this API serves solely as a placeholder that allows {@link SecurityConfHandler} to
+ * return a helpful error message (instead of the opaque 404 that users would get without this API).
+ */
+public class ModifyNoAuthPluginSecurityConfigAPI {
+  private final SecurityConfHandler securityConfHandler;
+
+  public ModifyNoAuthPluginSecurityConfigAPI(SecurityConfHandler securityConfHandler) {
+    this.securityConfHandler = securityConfHandler;
+  }
+
+  @EndPoint(
+      path = {"/cluster/security/authentication"},
+      method = POST,
+      permission = SECURITY_EDIT_PERM)
+  public void updateAuthenticationConfig(SolrQueryRequest req, SolrQueryResponse rsp)
+      throws Exception {
+    securityConfHandler.handleRequestBody(req, rsp);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyNoAuthzPluginSecurityConfigAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyNoAuthzPluginSecurityConfigAPI.java
new file mode 100644
index 00000000000..c90c5fd2b02
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyNoAuthzPluginSecurityConfigAPI.java
@@ -0,0 +1,50 @@
+/*
+ * 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.client.solrj.SolrRequest.METHOD.POST;
+import static org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.handler.admin.SecurityConfHandler;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for POST requests received when no authorization plugin is active.
+ *
+ * <p>Solr's security APIs only supports authz config modifications once an Authorization plugin is
+ * in place. So this API serves solely as a placeholder that allows {@link SecurityConfHandler} to
+ * return a helpful error message (instead of the opaque 404 that users would get without this API).
+ */
+public class ModifyNoAuthzPluginSecurityConfigAPI {
+  private final SecurityConfHandler securityConfHandler;
+
+  public ModifyNoAuthzPluginSecurityConfigAPI(SecurityConfHandler securityConfHandler) {
+    this.securityConfHandler = securityConfHandler;
+  }
+
+  @EndPoint(
+      path = {"/cluster/security/authorization"},
+      method = POST,
+      permission = SECURITY_EDIT_PERM)
+  public void updateAuthorizationConfig(SolrQueryRequest req, SolrQueryResponse rsp)
+      throws Exception {
+    securityConfHandler.handleRequestBody(req, rsp);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyRuleBasedAuthConfigAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyRuleBasedAuthConfigAPI.java
new file mode 100644
index 00000000000..03ba29bae73
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ModifyRuleBasedAuthConfigAPI.java
@@ -0,0 +1,64 @@
+/*
+ * 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.client.solrj.SolrRequest.METHOD.POST;
+import static org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+
+import java.util.Map;
+import org.apache.solr.api.Command;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.api.PayloadObj;
+import org.apache.solr.client.solrj.request.beans.SetRuleBasedAuthPermissionPayload;
+import org.apache.solr.client.solrj.request.beans.UpdateRuleBasedAuthPermissionPayload;
+
+/**
+ * V2 API to modify configuration options for the {@link
+ * org.apache.solr.security.RuleBasedAuthorizationPlugin}.
+ */
+@EndPoint(
+    path = {"/cluster/security/authorization"},
+    method = POST,
+    permission = SECURITY_EDIT_PERM)
+public class ModifyRuleBasedAuthConfigAPI {
+
+  @Command(name = "set-permission")
+  public void setPermission(PayloadObj<SetRuleBasedAuthPermissionPayload> permissionPayloadObj) {
+    // Method stub used only to produce v2 API spec; implementation/body empty.
+    throw new IllegalStateException();
+  }
+
+  @Command(name = "update-permission")
+  public void updatePermission(
+      PayloadObj<UpdateRuleBasedAuthPermissionPayload> permissionPayloadObj) {
+    // Method stub used only to produce v2 API spec; implementation/body empty.
+    throw new IllegalStateException();
+  }
+
+  @Command(name = "delete-permission")
+  public void deletePermission(PayloadObj<Integer> indexToDeletePayloadObj) {
+    // Method stub used only to produce v2 API spec; implementation/body empty.
+    throw new IllegalStateException();
+  }
+
+  @Command(name = "set-user-role")
+  public void setUserRole(PayloadObj<Map<String, Object>> userRoleMapPayloadObj) {
+    // Method stub used only to produce v2 API spec; implementation/body empty.
+    throw new IllegalStateException();
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
index e82f8b9c549..9281f88023c 100644
--- a/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
@@ -29,14 +29,16 @@ import javax.servlet.http.HttpServletResponse;
 import org.apache.http.HttpRequest;
 import org.apache.http.protocol.HttpContext;
 import org.apache.lucene.util.ResourceLoader;
+import org.apache.solr.api.AnnotatedApi;
+import org.apache.solr.api.Api;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.SpecProvider;
 import org.apache.solr.common.StringUtils;
 import org.apache.solr.common.util.CommandOperation;
-import org.apache.solr.common.util.Utils;
 import org.apache.solr.common.util.ValidatingJsonMap;
 import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.api.ModifyMultiPluginAuthConfigAPI;
 import org.apache.solr.metrics.SolrMetricsContext;
 import org.eclipse.jetty.client.api.Request;
 
@@ -266,7 +268,8 @@ public class MultiAuthPlugin extends AuthenticationPlugin
 
   @Override
   public ValidatingJsonMap getSpec() {
-    return Utils.getSpec("cluster.security.MultiPluginAuth.Commands").getSpec();
+    final List<Api> apis = AnnotatedApi.getApis(new ModifyMultiPluginAuthConfigAPI());
+    return apis.get(0).getSpec();
   }
 
   @Override
diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java
index 73ad414f2e8..fc917cad38a 100644
--- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java
+++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java
@@ -32,10 +32,12 @@ import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import org.apache.solr.api.AnnotatedApi;
+import org.apache.solr.api.Api;
 import org.apache.solr.common.SpecProvider;
 import org.apache.solr.common.util.CommandOperation;
-import org.apache.solr.common.util.Utils;
 import org.apache.solr.common.util.ValidatingJsonMap;
+import org.apache.solr.handler.admin.api.ModifyRuleBasedAuthConfigAPI;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -397,6 +399,7 @@ public abstract class RuleBasedAuthorizationPluginBase
 
   @Override
   public ValidatingJsonMap getSpec() {
-    return Utils.getSpec("cluster.security.RuleBasedAuthorization").getSpec();
+    final List<Api> apis = AnnotatedApi.getApis(new ModifyRuleBasedAuthConfigAPI());
+    return apis.get(0).getSpec();
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java b/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java
index ba7dd6e2968..e1aebf28c55 100644
--- a/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java
+++ b/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java
@@ -30,9 +30,11 @@ import java.util.List;
 import java.util.Map;
 import java.util.Random;
 import java.util.Set;
+import org.apache.solr.api.AnnotatedApi;
+import org.apache.solr.api.Api;
 import org.apache.solr.common.util.CommandOperation;
-import org.apache.solr.common.util.Utils;
 import org.apache.solr.common.util.ValidatingJsonMap;
+import org.apache.solr.handler.admin.api.ModifyBasicAuthConfigAPI;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -172,7 +174,8 @@ public class Sha256AuthenticationProvider
 
   @Override
   public ValidatingJsonMap getSpec() {
-    return Utils.getSpec("cluster.security.BasicAuth.Commands").getSpec();
+    final List<Api> apis = AnnotatedApi.getApis(new ModifyBasicAuthConfigAPI());
+    return apis.get(0).getSpec();
   }
 
   // TODO make private?
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java b/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java
index e4b00b88be1..45c33f19d9a 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java
@@ -66,6 +66,7 @@ import org.apache.http.entity.ByteArrayEntity;
 import org.apache.http.message.BasicHeader;
 import org.apache.http.util.EntityUtils;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.api.AnnotatedApi;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.BaseHttpSolrClient;
@@ -94,6 +95,7 @@ import org.apache.solr.common.util.ValidatingJsonMap;
 import org.apache.solr.core.ConfigSetProperties;
 import org.apache.solr.core.ConfigSetService;
 import org.apache.solr.core.TestSolrConfigHandler;
+import org.apache.solr.handler.admin.api.ModifyBasicAuthConfigAPI;
 import org.apache.solr.security.AuthorizationContext;
 import org.apache.solr.security.AuthorizationPlugin;
 import org.apache.solr.security.AuthorizationResponse;
@@ -1880,7 +1882,7 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
 
         @Override
         public ValidatingJsonMap getSpec() {
-          return Utils.getSpec("cluster.security.BasicAuth.Commands").getSpec();
+          return AnnotatedApi.getApis(new ModifyBasicAuthConfigAPI()).get(0).getSpec();
         }
 
         @Override
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/V2SecurityAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/V2SecurityAPIMappingTest.java
new file mode 100644
index 00000000000..1cc621a74f4
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/V2SecurityAPIMappingTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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;
+
+import java.util.HashMap;
+import java.util.Set;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.api.AnnotatedApi;
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
+import org.apache.solr.handler.admin.api.GetAuthenticationConfigAPI;
+import org.apache.solr.handler.admin.api.GetAuthorizationConfigAPI;
+import org.apache.solr.handler.admin.api.ModifyBasicAuthConfigAPI;
+import org.apache.solr.handler.admin.api.ModifyMultiPluginAuthConfigAPI;
+import org.apache.solr.handler.admin.api.ModifyNoAuthPluginSecurityConfigAPI;
+import org.apache.solr.handler.admin.api.ModifyNoAuthzPluginSecurityConfigAPI;
+import org.apache.solr.handler.admin.api.ModifyRuleBasedAuthConfigAPI;
+import org.junit.Before;
+import org.junit.Test;
+
+public class V2SecurityAPIMappingTest extends SolrTestCaseJ4 {
+
+  private ApiBag apiBag;
+  private SecurityConfHandler mockHandler;
+
+  @Before
+  public void setupApiBag() {
+    apiBag = new ApiBag(false);
+    mockHandler = null;
+  }
+
+  // Test the GET /cluster/security/[authentication|authorization] APIs registered regardless of
+  // security plugin
+  @Test
+  public void testMappingForUniversalGetApis() {
+    apiBag.registerObject(new GetAuthenticationConfigAPI(mockHandler));
+    apiBag.registerObject(new GetAuthorizationConfigAPI(mockHandler));
+
+    assertAnnotatedApiExistsFor("GET", "/cluster/security/authentication");
+    assertAnnotatedApiExistsFor("GET", "/cluster/security/authorization");
+  }
+
+  // Test the POST /cluster/security/[authentication|authorization] APIs registered when no plugins
+  // are configured.
+  @Test
+  public void testDefaultPostAPIs() {
+    apiBag.registerObject(new ModifyNoAuthPluginSecurityConfigAPI(mockHandler));
+    apiBag.registerObject(new ModifyNoAuthzPluginSecurityConfigAPI(mockHandler));
+
+    // Authc API
+    final AnnotatedApi updateAuthcConfig =
+        assertAnnotatedApiExistsFor("POST", "/cluster/security/authentication");
+    assertEquals(1, updateAuthcConfig.getCommands().size());
+    // empty-string is the placeholder for POST APIs without an explicit command.
+    assertEquals("", updateAuthcConfig.getCommands().keySet().iterator().next());
+
+    // Authz API
+    final AnnotatedApi updateAuthzConfig =
+        assertAnnotatedApiExistsFor("POST", "/cluster/security/authorization");
+    assertEquals(1, updateAuthzConfig.getCommands().size());
+    // empty-string is the placeholder for POST APIs without an explicit command.
+    assertEquals("", updateAuthzConfig.getCommands().keySet().iterator().next());
+  }
+
+  @Test
+  public void testBasicAuthApiMapping() {
+    apiBag.registerObject(new ModifyBasicAuthConfigAPI());
+
+    final AnnotatedApi updateAuthcConfig =
+        assertAnnotatedApiExistsFor("POST", "/cluster/security/authentication");
+    assertEquals(2, updateAuthcConfig.getCommands().size());
+    final Set<String> commandNames = updateAuthcConfig.getCommands().keySet();
+    assertTrue(
+        "Expected 'set-user' to be in the command list: " + commandNames,
+        commandNames.contains("set-user"));
+    assertTrue(
+        "Expected 'delete-user' to be in the command list: " + commandNames,
+        commandNames.contains("delete-user"));
+  }
+
+  @Test
+  public void testMultiAuthApiMapping() {
+    apiBag.registerObject(new ModifyMultiPluginAuthConfigAPI());
+
+    final AnnotatedApi updateAuthcConfig =
+        assertAnnotatedApiExistsFor("POST", "/cluster/security/authentication");
+    assertEquals(3, updateAuthcConfig.getCommands().size());
+    final Set<String> commandNames = updateAuthcConfig.getCommands().keySet();
+    assertTrue(
+        "Expected 'set-user' to be in the command list: " + commandNames,
+        commandNames.contains("set-user"));
+    assertTrue(
+        "Expected 'delete-user' to be in the command list: " + commandNames,
+        commandNames.contains("delete-user"));
+    assertTrue(
+        "Expected 'set-property' to be in the command list: " + commandNames,
+        commandNames.contains("set-property"));
+  }
+
+  @Test
+  public void testRuleBasedAuthzApiMapping() {
+    apiBag.registerObject(new ModifyRuleBasedAuthConfigAPI());
+
+    final AnnotatedApi updateAuthzConfig =
+        assertAnnotatedApiExistsFor("POST", "/cluster/security/authorization");
+    assertEquals(4, updateAuthzConfig.getCommands().size());
+    final Set<String> commandNames = updateAuthzConfig.getCommands().keySet();
+    assertTrue(
+        "Expected 'set-permission' to be in the command list: " + commandNames,
+        commandNames.contains("set-permission"));
+    assertTrue(
+        "Expected 'update-permission' to be in the command list: " + commandNames,
+        commandNames.contains("update-permission"));
+    assertTrue(
+        "Expected 'delete-permission' to be in the command list: " + commandNames,
+        commandNames.contains("delete-permission"));
+    assertTrue(
+        "Expected 'set-user-role' to be in the command list: " + commandNames,
+        commandNames.contains("set-user-role"));
+  }
+
+  private AnnotatedApi assertAnnotatedApiExistsFor(String method, String path) {
+    final HashMap<String, String> parts = new HashMap<>();
+    final Api api = apiBag.lookup(path, method, parts);
+    if (api == null) {
+      fail("Expected to find API for path [" + path + "], but no API mapping found.");
+    }
+    if (!(api instanceof AnnotatedApi)) {
+      fail(
+          "Expected AnnotatedApi for path ["
+              + path
+              + "], but found non-annotated API ["
+              + api
+              + "]");
+    }
+
+    return (AnnotatedApi) api;
+  }
+}
diff --git a/solr/modules/jwt-auth/src/java/org/apache/solr/handler/admin/api/ModifyJWTAuthPluginConfigAPI.java b/solr/modules/jwt-auth/src/java/org/apache/solr/handler/admin/api/ModifyJWTAuthPluginConfigAPI.java
new file mode 100644
index 00000000000..ea24208c66e
--- /dev/null
+++ b/solr/modules/jwt-auth/src/java/org/apache/solr/handler/admin/api/ModifyJWTAuthPluginConfigAPI.java
@@ -0,0 +1,39 @@
+/*
+ * 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.client.solrj.SolrRequest.METHOD.POST;
+import static org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM;
+
+import org.apache.solr.api.Command;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.api.PayloadObj;
+
+/** V2 API for modifying configuration for Solr's JWTAuthPlugin. */
+@EndPoint(
+    path = {"/cluster/security/authentication"},
+    method = POST,
+    permission = SECURITY_EDIT_PERM)
+public class ModifyJWTAuthPluginConfigAPI {
+
+  @Command(name = "set-property")
+  public void setProperties(PayloadObj<JWTConfigurationPayload> propertyPayload) {
+    // Method stub used only to produce v2 API spec; implementation/body empty.
+    throw new IllegalStateException();
+  }
+}
diff --git a/solr/modules/jwt-auth/src/java/org/apache/solr/handler/admin/api/package-info.java b/solr/modules/jwt-auth/src/java/org/apache/solr/handler/admin/api/package-info.java
new file mode 100644
index 00000000000..f16f5f29a4e
--- /dev/null
+++ b/solr/modules/jwt-auth/src/java/org/apache/solr/handler/admin/api/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+/** API endpoint and associated files for the JWT Authentication Plugin */
+package org.apache.solr.handler.admin.api;
diff --git a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
index 0b468063d77..e5d42ca8f81 100644
--- a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
+++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
@@ -49,6 +49,8 @@ import org.apache.http.HttpHeaders;
 import org.apache.http.HttpRequest;
 import org.apache.http.client.protocol.HttpClientContext;
 import org.apache.http.protocol.HttpContext;
+import org.apache.solr.api.AnnotatedApi;
+import org.apache.solr.api.Api;
 import org.apache.solr.client.solrj.impl.Http2SolrClient;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SpecProvider;
@@ -57,6 +59,7 @@ import org.apache.solr.common.util.CommandOperation;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.common.util.ValidatingJsonMap;
 import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.api.ModifyJWTAuthPluginConfigAPI;
 import org.apache.solr.security.AuthenticationPlugin;
 import org.apache.solr.security.ConfigEditablePlugin;
 import org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode;
@@ -705,7 +708,8 @@ public class JWTAuthPlugin extends AuthenticationPlugin
 
   @Override
   public ValidatingJsonMap getSpec() {
-    return Utils.getSpec("cluster.security.JwtAuth.Commands").getSpec();
+    final List<Api> apis = AnnotatedApi.getApis(new ModifyJWTAuthPluginConfigAPI());
+    return apis.get(0).getSpec();
   }
 
   /**
diff --git a/solr/modules/jwt-auth/src/test/org/apache/solr/handler/admin/api/V2JWTSecurityApiMappingTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/handler/admin/api/V2JWTSecurityApiMappingTest.java
new file mode 100644
index 00000000000..252f8b9f953
--- /dev/null
+++ b/solr/modules/jwt-auth/src/test/org/apache/solr/handler/admin/api/V2JWTSecurityApiMappingTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.HashMap;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.api.AnnotatedApi;
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
+import org.junit.Before;
+import org.junit.Test;
+
+public class V2JWTSecurityApiMappingTest extends SolrTestCaseJ4 {
+
+  private ApiBag apiBag;
+
+  @Before
+  public void setupApiBag() {
+    apiBag = new ApiBag(false);
+  }
+
+  @Test
+  public void testJwtConfigApiMapping() {
+    apiBag.registerObject(new ModifyJWTAuthPluginConfigAPI());
+
+    // Authc API
+    final AnnotatedApi updateAuthcConfig =
+        assertAnnotatedApiExistsFor("POST", "/cluster/security/authentication");
+    assertEquals(1, updateAuthcConfig.getCommands().size());
+    assertEquals("set-property", updateAuthcConfig.getCommands().keySet().iterator().next());
+  }
+
+  private AnnotatedApi assertAnnotatedApiExistsFor(String method, String path) {
+    final HashMap<String, String> parts = new HashMap<>();
+    final Api api = apiBag.lookup(path, method, parts);
+    if (api == null) {
+      fail("Expected to find API for path [" + path + "], but no API mapping found.");
+    }
+    if (!(api instanceof AnnotatedApi)) {
+      fail(
+          "Expected AnnotatedApi for path ["
+              + path
+              + "], but found non-annotated API ["
+              + api
+              + "]");
+    }
+
+    return (AnnotatedApi) api;
+  }
+}
diff --git a/solr/modules/jwt-auth/src/test/org/apache/solr/handler/admin/api/package-info.java b/solr/modules/jwt-auth/src/test/org/apache/solr/handler/admin/api/package-info.java
new file mode 100644
index 00000000000..563aabcd128
--- /dev/null
+++ b/solr/modules/jwt-auth/src/test/org/apache/solr/handler/admin/api/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Tests for the API(s) bundled with the jwt-auth module. */
+package org.apache.solr.handler.admin.api;
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/DeleteBasicAuthUserPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/DeleteBasicAuthUserPayload.java
new file mode 100644
index 00000000000..72ca11d69d2
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/DeleteBasicAuthUserPayload.java
@@ -0,0 +1,22 @@
+/*
+ * 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.solrj.request.beans;
+
+import org.apache.solr.common.util.ReflectMapWriter;
+
+public class DeleteBasicAuthUserPayload implements ReflectMapWriter {}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetRuleBasedAuthPermissionPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetRuleBasedAuthPermissionPayload.java
new file mode 100644
index 00000000000..7ada44aee09
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetRuleBasedAuthPermissionPayload.java
@@ -0,0 +1,43 @@
+/*
+ * 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.solrj.request.beans;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.common.annotation.JsonProperty;
+import org.apache.solr.common.util.ReflectMapWriter;
+
+public class SetRuleBasedAuthPermissionPayload implements ReflectMapWriter {
+  @JsonProperty public String name;
+
+  // TODO Do we support enum's: only acceptable values here are GET, POST, DELETE, and PUT
+  @JsonProperty public String method;
+
+  @JsonProperty public List<String> collection;
+
+  @JsonProperty public List<String> path;
+
+  @JsonProperty public Integer index;
+
+  @JsonProperty public Integer before;
+
+  @JsonProperty public Map<String, Object> params;
+
+  @JsonProperty(required = true)
+  public List<String> role;
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/UpdateRuleBasedAuthPermissionPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/UpdateRuleBasedAuthPermissionPayload.java
new file mode 100644
index 00000000000..f002a08250f
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/UpdateRuleBasedAuthPermissionPayload.java
@@ -0,0 +1,44 @@
+/*
+ * 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.solrj.request.beans;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.common.annotation.JsonProperty;
+import org.apache.solr.common.util.ReflectMapWriter;
+
+public class UpdateRuleBasedAuthPermissionPayload implements ReflectMapWriter {
+  @JsonProperty public String name;
+
+  // TODO Do we support enum's: only acceptable values here are GET, POST, DELETE, and PUT
+  @JsonProperty public String method;
+
+  @JsonProperty public List<String> collection;
+
+  @JsonProperty public List<String> path;
+
+  @JsonProperty(required = true)
+  public Integer index;
+
+  @JsonProperty public Integer before;
+
+  @JsonProperty public Map<String, Object> params;
+
+  @JsonProperty(required = true)
+  public List<String> role;
+}
diff --git a/solr/solrj/src/java/org/apache/solr/handler/admin/api/JWTConfigurationPayload.java b/solr/solrj/src/java/org/apache/solr/handler/admin/api/JWTConfigurationPayload.java
new file mode 100644
index 00000000000..07f1c63bd80
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/handler/admin/api/JWTConfigurationPayload.java
@@ -0,0 +1,87 @@
+/*
+ * 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 java.util.Map;
+import org.apache.solr.common.annotation.JsonProperty;
+import org.apache.solr.common.util.ReflectMapWriter;
+
+public class JWTConfigurationPayload implements ReflectMapWriter {
+
+  @JsonProperty public Boolean blockUnknown;
+
+  @JsonProperty public String principalClaim;
+
+  @JsonProperty public Boolean requireExp;
+
+  @JsonProperty public List<String> algAllowlist;
+
+  @JsonProperty public Long jwkCacheDur;
+
+  @JsonProperty public Map<String, Object> claimsMatch;
+
+  // TODO Should this be a List<String> instead of a whitespace delimited string?
+  @JsonProperty public String scope;
+
+  @JsonProperty public String realm;
+
+  // TODO Should this be a List<String> instead of a whitespace delimited string?
+  @JsonProperty public String rolesClaim;
+
+  @JsonProperty public String adminUiScope;
+
+  @JsonProperty public List<String> redirectUris;
+
+  @JsonProperty public Boolean requireIss;
+
+  @JsonProperty public List<Issuer> issuers;
+
+  @JsonProperty public String trustedCertsFile;
+
+  @JsonProperty public List<String> trustedCerts;
+
+  // Prior to supporting an array of issuers, legacy syntax supported properties for a single issuer
+  // at the top-level.
+  @JsonProperty public String name;
+  @JsonProperty public List<String> jwksUrl;
+  @JsonProperty public Map<String, Object> jwk;
+  @JsonProperty public String iss;
+  @JsonProperty public String aud;
+  @JsonProperty public String wellKnownUrl;
+  @JsonProperty public String authorizationEndpoint;
+  @JsonProperty public String clientId;
+
+  public static class Issuer implements ReflectMapWriter {
+    @JsonProperty public String name;
+
+    @JsonProperty public String wellKnownUrl;
+
+    @JsonProperty public String clientId;
+
+    @JsonProperty public List<String> jwksUrl;
+
+    @JsonProperty public Map<String, Object> jwk;
+
+    @JsonProperty public String iss;
+
+    @JsonProperty public String aud;
+
+    @JsonProperty public String authorizationEndpoint;
+  }
+}
diff --git a/solr/solrj/src/java/org/apache/solr/handler/admin/api/package-info.java b/solr/solrj/src/java/org/apache/solr/handler/admin/api/package-info.java
new file mode 100644
index 00000000000..993c5b4c4ed
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/handler/admin/api/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Payload implementations for APIs associated with the jwt-auth optional module */
+package org.apache.solr.handler.admin.api;
diff --git a/solr/solrj/src/resources/apispec/cluster.security.BasicAuth.Commands.json b/solr/solrj/src/resources/apispec/cluster.security.BasicAuth.Commands.json
deleted file mode 100644
index ba03e03324b..00000000000
--- a/solr/solrj/src/resources/apispec/cluster.security.BasicAuth.Commands.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "documentation": "https://solr.apache.org/guide/basic-authentication-plugin.html",
-  "description": "Modifies the configuration of Basic authentication, allowing you to add or remove users and their passwords.",
-  "methods": [
-    "POST"
-  ],
-  "url": {
-    "paths": [
-      "/cluster/security/authentication"
-    ]
-  },
-  "commands": {
-    "set-user": {
-      "type":"object",
-      "description": "The set-user command allows you to add users and change their passwords. Usernames and passwords are expressed as key-value pairs in a JSON object.",
-      "additionalProperties": true
-    },
-    "delete-user": {
-      "description": "Delete a user or a list of users. Passwords do not need to be provided, simply list the users in a JSON array, separated by colons.",
-      "type":"string"
-    }
-  }
-}
diff --git a/solr/solrj/src/resources/apispec/cluster.security.JwtAuth.Commands.json b/solr/solrj/src/resources/apispec/cluster.security.JwtAuth.Commands.json
deleted file mode 100644
index d4f43dfd11f..00000000000
--- a/solr/solrj/src/resources/apispec/cluster.security.JwtAuth.Commands.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "documentation": "https://solr.apache.org/guide/jwt-authentication-plugin.html",
-  "description": "Modifies the configuration of JWT token authentication.",
-  "methods": [
-    "POST"
-  ],
-  "url": {
-    "paths": [
-      "/cluster/security/authentication"
-    ]
-  },
-  "commands": {
-    "set-property": {
-      "type":"object",
-      "description": "The set-property command lets you set any of the configuration parameters supported by this plugin"
-    }
-  }
-}
diff --git a/solr/solrj/src/resources/apispec/cluster.security.MultiPluginAuth.Commands.json b/solr/solrj/src/resources/apispec/cluster.security.MultiPluginAuth.Commands.json
deleted file mode 100644
index e77f693c8a8..00000000000
--- a/solr/solrj/src/resources/apispec/cluster.security.MultiPluginAuth.Commands.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-  "documentation": "https://solr.apache.org/guide/basic-authentication-plugin.html",
-  "description": "Modifies the configuration of the multi-auth plugin. Each command should be wrapped in a single key object that identifies the scheme. The embedded command is then passed to the scheme specific plugin.",
-  "methods": [
-    "POST"
-  ],
-  "url": {
-    "paths": [
-      "/cluster/security/authentication"
-    ]
-  },
-  "commands": {
-    "set-user": {
-      "type":"object",
-      "description": "The set-user command allows you to add users and change their passwords. Usernames and passwords are expressed as key-value pairs in a JSON object.",
-      "additionalProperties": true
-    },
-    "delete-user": {
-      "description": "Delete a user or a list of users. Passwords do not need to be provided, simply list the users in a JSON array, separated by colons.",
-      "type":"object"
-    },
-    "set-property": {
-      "type":"object",
-      "description": "The set-property command lets you set any of the configuration parameters supported by this plugin"
-    }
-  }
-}
diff --git a/solr/solrj/src/resources/apispec/cluster.security.RuleBasedAuthorization.json b/solr/solrj/src/resources/apispec/cluster.security.RuleBasedAuthorization.json
deleted file mode 100644
index ad4aac1d9e3..00000000000
--- a/solr/solrj/src/resources/apispec/cluster.security.RuleBasedAuthorization.json
+++ /dev/null
@@ -1,129 +0,0 @@
-{
-  "documentation": "https://solr.apache.org/guide/rule-based-authorization-plugin.html",
-  "description": "Defines roles for accessing Solr, and assigns users to those roles. Use this API to change user authorizations to each of Solr's components.",
-  "methods": [
-    "POST"
-  ],
-  "url": {
-    "paths": [
-      "/cluster/security/authorization"
-    ]
-  },
-  "commands": {
-    "set-permission": {
-      "type":"object",
-      "description": "Create a new permission, overwrite an existing permission definition, or assign a pre-defined permission to a role.",
-      "properties": {
-        "name":{
-          "type":"string",
-          "description": "The name of the permission. The name will be used to update or delete the permission later."
-        },
-        "method":{
-          "type":"string",
-          "enum":["GET", "POST", "DELETE","PUT"],
-          "description": "HTTP methods that are allowed for this permission. You could allow only GET requests, or have a role that allows PUT and POST requests. The method values that are allowed for this property are GET, POST, PUT, DELETE and HEAD."
-        },
-
-        "collection":{
-          "type":"array",
-          "items": {
-            "type": "string"
-          },
-          "description":"The collection or collections the permission will apply to. When the path that will be allowed is collection-specific, such as when setting permissions to allow use of the Schema API, omitting the collection property will allow the defined path and/or method for all collections. However, when the path is one that is non-collection-specific, such as the Collections API, the collection value must be null. In this case, two permissions may need to be created; one fo [...]
-        },
-
-        "path":{
-          "type":"array",
-          "items": {
-            "type": "string"
-          },
-          "description":"A request handler name, such as /update or /select. A wild card is supported, to allow for all paths as appropriate (such as, /update/*)."
-        },
-        "index": {
-          "type": "integer",
-          "description": "The index of the permission you wish to overwrite. Skip this if it is a new permission that should be created."
-        },
-        "before":{
-          "type": "integer",
-          "description":"This property allows ordering of permissions. The value for this property is the name of the permission that this new permission should be placed before in security.json."
-        },
-        "params":{
-          "type":"object",
-          "additionalProperties":true,
-          "description": "The names and values of request parameters. This property can be omitted if all request parameters are allowed, but will restrict access only to the values provided if defined."
-        },
-        "role": {
-          "type": "array",
-          "items": {
-            "type": "string",
-            "description": "The name of the role(s) to give this permission. This name will be used to map user IDs to the role to grant these permissions. The value can be wildcard such as (*), which means that any user is OK, but no user is NOT OK."
-          }
-        }
-      },
-      "required": [
-        "role"
-      ]
-    },
-    "update-permission": {
-      "type":"object",
-      "properties": {
-        "name": {
-          "type": "string",
-          "description": "The name of the permission. The name will be used to update or delete the permission later."
-        },
-        "method": {
-          "type": "string",
-          "description": "HTTP methods that are allowed for this permission. You could allow only GET requests, or have a role that allows PUT and POST requests. The method values that are allowed for this property are GET, POST, PUT, DELETE and HEAD."
-        },
-        "collection": {
-          "type":"array",
-          "items": {
-            "type": "string"
-          },
-          "description": "The collection or collections the permission will apply to. When the path that will be allowed is collection-specific, such as when setting permissions to allow use of the Schema API, omitting the collection property will allow the defined path and/or method for all collections. However, when the path is one that is non-collection-specific, such as the Collections API, the collection value must be null. In this case, two permissions may need to be created; one f [...]
-        },
-        "path": {
-          "type":"array",
-          "items": {
-            "type": "string"
-          },
-          "description": "A request handler name, such as /update or /select. A wild card is supported, to allow for all paths as appropriate (such as, /update/*)."
-        },
-        "index": {
-          "type": "integer",
-          "description": "The index of the permission you wish to overwrite."
-        },
-        "before": {
-          "type": "integer",
-          "description": "This property allows ordering of permissions. The value for this property is the index of the permission that this new permission should be placed before in security.json."
-        },
-        "role": {
-          "type": "array",
-          "items": {
-            "type": "string",
-            "description": "The name of the role(s) to give this permission. This name will be used to map user IDs to the role to grant these permissions. The value can be wildcard such as (*), which means that any user is OK, but no user is NOT OK."
-          }
-        },
-        "params": {
-          "type": "object",
-          "additionalProperties": true,
-          "description": "The names and values of request parameters. This property can be omitted if all request parameters are allowed, but will restrict access only to the values provided if defined."
-        }
-      },
-      "required": [
-        "role",
-        "index"
-      ]
-    },
-    "delete-permission":{
-      "description":"delete a permission by its index",
-      "type":"integer"
-    },
-    "set-user-role": {
-      "type":"object",
-      "description": "A single command allows roles to be mapped to users. To remove a user's permission, you should set the role to null. The key is always a user id and the value is one or more role names.",
-      "additionalProperties":true
-
-    }
-  }
-}
diff --git a/solr/solrj/src/resources/apispec/cluster.security.authentication.Commands.json b/solr/solrj/src/resources/apispec/cluster.security.authentication.Commands.json
deleted file mode 100644
index b71911cf9e4..00000000000
--- a/solr/solrj/src/resources/apispec/cluster.security.authentication.Commands.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "documentation": "https://solr.apache.org/guide/securing-solr.html",
-  "description":"This is a placeholder output when no authentication is configured",
-  "methods": [
-    "POST"
-  ],
-  "url": {
-    "paths": [
-      "/cluster/security/authentication"
-    ]
-  }
-}
diff --git a/solr/solrj/src/resources/apispec/cluster.security.authentication.json b/solr/solrj/src/resources/apispec/cluster.security.authentication.json
deleted file mode 100644
index d912f147961..00000000000
--- a/solr/solrj/src/resources/apispec/cluster.security.authentication.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "documentation": "https://solr.apache.org/guide/authentication-and-authorization-plugins.html",
-  "description": "Shows the configuration for authentication, including users, classes (type of authentication) and other parameters.",
-  "methods": [
-    "GET"
-  ],
-  "url": {
-    "paths": [
-      "/cluster/security/authentication"
-    ]
-  }
-}
diff --git a/solr/solrj/src/resources/apispec/cluster.security.authorization.Commands.json b/solr/solrj/src/resources/apispec/cluster.security.authorization.Commands.json
deleted file mode 100644
index dfa47b552a0..00000000000
--- a/solr/solrj/src/resources/apispec/cluster.security.authorization.Commands.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "documentation": "https://solr.apache.org/guide/securing-solr.html",
-  "description":"This is a placeholder output when no authorization is configured",
-  "methods": [
-    "POST"
-  ],
-  "url": {
-    "paths": [
-      "/cluster/security/authorization"
-    ]
-  }
-
-}
diff --git a/solr/solrj/src/resources/apispec/cluster.security.authorization.json b/solr/solrj/src/resources/apispec/cluster.security.authorization.json
deleted file mode 100644
index 798f80453ad..00000000000
--- a/solr/solrj/src/resources/apispec/cluster.security.authorization.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "documentation": "https://solr.apache.org/guide/authentication-and-authorization-plugins.html",
-  "description":"Shows the configuration for authorization, including the classes (type of authorization), permissions, user-roles, and other parameters.",
-  "methods": [
-    "GET"
-  ],
-  "url": {
-    "paths": [
-      "/cluster/security/authorization"
-    ]
-  }
-
-}
diff --git a/solr/solrj/src/test/org/apache/solr/common/util/JsonValidatorTest.java b/solr/solrj/src/test/org/apache/solr/common/util/JsonValidatorTest.java
index d6e6361f3e9..225474a68e0 100644
--- a/solr/solrj/src/test/org/apache/solr/common/util/JsonValidatorTest.java
+++ b/solr/solrj/src/test/org/apache/solr/common/util/JsonValidatorTest.java
@@ -17,16 +17,10 @@
 package org.apache.solr.common.util;
 
 import java.util.List;
-import java.util.Map;
 import org.apache.solr.SolrTestCaseJ4;
 
 public class JsonValidatorTest extends SolrTestCaseJ4 {
 
-  public void testSchema() {
-    checkSchema("cluster.security.BasicAuth.Commands");
-    checkSchema("cluster.security.RuleBasedAuthorization");
-  }
-
   public void testSchemaValidation() {
     final JsonSchemaValidator personSchemaValidator =
         new JsonSchemaValidator(
@@ -187,19 +181,4 @@ public class JsonValidatorTest extends SolrTestCaseJ4 {
         mutuallyExclusivePropertiesValidator.validateJson(Utils.fromJSONString("" + "{'a':'val'}"));
     assertNull(errs);
   }
-
-  private void checkSchema(String name) {
-    ValidatingJsonMap spec = Utils.getSpec(name).getSpec();
-    @SuppressWarnings({"rawtypes"})
-    Map commands = (Map) spec.get("commands");
-    for (Object o : commands.entrySet()) {
-      @SuppressWarnings({"rawtypes"})
-      Map.Entry cmd = (Map.Entry) o;
-      try {
-        JsonSchemaValidator validator = new JsonSchemaValidator((Map) cmd.getValue());
-      } catch (Exception e) {
-        throw new RuntimeException("Error in command  " + cmd.getKey() + " in schema " + name, e);
-      }
-    }
-  }
 }