You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@solr.apache.org by GitBox <gi...@apache.org> on 2021/10/19 19:19:15 UTC

[GitHub] [solr] thelabdude opened a new pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

thelabdude opened a new pull request #355:
URL: https://github.com/apache/solr/pull/355


   see: https://issues.apache.org/jira/browse/SOLR-12666
   
   unit tests still in progress but tested locally with Basic and JWT (Keycloak's OIDC)
   


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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] janhoy commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
janhoy commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r737604315



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.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.lucene.util.ResourceLoader;
+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.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.
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_DEFAULT_SCHEME = "default-scheme";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader loader;
+
+  // We forward to this scheme where we're asked to auth w/o an Authorization header
+  private String defaultScheme = null;
+
+  // 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();
+  }
+
+  @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);
+    }
+
+    defaultScheme = (String) pluginConfig.get(PROPERTY_DEFAULT_SCHEME);
+    if (defaultScheme != null) {
+      if (!pluginMap.containsKey(defaultScheme)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Default scheme '" + defaultScheme + "' not configured!");
+      }
+    } else {
+      // first scheme listed in the config is the default
+      defaultScheme = pluginMap.keySet().iterator().next();
+    }
+  }
+
+  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(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
+    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)) {
+        return pluginMap.get(defaultScheme).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) {
+      String scheme = (String) c.getDataMap().get(PROPERTY_SCHEME);
+      if (scheme == null) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "All edit commands must include a 'scheme' attribute!");

Review comment:
       Sure, we can even mark the plugin as experimental in the first few releases until more integrations validate it.




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] thelabdude merged pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
thelabdude merged pull request #355:
URL: https://github.com/apache/solr/pull/355


   


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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] thelabdude commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
thelabdude commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r732902326



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthRuleBasedAuthorizationPlugin.java
##########
@@ -0,0 +1,102 @@
+/*
+ * 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.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.lucene.util.ResourceLoader;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.StringUtils;
+import org.apache.solr.core.CoreContainer;
+
+/**
+ * Authorization plugin designed to work with the MultiAuthPlugin to support different AuthorizationPlugin per scheme.
+ */
+public class MultiAuthRuleBasedAuthorizationPlugin extends RuleBasedAuthorizationPluginBase {
+  private final Map<String, RuleBasedAuthorizationPluginBase> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader 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) {
+    for (RuleBasedAuthorizationPluginBase plugin : pluginMap.values()) {
+      final Set<String> userRoles = plugin.getUserRoles(principal);
+      if (userRoles != null && !userRoles.isEmpty()) {
+        return userRoles; // first non-empty match wins

Review comment:
       Maybe I get rid of the `default-scheme` and the first in the list is considered the "default" ... that way, whenever we loop over the schemes, the default would apply first. Alternatively, we could merge the role sets into one? Either seems reasonable to me ... My guess is that 99% of the cases, they won't have overlapping users between the schemes




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] janhoy commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
janhoy commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r736677320



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.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.lucene.util.ResourceLoader;
+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.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.
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_DEFAULT_SCHEME = "default-scheme";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader loader;
+
+  // We forward to this scheme where we're asked to auth w/o an Authorization header
+  private String defaultScheme = null;
+
+  // 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();
+  }
+
+  @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);
+    }
+
+    defaultScheme = (String) pluginConfig.get(PROPERTY_DEFAULT_SCHEME);
+    if (defaultScheme != null) {
+      if (!pluginMap.containsKey(defaultScheme)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Default scheme '" + defaultScheme + "' not configured!");
+      }
+    } else {
+      // first scheme listed in the config is the default
+      defaultScheme = pluginMap.keySet().iterator().next();
+    }
+  }
+
+  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(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
+    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)) {
+        return pluginMap.get(defaultScheme).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) {
+      String scheme = (String) c.getDataMap().get(PROPERTY_SCHEME);
+      if (scheme == null) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "All edit commands must include a 'scheme' attribute!");

