| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.solr.handler.admin; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.invoke.MethodHandles; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| import com.google.common.collect.ImmutableList; |
| import org.apache.solr.api.ApiBag; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.params.CommonParams; |
| import org.apache.solr.common.util.Utils; |
| import org.apache.solr.core.CoreContainer; |
| import org.apache.solr.handler.RequestHandlerBase; |
| import org.apache.solr.handler.RequestHandlerUtils; |
| import org.apache.solr.request.SolrQueryRequest; |
| import org.apache.solr.response.SolrQueryResponse; |
| import org.apache.solr.security.AuthenticationPlugin; |
| import org.apache.solr.security.AuthorizationContext; |
| import org.apache.solr.security.AuthorizationPlugin; |
| import org.apache.solr.security.ConfigEditablePlugin; |
| import org.apache.solr.security.PermissionNameProvider; |
| import org.apache.solr.common.util.CommandOperation; |
| import org.apache.solr.api.Api; |
| import org.apache.solr.api.ApiBag.ReqHandlerToApi; |
| import org.apache.solr.common.SpecProvider; |
| import org.apache.solr.common.util.JsonSchemaValidator; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static org.apache.solr.common.SolrException.ErrorCode.SERVER_ERROR; |
| |
| public abstract class SecurityConfHandler extends RequestHandlerBase implements PermissionNameProvider { |
| private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); |
| protected 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 { |
| RequestHandlerUtils.setWt(req, CommonParams.JSON); |
| String httpMethod = (String) req.getContext().get("httpMethod"); |
| String path = (String) req.getContext().get("path"); |
| String key = path.substring(path.lastIndexOf('/')+1); |
| if ("GET".equals(httpMethod)) { |
| getConf(rsp, key); |
| } else if ("POST".equals(httpMethod)) { |
| Object plugin = getPlugin(key); |
| doEdit(req, rsp, path, key, plugin); |
| } |
| } |
| |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| private void doEdit(SolrQueryRequest req, SolrQueryResponse rsp, String path, final String key, final Object plugin) |
| throws IOException { |
| ConfigEditablePlugin configEditablePlugin = null; |
| |
| if (plugin == null) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No " + key + " plugin configured"); |
| } |
| if (plugin instanceof ConfigEditablePlugin) { |
| configEditablePlugin = (ConfigEditablePlugin) plugin; |
| } else { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, key + " plugin is not editable"); |
| } |
| |
| if (req.getContentStreams() == null) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No contentStream"); |
| } |
| List<CommandOperation> ops = CommandOperation.readCommands(req.getContentStreams(), rsp.getValues()); |
| if (ops == null) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No commands"); |
| } |
| for (int count = 1; count <= 3 ; count++ ) { |
| SecurityConfig securityConfig = getSecurityConfig(true); |
| Map<String, Object> data = securityConfig.getData(); |
| Map<String, Object> latestConf = (Map<String, Object>) data.get(key); |
| if (latestConf == null) { |
| throw new SolrException(SERVER_ERROR, "No configuration present for " + key); |
| } |
| List<CommandOperation> commandsCopy = CommandOperation.clone(ops); |
| Map<String, Object> out = configEditablePlugin.edit(Utils.getDeepCopy(latestConf, 4) , commandsCopy); |
| if (out == null) { |
| List<Map> errs = CommandOperation.captureErrors(commandsCopy); |
| if (!errs.isEmpty()) { |
| rsp.add(CommandOperation.ERR_MSGS, errs); |
| return; |
| } |
| log.debug("No edits made"); |
| return; |
| } else { |
| if(!Objects.equals(latestConf.get("class") , out.get("class"))){ |
| throw new SolrException(SERVER_ERROR, "class cannot be modified"); |
| } |
| Map meta = getMapValue(out, ""); |
| meta.put("v", securityConfig.getVersion()+1);//encode the expected zkversion |
| data.put(key, out); |
| |
| if(persistConf(securityConfig)) { |
| securityConfEdited(); |
| return; |
| } |
| } |
| log.debug("Security edit operation failed {} time(s)", count); |
| } |
| throw new SolrException(SERVER_ERROR, "Failed to persist security config after 3 attempts. Giving up"); |
| } |
| |
| /** |
| * Hook where you can do stuff after a config has been edited. Defaults to NOP |
| */ |
| protected void securityConfEdited() {} |
| |
| Object getPlugin(String key) { |
| Object plugin = null; |
| if ("authentication".equals(key)) plugin = cores.getAuthenticationPlugin(); |
| if ("authorization".equals(key)) plugin = cores.getAuthorizationPlugin(); |
| return plugin; |
| } |
| |
| protected abstract void getConf(SolrQueryResponse rsp, String key); |
| |
| public static Map<String, Object> getMapValue(Map<String, Object> lookupMap, String key) { |
| @SuppressWarnings({"unchecked"}) |
| Map<String, Object> m = (Map<String, Object>) lookupMap.get(key); |
| if (m == null) lookupMap.put(key, m = new LinkedHashMap<>()); |
| return m; |
| } |
| |
| @SuppressWarnings({"rawtypes"}) |
| public static List getListValue(Map<String, Object> lookupMap, String key) { |
| List l = (List) lookupMap.get(key); |
| if (l == null) lookupMap.put(key, l= new ArrayList<>()); |
| return l; |
| } |
| |
| @Override |
| public String getDescription() { |
| return "Edit or read security configuration"; |
| } |
| |
| @Override |
| public Category getCategory() { |
| return Category.ADMIN; |
| } |
| |
| /** |
| * Gets security.json from source |
| */ |
| public abstract SecurityConfig getSecurityConfig(boolean getFresh); |
| |
| /** |
| * Persist security.json to the source, optionally with a version |
| */ |
| protected abstract boolean persistConf(SecurityConfig securityConfig) throws IOException; |
| |
| /** |
| * Object to hold security.json as nested <code>Map<String,Object></code> and optionally its version. |
| * The version property is optional and defaults to -1 if not initialized. |
| * The data object defaults to EMPTY_MAP if not set |
| */ |
| public static class SecurityConfig { |
| @SuppressWarnings({"unchecked"}) |
| private Map<String, Object> data = Collections.EMPTY_MAP; |
| private int version = -1; |
| |
| public SecurityConfig() {} |
| |
| /** |
| * Sets the data as a Map |
| * @param data a Map |
| * @return SecurityConf object (builder pattern) |
| */ |
| public SecurityConfig setData(Map<String, Object> data) { |
| this.data = data; |
| return this; |
| } |
| |
| /** |
| * Sets the data as an Object, but the object needs to be of type Map |
| * @param data an Object of type Map<String,Object> |
| * @return SecurityConf object (builder pattern) |
| */ |
| @SuppressWarnings({"unchecked"}) |
| public SecurityConfig setData(Object data) { |
| if (data instanceof Map) { |
| this.data = (Map<String, Object>) data; |
| return this; |
| } else { |
| throw new SolrException(SERVER_ERROR, "Illegal format when parsing security.json, not object"); |
| } |
| } |
| |
| /** |
| * Sets version |
| * @param version integer for version. Depends on underlying storage |
| * @return SecurityConf object (builder pattern) |
| */ |
| public SecurityConfig setVersion(int version) { |
| this.version = version; |
| return this; |
| } |
| |
| public Map<String, Object> getData() { |
| return data; |
| } |
| |
| public int getVersion() { |
| return version; |
| } |
| |
| /** |
| * Set data from input stream |
| * @param securityJsonInputStream an input stream for security.json |
| * @return this (builder pattern) |
| */ |
| public SecurityConfig setData(InputStream securityJsonInputStream) { |
| return setData(Utils.fromJSON(securityJsonInputStream)); |
| } |
| |
| public String toString() { |
| return "SecurityConfig: version=" + version + ", data=" + Utils.toJSONString(data); |
| } |
| } |
| |
| private Collection<Api> apis; |
| private AuthenticationPlugin authcPlugin; |
| private AuthorizationPlugin authzPlugin; |
| |
| @Override |
| public Collection<Api> getApis() { |
| if (apis == null) { |
| synchronized (this) { |
| if (apis == null) { |
| Collection<Api> apis = new ArrayList<>(); |
| final SpecProvider authcCommands = Utils.getSpec("cluster.security.authentication.Commands"); |
| final SpecProvider authzCommands = Utils.getSpec("cluster.security.authorization.Commands"); |
| apis.add(new ReqHandlerToApi(this, Utils.getSpec("cluster.security.authentication"))); |
| apis.add(new ReqHandlerToApi(this, Utils.getSpec("cluster.security.authorization"))); |
| SpecProvider authcSpecProvider = () -> { |
| AuthenticationPlugin authcPlugin = cores.getAuthenticationPlugin(); |
| return authcPlugin != null && authcPlugin instanceof SpecProvider ? |
| ((SpecProvider) authcPlugin).getSpec() : |
| authcCommands.getSpec(); |
| }; |
| |
| apis.add(new ReqHandlerToApi(this, authcSpecProvider) { |
| @Override |
| public synchronized Map<String, JsonSchemaValidator> getCommandSchema() { |
| //it is possible that the Authentication plugin is modified since the last call. invalidate the |
| // the cached commandSchema |
| if(SecurityConfHandler.this.authcPlugin != cores.getAuthenticationPlugin()) commandSchema = null; |
| SecurityConfHandler.this.authcPlugin = cores.getAuthenticationPlugin(); |
| return super.getCommandSchema(); |
| } |
| }); |
| |
| SpecProvider authzSpecProvider = () -> { |
| AuthorizationPlugin authzPlugin = cores.getAuthorizationPlugin(); |
| return authzPlugin != null && authzPlugin instanceof SpecProvider ? |
| ((SpecProvider) authzPlugin).getSpec() : |
| authzCommands.getSpec(); |
| }; |
| apis.add(new ApiBag.ReqHandlerToApi(this, authzSpecProvider) { |
| @Override |
| public synchronized Map<String, JsonSchemaValidator> getCommandSchema() { |
| //it is possible that the Authorization plugin is modified since the last call. invalidate the |
| // the cached commandSchema |
| if(SecurityConfHandler.this.authzPlugin != cores.getAuthorizationPlugin()) commandSchema = null; |
| SecurityConfHandler.this.authzPlugin = cores.getAuthorizationPlugin(); |
| return super.getCommandSchema(); |
| } |
| }); |
| |
| this.apis = ImmutableList.copyOf(apis); |
| } |
| } |
| } |
| return this.apis; |
| } |
| |
| @Override |
| public Boolean registerV2() { |
| return Boolean.TRUE; |
| } |
| } |
| |