From 3280607410d67a53dbc39447e915d1a61d373f54 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Tue, 29 Jan 2019 14:17:37 -0700 Subject: [PATCH] Allow authorization engines as an extension (#37785) Authorization engines can now be registered by implementing a plugin, which also has a service implementation of a security extension. Only one extension may register an authorization engine and this engine will be used for all users except reserved realm users and internal users. --- build.gradle | 3 + .../build.gradle | 46 +++++ .../example/AuthorizationEnginePlugin.java | 30 +++ .../example/CustomAuthorizationEngine.java | 131 +++++++++++++ .../ExampleAuthorizationEngineExtension.java | 35 ++++ ...arch.xpack.core.security.SecurityExtension | 1 + .../example/CustomAuthorizationEngineIT.java | 163 ++++++++++++++++ .../CustomAuthorizationEngineTests.java | 177 ++++++++++++++++++ x-pack/docs/build.gradle | 1 - ...asciidoc => custom-authorization.asciidoc} | 91 ++++++--- .../authorization/managing-roles.asciidoc | 2 +- .../core/security/SecurityExtension.java | 13 ++ .../xpack/security/Security.java | 22 ++- .../security/authz/AuthorizationService.java | 33 ++-- .../authz/AuthorizationServiceTests.java | 79 +++++++- .../build.gradle | 2 +- 16 files changed, 781 insertions(+), 48 deletions(-) create mode 100644 plugins/examples/security-authorization-engine/build.gradle create mode 100644 plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/AuthorizationEnginePlugin.java create mode 100644 plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java create mode 100644 plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/ExampleAuthorizationEngineExtension.java create mode 100644 plugins/examples/security-authorization-engine/src/main/resources/META-INF/services/org.elasticsearch.xpack.core.security.SecurityExtension create mode 100644 plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineIT.java create mode 100644 plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java rename x-pack/docs/en/security/authorization/{custom-roles-provider.asciidoc => custom-authorization.asciidoc} (51%) diff --git a/build.gradle b/build.gradle index 4bd211a12b3b0..cd9aed4a259d3 100644 --- a/build.gradle +++ b/build.gradle @@ -232,6 +232,9 @@ allprojects { "org.elasticsearch.plugin:aggs-matrix-stats-client:${version}": ':modules:aggs-matrix-stats', "org.elasticsearch.plugin:percolator-client:${version}": ':modules:percolator', "org.elasticsearch.plugin:rank-eval-client:${version}": ':modules:rank-eval', + // for security example plugins + "org.elasticsearch.plugin:x-pack-core:${version}": ':x-pack:plugin:core', + "org.elasticsearch.client.x-pack-transport:${version}": ':x-pack:transport-client' ] /* diff --git a/plugins/examples/security-authorization-engine/build.gradle b/plugins/examples/security-authorization-engine/build.gradle new file mode 100644 index 0000000000000..d0d227e221b68 --- /dev/null +++ b/plugins/examples/security-authorization-engine/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'elasticsearch.esplugin' + +esplugin { + name 'security-authorization-engine' + description 'An example spi extension plugin for security that implements an Authorization Engine' + classname 'org.elasticsearch.example.AuthorizationEnginePlugin' + extendedPlugins = ['x-pack-security'] +} + +dependencies { + compileOnly "org.elasticsearch.plugin:x-pack-core:${version}" + testCompile "org.elasticsearch.client.x-pack-transport:${version}" +} + + +integTestRunner { + systemProperty 'tests.security.manager', 'false' +} + +integTestCluster { + dependsOn buildZip + setting 'xpack.security.enabled', 'true' + setting 'xpack.ilm.enabled', 'false' + setting 'xpack.ml.enabled', 'false' + setting 'xpack.monitoring.enabled', 'false' + setting 'xpack.license.self_generated.type', 'trial' + + // This is important, so that all the modules are available too. + // There are index templates that use token filters that are in analysis-module and + // processors are being used that are in ingest-common module. + distribution = 'default' + + setupCommand 'setupDummyUser', + 'bin/elasticsearch-users', 'useradd', 'test_user', '-p', 'x-pack-test-password', '-r', 'custom_superuser' + waitCondition = { node, ant -> + File tmpFile = new File(node.cwd, 'wait.success') + ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow", + dest: tmpFile.toString(), + username: 'test_user', + password: 'x-pack-test-password', + ignoreerrors: true, + retries: 10) + return tmpFile.exists() + } +} +check.dependsOn integTest diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/AuthorizationEnginePlugin.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/AuthorizationEnginePlugin.java new file mode 100644 index 0000000000000..1878bb90a0c85 --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/AuthorizationEnginePlugin.java @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.example; + +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.Plugin; + +/** + * Plugin class that is required so that the code contained here may be loaded as a plugin. + * Additional items such as settings and actions can be registered using this plugin class. + */ +public class AuthorizationEnginePlugin extends Plugin implements ActionPlugin { +} diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java new file mode 100644 index 0000000000000..84f9fddf738da --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java @@ -0,0 +1,131 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.example; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.metadata.AliasOrIndex; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl.IndexAccessControl; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * A custom implementation of an authorization engine. This engine is extremely basic in that it + * authorizes based upon the name of a single role. If users have this role they are granted access. + */ +public class CustomAuthorizationEngine implements AuthorizationEngine { + + @Override + public void resolveAuthorizationInfo(RequestInfo requestInfo, ActionListener listener) { + final Authentication authentication = requestInfo.getAuthentication(); + if (authentication.getUser().isRunAs()) { + final CustomAuthorizationInfo authenticatedUserAuthzInfo = + new CustomAuthorizationInfo(authentication.getUser().authenticatedUser().roles(), null); + listener.onResponse(new CustomAuthorizationInfo(authentication.getUser().roles(), authenticatedUserAuthzInfo)); + } else { + listener.onResponse(new CustomAuthorizationInfo(authentication.getUser().roles(), null)); + } + } + + @Override + public void authorizeRunAs(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, ActionListener listener) { + if (isSuperuser(requestInfo.getAuthentication().getUser().authenticatedUser())) { + listener.onResponse(AuthorizationResult.granted()); + } else { + listener.onResponse(AuthorizationResult.deny()); + } + } + + @Override + public void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + ActionListener listener) { + if (isSuperuser(requestInfo.getAuthentication().getUser())) { + listener.onResponse(AuthorizationResult.granted()); + } else { + listener.onResponse(AuthorizationResult.deny()); + } + } + + @Override + public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + AsyncSupplier indicesAsyncSupplier, + Function aliasOrIndexFunction, + ActionListener listener) { + if (isSuperuser(requestInfo.getAuthentication().getUser())) { + indicesAsyncSupplier.getAsync(ActionListener.wrap(resolvedIndices -> { + Map indexAccessControlMap = new HashMap<>(); + for (String name : resolvedIndices.getLocal()) { + indexAccessControlMap.put(name, new IndexAccessControl(true, FieldPermissions.DEFAULT, null)); + } + IndicesAccessControl indicesAccessControl = + new IndicesAccessControl(true, Collections.unmodifiableMap(indexAccessControlMap)); + listener.onResponse(new IndexAuthorizationResult(true, indicesAccessControl)); + }, listener::onFailure)); + } else { + listener.onResponse(new IndexAuthorizationResult(true, IndicesAccessControl.DENIED)); + } + } + + @Override + public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + Map aliasAndIndexLookup, ActionListener> listener) { + if (isSuperuser(requestInfo.getAuthentication().getUser())) { + listener.onResponse(new ArrayList<>(aliasAndIndexLookup.keySet())); + } else { + listener.onResponse(Collections.emptyList()); + } + } + + public static class CustomAuthorizationInfo implements AuthorizationInfo { + + private final String[] roles; + private final CustomAuthorizationInfo authenticatedAuthzInfo; + + CustomAuthorizationInfo(String[] roles, CustomAuthorizationInfo authenticatedAuthzInfo) { + this.roles = roles; + this.authenticatedAuthzInfo = authenticatedAuthzInfo; + } + + @Override + public Map asMap() { + return Collections.singletonMap("roles", roles); + } + + @Override + public CustomAuthorizationInfo getAuthenticatedUserAuthorizationInfo() { + return authenticatedAuthzInfo; + } + } + + private boolean isSuperuser(User user) { + return Arrays.binarySearch(user.roles(), "custom_superuser") > -1; + } +} diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/ExampleAuthorizationEngineExtension.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/ExampleAuthorizationEngineExtension.java new file mode 100644 index 0000000000000..cba064fae27ad --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/ExampleAuthorizationEngineExtension.java @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.example; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.core.security.SecurityExtension; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; + +/** + * Security extension class that registers the custom authorization engine to be used + */ +public class ExampleAuthorizationEngineExtension implements SecurityExtension { + + @Override + public AuthorizationEngine getAuthorizationEngine(Settings settings) { + return new CustomAuthorizationEngine(); + } +} diff --git a/plugins/examples/security-authorization-engine/src/main/resources/META-INF/services/org.elasticsearch.xpack.core.security.SecurityExtension b/plugins/examples/security-authorization-engine/src/main/resources/META-INF/services/org.elasticsearch.xpack.core.security.SecurityExtension new file mode 100644 index 0000000000000..73029aef8fd63 --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/main/resources/META-INF/services/org.elasticsearch.xpack.core.security.SecurityExtension @@ -0,0 +1 @@ +org.elasticsearch.example.ExampleAuthorizationEngineExtension \ No newline at end of file diff --git a/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineIT.java b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineIT.java new file mode 100644 index 0000000000000..9daf9bd01a8bc --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineIT.java @@ -0,0 +1,163 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.example; + +import org.apache.http.util.EntityUtils; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.xpack.core.XPackClientPlugin; +import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.core.security.client.SecurityClient; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; + +import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +/** + * Integration tests for the custom authorization engine. These tests are meant to be run against + * an external cluster with the custom authorization plugin installed to validate the functionality + * when running as a plugin + */ +public class CustomAuthorizationEngineIT extends ESIntegTestCase { + + @Override + protected Settings externalClusterClientSettings() { + final String token = "Basic " + + Base64.getEncoder().encodeToString(("test_user:x-pack-test-password").getBytes(StandardCharsets.UTF_8)); + return Settings.builder() + .put(ThreadContext.PREFIX + ".Authorization", token) + .put(NetworkModule.TRANSPORT_TYPE_KEY, "security4") + .build(); + } + + @Override + protected Collection> transportClientPlugins() { + return Collections.singleton(XPackClientPlugin.class); + } + + public void testClusterAction() throws IOException { + SecurityClient securityClient = new SecurityClient(client()); + securityClient.preparePutUser("custom_user", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "custom_superuser").get(); + + { + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user", new SecureString("x-pack-test-password".toCharArray()))); + Request request = new Request("GET", "_cluster/health"); + request.setOptions(options); + Response response = getRestClient().performRequest(request); + assertThat(response.getStatusLine().getStatusCode(), is(200)); + } + + { + securityClient.preparePutUser("custom_user2", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "not_superuser").get(); + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user2", new SecureString("x-pack-test-password".toCharArray()))); + Request request = new Request("GET", "_cluster/health"); + request.setOptions(options); + ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(request)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), is(403)); + } + } + + public void testIndexAction() throws IOException { + SecurityClient securityClient = new SecurityClient(client()); + securityClient.preparePutUser("custom_user", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "custom_superuser").get(); + + { + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user", new SecureString("x-pack-test-password".toCharArray()))); + Request request = new Request("PUT", "/index"); + request.setOptions(options); + Response response = getRestClient().performRequest(request); + assertThat(response.getStatusLine().getStatusCode(), is(200)); + } + + { + securityClient.preparePutUser("custom_user2", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "not_superuser").get(); + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user2", new SecureString("x-pack-test-password".toCharArray()))); + Request request = new Request("PUT", "/index"); + request.setOptions(options); + ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(request)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), is(403)); + } + } + + public void testRunAs() throws IOException { + SecurityClient securityClient = new SecurityClient(client()); + securityClient.preparePutUser("custom_user", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "custom_superuser").get(); + securityClient.preparePutUser("custom_user2", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "custom_superuser").get(); + securityClient.preparePutUser("custom_user3", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "not_superuser").get(); + + { + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user", new SecureString("x-pack-test-password".toCharArray()))); + options.addHeader("es-security-runas-user", "custom_user2"); + Request request = new Request("GET", "/_security/_authenticate"); + request.setOptions(options); + Response response = getRestClient().performRequest(request); + assertThat(response.getStatusLine().getStatusCode(), is(200)); + String responseStr = EntityUtils.toString(response.getEntity()); + assertThat(responseStr, containsString("custom_user2")); + } + + { + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user", new SecureString("x-pack-test-password".toCharArray()))); + options.addHeader("es-security-runas-user", "custom_user3"); + Request request = new Request("PUT", "/index"); + request.setOptions(options); + ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(request)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), is(403)); + } + + { + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user3", new SecureString("x-pack-test-password".toCharArray()))); + options.addHeader("es-security-runas-user", "custom_user2"); + Request request = new Request("PUT", "/index"); + request.setOptions(options); + ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(request)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), is(403)); + } + } +} diff --git a/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java new file mode 100644 index 0000000000000..e24e490767988 --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java @@ -0,0 +1,177 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.example; + +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.metadata.AliasOrIndex.Index; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationResult; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.IndexAuthorizationResult; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; +import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.Collections; + +import static org.hamcrest.Matchers.is; + +/** + * Unit tests for the custom authorization engine. These are basic tests that validate the + * engine's functionality outside of being used by the AuthorizationService + */ +public class CustomAuthorizationEngineTests extends ESTestCase { + + public void testGetAuthorizationInfo() { + PlainActionFuture future = new PlainActionFuture<>(); + CustomAuthorizationEngine engine = new CustomAuthorizationEngine(); + engine.resolveAuthorizationInfo(getRequestInfo(), future); + assertNotNull(future.actionGet()); + } + + public void testAuthorizeRunAs() { + final String action = "cluster:monitor/foo"; + final TransportRequest request = new TransportRequest() {}; + CustomAuthorizationEngine engine = new CustomAuthorizationEngine(); + // unauthorized + { + Authentication authentication = + new Authentication(new User("joe", new String[]{"custom_superuser"}, new User("bar", "not_superuser")), + new RealmRef("test", "test", "node"), new RealmRef("test", "test", "node")); + RequestInfo info = new RequestInfo(authentication, request, action); + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(info, future); + AuthorizationInfo authzInfo = future.actionGet(); + + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeRunAs(info, authzInfo, resultFuture); + AuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(false)); + assertThat(result.isAuditable(), is(true)); + } + + // authorized + { + Authentication authentication = + new Authentication(new User("joe", new String[]{"not_superuser"}, new User("bar", "custom_superuser")), + new RealmRef("test", "test", "node"), new RealmRef("test", "test", "node")); + RequestInfo info = new RequestInfo(authentication, request, action); + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(info, future); + AuthorizationInfo authzInfo = future.actionGet(); + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeRunAs(info, authzInfo, resultFuture); + AuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(true)); + assertThat(result.isAuditable(), is(true)); + } + } + + public void testAuthorizeClusterAction() { + CustomAuthorizationEngine engine = new CustomAuthorizationEngine(); + RequestInfo requestInfo = getRequestInfo(); + // authorized + { + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(requestInfo, future); + AuthorizationInfo authzInfo = future.actionGet(); + + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeClusterAction(requestInfo, authzInfo, resultFuture); + AuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(true)); + assertThat(result.isAuditable(), is(true)); + } + + // unauthorized + { + RequestInfo unauthReqInfo = + new RequestInfo(new Authentication(new User("joe", "not_superuser"), new RealmRef("test", "test", "node"), null), + requestInfo.getRequest(), requestInfo.getAction()); + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(unauthReqInfo, future); + AuthorizationInfo authzInfo = future.actionGet(); + + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeClusterAction(unauthReqInfo, authzInfo, resultFuture); + AuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(false)); + assertThat(result.isAuditable(), is(true)); + } + } + + public void testAuthorizeIndexAction() { + CustomAuthorizationEngine engine = new CustomAuthorizationEngine(); + // authorized + { + RequestInfo requestInfo = + new RequestInfo(new Authentication(new User("joe", "custom_superuser"), new RealmRef("test", "test", "node"), null), + new SearchRequest(), "indices:data/read/search"); + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(requestInfo, future); + AuthorizationInfo authzInfo = future.actionGet(); + + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeIndexAction(requestInfo, authzInfo, + listener -> listener.onResponse(new ResolvedIndices(Collections.singletonList("index"), Collections.emptyList())), + name -> name.equals("index") ? new Index(IndexMetaData.builder("index").build()) : null, resultFuture); + IndexAuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(true)); + assertThat(result.isAuditable(), is(true)); + IndicesAccessControl indicesAccessControl = result.getIndicesAccessControl(); + assertNotNull(indicesAccessControl.getIndexPermissions("index")); + assertThat(indicesAccessControl.getIndexPermissions("index").isGranted(), is(true)); + } + + // unauthorized + { + RequestInfo requestInfo = + new RequestInfo(new Authentication(new User("joe", "not_superuser"), new RealmRef("test", "test", "node"), null), + new SearchRequest(), "indices:data/read/search"); + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(requestInfo, future); + AuthorizationInfo authzInfo = future.actionGet(); + + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeIndexAction(requestInfo, authzInfo, + listener -> listener.onResponse(new ResolvedIndices(Collections.singletonList("index"), Collections.emptyList())), + name -> name.equals("index") ? new Index(IndexMetaData.builder("index").build()) : null, resultFuture); + IndexAuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(false)); + assertThat(result.isAuditable(), is(true)); + IndicesAccessControl indicesAccessControl = result.getIndicesAccessControl(); + assertNull(indicesAccessControl.getIndexPermissions("index")); + } + } + + private RequestInfo getRequestInfo() { + final String action = "cluster:monitor/foo"; + final TransportRequest request = new TransportRequest() {}; + final Authentication authentication = + new Authentication(new User("joe", "custom_superuser"), new RealmRef("test", "test", "node"), null); + return new RequestInfo(authentication, request, action); + } +} diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle index de2400c0e85f0..a818d5ac8eed4 100644 --- a/x-pack/docs/build.gradle +++ b/x-pack/docs/build.gradle @@ -13,7 +13,6 @@ buildRestTests.expectedUnconvertedCandidates = [ 'en/security/authentication/user-cache.asciidoc', 'en/security/authorization/run-as-privilege.asciidoc', 'en/security/ccs-clients-integrations/http.asciidoc', - 'en/security/authorization/custom-roles-provider.asciidoc', 'en/rest-api/watcher/stats.asciidoc', 'en/watcher/example-watches/watching-time-series-data.asciidoc', ] diff --git a/x-pack/docs/en/security/authorization/custom-roles-provider.asciidoc b/x-pack/docs/en/security/authorization/custom-authorization.asciidoc similarity index 51% rename from x-pack/docs/en/security/authorization/custom-roles-provider.asciidoc rename to x-pack/docs/en/security/authorization/custom-authorization.asciidoc index bb8942985b701..735fb26cc58a3 100644 --- a/x-pack/docs/en/security/authorization/custom-roles-provider.asciidoc +++ b/x-pack/docs/en/security/authorization/custom-authorization.asciidoc @@ -1,23 +1,23 @@ [role="xpack"] -[[custom-roles-provider]] -=== Custom roles provider extension +[[custom-roles-authorization]] +=== Customizing roles and authorization If you need to retrieve user roles from a system not supported out-of-the-box -by the {es} {security-features}, you can create a custom roles provider to -retrieve and resolve -roles. You implement a custom roles provider as an SPI loaded security extension -as part of an ordinary elasticsearch plugin. +or if the authorization system that is provided by the {es} {security-features} +does not meet your needs, a SPI loaded security extension can be implemented to +customize role retrieval and/or the authorization system. The SPI loaded +security extension is part of an ordinary elasticsearch plugin. [[implementing-custom-roles-provider]] ==== Implementing a custom roles provider -To create a custom roles provider: +To create a custom roles provider: . Implement the interface `BiConsumer, ActionListener>>`. That is to say, the implementation consists of one method that takes a set of strings, which are the role names to resolve, and an ActionListener, on which the set of resolved role descriptors are passed on as the response. -. The custom roles provider implementation must take special care to not block on any I/O +. The custom roles provider implementation must take special care to not block on any I/O operations. It is the responsibility of the implementation to ensure asynchronous behavior and non-blocking calls, which is made easier by the fact that the `ActionListener` is provided on which to send the response when the roles have been resolved and the response @@ -32,7 +32,7 @@ To package your custom roles provider as a plugin: [source,java] ---------------------------------------------------- @Override -public List, ActionListener>>> +public List, ActionListener>>> getRolesProviders(Settings settings, ResourceWatcherService resourceWatcherService) { ... } @@ -41,50 +41,81 @@ getRolesProviders(Settings settings, ResourceWatcherService resourceWatcherServi The `getRolesProviders` method is used to provide a list of custom roles providers that will be used to resolve role names, if the role names could not be resolved by the reserved roles or native roles stores. The list should be returned in the order that the custom role -providers should be invoked to resolve roles. For example, if `getRolesProviders` returns two -instances of roles providers, and both of them are able to resolve role `A`, then the resolved -role descriptor that will be used for role `A` will be the one resolved by the first roles +providers should be invoked to resolve roles. For example, if `getRolesProviders` returns two +instances of roles providers, and both of them are able to resolve role `A`, then the resolved +role descriptor that will be used for role `A` will be the one resolved by the first roles provider in the list. + +[[implementing-authorization-engine]] +==== Implementing an authorization engine + +To create an authorization engine, you need to: + +. Implement the `org.elasticsearch.xpack.core.security.authz.AuthorizationEngine` + interface in a class with the desired authorization behavior. +. Implement the `org.elasticsearch.xpack.core.security.authz.Authorization.AuthorizationInfo` + interface in a class that contains the necessary information to authorize the request. + +To package your authorization engine as a plugin: + +. Implement an extension class for your authorization engine that extends + `org.elasticsearch.xpack.core.security.SecurityExtension`. There you need to + override the following method: + [source,java] ---------------------------------------------------- @Override -public List getSettingsFilter() { +public AuthorizationEngine getAuthorizationEngine(Settings settings) { ... } ---------------------------------------------------- + -The `Plugin#getSettingsFilter` method returns a list of setting names that should be -filtered from the settings APIs as they may contain sensitive credentials. Note this method is not -part of the `SecurityExtension` interface, it's available as part of the elasticsearch plugin main class. +The `getAuthorizationEngine` method is used to provide the authorization engine +implementation. + +Sample code that illustrates the structure and implementation of a custom +authorization engine is provided in the +https://github.com/elastic/elasticsearch/tree/master/plugin/examples/security-example-authorization-engine[elasticsearch] +repository on GitHub. You can use this code as a starting point for creating your +own authorization engine. + +[[packing-extension-plugin]] +==== Implement an elasticsearch plugin + +In order to register the security extension for your custom roles provider or +authorization engine, you need to also implement an elasticsearch plugin that +contains the extension: +. Implement a plugin class that extends `org.elasticsearch.plugins.Plugin` . Create a build configuration file for the plugin; Gradle is our recommendation. +. Create a `plugin-descriptor.properties` file as described in + {plugins}/plugin-authors.html[Help for plugin authors]. . Create a `META-INF/services/org.elasticsearch.xpack.core.security.SecurityExtension` descriptor file for the extension that contains the fully qualified class name of your `org.elasticsearch.xpack.core.security.SecurityExtension` implementation . Bundle all in a single zip file. -[[using-custom-roles-provider]] -==== Using a custom roles provider to resolve roles +[[using-security-extension]] +==== Using the security extension -To use a custom roles provider: +To use a security extension: -. Install the roles provider extension on each node in the cluster. You run +. Install the plugin with the extension on each node in the cluster. You run `bin/elasticsearch-plugin` with the `install` sub-command and specify the URL pointing to the zip file that contains the extension. For example: + [source,shell] ---------------------------------------- -bin/elasticsearch-plugin install file:////my-roles-provider-1.0.zip +bin/elasticsearch-plugin install file:////my-extension-plugin-1.0.zip ---------------------------------------- -. Add any configuration parameters for any of the custom roles provider implementations -to `elasticsearch.yml`. The settings are not namespaced and you have access to any -settings when constructing the custom roles providers, although it is recommended to -have a namespacing convention for custom roles providers to keep your `elasticsearch.yml` -configuration easy to understand. +. Add any configuration parameters for implementations in the extension to the +`elasticsearch.yml` file. The settings are not namespaced and you have access to any +settings when constructing the extensions, although it is recommended to have a +namespacing convention for extensions to keep your `elasticsearch.yml` +configuration easy to understand. + -For example, if you have a custom roles provider that -resolves roles from reading a blob in an S3 bucket on AWS, then you would specify settings +For example, if you have a custom roles provider that +resolves roles from reading a blob in an S3 bucket on AWS, then you would specify settings in `elasticsearch.yml` such as: + [source,js] @@ -94,8 +125,8 @@ custom_roles_provider.s3_roles_provider.region: us-east-1 custom_roles_provider.s3_roles_provider.secret_key: xxx custom_roles_provider.s3_roles_provider.access_key: xxx ---------------------------------------- +// NOTCONSOLE + -These settings will be available as the first parameter in the `getRolesProviders` method, from -where you will create and return the custom roles provider instances. +These settings are passed as arguments to the methods in the `SecurityExtension` interface. . Restart Elasticsearch. diff --git a/x-pack/docs/en/security/authorization/managing-roles.asciidoc b/x-pack/docs/en/security/authorization/managing-roles.asciidoc index cac4eaac1fbfa..04fb12e19d75b 100644 --- a/x-pack/docs/en/security/authorization/managing-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/managing-roles.asciidoc @@ -179,7 +179,7 @@ There are two available mechanisms to define roles: using the _Role Management A or in local files on the {es} nodes. You can also implement custom roles providers. If you need to integrate with another system to retrieve user roles, you can build a custom roles provider plugin. For more information, -see <>. +see <>. [float] [[roles-management-ui]] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java index 82d31aa8a2993..9f0eb474a59c8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java @@ -11,6 +11,7 @@ import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; @@ -79,6 +80,18 @@ default AuthenticationFailureHandler getAuthenticationFailureHandler() { return Collections.emptyList(); } + /** + * Returns a authorization engine for authorizing requests, or null to use the default authorization mechanism. + * + * Only one installed extension may have an authorization engine. If more than + * one extension returns a non-null authorization engine, an error is raised. + * + * @param settings The configured settings for the node + */ + default AuthorizationEngine getAuthorizationEngine(Settings settings) { + return null; + } + /** * Loads the XPackSecurityExtensions from the given class loader */ diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 665f3ed6d35c0..948b9b7143e96 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -111,6 +111,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.accesscontrol.SecurityIndexSearcherWrapper; @@ -437,7 +438,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste // minimal getLicenseState().addListener(allRolesStore::invalidateAll); final AuthorizationService authzService = new AuthorizationService(settings, allRolesStore, clusterService, - auditTrailService, failureHandler, threadPool, anonymousUser); + auditTrailService, failureHandler, threadPool, anonymousUser, getAuthorizationEngine()); components.add(nativeRolesStore); // used by roles actions components.add(reservedRolesStore); // used by roles actions components.add(allRolesStore); // for SecurityFeatureSet and clear roles cache @@ -467,6 +468,25 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste return components; } + private AuthorizationEngine getAuthorizationEngine() { + AuthorizationEngine authorizationEngine = null; + String extensionName = null; + for (SecurityExtension extension : securityExtensions) { + final AuthorizationEngine extensionEngine = extension.getAuthorizationEngine(settings); + if (extensionEngine != null && authorizationEngine != null) { + throw new IllegalStateException("Extensions [" + extensionName + "] and [" + extension.toString() + "] " + + "both set an authorization engine"); + } + authorizationEngine = extensionEngine; + extensionName = extension.toString(); + } + + if (authorizationEngine != null) { + logger.debug("Using authorization engine from extension [" + extensionName + "]"); + } + return authorizationEngine; + } + private AuthenticationFailureHandler createAuthenticationFailureHandler(final Realms realms) { AuthenticationFailureHandler failureHandler = null; String extensionName = null; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 3e9f8db284042..98186af0503ba 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -24,6 +24,7 @@ import org.elasticsearch.action.update.UpdateAction; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; @@ -90,12 +91,13 @@ public class AuthorizationService { private final ThreadContext threadContext; private final AnonymousUser anonymousUser; private final AuthorizationEngine rbacEngine; + private final AuthorizationEngine authorizationEngine; private final boolean isAnonymousEnabled; private final boolean anonymousAuthzExceptionEnabled; public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, ClusterService clusterService, AuditTrailService auditTrail, AuthenticationFailureHandler authcFailureHandler, - ThreadPool threadPool, AnonymousUser anonymousUser) { + ThreadPool threadPool, AnonymousUser anonymousUser, @Nullable AuthorizationEngine authorizationEngine) { this.clusterService = clusterService; this.auditTrail = auditTrail; this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService); @@ -105,6 +107,7 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C this.isAnonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); this.anonymousAuthzExceptionEnabled = ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.get(settings); this.rbacEngine = new RBACEngine(settings, rolesStore); + this.authorizationEngine = authorizationEngine == null ? this.rbacEngine : authorizationEngine; this.settings = settings; } @@ -293,14 +296,22 @@ private void authorizeAction(final RequestInfo requestInfo, final String request } } - private AuthorizationEngine getRunAsAuthorizationEngine(final Authentication authentication) { - return ClientReservedRealm.isReserved(authentication.getUser().authenticatedUser().principal(), settings) ? - rbacEngine : rbacEngine; + // pkg-private for testing + AuthorizationEngine getRunAsAuthorizationEngine(final Authentication authentication) { + return getAuthorizationEngineForUser(authentication.getUser().authenticatedUser()); } - private AuthorizationEngine getAuthorizationEngine(final Authentication authentication) { - return ClientReservedRealm.isReserved(authentication.getUser().principal(), settings) ? - rbacEngine : rbacEngine; + // pkg-private for testing + AuthorizationEngine getAuthorizationEngine(final Authentication authentication) { + return getAuthorizationEngineForUser(authentication.getUser()); + } + + private AuthorizationEngine getAuthorizationEngineForUser(final User user) { + if (ClientReservedRealm.isReserved(user.principal(), settings) || isInternalUser(user)) { + return rbacEngine; + } else { + return authorizationEngine; + } } private void authorizeSystemUser(final Authentication authentication, final String action, final String requestId, @@ -490,13 +501,13 @@ private void putTransientIfNonExisting(String key, Object value) { } } - ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, TransportRequest request, - AuthorizationInfo authzInfo) { + private ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, + TransportRequest request, AuthorizationInfo authzInfo) { return denial(auditRequestId, authentication, action, request, authzInfo, null); } - ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, TransportRequest request, - AuthorizationInfo authzInfo, Exception cause) { + private ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, + TransportRequest request, AuthorizationInfo authzInfo, Exception cause) { auditTrail.accessDenied(auditRequestId, authentication, action, request, authzInfo); return denialException(authentication, action, cause); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 7bf07cb906b5b..2ab2bc4c657b7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -71,6 +71,7 @@ import org.elasticsearch.action.update.UpdateAction; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -94,8 +95,10 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; +import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; @@ -107,8 +110,11 @@ import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; +import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; +import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; @@ -132,6 +138,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CountDownLatch; +import java.util.function.Function; import java.util.function.Predicate; import static java.util.Arrays.asList; @@ -218,7 +225,7 @@ public void setup() { }).when(rolesStore).getRoles(any(User.class), any(FieldPermissionsCache.class), any(ActionListener.class)); roleMap.put(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName(), ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, - auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings)); + auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null); } private void authorize(Authentication authentication, String action, TransportRequest request) { @@ -642,7 +649,7 @@ public void testDenialForAnonymousUser() { Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "a_all").build(); final AnonymousUser anonymousUser = new AnonymousUser(settings); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, - new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser); + new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, null); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); @@ -669,7 +676,7 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() { .build(); final Authentication authentication = createAuthentication(new AnonymousUser(settings)); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, - new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings)); + new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); @@ -1292,6 +1299,72 @@ public void testProxyRequestAuthenticationDeniedWithReadPrivileges() { authzInfoRoles(new String[]{role.getName()})); } + public void testAuthorizationEngineSelection() { + final AuthorizationEngine engine = new AuthorizationEngine() { + @Override + public void resolveAuthorizationInfo(RequestInfo requestInfo, ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void authorizeRunAs(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + AsyncSupplier indicesAsyncSupplier, + Function aliasOrIndexFunction, + ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + Map aliasAndIndexLookup, ActionListener> listener) { + throw new UnsupportedOperationException("not implemented"); + } + }; + + authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, + auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), + engine); + Authentication authentication = createAuthentication(new User("test user", "a_all")); + assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + + authentication = createAuthentication(new User("runas", new String[] { "runas_role" }, new User("runner", "runner_role"))); + assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + + authentication = createAuthentication(new User("runas", new String[] { "runas_role" }, new ElasticUser(true))); + assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + + authentication = createAuthentication(new User("elastic", new String[] { "superuser" }, new User("runner", "runner_role"))); + assertNotEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + + authentication = createAuthentication(new User("kibana", new String[] { "kibana_system" }, new ElasticUser(true))); + assertNotEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + + authentication = createAuthentication(randomFrom(XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, + new ElasticUser(true), new KibanaUser(true))); + assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + } + static AuthorizationInfo authzInfoRoles(String[] expectedRoles) { return Matchers.argThat(new RBACAuthorizationInfoRoleMatcher(expectedRoles)); } diff --git a/x-pack/qa/security-example-spi-extension/build.gradle b/x-pack/qa/security-example-spi-extension/build.gradle index 664e5f715bbb1..1ff65519c367d 100644 --- a/x-pack/qa/security-example-spi-extension/build.gradle +++ b/x-pack/qa/security-example-spi-extension/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.esplugin' esplugin { name 'spi-extension' - description 'An example spi extension pluing for xpack security' + description 'An example spi extension plugin for security' classname 'org.elasticsearch.example.SpiExtensionPlugin' extendedPlugins = ['x-pack-security'] }