Review comment:
       This will probably be easier to judge once someone tries to integrate this into some script or tool that needs to configure multiple auth programatically.




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] thelabdude commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
thelabdude commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r732899780



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.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.lucene.util.ResourceLoader;
+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.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.
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_DEFAULT_SCHEME = "default-scheme";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader loader;
+
+  // We forward to this scheme where we're asked to auth w/o an Authorization header
+  private String defaultScheme = null;
+
+  // 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();
+  }
+
+  @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);
+    }
+
+    defaultScheme = (String) pluginConfig.get(PROPERTY_DEFAULT_SCHEME);
+    if (defaultScheme != null) {
+      if (!pluginMap.containsKey(defaultScheme)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Default scheme '" + defaultScheme + "' not configured!");
+      }
+    } else {
+      // first scheme listed in the config is the default

Review comment:
       It's a `LinkedHashMap`, so the keySet is a `LinkedKeySet` which maintains order




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] janhoy commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
janhoy commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r733187520



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.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.lucene.util.ResourceLoader;
+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.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.
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_DEFAULT_SCHEME = "default-scheme";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader loader;
+
+  // We forward to this scheme where we're asked to auth w/o an Authorization header
+  private String defaultScheme = null;
+
+  // 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();
+  }
+
+  @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);
+    }
+
+    defaultScheme = (String) pluginConfig.get(PROPERTY_DEFAULT_SCHEME);
+    if (defaultScheme != null) {
+      if (!pluginMap.containsKey(defaultScheme)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Default scheme '" + defaultScheme + "' not configured!");
+      }
+    } else {
+      // first scheme listed in the config is the default
+      defaultScheme = pluginMap.keySet().iterator().next();
+    }
+  }
+
+  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(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
+    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)) {
+        return pluginMap.get(defaultScheme).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) {
+      String scheme = (String) c.getDataMap().get(PROPERTY_SCHEME);
+      if (scheme == null) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "All edit commands must include a 'scheme' attribute!");

Review comment:
       I think scheme meeds to be indicated on a "higher" level. Take the `{"set-user": {"tom":"TomIsCool", "harry":"HarrysSecret"}}` command for instance - it would create a user "scheme" with password "bearer" :-) 
   
   If CoreContainer was aware of the multi-plugin situation we could indicate scheme in the url path, i.e.
   ```
   curl http://localhost:8983/solr/admin/authentication/basic -d  '{"set-property": {"blockUnknown":false}}'
   ```
   If our plugin had the power to add URL sub-paths below itself, then this could be doable...
   
   Or we could add it as a top-level k/v, and require that you cannot mix commands to multiple plugins in the same call:
   ```
   {"scheme":"basic", "set-property":{...}}
   ```
   




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] thelabdude commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
thelabdude commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r732933292



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.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.lucene.util.ResourceLoader;
+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.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.
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_DEFAULT_SCHEME = "default-scheme";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader loader;
+
+  // We forward to this scheme where we're asked to auth w/o an Authorization header
+  private String defaultScheme = null;
+
+  // 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();
+  }
+
+  @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);
+    }
+
+    defaultScheme = (String) pluginConfig.get(PROPERTY_DEFAULT_SCHEME);
+    if (defaultScheme != null) {
+      if (!pluginMap.containsKey(defaultScheme)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Default scheme '" + defaultScheme + "' not configured!");
+      }
+    } else {
+      // first scheme listed in the config is the default
+      defaultScheme = pluginMap.keySet().iterator().next();
+    }
+  }
+
+  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(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
+    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)) {
+        return pluginMap.get(defaultScheme).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) {
+      String scheme = (String) c.getDataMap().get(PROPERTY_SCHEME);
+      if (scheme == null) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "All edit commands must include a 'scheme' attribute!");

