diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 1aea04cf35f..4a439ac62fd 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -27,6 +27,9 @@ Detailed Change List * SOLR-8765: Enforce required parameters at query construction time in the SolrJ Collections API, add static factory methods, and deprecate old setter methods. (Alan Woodward, Jason Gerlowski) +* SOLR-8842: authorization APIs do not use name as an identifier for a permission + for update, delete
 commands and 'before' attribute (noble) + New Features ---------------------- @@ -66,6 +69,10 @@ Other Changes * SOLR-8866: UpdateLog will now throw an exception if it doesn't know how to serialize a value. (David Smiley) +* SOLR-8842: security rules made more foolproof by asking the requesthandler about the well known + permission name.
 The APIs are also modified to ue 'index' as the unique identifier instead of name. + Name is an optional attribute
 now and only to be used when specifying well-known permissions (noble) + ================== 6.0.0 ================== Consult the LUCENE_CHANGES.txt file for additional, low level, changes in this release diff --git a/solr/contrib/extraction/src/java/org/apache/solr/handler/extraction/ExtractingRequestHandler.java b/solr/contrib/extraction/src/java/org/apache/solr/handler/extraction/ExtractingRequestHandler.java index d1bd4c7d64e..af70c623a33 100644 --- a/solr/contrib/extraction/src/java/org/apache/solr/handler/extraction/ExtractingRequestHandler.java +++ b/solr/contrib/extraction/src/java/org/apache/solr/handler/extraction/ExtractingRequestHandler.java @@ -23,6 +23,8 @@ import org.apache.solr.common.util.DateUtil; import org.apache.solr.common.util.NamedList; import org.apache.solr.core.SolrCore; import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.security.AuthorizationContext; +import org.apache.solr.security.PermissionNameProvider; import org.apache.solr.update.processor.UpdateRequestProcessor; import org.apache.solr.util.plugin.SolrCoreAware; import org.apache.solr.handler.ContentStreamHandlerBase; @@ -45,7 +47,7 @@ import java.util.Map; * Handler for rich documents like PDF or Word or any other file format that Tika handles that need the text to be extracted * first from the document. */ -public class ExtractingRequestHandler extends ContentStreamHandlerBase implements SolrCoreAware { +public class ExtractingRequestHandler extends ContentStreamHandlerBase implements SolrCoreAware , PermissionNameProvider { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -61,6 +63,11 @@ public class ExtractingRequestHandler extends ContentStreamHandlerBase implement protected SolrContentHandlerFactory factory; + @Override + public PermissionNameProvider.Name getPermissionName(AuthorizationContext request) { + return PermissionNameProvider.Name.READ_PERM; + } + @Override public void init(NamedList args) { super.init(args); diff --git a/solr/core/src/java/org/apache/solr/handler/SQLHandler.java b/solr/core/src/java/org/apache/solr/handler/SQLHandler.java index fe831877653..11fee935847 100644 --- a/solr/core/src/java/org/apache/solr/handler/SQLHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/SQLHandler.java @@ -59,6 +59,8 @@ import org.apache.solr.core.CoreContainer; import org.apache.solr.core.SolrCore; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.security.AuthorizationContext; +import org.apache.solr.security.PermissionNameProvider; import org.apache.solr.util.plugin.SolrCoreAware; import java.util.List; @@ -70,7 +72,7 @@ import org.slf4j.LoggerFactory; import com.facebook.presto.sql.parser.SqlParser; -public class SQLHandler extends RequestHandlerBase implements SolrCoreAware { +public class SQLHandler extends RequestHandlerBase implements SolrCoreAware , PermissionNameProvider { private static String defaultZkhost = null; private static String defaultWorkerCollection = null; @@ -93,6 +95,11 @@ public class SQLHandler extends RequestHandlerBase implements SolrCoreAware { } } + @Override + public PermissionNameProvider.Name getPermissionName(AuthorizationContext request) { + return PermissionNameProvider.Name.READ_PERM; + } + public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { SolrParams params = req.getParams(); params = adjustParams(params); diff --git a/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java b/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java index 4279864b6ba..da24c254def 100644 --- a/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java @@ -42,13 +42,15 @@ import org.apache.solr.schema.IndexSchema; import org.apache.solr.schema.ManagedIndexSchema; import org.apache.solr.schema.SchemaManager; import org.apache.solr.schema.ZkIndexSchemaReader; +import org.apache.solr.security.AuthorizationContext; +import org.apache.solr.security.PermissionNameProvider; import org.apache.solr.util.plugin.SolrCoreAware; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.apache.solr.common.params.CommonParams.JSON; -public class SchemaHandler extends RequestHandlerBase implements SolrCoreAware { +public class SchemaHandler extends RequestHandlerBase implements SolrCoreAware, PermissionNameProvider { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private boolean isImmutableConfigSet = false; @@ -99,6 +101,18 @@ public class SchemaHandler extends RequestHandlerBase implements SolrCoreAware { } } + @Override + public PermissionNameProvider.Name getPermissionName(AuthorizationContext ctx) { + switch (ctx.getHttpMethod()) { + case "GET": + return PermissionNameProvider.Name.SCHEMA_READ_PERM; + case "POST": + return PermissionNameProvider.Name.SCHEMA_EDIT_PERM; + default: + return null; + } + } + private void handleGET(SolrQueryRequest req, SolrQueryResponse rsp) { try { String path = (String) req.getContext().get("path"); diff --git a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java index b73273a3ab3..eac9b11684e 100644 --- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java @@ -37,7 +37,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrRequest; @@ -68,6 +67,9 @@ import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.request.SolrRequestHandler; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.schema.SchemaManager; +import org.apache.solr.security.AuthorizationContext; +import org.apache.solr.security.PermissionNameProvider; +import org.apache.solr.security.PermissionNameProvider.Name; import org.apache.solr.util.CommandOperation; import org.apache.solr.util.DefaultSolrThreadFactory; import org.apache.solr.util.RTimer; @@ -88,7 +90,7 @@ import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME; import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME_IN_OVERLAY; import static org.apache.solr.schema.FieldType.CLASS_NAME; -public class SolrConfigHandler extends RequestHandlerBase implements SolrCoreAware { +public class SolrConfigHandler extends RequestHandlerBase implements SolrCoreAware, PermissionNameProvider { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); public static final String CONFIGSET_EDITING_DISABLED_ARG = "disable.configEdit"; public static final boolean configEditing_disabled = Boolean.getBoolean(CONFIGSET_EDITING_DISABLED_ARG); @@ -745,6 +747,18 @@ public class SolrConfigHandler extends RequestHandlerBase implements SolrCoreAwa return activeReplicaCoreUrls; } + @Override + public Name getPermissionName(AuthorizationContext ctx) { + switch (ctx.getHttpMethod()) { + case "GET": + return Name.CONFIG_READ_PERM; + case "POST": + return Name.CONFIG_EDIT_PERM; + default: + return null; + } + } + private static class PerReplicaCallable extends SolrRequest implements Callable { String coreUrl; String prop; diff --git a/solr/core/src/java/org/apache/solr/handler/StreamHandler.java b/solr/core/src/java/org/apache/solr/handler/StreamHandler.java index 113fa931c8e..c7bac9f6853 100644 --- a/solr/core/src/java/org/apache/solr/handler/StreamHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/StreamHandler.java @@ -50,11 +50,13 @@ import org.apache.solr.core.CoreContainer; import org.apache.solr.core.SolrCore; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.security.AuthorizationContext; +import org.apache.solr.security.PermissionNameProvider; import org.apache.solr.util.plugin.SolrCoreAware; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class StreamHandler extends RequestHandlerBase implements SolrCoreAware { +public class StreamHandler extends RequestHandlerBase implements SolrCoreAware, PermissionNameProvider { static SolrClientCache clientCache = new SolrClientCache(); private StreamFactory streamFactory = new StreamFactory(); @@ -62,6 +64,11 @@ public class StreamHandler extends RequestHandlerBase implements SolrCoreAware { private String coreName; private Map daemons = new HashMap(); + @Override + public PermissionNameProvider.Name getPermissionName(AuthorizationContext request) { + return PermissionNameProvider.Name.READ_PERM; + } + public void inform(SolrCore core) { /* The stream factory will always contain the zkUrl for the given collection diff --git a/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandler.java b/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandler.java index b8d2a89644f..7c973318a5a 100644 --- a/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandler.java @@ -36,14 +36,17 @@ import org.apache.solr.handler.loader.JsonLoader; import org.apache.solr.handler.loader.XMLLoader; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.security.AuthorizationContext; +import org.apache.solr.security.PermissionNameProvider; import org.apache.solr.update.processor.UpdateRequestProcessor; import static org.apache.solr.common.params.CommonParams.PATH; +import static org.apache.solr.security.PermissionNameProvider.Name.UPDATE_PERM; /** * UpdateHandler that uses content-type to pick the right Loader */ -public class UpdateRequestHandler extends ContentStreamHandlerBase { +public class UpdateRequestHandler extends ContentStreamHandlerBase implements PermissionNameProvider { // XML Constants public static final String ADD = "add"; @@ -150,6 +153,10 @@ public class UpdateRequestHandler extends ContentStreamHandlerBase { return registry; } + @Override + public PermissionNameProvider.Name getPermissionName(AuthorizationContext ctx) { + return UPDATE_PERM; + } @Override protected ContentStreamLoader newLoader(SolrQueryRequest req, final UpdateRequestProcessor processor) { diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java index b4d0a1d06d9..f0a332ebbf4 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java @@ -62,6 +62,7 @@ import org.apache.solr.common.cloud.ZkCoreNodeProps; import org.apache.solr.common.cloud.ZkNodeProps; import org.apache.solr.common.cloud.ZkStateReader; import org.apache.solr.common.params.CollectionAdminParams; +import org.apache.solr.common.params.CollectionParams; import org.apache.solr.common.params.CollectionParams.CollectionAction; import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.params.CoreAdminParams; @@ -77,6 +78,8 @@ import org.apache.solr.handler.RequestHandlerBase; import org.apache.solr.handler.component.ShardHandler; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.security.AuthorizationContext; +import org.apache.solr.security.PermissionNameProvider; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.slf4j.Logger; @@ -122,7 +125,7 @@ import static org.apache.solr.common.params.CoreAdminParams.INSTANCE_DIR; import static org.apache.solr.common.params.ShardParams._ROUTE_; import static org.apache.solr.common.util.StrUtils.formatString; -public class CollectionsHandler extends RequestHandlerBase { +public class CollectionsHandler extends RequestHandlerBase implements PermissionNameProvider { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); protected final CoreContainer coreContainer; @@ -144,6 +147,16 @@ public class CollectionsHandler extends RequestHandlerBase { this.coreContainer = coreContainer; } + @Override + public PermissionNameProvider.Name getPermissionName(AuthorizationContext ctx) { + String action = ctx.getParams().get("action"); + if (action == null) return null; + CollectionParams.CollectionAction collectionAction = CollectionParams.CollectionAction.get(action); + if (collectionAction == null) return null; + return collectionAction.isWrite ? + PermissionNameProvider.Name.COLL_EDIT_PERM : + PermissionNameProvider.Name.COLL_READ_PERM; + } @Override final public void init(NamedList args) { diff --git a/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java index 285ea65e1b9..0f4dd7bc847 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java @@ -33,17 +33,31 @@ import org.apache.solr.handler.RequestHandlerBase; import org.apache.solr.handler.SolrConfigHandler; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.security.AuthorizationContext; import org.apache.solr.security.ConfigEditablePlugin; +import org.apache.solr.security.PermissionNameProvider; import org.apache.solr.util.CommandOperation; import org.apache.zookeeper.KeeperException; -public class SecurityConfHandler extends RequestHandlerBase { +public class SecurityConfHandler extends RequestHandlerBase implements PermissionNameProvider { private CoreContainer cores; public SecurityConfHandler(CoreContainer coreContainer) { this.cores = coreContainer; } + @Override + public PermissionNameProvider.Name getPermissionName(AuthorizationContext ctx) { + switch (ctx.getHttpMethod()) { + case "GET": + return PermissionNameProvider.Name.SECURITY_READ_PERM; + case "POST": + return PermissionNameProvider.Name.SECURITY_EDIT_PERM; + default: + return null; + } + } + @Override public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { SolrConfigHandler.setWt(req, CommonParams.JSON); diff --git a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java index 3518ecbc28a..2e0f0da611f 100644 --- a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java @@ -26,7 +26,6 @@ import java.util.List; import java.util.Set; import org.apache.lucene.index.ExitableDirectoryReader; -import org.apache.lucene.util.Version; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.cloud.ZkController; import org.apache.solr.common.SolrDocumentList; @@ -45,6 +44,8 @@ import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.search.SolrQueryTimeoutImpl; import org.apache.solr.search.facet.FacetModule; +import org.apache.solr.security.AuthorizationContext; +import org.apache.solr.security.PermissionNameProvider; import org.apache.solr.util.RTimerTree; import org.apache.solr.util.SolrPluginUtils; import org.apache.solr.util.plugin.PluginInfoInitialized; @@ -60,8 +61,7 @@ import static org.apache.solr.common.params.CommonParams.PATH; * Refer SOLR-281 * */ -public class SearchHandler extends RequestHandlerBase implements SolrCoreAware , PluginInfoInitialized -{ +public class SearchHandler extends RequestHandlerBase implements SolrCoreAware , PluginInfoInitialized, PermissionNameProvider { static final String INIT_COMPONENTS = "components"; static final String INIT_FIRST_COMPONENTS = "first-components"; static final String INIT_LAST_COMPONENTS = "last-components"; @@ -98,6 +98,11 @@ public class SearchHandler extends RequestHandlerBase implements SolrCoreAware , } } + @Override + public PermissionNameProvider.Name getPermissionName(AuthorizationContext ctx) { + return PermissionNameProvider.Name.READ_PERM; + } + /** * Initialize the components based on name. Note, if using INIT_FIRST_COMPONENTS or INIT_LAST_COMPONENTS, * then the {@link DebugComponent} will always occur last. If this is not desired, then one must explicitly declare all components using diff --git a/solr/core/src/java/org/apache/solr/security/AuthorizationContext.java b/solr/core/src/java/org/apache/solr/security/AuthorizationContext.java index 7e65a63b09c..8f0fa573c21 100644 --- a/solr/core/src/java/org/apache/solr/security/AuthorizationContext.java +++ b/solr/core/src/java/org/apache/solr/security/AuthorizationContext.java @@ -21,6 +21,7 @@ import java.util.Enumeration; import java.util.List; import org.apache.solr.common.params.SolrParams; +import org.apache.solr.request.SolrRequestHandler; /** * Request context for Solr to be used by Authorization plugin. @@ -57,4 +58,6 @@ public abstract class AuthorizationContext { public enum RequestType {READ, WRITE, ADMIN, UNKNOWN} + public abstract Object getHandler(); + } \ No newline at end of file diff --git a/solr/core/src/java/org/apache/solr/security/AutorizationEditOperation.java b/solr/core/src/java/org/apache/solr/security/AutorizationEditOperation.java new file mode 100644 index 00000000000..9deae6d6af9 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/AutorizationEditOperation.java @@ -0,0 +1,176 @@ +package org.apache.solr.security; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.solr.util.CommandOperation; + +import static org.apache.solr.common.params.CommonParams.NAME; +import static org.apache.solr.common.util.Utils.getDeepCopy; +import static org.apache.solr.handler.admin.SecurityConfHandler.getListValue; +import static org.apache.solr.handler.admin.SecurityConfHandler.getMapValue; + +/* + * 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. + */ + + +enum AutorizationEditOperation { + SET_USER_ROLE("set-user-role") { + @Override + public Map edit(Map latestConf, CommandOperation op) { + Map roleMap = getMapValue(latestConf, "user-role"); + Map map = op.getDataMap(); + if (op.hasError()) return null; + for (Map.Entry e : map.entrySet()) { + if (e.getValue() == null) { + roleMap.remove(e.getKey()); + continue; + } + if (e.getValue() instanceof String || e.getValue() instanceof List) { + roleMap.put(e.getKey(), e.getValue()); + } else { + op.addError("Unexpected value "); + return null; + } + } + return latestConf; + } + }, + SET_PERMISSION("set-permission") { + @Override + public Map edit(Map latestConf, CommandOperation op) { + Integer index = op.getInt("index", null); + Integer beforeIdx = op.getInt("before",null); + Map dataMap = op.getDataMap(); + if (op.hasError()) return null; + dataMap = getDeepCopy(dataMap, 3); + dataMap.remove("before"); + if (beforeIdx != null && index != null) { + op.addError("Cannot use 'index' and 'before together "); + return null; + } + + for (String key : dataMap.keySet()) { + if (!Permission.knownKeys.contains(key)) op.addError("Unknown key, " + key); + } + try { + Permission.load(dataMap); + } catch (Exception e) { + op.addError(e.getMessage()); + return null; + } + if(op.hasError()) return null; + List permissions = getListValue(latestConf, "permissions"); + setIndex(permissions); + List permissionsCopy = new ArrayList<>(); + boolean beforeSatisfied = beforeIdx == null; + boolean indexSatisfied = index == null; + for (int i = 0; i < permissions.size(); i++) { + Map perm = permissions.get(i); + Integer thisIdx = (int) perm.get("index"); + if (thisIdx.equals(beforeIdx)) { + beforeSatisfied = true; + permissionsCopy.add(dataMap); + permissionsCopy.add(perm); + } else if (thisIdx.equals(index)) { + //overwriting an existing one + indexSatisfied = true; + permissionsCopy.add(dataMap); + } else { + permissionsCopy.add(perm); + } + } + + if (!beforeSatisfied) { + op.addError("Invalid 'before' :" + beforeIdx); + return null; + } + if (!indexSatisfied) { + op.addError("Invalid 'index' :" + index); + return null; + } + + if (!permissionsCopy.contains(dataMap)) permissionsCopy.add(dataMap); + latestConf.put("permissions", permissionsCopy); + setIndex(permissionsCopy); + return latestConf; + } + + }, + UPDATE_PERMISSION("update-permission") { + @Override + public Map edit(Map latestConf, CommandOperation op) { + Integer index = op.getInt("index"); + if (op.hasError()) return null; + List permissions = (List) getListValue(latestConf, "permissions"); + setIndex(permissions); + for (Map permission : permissions) { + if (index.equals(permission.get("index"))) { + LinkedHashMap copy = new LinkedHashMap<>(permission); + copy.putAll(op.getDataMap()); + op.setCommandData(copy); + return SET_PERMISSION.edit(latestConf, op); + } + } + op.addError("No such permission " + name); + return null; + } + }, + DELETE_PERMISSION("delete-permission") { + @Override + public Map edit(Map latestConf, CommandOperation op) { + Integer id = op.getInt(""); + if(op.hasError()) return null; + List p = getListValue(latestConf, "permissions"); + setIndex(p); + List c = p.stream().filter(map -> !id.equals(map.get("index"))).collect(Collectors.toList()); + if(c.size() == p.size()){ + op.addError("No such index :"+ id); + return null; + } + latestConf.put("permissions", c); + return latestConf; + } + }; + + public abstract Map edit(Map latestConf, CommandOperation op); + + public final String name; + + public String getOperationName() { + return name; + } + + AutorizationEditOperation(String s) { + this.name = s; + } + + public static AutorizationEditOperation get(String name) { + for (AutorizationEditOperation o : values()) if (o.name.equals(name)) return o; + return null; + } + + static void setIndex(List permissionsCopy) { + AtomicInteger counter = new AtomicInteger(0); + permissionsCopy.stream().forEach(map -> map.put("index", counter.incrementAndGet())); + } +} diff --git a/solr/core/src/java/org/apache/solr/security/Permission.java b/solr/core/src/java/org/apache/solr/security/Permission.java new file mode 100644 index 00000000000..2ec1fcfbc3c --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/Permission.java @@ -0,0 +1,119 @@ +package org.apache.solr.security; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import com.google.common.collect.ImmutableSet; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.params.CommonParams; + +import static java.util.Collections.singleton; +import static org.apache.solr.common.params.CommonParams.NAME; + +/* + * 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. + */ + + +class Permission { + String name; + Set path, role, collections, method; + Map params; + PermissionNameProvider.Name wellknownName; + + private Permission() { + } + + static Permission load(Map m) { + Permission p = new Permission(); + String name = (String) m.get(NAME); + if (!m.containsKey("role")) throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "role not specified"); + p.role = readValueAsSet(m, "role"); + if (PermissionNameProvider.Name.get(name)!= null) { + p.wellknownName = PermissionNameProvider.Name.get(name); + HashSet disAllowed = new HashSet<>(knownKeys); + disAllowed.remove("role");//these are the only + disAllowed.remove(NAME);//allowed keys for well-known permissions + disAllowed.remove("collection");//allowed keys for well-known permissions + disAllowed.remove("index"); + for (String s : disAllowed) { + if (m.containsKey(s)) + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, s + " is not a valid key for the permission : " + name); + } + + } + p.name = name; + p.path = readSetSmart(name, m, "path"); + p.collections = readSetSmart(name, m, "collection"); + p.method = readSetSmart(name, m, "method"); + p.params = (Map) m.get("params"); + return p; + } + + /** + * This checks for the defaults available other rules for the keys + */ + private static Set readSetSmart(String permissionName, Map m, String key) { + if(PermissionNameProvider.values.containsKey(permissionName) && !m.containsKey(key) && "collection".equals(key)) { + return PermissionNameProvider.Name.get(permissionName).collName; + } + Set set = readValueAsSet(m, key); + if ("method".equals(key)) { + if (set != null) { + for (String s : set) if (!HTTP_METHODS.contains(s)) return null; + } + return set; + } + return set == null ? singleton(null) : set; + } + /** + * read a key value as a set. if the value is a single string , + * return a singleton set + * + * @param m the map from which to lookup + * @param key the key with which to do lookup + */ + static Set readValueAsSet(Map m, String key) { + Set result = new HashSet<>(); + Object val = m.get(key); + if (val == null) { + if("collection".equals(key)) { + //for collection collection: null means a core admin/ collection admin request + // otherwise it means a request where collection name is ignored + return m.containsKey(key) ? singleton(null) : singleton("*"); + } + return null; + } + if (val instanceof Collection) { + Collection list = (Collection) val; + for (Object o : list) result.add(String.valueOf(o)); + } else if (val instanceof String) { + result.add((String) val); + } else { + throw new RuntimeException("Bad value for : " + key); + } + return result.isEmpty() ? null : Collections.unmodifiableSet(result); + } + + + static final Set knownKeys = ImmutableSet.of("collection", "role", "params", "path", "method", NAME,"index"); + public static final Set HTTP_METHODS = ImmutableSet.of("GET", "POST", "DELETE", "PUT", "HEAD"); + +} diff --git a/solr/core/src/java/org/apache/solr/security/PermissionNameProvider.java b/solr/core/src/java/org/apache/solr/security/PermissionNameProvider.java new file mode 100644 index 00000000000..2dec4333047 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/PermissionNameProvider.java @@ -0,0 +1,72 @@ +package org.apache.solr.security; + +/* + * 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. + */ + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static java.util.Collections.unmodifiableMap; +import static java.util.Collections.unmodifiableSet; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; + +/** + * A requestHandler should implement this interface to provide the well known permission + * at request time + */ +public interface PermissionNameProvider { + enum Name { + COLL_EDIT_PERM("collection-admin-edit", null), + COLL_READ_PERM("collection-admin-read", null), + READ_PERM("read", "*"), + UPDATE_PERM("update", "*"), + CONFIG_EDIT_PERM("config-edit", "*"), + CONFIG_READ_PERM("config-read", "*"), + SCHEMA_READ_PERM("schema-read", "*"), + SCHEMA_EDIT_PERM("schema-edit", "*"), + SECURITY_EDIT_PERM("security-edit", null), + SECURITY_READ_PERM("security-read", null), + ALL("all", unmodifiableSet(new HashSet<>(asList("*", null)))) + ; + final String name; + final Set collName; + + Name(String s, Object collName) { + name = s; + this.collName = collName instanceof Set? (Set)collName : singleton((String)collName); + } + + public static Name get(String s) { + return values.get(s); + } + + public String getPermissionName() { + return name; + } + } + + Set NULL = singleton(null); + Set ANY = singleton("*"); + + Map values = unmodifiableMap(asList(Name.values()).stream().collect(toMap(Name::getPermissionName, identity()))); + + Name getPermissionName(AuthorizationContext request); +} diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java index 1d4a9b8c44b..3145113c6ea 100644 --- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPlugin.java @@ -20,31 +20,23 @@ import java.io.IOException; import java.lang.invoke.MethodHandles; import java.security.Principal; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Predicate; -import com.google.common.collect.ImmutableSet; -import org.apache.solr.common.SolrException; -import org.apache.solr.common.params.CollectionParams; -import org.apache.solr.common.util.Utils; import org.apache.solr.util.CommandOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static java.util.Collections.singleton; -import static org.apache.solr.common.util.Utils.fromJSONResource; +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableMap; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; import static org.apache.solr.handler.admin.SecurityConfHandler.getListValue; import static org.apache.solr.handler.admin.SecurityConfHandler.getMapValue; -import static org.apache.solr.common.params.CommonParams.NAME; -import static org.apache.solr.common.util.Utils.getDeepCopy; public class RuleBasedAuthorizationPlugin implements AuthorizationPlugin, ConfigEditablePlugin { @@ -120,21 +112,30 @@ public class RuleBasedAuthorizationPlugin implements AuthorizationPlugin, Config loopPermissions: for (int i = 0; i < permissions.size(); i++) { Permission permission = permissions.get(i); - if (permission.method != null && !permission.method.contains(context.getHttpMethod())) { - //this permissions HTTP method does not match this rule. try other rules - continue; - } - if(permission.predicate != null){ - if(!permission.predicate.test(context)) continue ; - } - - if (permission.params != null) { - for (Map.Entry e : permission.params.entrySet()) { - String paramVal = context.getParams().get(e.getKey()); - Object val = e.getValue(); - if (val instanceof List) { - if (!((List) val).contains(paramVal)) continue loopPermissions; - } else if (!Objects.equals(val, paramVal)) continue loopPermissions; + if (PermissionNameProvider.values.containsKey(permission.name)) { + if (context.getHandler() instanceof PermissionNameProvider) { + PermissionNameProvider handler = (PermissionNameProvider) context.getHandler(); + PermissionNameProvider.Name permissionName = handler.getPermissionName(context); + if (permissionName == null || !permission.name.equals(permissionName.name)) { + continue; + } + } else { + //all is special. it can match any + if(permission.wellknownName != PermissionNameProvider.Name.ALL) continue; + } + } else { + if (permission.method != null && !permission.method.contains(context.getHttpMethod())) { + //this permissions HTTP method does not match this rule. try other rules + continue; + } + if (permission.params != null) { + for (Map.Entry e : permission.params.entrySet()) { + String paramVal = context.getParams().get(e.getKey()); + Object val = e.getValue(); + if (val instanceof List) { + if (!((List) val).contains(paramVal)) continue loopPermissions; + } else if (!Objects.equals(val, paramVal)) continue loopPermissions; + } } } @@ -169,7 +170,7 @@ public class RuleBasedAuthorizationPlugin implements AuthorizationPlugin, Config for (Object o : map.entrySet()) { Map.Entry e = (Map.Entry) o; String roleName = (String) e.getKey(); - usersVsRoles.put(roleName, readValueAsSet(map, roleName)); + usersVsRoles.put(roleName, Permission.readValueAsSet(map, roleName)); } List perms = getListValue(initInfo, "permissions"); for (Map o : perms) { @@ -198,74 +199,10 @@ public class RuleBasedAuthorizationPlugin implements AuthorizationPlugin, Config } } - /** - * read a key value as a set. if the value is a single string , - * return a singleton set - * - * @param m the map from which to lookup - * @param key the key with which to do lookup - */ - static Set readValueAsSet(Map m, String key) { - Set result = new HashSet<>(); - Object val = m.get(key); - if (val == null) { - if("collection".equals(key)){ - //for collection collection: null means a core admin/ collection admin request - // otherwise it means a request where collection name is ignored - return m.containsKey(key) ? singleton(null) : singleton("*"); - } - return null; - } - if (val instanceof Collection) { - Collection list = (Collection) val; - for (Object o : list) result.add(String.valueOf(o)); - } else if (val instanceof String) { - result.add((String) val); - } else { - throw new RuntimeException("Bad value for : " + key); - } - return result.isEmpty() ? null : Collections.unmodifiableSet(result); - } @Override public void close() throws IOException { } - static class Permission { - String name; - Set path, role, collections, method; - Map params; - Predicate predicate; - - private Permission() { - } - - static Permission load(Map m) { - Permission p = new Permission(); - String name = (String) m.get(NAME); - if (!m.containsKey("role")) throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "role not specified"); - p.role = readValueAsSet(m, "role"); - if (well_known_permissions.containsKey(name)) { - HashSet disAllowed = new HashSet<>(knownKeys); - disAllowed.remove("role");//these are the only - disAllowed.remove(NAME);//allowed keys for well-known permissions - for (String s : disAllowed) { - if (m.containsKey(s)) - throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, s + " is not a valid key for the permission : " + name); - } - p.predicate = (Predicate) ((Map) well_known_permissions.get(name)).get(Predicate.class.getName()); - m = well_known_permissions.get(name); - } - p.name = name; - p.path = readSetSmart(name, m, "path"); - p.collections = readSetSmart(name, m, "collection"); - p.method = readSetSmart(name, m, "method"); - p.params = (Map) m.get("params"); - return p; - } - - static final Set knownKeys = ImmutableSet.of("collection", "role", "params", "path", "method", NAME); - } - enum MatchStatus { USER_REQUIRED(AuthorizationResponse.PROMPT), NO_PERMISSIONS_FOUND(AuthorizationResponse.OK), @@ -279,33 +216,12 @@ public class RuleBasedAuthorizationPlugin implements AuthorizationPlugin, Config } } - /** - * This checks for the defaults available other rules for the keys - */ - private static Set readSetSmart(String permissionName, Map m, String key) { - Set set = readValueAsSet(m, key); - if (set == null && well_known_permissions.containsKey(permissionName)) { - set = readValueAsSet((Map) well_known_permissions.get(permissionName), key); - } - if ("method".equals(key)) { - if (set != null) { - for (String s : set) if (!HTTP_METHODS.contains(s)) return null; - } - return set; - } - return set == null ? singleton(null) : set; - } + @Override public Map edit(Map latestConf, List commands) { for (CommandOperation op : commands) { - OPERATION operation = null; - for (OPERATION o : OPERATION.values()) { - if (o.name.equals(op.name)) { - operation = o; - break; - } - } + AutorizationEditOperation operation = ops.get(op.name); if (operation == null) { op.unknownOperation(); return null; @@ -317,152 +233,6 @@ public class RuleBasedAuthorizationPlugin implements AuthorizationPlugin, Config return latestConf; } - enum OPERATION { - SET_USER_ROLE("set-user-role") { - @Override - public Map edit(Map latestConf, CommandOperation op) { - Map roleMap = getMapValue(latestConf, "user-role"); - Map map = op.getDataMap(); - if (op.hasError()) return null; - for (Map.Entry e : map.entrySet()) { - if (e.getValue() == null) { - roleMap.remove(e.getKey()); - continue; - } - if (e.getValue() instanceof String || e.getValue() instanceof List) { - roleMap.put(e.getKey(), e.getValue()); - } else { - op.addError("Unexpected value "); - return null; - } - } - return latestConf; - } - }, - SET_PERMISSION("set-permission") { - @Override - public Map edit(Map latestConf, CommandOperation op) { - String name = op.getStr(NAME); - Map dataMap = op.getDataMap(); - if (op.hasError()) return null; - dataMap = getDeepCopy(dataMap, 3); - String before = (String) dataMap.remove("before"); - for (String key : dataMap.keySet()) { - if (!Permission.knownKeys.contains(key)) op.addError("Unknown key, " + key); - } - try { - Permission.load(dataMap); - } catch (Exception e) { - op.addError(e.getMessage()); - return null; - } - List permissions = getListValue(latestConf, "permissions"); - List permissionsCopy = new ArrayList<>(); - boolean added = false; - for (Map e : permissions) { - Object n = e.get(NAME); - if (n.equals(before) || n.equals(name)) { - added = true; - permissionsCopy.add(dataMap); - } - if (!n.equals(name)) permissionsCopy.add(e); - } - if (!added && before != null) { - op.addError("Invalid 'before' :" + before); - return null; - } - if (!added) permissionsCopy.add(dataMap); - latestConf.put("permissions", permissionsCopy); - return latestConf; - } - }, - UPDATE_PERMISSION("update-permission") { - @Override - public Map edit(Map latestConf, CommandOperation op) { - String name = op.getStr(NAME); - if (op.hasError()) return null; - for (Map permission : (List) getListValue(latestConf, "permissions")) { - if (name.equals(permission.get(NAME))) { - LinkedHashMap copy = new LinkedHashMap<>(permission); - copy.putAll(op.getDataMap()); - op.setCommandData(copy); - return SET_PERMISSION.edit(latestConf, op); - } - } - op.addError("No such permission " + name); - return null; - } - }, - DELETE_PERMISSION("delete-permission") { - @Override - public Map edit(Map latestConf, CommandOperation op) { - List names = op.getStrs(""); - if (names == null || names.isEmpty()) { - op.addError("Invalid command"); - return null; - } - names = new ArrayList<>(names); - List copy = new ArrayList<>(); - List p = getListValue(latestConf, "permissions"); - for (Map map : p) { - Object n = map.get(NAME); - if (names.contains(n)) { - names.remove(n); - continue; - } else { - copy.add(map); - } - } - if (!names.isEmpty()) { - op.addError("Unknown permission name(s) " + names); - return null; - } - latestConf.put("permissions", copy); - return latestConf; - } - }; - - public abstract Map edit(Map latestConf, CommandOperation op); - - public final String name; - - OPERATION(String s) { - this.name = s; - } - - public static OPERATION get(String name) { - for (OPERATION o : values()) if (o.name.equals(name)) return o; - return null; - } - } - - public static final Set HTTP_METHODS = ImmutableSet.of("GET", "POST", "DELETE", "PUT", "HEAD"); - - private static final Map> well_known_permissions = - (Map>) fromJSONResource("WellKnownPermissions.json"); - - static { - ((Map) well_known_permissions.get("collection-admin-edit")).put(Predicate.class.getName(), getCollectionActionPredicate(true)); - ((Map) well_known_permissions.get("collection-admin-read")).put(Predicate.class.getName(), getCollectionActionPredicate(false)); - } - - private static Predicate getCollectionActionPredicate(final boolean isEdit) { - return new Predicate() { - @Override - public boolean test(AuthorizationContext context) { - String action = context.getParams().get("action"); - if (action == null) return false; - CollectionParams.CollectionAction collectionAction = CollectionParams.CollectionAction.get(action); - if (collectionAction == null) return false; - return isEdit ? collectionAction.isWrite : !collectionAction.isWrite; - } - }; - } - - - public static void main(String[] args) { - System.out.println(Utils.toJSONString(well_known_permissions)); - - } + private static final Map ops = unmodifiableMap(asList(AutorizationEditOperation.values()).stream().collect(toMap(AutorizationEditOperation::getOperationName, identity()))); } diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java index f291b2f9786..63cfb7cc9a2 100644 --- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java +++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java @@ -984,6 +984,11 @@ public class HttpSolrCall { return getReq().getMethod(); } + @Override + public Object getHandler() { + return handler; + } + @Override public String toString() { StringBuilder response = new StringBuilder("userPrincipal: [").append(getUserPrincipal()).append("]") diff --git a/solr/core/src/java/org/apache/solr/util/CommandOperation.java b/solr/core/src/java/org/apache/solr/util/CommandOperation.java index b6a786890fd..b1fda004f34 100644 --- a/solr/core/src/java/org/apache/solr/util/CommandOperation.java +++ b/solr/core/src/java/org/apache/solr/util/CommandOperation.java @@ -93,6 +93,12 @@ public class CommandOperation { } private Object getMapVal(String key) { + if("".equals(key)){ + if (commandData instanceof Map) { + addError("value of the command is an object should be primitive"); + } + return commandData; + } if (commandData instanceof Map) { Map metaData = (Map) commandData; return metaData.get(key); @@ -297,4 +303,25 @@ public class CommandOperation { } + public Integer getInt(String name, Integer def) { + Object o = getVal(name); + if (o == null) return def; + if (o instanceof Number) { + Number number = (Number) o; + return number.intValue(); + } else { + try { + return Integer.parseInt(o.toString()); + } catch (NumberFormatException e) { + addError(StrUtils.formatString("{0} is not a valid integer", name)); + return null; + } + } + } + + public Integer getInt(String name) { + Object o = getVal(name); + if(o == null) return null; + return getInt(name, null); + } } diff --git a/solr/core/src/resources/WellKnownPermissions.json b/solr/core/src/resources/WellKnownPermissions.json deleted file mode 100644 index 026e511b75c..00000000000 --- a/solr/core/src/resources/WellKnownPermissions.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "security-edit":{ - "path":[ - "/admin/authentication", - "/admin/authorization"], - "collection":null, - "method":"POST"}, - "security-read":{ - "path":[ - "/admin/authentication", - "/admin/authorization"], - "collection":null, - "method":"GET"}, - "schema-edit":{ - "method":"POST", - "path":"/schema/*"}, - "collection-admin-edit":{ - "collection":null, - "path":"/admin/collections"}, - "collection-admin-read":{ - "collection":null, - "path":"/admin/collections"}, - "schema-read":{ - "method":"GET", - "path":"/schema/*"}, - "config-read":{ - "method":"GET", - "path":"/config/*"}, - "update":{"path":"/update/*"}, - "read":{"path":[ - "/select", - "/get", - "/browse", - "/tvrh", - "/terms", - "/clustering", - "/elevate", - "/export", - "/spell", - "/clustering"]}, - "config-edit":{ - "method":"POST", - "path":"/config/*"}, - "all":{"collection":[ - "*", - null]} -} diff --git a/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java index a857e770b50..5e3d4071d24 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java @@ -70,16 +70,12 @@ public class SecurityConfHandlerTest extends SolrTestCaseJ4 { - command = "{'set-user-role': { 'tom': ['admin','dev']},\n" + + command = "{'set-permission':{ collection : acoll ,\n" + + " path : '/nonexistentpath',\n" + + " role :guest },\n" + + "'set-user-role': { 'tom': ['admin','dev']},"+ "'set-permission':{'name': 'security-edit',\n" + - " 'role': 'admin'\n" + - " },\n" + - "'set-permission':{'name':'some-permission',\n" + - " 'collection':'acoll',\n" + - " 'path':'/nonexistentpath',\n" + - " 'role':'guest',\n" + - " 'before':'security-edit'\n" + - " }\n" + + " 'role': 'admin'}\n" + "}"; req = new LocalSolrQueryRequest(null, new ModifiableSolrParams()); @@ -98,13 +94,11 @@ public class SecurityConfHandlerTest extends SolrTestCaseJ4 { List permissions = (List) authzconf.get("permissions"); assertEquals(2, permissions.size()); for (Map p : permissions) { - assertEquals("some-permission", p.get("name")); + assertEquals("acoll", p.get("collection")); break; } - - command = "{\n" + - "'set-permission':{'name': 'security-edit',\n" + + "'set-permission':{index : 2, name : security-edit,\n" + " 'role': ['admin','dev']\n" + " }}"; req = new LocalSolrQueryRequest(null, new ModifiableSolrParams()); @@ -124,7 +118,7 @@ public class SecurityConfHandlerTest extends SolrTestCaseJ4 { assertEquals( "dev", rol.get(1)); command = "{\n" + - "'update-permission':{'name': 'some-permission',\n" + + "'update-permission':{'index': 1,\n" + " 'role': ['guest','admin']\n" + " }}"; req = new LocalSolrQueryRequest(null, new ModifiableSolrParams()); @@ -138,7 +132,7 @@ public class SecurityConfHandlerTest extends SolrTestCaseJ4 { permissions = (List) authzconf.get("permissions"); p = permissions.get(0); - assertEquals("some-permission", p.get("name")); + assertEquals("acoll", p.get("collection")); rol = (List) p.get("role"); assertEquals( "guest", rol.get(0)); assertEquals( "admin", rol.get(1)); @@ -146,8 +140,8 @@ public class SecurityConfHandlerTest extends SolrTestCaseJ4 { command = "{\n" + - "'delete-permission': 'some-permission',\n" + - "'set-user-role':{'tom':null}\n" + + "delete-permission: 1,\n" + + " set-user-role : { tom :null}\n" + "}"; req = new LocalSolrQueryRequest(null, new ModifiableSolrParams()); req.getContext().put("httpMethod","POST"); @@ -167,7 +161,7 @@ public class SecurityConfHandlerTest extends SolrTestCaseJ4 { assertFalse("some-permission".equals(permission.get("name"))); } command = "{\n" + - "'set-permission':{'name': 'security-edit',\n" + + "'set-permission':{index : 2, 'name': 'security-edit',\n" + " 'method':'POST',"+ // -ve test security edit is a well-known permission , only role attribute should be provided " 'role': 'admin'\n" + " }}"; diff --git a/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java index ab02a3e1eac..96a8c146eb0 100644 --- a/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java +++ b/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java @@ -140,8 +140,7 @@ public class BasicAuthIntegrationTest extends TestMiniSolrCloudClusterBase { httpPost = new HttpPost(baseUrl + authzPrefix); setBasicAuthHeader(httpPost, "harry", "HarryIsUberCool"); httpPost.setEntity(new ByteArrayEntity(Utils.toJSON(singletonMap("set-permission", Utils.makeMap - ("name", "x-update", - "collection", "x", + ("collection", "x", "path", "/update/*", "role", "dev"))))); diff --git a/solr/core/src/test/org/apache/solr/security/MockAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/MockAuthorizationPlugin.java index f307553e81f..1cbe8498e1c 100644 --- a/solr/core/src/test/org/apache/solr/security/MockAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/MockAuthorizationPlugin.java @@ -26,7 +26,7 @@ import org.apache.solr.common.SolrException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class MockAuthorizationPlugin implements AuthorizationPlugin{ +public class MockAuthorizationPlugin implements AuthorizationPlugin { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); static final HashSet denyUsers = new HashSet<>(); @@ -34,8 +34,8 @@ public class MockAuthorizationPlugin implements AuthorizationPlugin{ @Override public AuthorizationResponse authorize(AuthorizationContext context) { - String uname = context.getUserPrincipal()== null? null : context.getUserPrincipal().getName(); - if(predicate != null){ + String uname = context.getUserPrincipal() == null ? null : context.getUserPrincipal().getName(); + if (predicate != null) { try { predicate.test(context); return new AuthorizationResponse(200); @@ -45,9 +45,9 @@ public class MockAuthorizationPlugin implements AuthorizationPlugin{ } - if(uname == null) uname = context.getParams().get("uname"); + if (uname == null) uname = context.getParams().get("uname"); log.info("User request: " + uname); - if(denyUsers.contains(uname)) + if (denyUsers.contains(uname)) return new AuthorizationResponse(403); else return new AuthorizationResponse(200); diff --git a/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java index 62f4db407b6..7a92f8f567e 100644 --- a/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/TestRuleBasedAuthorizationPlugin.java @@ -16,9 +16,13 @@ */ package org.apache.solr.security; +import java.io.IOException; +import java.io.StringReader; import java.security.Principal; +import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -27,12 +31,20 @@ import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.common.params.MapSolrParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.Utils; +import org.apache.solr.handler.ReplicationHandler; +import org.apache.solr.handler.SchemaHandler; +import org.apache.solr.handler.UpdateRequestHandler; +import org.apache.solr.handler.admin.CollectionsHandler; +import org.apache.solr.handler.component.SearchHandler; import org.apache.solr.security.AuthorizationContext.CollectionRequest; import org.apache.solr.security.AuthorizationContext.RequestType; +import org.apache.solr.util.CommandOperation; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; +import static org.apache.solr.common.util.Utils.getObjectByPath; import static org.apache.solr.common.util.Utils.makeMap; +import static org.apache.solr.util.CommandOperation.captureErrors; public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { String permissions = "{" + @@ -67,43 +79,51 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { checkRules(makeMap("resource", "/update/json/docs", "httpMethod", "POST", "userPrincipal", "unknownuser", - "collectionRequests", "freeforall" ) + "collectionRequests", "freeforall", + "handler", new UpdateRequestHandler()) , STATUS_OK); checkRules(makeMap("resource", "/update/json/docs", "httpMethod", "POST", "userPrincipal", "tim", - "collectionRequests", "mycoll") + "collectionRequests", "mycoll", + "handler", new UpdateRequestHandler()) , STATUS_OK); checkRules(makeMap("resource", "/update/json/docs", "httpMethod", "POST", - "collectionRequests", "mycoll" ) + "collectionRequests", "mycoll", + "handler", new UpdateRequestHandler()) , PROMPT_FOR_CREDENTIALS); checkRules(makeMap("resource", "/schema", "userPrincipal", "somebody", "collectionRequests", "mycoll", - "httpMethod", "POST") + "httpMethod", "POST", + "handler", new SchemaHandler()) , FORBIDDEN); checkRules(makeMap("resource", "/schema", "userPrincipal", "somebody", "collectionRequests", "mycoll", - "httpMethod", "GET") + "httpMethod", "GET", + "handler", new SchemaHandler() + ) , STATUS_OK); checkRules(makeMap("resource", "/schema/fields", "userPrincipal", "somebody", "collectionRequests", "mycoll", - "httpMethod", "GET") + "httpMethod", "GET", + "handler", new SchemaHandler()) , STATUS_OK); checkRules(makeMap("resource", "/schema", "userPrincipal", "somebody", "collectionRequests", "mycoll", - "httpMethod", "POST" ) + "httpMethod", "POST", + "handler", new SchemaHandler()) , FORBIDDEN); checkRules(makeMap("resource", "/admin/collections", @@ -111,6 +131,7 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { "requestType", RequestType.ADMIN, "collectionRequests", null, "httpMethod", "GET", + "handler", new CollectionsHandler(), "params", new MapSolrParams(singletonMap("action", "LIST"))) , STATUS_OK); @@ -119,6 +140,7 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { "requestType", RequestType.ADMIN, "collectionRequests", null, "httpMethod", "GET", + "handler", new CollectionsHandler(), "params", new MapSolrParams(singletonMap("action", "LIST"))) , STATUS_OK); @@ -126,6 +148,7 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { "userPrincipal", null, "requestType", RequestType.ADMIN, "collectionRequests", null, + "handler", new CollectionsHandler(), "params", new MapSolrParams(singletonMap("action", "CREATE"))) , PROMPT_FOR_CREDENTIALS); @@ -133,6 +156,7 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { "userPrincipal", null, "requestType", RequestType.ADMIN, "collectionRequests", null, + "handler", new CollectionsHandler(), "params", new MapSolrParams(singletonMap("action", "RELOAD"))) , PROMPT_FOR_CREDENTIALS); @@ -141,6 +165,7 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { "userPrincipal", "somebody", "requestType", RequestType.ADMIN, "collectionRequests", null, + "handler", new CollectionsHandler(), "params", new MapSolrParams(singletonMap("action", "CREATE"))) , FORBIDDEN); @@ -148,11 +173,13 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { "userPrincipal", "tim", "requestType", RequestType.ADMIN, "collectionRequests", null, + "handler", new CollectionsHandler(), "params", new MapSolrParams(singletonMap("action", "CREATE"))) , STATUS_OK); checkRules(makeMap("resource", "/select", "httpMethod", "GET", + "handler", new SearchHandler(), "collectionRequests", singletonList(new CollectionRequest("mycoll")), "userPrincipal", "joe") , FORBIDDEN); @@ -165,12 +192,14 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { checkRules(makeMap("resource", "/replication", "httpMethod", "POST", "userPrincipal", "tim", + "handler", new ReplicationHandler(), "collectionRequests", singletonList(new CollectionRequest("mycoll")) ) , FORBIDDEN, rules); checkRules(makeMap("resource", "/replication", "httpMethod", "POST", "userPrincipal", "cio", + "handler", new ReplicationHandler(), "collectionRequests", singletonList(new CollectionRequest("mycoll")) ) , STATUS_OK, rules); @@ -178,11 +207,61 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { "userPrincipal", "tim", "requestType", AuthorizationContext.RequestType.ADMIN, "collectionRequests", null, + "handler", new CollectionsHandler(), "params", new MapSolrParams(singletonMap("action", "CREATE"))) , STATUS_OK, rules); } + public void testEditRules() throws IOException { + Perms perms = new Perms(); + perms.runCmd("{set-permission : {name: config-edit, role: admin } }", true); + assertEquals("config-edit", getObjectByPath(perms.conf, false, "permissions[0]/name")); + assertEquals(1 , perms.getVal("permissions[0]/index")); + assertEquals("admin" , perms.getVal("permissions[0]/role")); + perms.runCmd("{set-permission : {name: config-edit, role: [admin, dev], index:2 } }", false); + perms.runCmd("{set-permission : {name: config-edit, role: [admin, dev], index:1}}", true); + Collection roles = (Collection) perms.getVal("permissions[0]/role"); + assertEquals(2, roles.size()); + assertTrue(roles.contains("admin")); + assertTrue(roles.contains("dev")); + perms.runCmd("{set-permission : {role: [admin, dev], collection: x , path: '/a/b' , method :[GET, POST] }}", true); + assertNotNull(perms.getVal("permissions[1]")); + assertEquals("x", perms.getVal("permissions[1]/collection")); + assertEquals("/a/b", perms.getVal("permissions[1]/path")); + perms.runCmd("{update-permission : {index : 2, method : POST }}", true); + assertEquals("POST" , perms.getVal("permissions[1]/method")); + perms.runCmd("{set-permission : {name : read, collection : y, role:[guest, dev] , before :2}}", true); + assertNotNull(perms.getVal("permissions[2]")); + assertEquals("y", perms.getVal("permissions[1]/collection")); + assertEquals("read", perms.getVal("permissions[1]/name")); + perms.runCmd("{delete-permission : 3}", true); + assertTrue(captureErrors(perms.parsedCommands).isEmpty()); + assertEquals("y",perms.getVal("permissions[1]/collection")); + } + + static class Perms { + Map conf = new HashMap<>(); + RuleBasedAuthorizationPlugin plugin = new RuleBasedAuthorizationPlugin(); + List parsedCommands; + + public void runCmd(String cmds, boolean failOnError) throws IOException { + parsedCommands = CommandOperation.parse(new StringReader(cmds)); + LinkedList ll = new LinkedList(); + Map edited = plugin.edit(conf, parsedCommands); + if(edited!= null) conf = edited; + List maps = captureErrors(parsedCommands); + if(failOnError){ + assertTrue("unexpected error ,"+maps , maps.isEmpty()); + } else { + assertFalse("expected error", maps.isEmpty()); + } + } + public Object getVal(String path){ + return getObjectByPath(conf,false, path); + } + } + private void checkRules(Map values, int expected) { checkRules(values,expected,(Map) Utils.fromJSONString(permissions)); } @@ -253,6 +332,12 @@ public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 { return (String) values.get("httpMethod"); } + @Override + public Object getHandler() { + Object handler = values.get("handler"); + return handler instanceof String ? (PermissionNameProvider) request -> PermissionNameProvider.Name.get((String) handler) : handler; + } + @Override public String getResource() { return (String) values.get("resource");