You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by th...@apache.org on 2021/11/01 22:36:18 UTC

[lucene-solr] branch branch_8x updated: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic (#2598)

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

thelabdude pushed a commit to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git


The following commit(s) were added to refs/heads/branch_8x by this push:
     new df42132  SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic (#2598)
df42132 is described below

commit df42132ba88dddf9b532e680457537a3138fc784
Author: Timothy Potter <th...@gmail.com>
AuthorDate: Mon Nov 1 16:36:04 2021 -0600

    SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic (#2598)
---
 solr/CHANGES.txt                                   |   2 +
 .../java/org/apache/solr/core/CoreContainer.java   |   6 +-
 .../org/apache/solr/security/BasicAuthPlugin.java  |   2 +-
 .../org/apache/solr/security/MultiAuthPlugin.java  | 273 +++++++++++++++++++++
 .../MultiAuthRuleBasedAuthorizationPlugin.java     | 150 +++++++++++
 .../solr/security/multi_auth_plugin_security.json  |  65 +++++
 .../solr/security/BasicAuthStandaloneTest.java     |  32 ++-
 .../apache/solr/security/MultiAuthPluginTest.java  | 257 +++++++++++++++++++
 .../src/basic-authentication-plugin.adoc           |  64 +++++
 .../src/jwt-authentication-plugin.adoc             |   6 +
 .../src/rule-based-authorization-plugin.adoc       |  38 ++-
 .../cluster.security.MultiPluginAuth.Commands.json |  27 ++
 12 files changed, 909 insertions(+), 13 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 8907a5d..b2500e2 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -16,6 +16,8 @@ New Features
 
 * SOLR-15708: SolrJ support for ConfigSet uploading (ab, hossman)
 
+* SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic (Timothy Potter, janhoy)
+
 Improvements
 ---------------------
 
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index e62b714..e893404 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -399,7 +399,11 @@ public class CoreContainer {
       }
       log.info("Initializing authorization plugin: {}", klas);
       authorizationPlugin = new SecurityPluginHolder<>(newVersion,
-          getResourceLoader().newInstance(klas, AuthorizationPlugin.class));
+          getResourceLoader().newInstance(klas,
+              AuthorizationPlugin.class,
+              null,
+              new Class<?>[]{CoreContainer.class},
+              new Object[]{this}));
 
       // Read and pass the authorization context to the plugin
       authorizationPlugin.plugin.init(authorizationConf);
diff --git a/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java
index 377cdb3..eb4aaba 100644
--- a/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java
@@ -274,7 +274,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
    * @param request the servlet request
    * @return true if the request is AJAX request
    */
-  private boolean isAjaxRequest(HttpServletRequest request) {
+  static boolean isAjaxRequest(HttpServletRequest request) {
     return "XMLHttpRequest".equalsIgnoreCase(request.getHeader(X_REQUESTED_WITH_HEADER));
   }
   
diff --git a/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
new file mode 100644
index 0000000..3f782fa
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.security;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.http.HttpRequest;
+import org.apache.http.protocol.HttpContext;
+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.core.SolrResourceLoader;
+import org.apache.solr.metrics.SolrMetricsContext;
+import org.eclipse.jetty.client.api.Request;
+
+/**
+ * Authentication plugin that supports multiple Authorization schemes, such as Bearer and Basic.
+ * The impl simply delegates to one of Solr's other AuthenticationPlugins, such as the BasicAuthPlugin or JWTAuthPlugin.
+ *
+ * @lucene.experimental
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private static final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final SolrResourceLoader loader;
+
+  // Get the loader from the CoreContainer so we can load the sub-plugins, such as the BasicAuthPlugin for Basic
+  public MultiAuthPlugin(CoreContainer cc) {
+    this.loader = cc.getResourceLoader();
+  }
+
+  @SuppressWarnings({"unchecked"})
+  static boolean applyEditCommandToSchemePlugin(String scheme, ConfigEditablePlugin plugin, CommandOperation c, Map<String, Object> latestConf) {
+    boolean madeChanges = false;
+    // Send in the config for the plugin only
+    Map<String, Object> latestPluginConf = null;
+    int updateAt = -1;
+    List<Map<String, Object>> schemes = (List<Map<String, Object>>) latestConf.get(PROPERTY_SCHEMES);
+    for (int i = 0; i < schemes.size(); i++) {
+      Map<String, Object> schemeConfig = schemes.get(i);
+      if (scheme.equals(schemeConfig.get(PROPERTY_SCHEME))) {
+        latestPluginConf = withoutScheme(schemeConfig);
+        updateAt = i; // for updating
+        break;
+      }
+    }
+
+    // shouldn't happen
+    if (latestPluginConf == null) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, "Config for scheme '" + scheme + "' not found!");
+    }
+
+    Map<String, Object> updated = plugin.edit(latestPluginConf, Collections.singletonList(c));
+    if (updated != null) {
+      madeChanges = true;
+      schemes.set(updateAt, withScheme(scheme, updated));
+    }
+
+    return madeChanges;
+  }
+
+  private static Map<String, Object> withoutScheme(final Map<String, Object> data) {
+    Map<String, Object> updatedData = new HashMap<>(data);
+    updatedData.remove(PROPERTY_SCHEME);
+    return updatedData;
+  }
+
+  private static Map<String, Object> withScheme(final String scheme, final Map<String, Object> data) {
+    Map<String, Object> updatedData = new HashMap<>(data);
+    updatedData.put(PROPERTY_SCHEME, scheme);
+    return updatedData;
+  }
+
+  @Override
+  @SuppressWarnings({"unchecked"})
+  public void init(Map<String, Object> pluginConfig) {
+    Object o = pluginConfig.get(PROPERTY_SCHEMES);
+    if (!(o instanceof List)) {
+      throw new SolrException(ErrorCode.SERVER_ERROR, "Invalid config: MultiAuthPlugin requires a list of schemes!");
+    }
+
+    List<Object> schemeList = (List<Object>) o;
+    // if you only have one scheme, then you don't need to use this class
+    if (schemeList.size() < 2) {
+      throw new SolrException(ErrorCode.SERVER_ERROR, "Invalid config: MultiAuthPlugin requires at least two schemes!");
+    }
+
+    for (Object s : schemeList) {
+      if (!(s instanceof Map)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Invalid scheme config, expected JSON object but found: " + s);
+      }
+      initPluginForScheme((Map<String, Object>) s);
+    }
+  }
+
+  protected void initPluginForScheme(Map<String, Object> schemeMap) {
+    Map<String, Object> schemeConfig = new HashMap<>(schemeMap);
+
+    String scheme = (String) schemeConfig.remove(PROPERTY_SCHEME);
+    if (StringUtils.isEmpty(scheme)) {
+      throw new SolrException(ErrorCode.SERVER_ERROR, "'scheme' is a required attribute: " + schemeMap);
+    }
+
+    String clazz = (String) schemeConfig.remove("class");
+    if (StringUtils.isEmpty(clazz)) {
+      throw new SolrException(ErrorCode.SERVER_ERROR, "'class' is a required attribute: " + schemeMap);
+    }
+
+    AuthenticationPlugin pluginForScheme = loader.newInstance(clazz, AuthenticationPlugin.class);
+    pluginForScheme.init(schemeConfig);
+    pluginMap.put(scheme.toLowerCase(Locale.ROOT), pluginForScheme);
+  }
+
+  @Override
+  public void initializeMetrics(SolrMetricsContext parentContext, String scope) {
+    for (AuthenticationPlugin plugin : pluginMap.values()) {
+      plugin.initializeMetrics(parentContext, scope);
+    }
+  }
+
+  private String getSchemeFromAuthHeader(final String authHeader) {
+    final int firstSpace = authHeader.indexOf(' ');
+    return (firstSpace != -1) ? authHeader.substring(0, firstSpace).toLowerCase(Locale.ROOT) : UNKNOWN_SCHEME;
+  }
+
+  @Override
+  public boolean doAuthenticate(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws Exception {
+    HttpServletRequest request = (HttpServletRequest) servletRequest;
+    HttpServletResponse response = (HttpServletResponse) servletResponse;
+
+    final String authHeader = request.getHeader(AUTHORIZATION_HEADER);
+
+    // if no Authorization header but is an AJAX request, forward to the default scheme so it can handle it
+    if (authHeader == null) {
+      if (BasicAuthPlugin.isAjaxRequest(request)) {
+        // use the first scheme listed as the default
+        return pluginMap.values().iterator().next().doAuthenticate(request, response, filterChain);
+      }
+
+      throw new SolrException(ErrorCode.UNAUTHORIZED, "No Authorization header");
+    }
+
+    final String scheme = getSchemeFromAuthHeader(authHeader);
+    final AuthenticationPlugin plugin = pluginMap.get(scheme);
+    if (plugin == null) {
+      throw new SolrException(ErrorCode.SERVER_ERROR, "Authorization scheme '" + scheme + "' not supported!");
+    }
+
+    pluginInRequest.set(plugin);
+    return plugin.doAuthenticate(request, response, filterChain);
+  }
+
+  @Override
+  public void close() throws IOException {
+    IOException exc = null;
+    for (AuthenticationPlugin plugin : pluginMap.values()) {
+      try {
+        plugin.close();
+      } catch (IOException ioExc) {
+        if (exc == null) {
+          exc = ioExc;
+        }
+      }
+    }
+
+    if (exc != null) {
+      throw exc;
+    }
+  }
+
+  @Override
+  public void closeRequest() {
+    AuthenticationPlugin plugin = pluginInRequest.get();
+    if (plugin != null) {
+      plugin.closeRequest();
+      pluginInRequest.remove();
+    }
+  }
+
+  @Override
+  protected boolean interceptInternodeRequest(HttpRequest httpRequest, HttpContext httpContext) {
+    for (AuthenticationPlugin plugin : pluginMap.values()) {
+      if (plugin.interceptInternodeRequest(httpRequest, httpContext)) {
+        return true; // first one to fire wins
+      }
+    }
+    return false;
+  }
+
+  @Override
+  protected boolean interceptInternodeRequest(Request request) {
+    for (AuthenticationPlugin plugin : pluginMap.values()) {
+      if (plugin.interceptInternodeRequest(request)) {
+        return true; // first one to fire wins
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public ValidatingJsonMap getSpec() {
+    return Utils.getSpec("cluster.security.MultiPluginAuth.Commands").getSpec();
+  }
+
+  @Override
+  public Map<String, Object> edit(Map<String, Object> latestConf, List<CommandOperation> commands) {
+    boolean madeChanges = false;
+
+    for (CommandOperation c : commands) {
+      Map<String, Object> dataMap = c.getDataMap();
+      // expect the "scheme" wrapper map around the actual command data
+      if (dataMap == null || dataMap.size() != 1) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "All edit commands must include a 'scheme' wrapper object!");
+      }
+
+      final String scheme = dataMap.keySet().iterator().next().toLowerCase(Locale.ROOT);
+      AuthenticationPlugin plugin = pluginMap.get(scheme);
+      if (plugin == null) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "No authentication plugin configured for the '" + scheme + "' scheme!");
+      }
+      if (!(plugin instanceof ConfigEditablePlugin)) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "Plugin for scheme '" + scheme + "' is not editable!");
+      }
+
+      CommandOperation cmdForPlugin = new CommandOperation(c.name, dataMap.get(scheme));
+      if (applyEditCommandToSchemePlugin(scheme, (ConfigEditablePlugin) plugin, cmdForPlugin, latestConf)) {
+        madeChanges = true;
+      }
+      // copy over any errors from the cloned command
+      for (String err : cmdForPlugin.getErrors()) {
+        c.addError(err);
+      }
+    }
+
+    return madeChanges ? latestConf : null;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/security/MultiAuthRuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/MultiAuthRuleBasedAuthorizationPlugin.java
new file mode 100644
index 0000000..7ff92de
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/security/MultiAuthRuleBasedAuthorizationPlugin.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.security;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.StringUtils;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrResourceLoader;
+
+import static org.apache.solr.security.MultiAuthPlugin.applyEditCommandToSchemePlugin;
+
+/**
+ * Authorization plugin designed to work with the MultiAuthPlugin to support different AuthorizationPlugin per scheme.
+ *
+ * @lucene.experimental
+ */
+public class MultiAuthRuleBasedAuthorizationPlugin extends RuleBasedAuthorizationPluginBase {
+  private final Map<String, RuleBasedAuthorizationPluginBase> pluginMap = new LinkedHashMap<>();
+  private final SolrResourceLoader loader;
+
+  // Need the CC to get the resource loader for loading the sub-plugins
+  public MultiAuthRuleBasedAuthorizationPlugin(CoreContainer cc) {
+    this.loader = cc.getResourceLoader();
+  }
+
+  @Override
+  @SuppressWarnings({"unchecked"})
+  public void init(Map<String, Object> initInfo) {
+    super.init(initInfo);
+
+    Object o = initInfo.get(MultiAuthPlugin.PROPERTY_SCHEMES);
+    if (!(o instanceof List)) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid config: " + getClass().getName() + " requires a list of schemes!");
+    }
+
+    List<Object> schemeList = (List<Object>) o;
+    if (schemeList.size() < 2) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid config: " + getClass().getName() + " requires at least two schemes!");
+    }
+
+    for (Object s : schemeList) {
+      if (!(s instanceof Map)) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid scheme config, expected JSON object but found: " + s);
+      }
+      initPluginForScheme((Map<String, Object>) s);
+    }
+  }
+
+  protected void initPluginForScheme(Map<String, Object> schemeMap) {
+    Map<String, Object> schemeConfig = new HashMap<>(schemeMap);
+
+    String scheme = (String) schemeConfig.remove(MultiAuthPlugin.PROPERTY_SCHEME);
+    if (StringUtils.isEmpty(scheme)) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "'scheme' is a required attribute: " + schemeMap);
+    }
+
+    String clazz = (String) schemeConfig.remove("class");
+    if (StringUtils.isEmpty(clazz)) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "'class' is a required attribute: " + schemeMap);
+    }
+
+    RuleBasedAuthorizationPluginBase pluginForScheme = loader.newInstance(clazz, RuleBasedAuthorizationPluginBase.class);
+    pluginForScheme.init(schemeConfig);
+    pluginMap.put(scheme.toLowerCase(Locale.ROOT), pluginForScheme);
+  }
+
+  /**
+   * Pulls roles from the Principal
+   *
+   * @param principal the user Principal which should contain roles
+   * @return set of roles as strings
+   */
+  @Override
+  public Set<String> getUserRoles(Principal principal) {
+    Set<String> mergedRoles = new HashSet<>();
+    for (RuleBasedAuthorizationPluginBase plugin : pluginMap.values()) {
+      final Set<String> userRoles = plugin.getUserRoles(principal);
+      if (userRoles != null) {
+        mergedRoles.addAll(userRoles);
+      }
+    }
+    return mergedRoles;
+  }
+
+  @Override
+  public Map<String, Object> edit(Map<String, Object> latestConf, List<CommandOperation> commands) {
+    boolean madeChanges = false;
+    for (CommandOperation c : commands) {
+
+      // just let the base class handle permission commands
+      if (c.name.endsWith("-permission")) {
+        Map<String, Object> updated = super.edit(latestConf, Collections.singletonList(c));
+        if (updated != null) {
+          madeChanges = true;
+          latestConf = updated;
+        }
+        continue;
+      }
+
+      Map<String, Object> dataMap = c.getDataMap();
+      // expect the "scheme" wrapper map around the actual command data
+      if (dataMap == null || dataMap.size() != 1) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "All edit commands must include a 'scheme' wrapper object!");
+      }
+
+      final String scheme = dataMap.keySet().iterator().next().toLowerCase(Locale.ROOT);
+      RuleBasedAuthorizationPluginBase plugin = pluginMap.get(scheme);
+      if (plugin == null) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No authorization plugin configured for the '" + scheme +
+            "' scheme! Did you forget to wrap the command with a scheme object?");
+      }
+
+      CommandOperation cmdForPlugin = new CommandOperation(c.name, dataMap.get(scheme));
+      if (applyEditCommandToSchemePlugin(scheme, plugin, cmdForPlugin, latestConf)) {
+        madeChanges = true;
+      }
+      // copy over any errors from the cloned command
+      for (String err : cmdForPlugin.getErrors()) {
+        c.addError(err);
+      }
+    }
+
+    return madeChanges ? latestConf : null;
+  }
+}
diff --git a/solr/core/src/test-files/solr/security/multi_auth_plugin_security.json b/solr/core/src/test-files/solr/security/multi_auth_plugin_security.json
new file mode 100644
index 0000000..0fd98fb
--- /dev/null
+++ b/solr/core/src/test-files/solr/security/multi_auth_plugin_security.json
@@ -0,0 +1,65 @@
+{
+  "authentication": {
+    "class": "solr.MultiAuthPlugin",
+    "schemes": [{
+      "scheme": "basic",
+      "blockUnknown": false,
+      "class": "solr.BasicAuthPlugin",
+      "credentials": {
+        "admin": "orwp2Ghgj39lmnrZOTm7Qtre1VqHFDfwAEzr0ApbN3Y= Ju5osoAqOX8iafhWpPP01E5P+sg8tK8tHON7rCYZRRw="
+      },
+      "forwardCredentials": false
+    },{
+      "scheme": "mock",
+      "class": "org.apache.solr.security.MultiAuthPluginTest$MockAuthPluginForTesting",
+      "blockUnknown": true
+    }]
+  },
+  "authorization": {
+    "class": "solr.MultiAuthRuleBasedAuthorizationPlugin",
+    "schemes": [
+      {
+        "scheme": "basic",
+        "class": "solr.RuleBasedAuthorizationPlugin",
+        "user-role": {
+          "admin": ["admin"],
+          "mock": ["admin"]
+        }
+      },
+      {
+        "scheme": "mock",
+        "class": "solr.ExternalRoleRuleBasedAuthorizationPlugin"
+      }
+    ],
+    "permissions": [
+      {
+        "name": "read",
+        "role": [
+          "admin",
+          "users"
+        ]
+      },
+      {
+        "name": "update",
+        "role": [
+          "admin"
+        ]
+      },
+      {
+        "name": "security-read",
+        "role": "admin"
+      },
+      {
+        "name": "security-edit",
+        "role": "admin"
+      },
+      {
+        "name": "all",
+        "role": [
+          "admin",
+          "users"
+        ]
+      }
+    ]
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/security/BasicAuthStandaloneTest.java b/solr/core/src/test/org/apache/solr/security/BasicAuthStandaloneTest.java
index 406ce58..58b245c 100644
--- a/solr/core/src/test/org/apache/solr/security/BasicAuthStandaloneTest.java
+++ b/solr/core/src/test/org/apache/solr/security/BasicAuthStandaloneTest.java
@@ -25,6 +25,7 @@ import java.nio.file.Paths;
 import java.util.Collections;
 import java.util.Properties;
 
+import org.apache.http.Header;
 import org.apache.http.HttpResponse;
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.methods.HttpPost;
@@ -54,8 +55,8 @@ import static org.apache.solr.security.BasicAuthIntegrationTest.verifySecuritySt
 public class BasicAuthStandaloneTest extends SolrTestCaseJ4 {
 
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-  private Path ROOT_DIR = Paths.get(TEST_HOME());
-  private Path CONF_DIR = ROOT_DIR.resolve("configsets").resolve("configset-2").resolve("conf");
+  private static final Path ROOT_DIR = Paths.get(TEST_HOME());
+  private static final Path CONF_DIR = ROOT_DIR.resolve("configsets").resolve("configset-2").resolve("conf");
 
   SecurityConfHandlerLocalForTesting securityConfHandler;
   SolrInstance instance = null;
@@ -145,13 +146,17 @@ public class BasicAuthStandaloneTest extends SolrTestCaseJ4 {
     }
   }
 
-  private void doHttpPost(HttpClient cl, String url, String jsonCommand, String basicUser, String basicPass) throws IOException {
+  static void doHttpPost(HttpClient cl, String url, String jsonCommand, String basicUser, String basicPass) throws IOException {
     doHttpPost(cl, url, jsonCommand, basicUser, basicPass, 200);
   }
 
-  private void doHttpPost(HttpClient cl, String url, String jsonCommand, String basicUser, String basicPass, int expectStatusCode) throws IOException {
+  static void doHttpPost(HttpClient cl, String url, String jsonCommand, String basicUser, String basicPass, int expectStatusCode) throws IOException {
+    doHttpPostWithHeader(cl, url, jsonCommand, getBasicAuthHeader(basicUser, basicPass), expectStatusCode);
+  }
+
+  static void doHttpPostWithHeader(HttpClient cl, String url, String jsonCommand, Header header, int expectStatusCode) throws IOException {
     HttpPost httpPost = new HttpPost(url);
-    setBasicAuthHeader(httpPost, basicUser, basicPass);
+    httpPost.setHeader(header);
     httpPost.setEntity(new ByteArrayEntity(jsonCommand.replaceAll("'", "\"").getBytes(UTF_8)));
     httpPost.addHeader("Content-Type", "application/json; charset=UTF-8");
     HttpResponse r = cl.execute(httpPost);
@@ -160,14 +165,21 @@ public class BasicAuthStandaloneTest extends SolrTestCaseJ4 {
     assertEquals("proper_cred sent, but access denied", expectStatusCode, statusCode);
   }
 
-  public static void setBasicAuthHeader(AbstractHttpMessage httpMsg, String user, String pwd) {
+  private static Header getBasicAuthHeader(String user, String pwd) {
     String userPass = user + ":" + pwd;
     String encoded = Base64.byteArrayToBase64(userPass.getBytes(UTF_8));
-    httpMsg.setHeader(new BasicHeader("Authorization", "Basic " + encoded));
-    log.info("Added Basic Auth security Header {}",encoded );
+    return new BasicHeader("Authorization", "Basic " + encoded);
+  }
+
+  public static void setBasicAuthHeader(AbstractHttpMessage httpMsg, String user, String pwd) {
+    final Header basicAuthHeader = getBasicAuthHeader(user, pwd);
+    httpMsg.setHeader(basicAuthHeader);
+    if (log.isInfoEnabled()) {
+      log.info("Added Basic Auth security Header {}", basicAuthHeader.getValue());
+    }
   }
 
-  private JettySolrRunner createAndStartJetty(SolrInstance instance) throws Exception {
+  static JettySolrRunner createAndStartJetty(SolrInstance instance) throws Exception {
     Properties nodeProperties = new Properties();
     nodeProperties.setProperty("solr.data.dir", instance.getDataDir().toString());
     JettySolrRunner jetty = new JettySolrRunner(instance.getHomeDir().toString(), nodeProperties, buildJettyConfig("/solr"));
@@ -176,7 +188,7 @@ public class BasicAuthStandaloneTest extends SolrTestCaseJ4 {
   }
   
   
-  private class SolrInstance {
+  static class SolrInstance {
     String name;
     Integer port;
     Path homeDir;
diff --git a/solr/core/src/test/org/apache/solr/security/MultiAuthPluginTest.java b/solr/core/src/test/org/apache/solr/security/MultiAuthPluginTest.java
new file mode 100644
index 0000000..4566185
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/security/MultiAuthPluginTest.java
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.security;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.http.client.HttpClient;
+import org.apache.http.message.BasicHeader;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.embedded.JettySolrRunner;
+import org.apache.solr.client.solrj.impl.HttpClientUtil;
+import org.apache.solr.client.solrj.impl.HttpSolrClient;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.handler.admin.SecurityConfHandler;
+import org.apache.solr.handler.admin.SecurityConfHandlerLocalForTesting;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.apache.solr.cloud.SolrCloudAuthTestCase.NOT_NULL_PREDICATE;
+import static org.apache.solr.security.BasicAuthIntegrationTest.verifySecurityStatus;
+import static org.apache.solr.security.BasicAuthStandaloneTest.SolrInstance;
+import static org.apache.solr.security.BasicAuthStandaloneTest.createAndStartJetty;
+import static org.apache.solr.security.BasicAuthStandaloneTest.doHttpPost;
+import static org.apache.solr.security.BasicAuthStandaloneTest.doHttpPostWithHeader;
+
+public class MultiAuthPluginTest extends SolrTestCaseJ4 {
+
+  private static final String authcPrefix = "/admin/authentication";
+  private static final String authzPrefix = "/admin/authorization";
+  
+  final Predicate<Object> NULL_PREDICATE = Objects::isNull;
+  SecurityConfHandlerLocalForTesting securityConfHandler;
+  JettySolrRunner jetty;
+
+  @Before
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    SolrInstance instance = new SolrInstance("inst", null);
+    instance.setUp();
+    jetty = createAndStartJetty(instance);
+    securityConfHandler = new SecurityConfHandlerLocalForTesting(jetty.getCoreContainer());
+    HttpClientUtil.clearRequestInterceptors(); // Clear out any old Authorization headers
+  }
+
+  @Override
+  @After
+  public void tearDown() throws Exception {
+    if (jetty != null) {
+      jetty.stop();
+      jetty = null;
+    }
+    super.tearDown();
+  }
+
+  @Test
+  public void testMultiAuthEditAPI() throws Exception {
+    final String user = "admin";
+    final String pass = "SolrRocks";
+
+    HttpClient cl = null;
+    HttpSolrClient httpSolrClient = null;
+    try {
+      cl = HttpClientUtil.createClient(null);
+      String baseUrl = buildUrl(jetty.getLocalPort(), "/solr");
+      httpSolrClient = getHttpSolrClient(baseUrl);
+
+      verifySecurityStatus(cl, baseUrl + authcPrefix, "/errorMessages", null, 5);
+
+      // Initialize security.json with multiple auth plugins configured
+      String multiAuthPluginSecurityJson =
+          FileUtils.readFileToString(TEST_PATH().resolve("security").resolve("multi_auth_plugin_security.json").toFile(), StandardCharsets.UTF_8);
+      securityConfHandler.persistConf(new SecurityConfHandler.SecurityConfig().setData(Utils.fromJSONString(multiAuthPluginSecurityJson)));
+      securityConfHandler.securityConfEdited();
+      verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/class", "solr.MultiAuthPlugin", 5, user, pass);
+      verifySecurityStatus(cl, baseUrl + authzPrefix, "authorization/class", "solr.MultiAuthRuleBasedAuthorizationPlugin", 5, user, pass);
+
+      // For the multi-auth plugin, every command is wrapped with an object that identifies the "scheme"
+      String command = "{\n" +
+          "'set-user': {'harry':'HarryIsCool'}\n" +
+          "}";
+      // no scheme identified!
+      doHttpPost(cl, baseUrl + authcPrefix, command, user, pass, 400);
+
+      command = "{\n" +
+          "'set-user': { 'foo': {'harry':'HarryIsCool'} }\n" +
+          "}";
+      // no "foo" scheme configured
+      doHttpPost(cl, baseUrl + authcPrefix, command, user, pass, 400);
+
+      command = "{\n" +
+          "'set-user': { 'basic': {'harry':'HarryIsCool'} }\n" +
+          "}";
+
+      // no creds, should fail ...
+      doHttpPost(cl, baseUrl + authcPrefix, command, null, null, 401);
+      // with basic creds, should pass ...
+      doHttpPost(cl, baseUrl + authcPrefix, command, user, pass, 200);
+      verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/schemes[0]/credentials/harry", NOT_NULL_PREDICATE, 5, user, pass);
+
+      // authz command but missing the "scheme" wrapper
+      command = "{\n" +
+          "'set-user-role': {'harry':['users']}\n" +
+          "}";
+      doHttpPost(cl, baseUrl + authzPrefix, command, user, pass, 400);
+
+      // add "harry" to the "users" role ...
+      command = "{\n" +
+          "'set-user-role': { 'basic': {'harry':['users']} }\n" +
+          "}";
+      doHttpPost(cl, baseUrl + authzPrefix, command, user, pass, 200);
+      verifySecurityStatus(cl, baseUrl + authzPrefix, "authorization/schemes[0]/user-role/harry", NOT_NULL_PREDICATE, 5, user, pass);
+
+      // give the users role a custom permission
+      verifySecurityStatus(cl, baseUrl + authzPrefix, "authorization/permissions[5]", NULL_PREDICATE, 5, user, pass);
+      command = "{\n" +
+          "'set-permission': { 'name':'k8s-zk', 'role':'users', 'collection':null, 'path':'/admin/zookeeper/status' }\n" +
+          "}";
+      doHttpPost(cl, baseUrl + authzPrefix, command, user, pass, 200);
+      verifySecurityStatus(cl, baseUrl + authzPrefix, "authorization/permissions[5]/path", new ExpectedValuePredicate("/admin/zookeeper/status"), 5, user, pass);
+
+      command = "{\n" +
+          "'update-permission': { 'index':'6', 'name':'k8s-zk', 'role':'users', 'collection':null, 'path':'/admin/zookeeper/status2' }\n" +
+          "}";
+      doHttpPost(cl, baseUrl + authzPrefix, command, user, pass, 200);
+      verifySecurityStatus(cl, baseUrl + authzPrefix, "authorization/permissions[5]/path", new ExpectedValuePredicate("/admin/zookeeper/status2"), 5, user, pass);
+
+      // delete the permission
+      command = "{\n" +
+          "'delete-permission': 6\n" +
+          "}";
+      doHttpPost(cl, baseUrl + authzPrefix, command, user, pass, 200);
+      verifySecurityStatus(cl, baseUrl + authzPrefix, "authorization/permissions[5]", NULL_PREDICATE, 5, user, pass);
+
+      // delete the user
+      command = "{\n" +
+          "'delete-user': { 'basic': 'harry' }\n" +
+          "}";
+
+      doHttpPost(cl, baseUrl + authcPrefix, command, user, pass, 200);
+      verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/schemes[0]/credentials/harry", NULL_PREDICATE, 5, user, pass);
+
+      // update the property on the mock (just to test routing to the mock plugin)
+      command = "{\n" +
+          "'set-property': { 'mock': { 'blockUnknown':false } }\n" +
+          "}";
+
+      doHttpPostWithHeader(cl, baseUrl + authcPrefix, command, new BasicHeader("Authorization", "mock foo"), 200);
+      verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/schemes[1]/blockUnknown", new ExpectedValuePredicate(Boolean.FALSE), 5, user, pass);
+    } finally {
+      if (cl != null) {
+        HttpClientUtil.close(cl);
+      }
+      if (httpSolrClient != null) {
+        httpSolrClient.close();
+      }
+    }
+  }
+
+  private static final class MockPrincipal implements Principal, Serializable {
+    @Override
+    public String getName() {
+      return "mock";
+    }
+  }
+
+  public static final class MockAuthPluginForTesting extends AuthenticationPlugin implements ConfigEditablePlugin {
+
+    @Override
+    public void init(Map<String, Object> pluginConfig) {
+
+    }
+
+    @Override
+    public boolean doAuthenticate(ServletRequest request, ServletResponse response, FilterChain filterChain) throws Exception {
+      Principal principal = new MockPrincipal();
+      request = wrapWithPrincipal((HttpServletRequest)request, principal, "mock");
+      filterChain.doFilter(request, response);
+      return true;
+    }
+
+    HttpServletRequest wrapWithPrincipal(HttpServletRequest request, Principal principal, String username) {
+      return new HttpServletRequestWrapper(request) {
+        @Override
+        public Principal getUserPrincipal() {
+          return principal;
+        }
+
+        @Override
+        public String getRemoteUser() {
+          return username;
+        }
+      };
+    }
+
+    @Override
+    public Map<String, Object> edit(Map<String, Object> latestConf, List<CommandOperation> commands) {
+      for (CommandOperation op : commands) {
+        if ("set-property".equals(op.name)) {
+          for (Map.Entry<String, Object> e : op.getDataMap().entrySet()) {
+            if ("blockUnknown".equals(e.getKey())) {
+              latestConf.put(e.getKey(), e.getValue());
+              return latestConf;
+            } else {
+              op.addError("Unknown property " + e.getKey());
+            }
+          }
+        } else {
+          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unsupported command: " + op.name);
+        }
+      }
+      return null;
+    }
+  }
+
+  private static final class ExpectedValuePredicate implements Predicate<Object> {
+    final Object expected;
+
+    ExpectedValuePredicate(Object exp) {
+      this.expected = exp;
+    }
+
+    @Override
+    public boolean test(Object s) {
+      return expected.equals(s);
+    }
+  }
+}
diff --git a/solr/solr-ref-guide/src/basic-authentication-plugin.adoc b/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
index 05f77ea..47d6f94 100644
--- a/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
@@ -85,6 +85,46 @@ There are a few things to keep in mind when using the Basic authentication plugi
 * A user who has access to write permissions to `security.json` will be able to modify all the permissions and how users have been assigned permissions. Special care should be taken to only grant access to editing security to appropriate users.
 * Your network should, of course, be secure. Even with Basic authentication enabled, you should not unnecessarily expose Solr to the outside world.
 
+== Combining Basic Authentication with Other Schemes
+:experimental:
+
+When using other authentication schemes, such as the <<jwt-authentication-plugin.adoc#,JWT Authentication Plugin>>, you may still want to use Basic authentication for a small set of "service account" oriented client applications.
+Solr provides the `MultiAuthPlugin` to support multiple authentication schemes. For example, you may want to integrate Solr with an OIDC provider for user accounts,
+but also use Basic for authenticating requests coming from the Prometheus metrics exporter. The `MultiAuthPlugin` uses the scheme of the `Authorization` header to determine which
+plugin should handle each request. The `MultiAuthPlugin` is useful when running Solr on Kubernetes as you can delegate user management and authentication to an OIDC provider for end-users,
+but also secure the liveness and readiness endpoints using `Basic` authentication, as you would not want Kubernetes to use OIDC when testing the probe endpoints.
+
+The following example illustrates how to configure the `MultiAuthPlugin` to  support the `Basic` and `Bearer` schemes.
+
+[source,json]
+----
+{
+  "authentication": {
+    "class": "solr.MultiAuthPlugin",
+    "schemes": [{
+      "scheme": "bearer",
+      "blockUnknown": true,
+      "class": "solr.JWTAuthPlugin",
+      "wellKnownUrl": "https://OIDC_PROVIDER_URL/.well-known/openid-configuration",
+      "clientId": "solr",
+      "redirectUris": "http://localhost:8983/solr/",
+      "rolesClaim": "groups"
+    },{
+      "scheme": "basic",
+      "blockUnknown": true,
+      "class": "solr.BasicAuthPlugin",
+      "credentials": {
+        "k8s-oper": "PASSWORD SALT & HASH"
+      },
+      "forwardCredentials": false
+    }]
+  }
+}
+----
+For un-authenticated AJAX requests from the Solr Admin UI (i.e. requests without an `Authorization` header),
+the `MultiAuthPlugin` forwards the request to the first plugin listed in the `schemes` list. In the example above,
+users will need to authenticate to the OIDC provider to login to the Admin UI.
+
 == Editing Basic Authentication Plugin Configuration
 
 An Authentication API allows modifying user IDs and passwords. The API provides an endpoint with specific commands to set user details or delete a user.
@@ -199,6 +239,30 @@ curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentica
 ====
 --
 
+=== Edit Plugin Configuration Using the MultiAuthPlugin
+
+When using the `MultiAuthPlugin`, you need to wrap the command data with a single-keyed object that identifies the `scheme`.
+For instance, the `set-user` command for the `Basic` plugin would be:
+
+[source,json]
+----
+{
+  "set-user": {
+    "basic": {"tom":"TomIsCool", "harry":"HarrysSecret"}
+  }
+}
+----
+
+Set a property on the `Basic` plugin when using the `MultiAuthPlugin`:
+[source,json]
+----
+{
+  "set-property": {
+    "basic": {"realm":"My Solr users"}
+  }
+}
+----
+
 == Using Basic Auth with SolrJ
 
 There are two main ways to use SolrJ with Solr servers protected by basic authentication: either the permissions can be set on each individual request, or the underlying http client can be configured to add credentials to all requests that it sends.
diff --git a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
index 4ae7c3f..e2797eb 100644
--- a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
@@ -167,6 +167,12 @@ In production environments you should always use SSL protected HTTPS connections
 However, in development, it may be useful to use regular HTTP URLs, and bypass the security check that Solr performs.
 To support this you can set the environment variable `-Dsolr.auth.jwt.allowOutboundHttp=true` at startup.
 
+=== Multiple Authentication Schemes
+
+Solr provides the <<basic-authentication-plugin.adoc#combining-basic-authentication-with-other-schemes,MultiAuthPlugin>> to support multiple authentication schemes based on the `Authorization` header.
+This allows you to configure Solr to delegate user management and authentication to an OIDC provider using the `JWTAuthPlugin`,
+but also allow a small set of service accounts to use `Basic` authentication when using OIDC is not supported or practical.
+
 == Editing JWT Authentication Plugin Configuration
 
 All properties mentioned above, except the 'issuers' array, can be set or changed using the <<basic-authentication-plugin.adoc#editing-basic-authentication-plugin-configuration,Authentication API>>. You can thus start with a simple configuration with only `class` configured and then configure the rest using the API.
diff --git a/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc b/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc
index fcb108d..6546e1c 100644
--- a/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc
+++ b/solr/solr-ref-guide/src/rule-based-authorization-plugin.adoc
@@ -37,10 +37,11 @@ The users that RBAP sees come from whatever authentication plugin has been confi
 
 === Roles
 
-Roles help bridge the gap between users and permissions. The roles can be used with any of the authentication plugins or with a custom authentication plugin if you have created one. You will only need to ensure that logged-in users are mapped to the roles defined by the plugin. There are two implementations of the plugin, which only differs in how the user's roles are obtained:
+Roles help bridge the gap between users and permissions. The roles can be used with any of the authentication plugins or with a custom authentication plugin if you have created one. You will only need to ensure that logged-in users are mapped to the roles defined by the plugin. There are three implementations of the plugin, which only differ in how the user's roles are obtained:
 
 * `RuleBasedAuthorizationPlugin`: The role-to-user mappings must be defined explicitly in `security.json` for every possible authenticated user.
 * `ExternalRoleRuleBasedAuthorizationPlugin`: The role-to-user mappings are managed externally. This plugin expects the AuthenticationPlugin to provide a Principal that has the roles information as well, implementing the `VerifiedUserRoles` interface.
+* `MultiAuthRuleBasedAuthorizationPlugin`: For use with the `MultiAuthPlugin` to source user roles from multiple plugins.
 
 === Permissions
 
@@ -149,6 +150,41 @@ Let's walk through this example:
 
 Only requests from users having a JWT token with role "admin" will be granted the `security-edit` permission.
 
+=== Multiple Authorization Plugins
+
+If your `security.json` config uses the `MultiAuthPlugin`, you want to use the `MultiAuthRuleBasedAuthorizationPlugin` to use a different authorization plugin for each authentication plugin.
+
+The following example illustrates using the `MultiAuthRuleBasedAuthorizationPlugin` to configure an authorization plugin for the `Basic` and `Bearer` schemes:
+[source,json]
+----
+{
+  "authorization": {
+    "class": "solr.MultiAuthRuleBasedAuthorizationPlugin",
+    "schemes": [
+      {
+        "scheme": "basic",
+        "class": "solr.RuleBasedAuthorizationPlugin",
+        "user-role": {
+          "k8s-oper": ["k8s"]
+        }
+      },
+      {
+        "scheme": "bearer",
+        "class": "solr.ExternalRoleRuleBasedAuthorizationPlugin"
+      }
+    ],
+    "permissions": []
+  }
+}
+----
+
+It would be uncommon for the same user account to exist in both plugins.
+However, the `MultiAuthRuleBasedAuthorizationPlugin` combines the roles from all plugins together when determining the roles for a user.
+
+Users should take special care to lock down the exact set of endpoints that service accounts need access to when using Basic authentication.
+For example, if the `MultiAuthPlugin` allows a `k8s-oper` user to use Basic authentication (while all other users go through OIDC), then
+the permissions configured for the `k8s-oper` user should only allow access to specific endpoints, such as `/admin/info/system`.
+
 == Permissions
 
 Solr's Rule-Based Authorization plugin supports a flexible and powerful permission syntax.  RBAP supports two types of permissions, each with a slightly different syntax.
diff --git a/solr/solrj/src/resources/apispec/cluster.security.MultiPluginAuth.Commands.json b/solr/solrj/src/resources/apispec/cluster.security.MultiPluginAuth.Commands.json
new file mode 100644
index 0000000..c656f49
--- /dev/null
+++ b/solr/solrj/src/resources/apispec/cluster.security.MultiPluginAuth.Commands.json
@@ -0,0 +1,27 @@
+{
+  "documentation": "https://lucene.apache.org/solr/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"
+    }
+  }
+}