Review comment:
       The edit impl there was a quick hack to see what was possible. I don't love all the adding / removing of the scheme from the maps being manipulated, so maybe I just add an extra layer of wrapping, e.g.
   ```
   {
     "set-property": {
       "bearer": {
         "blockUnknown":true
       }
     }
   }
   ```
   Although not sure that ^^ is any better than:
   ```
   {"set-property": {"blockUnknown":true, "scheme":"bearer"}}
   ```
   Ultimately I think it comes down to us just documenting what this plugin expects when editing.
   
   And yes, definitely want to incorporate support for this plugin into the Admin UI screen.




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] janhoy commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
janhoy commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r732636861



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.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.lucene.util.ResourceLoader;
+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.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.
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_DEFAULT_SCHEME = "default-scheme";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader loader;
+
+  // We forward to this scheme where we're asked to auth w/o an Authorization header
+  private String defaultScheme = null;
+
+  // 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();
+  }
+
+  @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);
+    }
+
+    defaultScheme = (String) pluginConfig.get(PROPERTY_DEFAULT_SCHEME);
+    if (defaultScheme != null) {
+      if (!pluginMap.containsKey(defaultScheme)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Default scheme '" + defaultScheme + "' not configured!");
+      }
+    } else {
+      // first scheme listed in the config is the default

Review comment:
       I think for `Set`, order is not guaranteed, so this is not deterministic? Perhaps use `schemeList.get(0).get("scheme")` instead?

##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthRuleBasedAuthorizationPlugin.java
##########
@@ -0,0 +1,102 @@
+/*
+ * 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.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.lucene.util.ResourceLoader;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.StringUtils;
+import org.apache.solr.core.CoreContainer;
+
+/**
+ * Authorization plugin designed to work with the MultiAuthPlugin to support different AuthorizationPlugin per scheme.
+ */
+public class MultiAuthRuleBasedAuthorizationPlugin extends RuleBasedAuthorizationPluginBase {
+  private final Map<String, RuleBasedAuthorizationPluginBase> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader 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) {
+    for (RuleBasedAuthorizationPluginBase plugin : pluginMap.values()) {
+      final Set<String> userRoles = plugin.getUserRoles(principal);
+      if (userRoles != null && !userRoles.isEmpty()) {
+        return userRoles; // first non-empty match wins

Review comment:
       I guess this works if a certain user is registered only in one scheme, i.e. either BasicAuth or JWT. Think it's safe..




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] thelabdude commented on pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
thelabdude commented on pull request #355:
URL: https://github.com/apache/solr/pull/355#issuecomment-951348468


   This is mostly ready to go, minus the ref guide updates, will get those done soon.


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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] thelabdude commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
thelabdude commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r736649203



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.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.lucene.util.ResourceLoader;
+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.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.
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_DEFAULT_SCHEME = "default-scheme";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader loader;
+
+  // We forward to this scheme where we're asked to auth w/o an Authorization header
+  private String defaultScheme = null;
+
+  // 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();
+  }
+
+  @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);
+    }
+
+    defaultScheme = (String) pluginConfig.get(PROPERTY_DEFAULT_SCHEME);
+    if (defaultScheme != null) {
+      if (!pluginMap.containsKey(defaultScheme)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Default scheme '" + defaultScheme + "' not configured!");
+      }
+    } else {
+      // first scheme listed in the config is the default
+      defaultScheme = pluginMap.keySet().iterator().next();
+    }
+  }
+
+  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(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
+    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)) {
+        return pluginMap.get(defaultScheme).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) {
+      String scheme = (String) c.getDataMap().get(PROPERTY_SCHEME);
+      if (scheme == null) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "All edit commands must include a 'scheme' attribute!");

Review comment:
       I went with a wrapping single key object to identify the scheme, e.g.
   ```
   {
     "set-user": {
       "basic": {"tom":"TomIsCool", "harry":"HarrysSecret"}
     }
   }
   ```




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] janhoy commented on pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
janhoy commented on pull request #355:
URL: https://github.com/apache/solr/pull/355#issuecomment-948521927


   A concern with enabling BasicAuth is that it is less secure than OIDC which has a expiry of tokens, while a password is long-lived. To mitigate this added surface area, I wonder how easy it would be to "lock down" what a BasicAuth user can do in the system, such as limiting the role's permissions to block any request except /admin/system/info and /admin/collection?cmd=CLUSTERSTATUS. I think this is doable in authorization..


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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] janhoy commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
janhoy commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r736671982



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.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.lucene.util.ResourceLoader;
+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.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.
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_DEFAULT_SCHEME = "default-scheme";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader loader;
+
+  // We forward to this scheme where we're asked to auth w/o an Authorization header
+  private String defaultScheme = null;
+
+  // 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();
+  }
+
+  @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);
+    }
+
+    defaultScheme = (String) pluginConfig.get(PROPERTY_DEFAULT_SCHEME);
+    if (defaultScheme != null) {
+      if (!pluginMap.containsKey(defaultScheme)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Default scheme '" + defaultScheme + "' not configured!");
+      }
+    } else {
+      // first scheme listed in the config is the default
+      defaultScheme = pluginMap.keySet().iterator().next();
+    }
+  }
+
+  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(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
+    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)) {
+        return pluginMap.get(defaultScheme).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) {
+      String scheme = (String) c.getDataMap().get(PROPERTY_SCHEME);
+      if (scheme == null) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "All edit commands must include a 'scheme' attribute!");

