diff --git a/.gitignore b/.gitignore index aa7ca99..6fd04f7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ data/ puppet/.vagrant test.sh .vagrant/ +.idea/ +*.iml diff --git a/pom.xml b/pom.xml index c7f8a29..b79d708 100755 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ opendistro_security_advanced_modules - 0.7.0.1 + 0.7.0.2 jar Open Distro Security Advanced Modules for Elasticsearch @@ -34,7 +34,7 @@ 2016 - 0.7.0.1 + 0.7.0.2 6.5.4 @@ -55,7 +55,7 @@ https://github.com/opendistro-for-elasticsearch/security-advanced-modules scm:git:git@github.com:opendistro-for-elasticsearch/security-advanced-modules.git scm:git:git@github.com:opendistro-for-elasticsearch/security-advanced-modules.git - v0.7.0.1 + v0.7.0.2 diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/dlic/rest/api/OpenDistroSecurityConfigAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/dlic/rest/api/OpenDistroSecurityConfigAction.java index 94e82ae..eb7a7a2 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/security/dlic/rest/api/OpenDistroSecurityConfigAction.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/dlic/rest/api/OpenDistroSecurityConfigAction.java @@ -15,6 +15,7 @@ package com.amazon.opendistroforelasticsearch.security.dlic.rest.api; +import java.io.IOException; import java.nio.file.Path; import org.elasticsearch.client.Client; @@ -23,60 +24,91 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.rest.RestRequest.Method; import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestRequest.Method; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.threadpool.ThreadPool; import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog; import com.amazon.opendistroforelasticsearch.security.configuration.AdminDNs; import com.amazon.opendistroforelasticsearch.security.configuration.IndexBaseConfigurationRepository; +import com.amazon.opendistroforelasticsearch.security.dlic.rest.support.Utils; import com.amazon.opendistroforelasticsearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import com.amazon.opendistroforelasticsearch.security.dlic.rest.validation.NoOpValidator; +import com.amazon.opendistroforelasticsearch.security.dlic.rest.validation.SecurityConfigValidator; import com.amazon.opendistroforelasticsearch.security.privileges.PrivilegesEvaluator; import com.amazon.opendistroforelasticsearch.security.ssl.transport.PrincipalExtractor; import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; -public class OpenDistroSecurityConfigAction extends AbstractApiAction { +public class OpenDistroSecurityConfigAction extends PatchableResourceApiAction { + + private final boolean allowPutOrPatch; @Inject public OpenDistroSecurityConfigAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final IndexBaseConfigurationRepository cl, final ClusterService cs, - final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { + final AdminDNs adminDNs, final IndexBaseConfigurationRepository cl, final ClusterService cs, final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - controller.registerHandler(Method.GET, "/_opendistro/_security/api/config/", this); - } + allowPutOrPatch = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, false); + controller.registerHandler(Method.GET, "/_opendistro/_security/api/securityconfig/", this); + + if (allowPutOrPatch) { + controller.registerHandler(Method.PUT, "/_opendistro/_security/api/securityconfig/{name}", this); + controller.registerHandler(Method.PATCH, "/_opendistro/_security/api/securityconfig/", this); + } + + } @Override - protected Tuple handleGet(RestRequest request, Client client, - final Settings.Builder additionalSettingsBuilder) throws Throwable { + protected Tuple handleApiRequest(RestRequest request, Client client) throws Throwable { + if (request.method() == Method.PATCH && !allowPutOrPatch) { + return notImplemented(Method.PATCH); + } else { + return super.handleApiRequest(request, client); + } + } + @Override + protected Tuple handleGet(RestRequest request, Client client, final Settings.Builder additionalSettingsBuilder) throws IOException { final Settings configurationSettings = loadAsSettings(getConfigName(), true); - return new Tuple(new String[0], new BytesRestResponse(RestStatus.OK, convertToJson(configurationSettings))); } @Override - protected Tuple handlePut(final RestRequest request, final Client client, - final Settings.Builder additionalSettings) throws Throwable { - return notImplemented(Method.PUT); + protected Tuple handlePut(final RestRequest request, final Client client, final Settings.Builder additionalSettings) + throws Throwable { + if (allowPutOrPatch) { + if (!"opendistro_security".equals(request.param("name"))) { + return badRequestResponse("name must be opendistro_security"); + } + return super.handlePut(request, client, additionalSettings); + } else { + return notImplemented(Method.PUT); + } } @Override - protected Tuple handleDelete(final RestRequest request, final Client client, - final Settings.Builder additionalSettings) throws Throwable { + protected Tuple handleDelete(final RestRequest request, final Client client, final Settings.Builder additionalSettings) { return notImplemented(Method.DELETE); } + @Override + protected Tuple handlePost(final RestRequest request, final Client client, final Settings.Builder additionalSetting) + throws IOException { + return notImplemented(Method.POST); + } + @Override protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new NoOpValidator(request, ref, this.settings, param); + return new SecurityConfigValidator(request, ref, this.settings, param); } @Override diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/dlic/rest/validation/SecurityConfigValidator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/dlic/rest/validation/SecurityConfigValidator.java new file mode 100644 index 0000000..a60ab77 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/dlic/rest/validation/SecurityConfigValidator.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.dlic.rest.validation; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.RestRequest; + +public class SecurityConfigValidator extends AbstractConfigurationValidator { + + public SecurityConfigValidator(final RestRequest request, BytesReference ref, final Settings esSettings, Object... param) { + super(request, ref, esSettings, param); + this.payloadMandatory = true; + allowedKeys.put("dynamic", DataType.OBJECT); + } + +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/security/dlic/rest/api/SecurityConfigApiTest.java b/src/test/java/com/amazon/opendistroforelasticsearch/security/dlic/rest/api/SecurityConfigApiTest.java new file mode 100644 index 0000000..59d9611 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/security/dlic/rest/api/SecurityConfigApiTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.dlic.rest.api; + +import org.apache.http.Header; +import org.apache.http.HttpStatus; +import org.elasticsearch.common.settings.Settings; +import org.junit.Assert; +import org.junit.Test; + +import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants; +import com.amazon.opendistroforelasticsearch.security.test.helper.file.FileHelper; +import com.amazon.opendistroforelasticsearch.security.test.helper.rest.RestHelper.HttpResponse; + +public class SecurityConfigApiTest extends AbstractRestApiUnitTest { + + @Test + public void testSecurityConfigApiRead() throws Exception { + + setup(); + + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendHTTPClientCertificate = true; + + HttpResponse response = rh.executeGetRequest("/_opendistro/_security/api/securityconfig", new Header[0]); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + + response = rh.executePutRequest("/_opendistro/_security/api/securityconfig", "{\"xxx\": 1}", new Header[0]); + Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); + + response = rh.executePostRequest("/_opendistro/_security/api/securityconfig", "{\"xxx\": 1}", new Header[0]); + Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); + + response = rh.executePatchRequest("/_opendistro/_security/api/securityconfig", "{\"xxx\": 1}", new Header[0]); + Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); + + response = rh.executeDeleteRequest("/_opendistro/_security/api/securityconfig", new Header[0]); + Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); + + } + + @Test + public void testSecurityConfigApiWrite() throws Exception { + + Settings settings = Settings.builder().put(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).build(); + setup(settings); + + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendHTTPClientCertificate = true; + + HttpResponse response = rh.executeGetRequest("/_opendistro/_security/api/securityconfig", new Header[0]); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + + response = rh.executePutRequest("/_opendistro/_security/api/securityconfig/opendistro_security_xxx", FileHelper.loadFile("restapi/securityconfig.json"), new Header[0]); + Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + response = rh.executePutRequest("/_opendistro/_security/api/securityconfig/opendistro_security", FileHelper.loadFile("restapi/securityconfig.json"), new Header[0]); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + + response = rh.executeGetRequest("/_opendistro/_security/api/securityconfig", new Header[0]); + System.out.println(response.getBody()); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + + response = rh.executePostRequest("/_opendistro/_security/api/securityconfig", "{\"xxx\": 1}", new Header[0]); + Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); + + response = rh.executePatchRequest("/_opendistro/_security/api/securityconfig", "[{\"op\": \"replace\",\"path\": \"/opendistro_security/dynamic/hosts_resolver_mode\",\"value\": \"other\"}]", new Header[0]); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + + response = rh.executeDeleteRequest("/_opendistro/_security/api/securityconfig", new Header[0]); + Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); + + } +} diff --git a/src/test/resources/restapi/securityconfig.json b/src/test/resources/restapi/securityconfig.json new file mode 100644 index 0000000..883677d --- /dev/null +++ b/src/test/resources/restapi/securityconfig.json @@ -0,0 +1,129 @@ +{ + "dynamic":{ + "filtered_alias_mode":"warn", + "disable_rest_auth": false, + "disable_intertransport_auth":false, + "respect_request_indices_options":false, + "kibana":{ + "multitenancy_enabled":true, + "server_username":"kibanaserver", + "index":".kibana" + }, + "http":{ + "anonymous_auth_enabled":false, + "xff":{ + "enabled":false, + "internalProxies":"192\\.168\\.0\\.10|192\\.168\\.0\\.11", + "remoteIpHeader":"x-forwarded-for", + "proxiesHeader":"x-forwarded-by", + "trustedProxies":"proxy1|proxy2" + } + }, + "authc":{ + "authentication_domain_kerb":{ + "http_enabled":false, + "transport_enabled":false, + "order":3, + "http_authenticator":{ + "challenge":true, + "type":"kerberos", + "config":{ + + } + }, + "authentication_backend":{ + "type":"noop", + "config":{ + + } + } + }, + "authentication_domain_clientcert":{ + "http_enabled":false, + "transport_enabled":false, + "order":1, + "http_authenticator":{ + "challenge":true, + "type":"clientcert", + "config":{ + + } + }, + "authentication_backend":{ + "type":"noop", + "config":{ + + } + } + }, + "authentication_domain_proxy":{ + "http_enabled":false, + "transport_enabled":false, + "order":2, + "http_authenticator":{ + "challenge":true, + "type":"proxy", + "config":{ + "user_header":"x-proxy-user", + "roles_header":"x-proxy-roles" + } + }, + "authentication_backend":{ + "type":"noop", + "config":{ + + } + } + }, + "authentication_domain_basic_internal":{ + "http_enabled":true, + "transport_enabled":true, + "order":0, + "http_authenticator":{ + "challenge":true, + "type":"basic", + "config":{ + + } + }, + "authentication_backend":{ + "type":"intern", + "config":{ + + } + } + } + }, + "authz":{ + "roles_from_xxx":{ + "http_enabled":false, + "transport_enabled":false, + "order":0, + "authorization_backend":{ + "type":"xxx", + "config":{ + + } + } + }, + "roles_from_myldap":{ + "http_enabled":false, + "transport_enabled":false, + "order":0, + "authorization_backend":{ + "type":"ldap", + "config":{ + "rolesearch":"(uniqueMember={0})", + "resolve_nested_roles":true, + "rolebase":"ou=groups,o=TEST", + "rolename":"cn" + } + } + } + }, + "do_not_fail_on_forbidden":false, + "multi_rolespan_enabled":false, + "hosts_resolver_mode":"ip-only" + } + +}