Review comment:
       I think 'set-user' is a BasicAuth only command, so feels a bit strange this. Woudl probably be easier to write generic command-wrapping logic if scheme was upper level
   ```
   {
     "schemes": {
       "basic": {
         "set-user": {"tom":"TomIsCool", "harry":"HarrysSecret"},
         "set-property": {"foo": "bar"}
       },
       "bearer": {
         "set-property": {"blockUnknown": "false"}
       }
     }
   }
   ```




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] janhoy commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
janhoy commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r732643771



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.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.lucene.util.ResourceLoader;
+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.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.
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_DEFAULT_SCHEME = "default-scheme";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader loader;
+
+  // We forward to this scheme where we're asked to auth w/o an Authorization header
+  private String defaultScheme = null;
+
+  // 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();
+  }
+
+  @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);
+    }
+
+    defaultScheme = (String) pluginConfig.get(PROPERTY_DEFAULT_SCHEME);
+    if (defaultScheme != null) {
+      if (!pluginMap.containsKey(defaultScheme)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Default scheme '" + defaultScheme + "' not configured!");
+      }
+    } else {
+      // first scheme listed in the config is the default
+      defaultScheme = pluginMap.keySet().iterator().next();
+    }
+  }
+
+  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(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
+    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)) {
+        return pluginMap.get(defaultScheme).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) {
+      String scheme = (String) c.getDataMap().get(PROPERTY_SCHEME);
+      if (scheme == null) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "All edit commands must include a 'scheme' attribute!");

Review comment:
       So the `edit()` command can now take a `scheme` property to say which sub plugin to edit? Do you plan to implement this in AdminUI, so we can still edit BasicAuth in UI? Will it have other implications elsewhere? I guess users just need to stik a `scheme: foo` property in their existing edit commands to target the correct plugin?




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] janhoy commented on pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
janhoy commented on pull request #355:
URL: https://github.com/apache/solr/pull/355#issuecomment-953053969


   > Not sure why this is a concern? Locking down endpoints to a specific role (or roles) is why we have the authorization framework. The introduction of the `MultiAuthPlugin` really has no bearing on how people choose to configure their permissions.
   
   Yes, probably you have a small set of BasicAuth users for tools, and if those tools only need R/O access to some endpoints then the security is good. But if you want all of bin/solr features to use BasicAuth, then you need to give that user several WRITE/ADMIN permissions, and a PW leak becomes a real convern.


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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] thelabdude merged pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
thelabdude merged pull request #355:
URL: https://github.com/apache/solr/pull/355


   


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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] thelabdude merged pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
thelabdude merged pull request #355:
URL: https://github.com/apache/solr/pull/355


   


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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] thelabdude commented on pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
thelabdude commented on pull request #355:
URL: https://github.com/apache/solr/pull/355#issuecomment-952089422


   > A concern with enabling BasicAuth is that it is less secure than OIDC which has a expiry of tokens, while a password is long-lived. To mitigate this added surface area, I wonder how easy it would be to "lock down" what a BasicAuth user can do in the system, such as limiting the role's permissions to block any request except /admin/system/info and /admin/collection?cmd=CLUSTERSTATUS. I think this is doable in authorization..
   
   Not sure why this is a concern? Locking down endpoints to a specific role (or roles) is why we have the authorization framework. The introduction of the `MultiAuthPlugin` really has no bearing on how people choose to configure their permissions.


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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] thelabdude commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
thelabdude commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r736686999



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.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.lucene.util.ResourceLoader;
+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.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.
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_DEFAULT_SCHEME = "default-scheme";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader loader;
+
+  // We forward to this scheme where we're asked to auth w/o an Authorization header
+  private String defaultScheme = null;
+
+  // 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();
+  }
+
+  @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);
+    }
+
+    defaultScheme = (String) pluginConfig.get(PROPERTY_DEFAULT_SCHEME);
+    if (defaultScheme != null) {
+      if (!pluginMap.containsKey(defaultScheme)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Default scheme '" + defaultScheme + "' not configured!");
+      }
+    } else {
+      // first scheme listed in the config is the default
+      defaultScheme = pluginMap.keySet().iterator().next();
+    }
+  }
+
+  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(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
+    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)) {
+        return pluginMap.get(defaultScheme).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) {
+      String scheme = (String) c.getDataMap().get(PROPERTY_SCHEME);
+      if (scheme == null) {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "All edit commands must include a 'scheme' attribute!");

Review comment:
       You have multiple plugins so you need to identify which the command applies to. Of course `basic` only supports `set-user` but what if someone writes a `digest` plugin and it also supports `set-user`, now my approach isn't all that "strange". I think having the outer `schemes` wrapper is cumbersome for single command requests. At this point, not sure it matters but I don't want to keep going around and around on silly JSON nuances as that requires code and doc changes each time.




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] janhoy commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
janhoy commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r733175637



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthPlugin.java
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.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.lucene.util.ResourceLoader;
+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.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.
+ */
+public class MultiAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin, SpecProvider {
+  public static final String PROPERTY_SCHEMES = "schemes";
+  public static final String PROPERTY_DEFAULT_SCHEME = "default-scheme";
+  public static final String PROPERTY_SCHEME = "scheme";
+  public static final String AUTHORIZATION_HEADER = "Authorization";
+
+  private static final ThreadLocal<AuthenticationPlugin> pluginInRequest = new ThreadLocal<>();
+  private final String UNKNOWN_SCHEME = "";
+
+  private final Map<String, AuthenticationPlugin> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader loader;
+
+  // We forward to this scheme where we're asked to auth w/o an Authorization header
+  private String defaultScheme = null;
+
+  // 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();
+  }
+
+  @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);
+    }
+
+    defaultScheme = (String) pluginConfig.get(PROPERTY_DEFAULT_SCHEME);
+    if (defaultScheme != null) {
+      if (!pluginMap.containsKey(defaultScheme)) {
+        throw new SolrException(ErrorCode.SERVER_ERROR, "Default scheme '" + defaultScheme + "' not configured!");
+      }
+    } else {
+      // first scheme listed in the config is the default

Review comment:
       Ah, thanks.




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] janhoy commented on a change in pull request #355: SOLR-12666: Add authn & authz plugins that supports multiple authentication schemes, such as Bearer and Basic

Posted by GitBox <gi...@apache.org>.
janhoy commented on a change in pull request #355:
URL: https://github.com/apache/solr/pull/355#discussion_r733189627



##########
File path: solr/core/src/java/org/apache/solr/security/MultiAuthRuleBasedAuthorizationPlugin.java
##########
@@ -0,0 +1,102 @@
+/*
+ * 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.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.lucene.util.ResourceLoader;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.StringUtils;
+import org.apache.solr.core.CoreContainer;
+
+/**
+ * Authorization plugin designed to work with the MultiAuthPlugin to support different AuthorizationPlugin per scheme.
+ */
+public class MultiAuthRuleBasedAuthorizationPlugin extends RuleBasedAuthorizationPluginBase {
+  private final Map<String, RuleBasedAuthorizationPluginBase> pluginMap = new LinkedHashMap<>();
+  private final ResourceLoader 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) {
+    for (RuleBasedAuthorizationPluginBase plugin : pluginMap.values()) {
+      final Set<String> userRoles = plugin.getUserRoles(principal);
+      if (userRoles != null && !userRoles.isEmpty()) {
+        return userRoles; // first non-empty match wins

Review comment:
       +1 for merge. It's easy and we can document that same username across schemes will be given the sum of the roles.




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

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org