From 30b01fe00df16ead8ed0964c3985f4708238cce9 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Mon, 6 Apr 2020 14:10:30 +1000 Subject: [PATCH] Resolve SSO roles by pattern (#54777) This changes a SamlServiceProvider to have a function that maps from an "action-name" to set of role-names instead of a Map that does so. The on-disk representation of this mapping is a set of Java Regexp Patterns, for which the first matching group is the role name. For example "sso:(\w+)" would map any action that started with "sso:" to the corresponding role name (e.g. "sso:superuser" -> "superuser"). Backport of: #54440 --- .../idp/saml-service-provider-template.json | 3 +- .../idp/IdentityProviderAuthenticationIT.java | 17 ++- .../xpack/idp/IdpRestTestCase.java | 14 ++ .../idp/ManageServiceProviderRestIT.java | 17 ++- .../idp/WildcardServiceProviderRestIT.java | 10 ++ .../src/test/resources/roles.yml | 2 +- .../src/test/resources/wildcard_services.json | 8 +- .../xpack/idp/IdentityProviderPlugin.java | 7 +- .../ApplicationActionsResolver.java | 136 ++++++++++++++++++ .../privileges/ServiceProviderPrivileges.java | 16 +-- .../idp/privileges/UserPrivilegeResolver.java | 87 ++++++----- .../saml/sp/SamlServiceProviderDocument.java | 24 ++-- .../saml/sp/SamlServiceProviderFactory.java | 20 ++- .../PutSamlServiceProviderRequestTests.java | 8 +- .../idp/action/SamlIdentityProviderTests.java | 51 ++++++- .../UserPrivilegeResolverTests.java | 71 ++++++--- .../sp/SamlServiceProviderDocumentTests.java | 10 +- .../sp/SamlServiceProviderIndexTests.java | 9 +- .../sp/SamlServiceProviderResolverTests.java | 13 +- .../WildcardServiceProviderResolverTests.java | 9 +- .../test/IdentityProviderIntegTestCase.java | 4 +- 21 files changed, 398 insertions(+), 138 deletions(-) create mode 100644 x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ApplicationActionsResolver.java diff --git a/x-pack/plugin/core/src/main/resources/org/elasticsearch/xpack/idp/saml-service-provider-template.json b/x-pack/plugin/core/src/main/resources/org/elasticsearch/xpack/idp/saml-service-provider-template.json index 8dfbcd24661..60f964fe134 100644 --- a/x-pack/plugin/core/src/main/resources/org/elasticsearch/xpack/idp/saml-service-provider-template.json +++ b/x-pack/plugin/core/src/main/resources/org/elasticsearch/xpack/idp/saml-service-provider-template.json @@ -57,8 +57,7 @@ "type": "keyword" }, "roles": { - "type": "object", - "dynamic": false + "type": "keyword" } } }, diff --git a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/IdentityProviderAuthenticationIT.java b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/IdentityProviderAuthenticationIT.java index af2920b8545..612e4bb8e9f 100644 --- a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/IdentityProviderAuthenticationIT.java +++ b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/IdentityProviderAuthenticationIT.java @@ -13,6 +13,7 @@ import org.elasticsearch.client.Response; import org.elasticsearch.client.RestClient; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Set; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -44,8 +45,12 @@ public class IdentityProviderAuthenticationIT extends IdpRestTestCase { private final String REALM_NAME = "cloud-saml"; @Before - public void createUsers() throws IOException { + public void setupSecurityData() throws IOException { setUserPassword("kibana", new SecureString("kibana".toCharArray())); + createApplicationPrivileges("elastic-cloud", org.elasticsearch.common.collect.Map.of( + "deployment_admin", Set.of("sso:admin"), + "deployment_viewer", Set.of("sso:viewer")) + ); } public void testRegistrationAndIdpInitiatedSso() throws Exception { @@ -54,10 +59,7 @@ public class IdentityProviderAuthenticationIT extends IdpRestTestCase { request.put("acs", SP_ACS); final Map privilegeMap = new HashMap<>(); privilegeMap.put("resource", SP_ENTITY_ID); - final Map roleMap = new HashMap<>(); - roleMap.put("superuser", "role:superuser"); - roleMap.put("viewer", "role:viewer"); - privilegeMap.put("roles", roleMap); + privilegeMap.put("roles", Set.of("sso:(\\w+)")); request.put("privileges", privilegeMap); final Map attributeMap = new HashMap<>(); attributeMap.put("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"); @@ -78,10 +80,7 @@ public class IdentityProviderAuthenticationIT extends IdpRestTestCase { request.put("acs", SP_ACS); final Map privilegeMap = new HashMap<>(); privilegeMap.put("resource", SP_ENTITY_ID); - final Map roleMap = new HashMap<>(); - roleMap.put("superuser", "role:superuser"); - roleMap.put("viewer", "role:viewer"); - privilegeMap.put("roles", roleMap); + privilegeMap.put("roles", Set.of("sso:(\\w+)")); request.put("privileges", privilegeMap); final Map attributeMap = new HashMap<>(); attributeMap.put("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"); diff --git a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/IdpRestTestCase.java b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/IdpRestTestCase.java index 387256fda52..24e95983b29 100644 --- a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/IdpRestTestCase.java +++ b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/IdpRestTestCase.java @@ -12,10 +12,12 @@ import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.security.ChangePasswordRequest; import org.elasticsearch.client.security.DeleteRoleRequest; import org.elasticsearch.client.security.DeleteUserRequest; +import org.elasticsearch.client.security.PutPrivilegesRequest; import org.elasticsearch.client.security.PutRoleRequest; import org.elasticsearch.client.security.PutUserRequest; import org.elasticsearch.client.security.RefreshPolicy; import org.elasticsearch.client.security.user.User; +import org.elasticsearch.client.security.user.privileges.ApplicationPrivilege; import org.elasticsearch.client.security.user.privileges.ApplicationResourcePrivileges; import org.elasticsearch.client.security.user.privileges.IndicesPrivileges; import org.elasticsearch.client.security.user.privileges.Role; @@ -34,10 +36,13 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import static java.util.Arrays.asList; import static java.util.Collections.emptyMap; +import static org.elasticsearch.common.collect.List.copyOf; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -106,6 +111,15 @@ public abstract class IdpRestTestCase extends ESRestTestCase { client.security().deleteRole(request, RequestOptions.DEFAULT); } + protected void createApplicationPrivileges(String applicationName, Map> privileges) throws IOException { + final RestHighLevelClient client = getHighLevelAdminClient(); + final List applicationPrivileges = privileges.entrySet().stream() + .map(e -> new ApplicationPrivilege(applicationName, e.getKey(), copyOf(e.getValue()), null)) + .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + final PutPrivilegesRequest request = new PutPrivilegesRequest(applicationPrivileges, RefreshPolicy.IMMEDIATE); + client.security().putPrivileges(request, RequestOptions.DEFAULT); + } + protected void setUserPassword(String username, SecureString password) throws IOException { final RestHighLevelClient client = getHighLevelAdminClient(); final ChangePasswordRequest request = new ChangePasswordRequest(username, password.getChars(), RefreshPolicy.NONE); diff --git a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/ManageServiceProviderRestIT.java b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/ManageServiceProviderRestIT.java index 2b699c9f903..0283c8e72d6 100644 --- a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/ManageServiceProviderRestIT.java +++ b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/ManageServiceProviderRestIT.java @@ -9,13 +9,15 @@ import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.collect.Set; import org.elasticsearch.common.xcontent.ObjectPath; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderIndex; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderIndex.DocumentVersion; +import org.junit.Before; import java.io.IOException; -import java.util.Map; import java.util.HashMap; +import java.util.Map; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -31,6 +33,14 @@ public class ManageServiceProviderRestIT extends IdpRestTestCase { // From SAMLConstants private final String REDIRECT_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"; + @Before + public void defineApplicationPrivileges() throws IOException { + super.createApplicationPrivileges("elastic-cloud", org.elasticsearch.common.collect.Map.of( + "deployment_admin", Set.of("sso:superuser"), + "deployment_viewer", Set.of("sso:viewer") + )); + } + public void testCreateAndDeleteServiceProvider() throws Exception { final String entityId = "ec:" + randomAlphaOfLength(8) + ":" + randomAlphaOfLength(12); final Map request = new HashMap<>(); @@ -38,10 +48,7 @@ public class ManageServiceProviderRestIT extends IdpRestTestCase { request.put("acs", "https://sp1.test.es.elasticsearch.org/saml/acs"); final Map privilegeMap = new HashMap<>(); privilegeMap.put("resource", entityId); - final Map roleMap = new HashMap<>(); - roleMap.put("superuser", "role:superuser"); - roleMap.put("viewer", "role:viewer"); - privilegeMap.put("roles", roleMap); + privilegeMap.put("roles", Set.of("role:(\\w+)")); request.put("privileges", privilegeMap); final Map attributeMap = new HashMap<>(); attributeMap.put("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"); diff --git a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/WildcardServiceProviderRestIT.java b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/WildcardServiceProviderRestIT.java index 4c4b4eafb9a..a0d43df3e61 100644 --- a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/WildcardServiceProviderRestIT.java +++ b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/WildcardServiceProviderRestIT.java @@ -11,10 +11,12 @@ import org.elasticsearch.client.security.user.User; import org.elasticsearch.client.security.user.privileges.ApplicationResourcePrivileges; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.common.collect.Set; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.junit.Before; import java.io.IOException; import java.util.Arrays; @@ -34,6 +36,14 @@ public class WildcardServiceProviderRestIT extends IdpRestTestCase { // From SAMLConstants private final String REDIRECT_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"; + @Before + public void defineApplicationPrivileges() throws IOException { + super.createApplicationPrivileges("elastic-cloud", org.elasticsearch.common.collect.Map.of( + "deployment_admin", Set.of("sso:admin"), + "deployment_viewer", Set.of("sso:viewer") + )); + } + public void testGetWildcardServiceProviderMetadata() throws Exception { final String owner = randomAlphaOfLength(8); final String service = randomAlphaOfLength(8); diff --git a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/roles.yml b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/roles.yml index b69126d79e5..0867c806f31 100644 --- a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/roles.yml +++ b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/roles.yml @@ -8,4 +8,4 @@ idp_user: applications: - application: elastic-cloud resources: ["ec:123456:abcdefg"] - privileges: ["role:viewer"] + privileges: ["sso:viewer"] diff --git a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/wildcard_services.json b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/wildcard_services.json index 4f95ae6560f..a6340f0f6ef 100644 --- a/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/wildcard_services.json +++ b/x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/wildcard_services.json @@ -8,9 +8,7 @@ "name": "Application 1 ({{service}})", "privileges": { "resource": "sso:{{entity_id}}", - "roles": { - "admin": "sso:admin" - } + "roles": [ "sso:(.*)" ] }, "attributes": { "principal": "saml:attribute:principal", @@ -28,9 +26,7 @@ "name": "Application 2 ({{service}})", "privileges": { "resource": "sso:{{entity_id}}", - "roles": { - "admin": "sso:admin" - } + "roles": [ "sso:(.*)" ] }, "attributes": { "principal": "saml:attribute:principal", diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java index 19750de2aec..73f91788469 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java @@ -44,6 +44,7 @@ import org.elasticsearch.xpack.idp.action.TransportPutSamlServiceProviderAction; import org.elasticsearch.xpack.idp.action.TransportSamlInitiateSingleSignOnAction; import org.elasticsearch.xpack.idp.action.TransportSamlMetadataAction; import org.elasticsearch.xpack.idp.action.TransportSamlValidateAuthnRequestAction; +import org.elasticsearch.xpack.idp.privileges.ApplicationActionsResolver; import org.elasticsearch.xpack.idp.privileges.UserPrivilegeResolver; import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProviderBuilder; @@ -95,10 +96,11 @@ public class IdentityProviderPlugin extends Plugin implements ActionPlugin { SamlInit.initialize(); final SamlServiceProviderIndex index = new SamlServiceProviderIndex(client, clusterService); final SecurityContext securityContext = new SecurityContext(settings, threadPool.getThreadContext()); - final UserPrivilegeResolver userPrivilegeResolver = new UserPrivilegeResolver(client, securityContext); - final ServiceProviderDefaults serviceProviderDefaults = ServiceProviderDefaults.forSettings(settings); + final ApplicationActionsResolver actionsResolver = new ApplicationActionsResolver(settings, serviceProviderDefaults, client); + final UserPrivilegeResolver userPrivilegeResolver = new UserPrivilegeResolver(client, securityContext, actionsResolver); + final SamlServiceProviderFactory serviceProviderFactory = new SamlServiceProviderFactory(serviceProviderDefaults); final SamlServiceProviderResolver registeredServiceProviderResolver = new SamlServiceProviderResolver(settings, index, serviceProviderFactory); @@ -158,6 +160,7 @@ public class IdentityProviderPlugin extends Plugin implements ActionPlugin { settings.addAll(ServiceProviderCacheSettings.getSettings()); settings.addAll(ServiceProviderDefaults.getSettings()); settings.addAll(WildcardServiceProviderResolver.getSettings()); + settings.addAll(ApplicationActionsResolver.getSettings()); settings.addAll(X509KeyPairSettings.withPrefix("xpack.idp.signing.", false).getAllSettings()); settings.addAll(X509KeyPairSettings.withPrefix("xpack.idp.metadata_signing.", false).getAllSettings()); return Collections.unmodifiableList(settings); diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ApplicationActionsResolver.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ApplicationActionsResolver.java new file mode 100644 index 00000000000..403c1fd5e68 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ApplicationActionsResolver.java @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * ELASTICSEARCH CONFIDENTIAL + * __________________ + * + * [2020] Elasticsearch Incorporated. All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Elasticsearch Incorporated and its suppliers, + * if any. The intellectual and technical concepts contained + * herein are proprietary to Elasticsearch Incorporated + * and its suppliers and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Elasticsearch Incorporated. + */ + +package org.elasticsearch.xpack.idp.privileges; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.OriginSettingClient; +import org.elasticsearch.common.cache.Cache; +import org.elasticsearch.common.cache.CacheBuilder; +import org.elasticsearch.common.collect.List; +import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.ClientHelper; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesRequest; +import org.elasticsearch.xpack.idp.saml.sp.ServiceProviderDefaults; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ApplicationActionsResolver extends AbstractLifecycleComponent { + + private static final int CACHE_SIZE_DEFAULT = 100; + private static final TimeValue CACHE_TTL_DEFAULT = TimeValue.timeValueMinutes(90); + + public static final Setting CACHE_SIZE + = Setting.intSetting("xpack.idp.privileges.cache.size", CACHE_SIZE_DEFAULT, Setting.Property.NodeScope); + public static final Setting CACHE_TTL + = Setting.timeSetting("xpack.idp.privileges.cache.ttl", CACHE_TTL_DEFAULT, Setting.Property.NodeScope); + + private final Logger logger = LogManager.getLogger(); + + private final ServiceProviderDefaults defaults; + private final Client client; + private final Cache> cache; + + public ApplicationActionsResolver(Settings settings, ServiceProviderDefaults defaults, Client client) { + this.defaults = defaults; + this.client = new OriginSettingClient(client, ClientHelper.IDP_ORIGIN); + + final TimeValue cacheTtl = CACHE_TTL.get(settings); + this.cache = CacheBuilder.>builder() + .setMaximumWeight(CACHE_SIZE.get(settings)) + .setExpireAfterWrite(cacheTtl) + .build(); + + // Preload the cache at 2/3 of its expiry time (TTL). This means that we should never have an empty cache, but if for some reason + // the preload thread stops running, we will still automatically refresh the cache on access. + final TimeValue preloadInterval = TimeValue.timeValueMillis(cacheTtl.millis() * 2 / 3); + client.threadPool().scheduleWithFixedDelay(this::loadPrivilegesForDefaultApplication, preloadInterval, ThreadPool.Names.GENERIC); + } + + public static Collection> getSettings() { + return List.of(CACHE_SIZE, CACHE_TTL); + } + + @Override + protected void doStart() { + loadPrivilegesForDefaultApplication(); + } + + private void loadPrivilegesForDefaultApplication() { + loadActions(defaults.applicationName, ActionListener.wrap( + actions -> logger.info("Found actions [{}] defined within application privileges for [{}]", actions, defaults.applicationName), + ex -> logger.warn(new ParameterizedMessage( + "Failed to load application privileges actions for application [{}]", defaults.applicationName), ex) + )); + } + + @Override + protected void doStop() { + // no-op + } + + @Override + protected void doClose() throws IOException { + // no-op + } + + public void getActions(String application, ActionListener> listener) { + final Set actions = this.cache.get(application); + if (actions == null || actions.isEmpty()) { + loadActions(application, listener); + } else { + listener.onResponse(actions); + } + } + + private void loadActions(String applicationName, ActionListener> listener) { + final GetPrivilegesRequest request = new GetPrivilegesRequest(); + request.application(applicationName); + this.client.execute(GetPrivilegesAction.INSTANCE, request, ActionListener.wrap( + response -> { + final Set fixedActions = Stream.of(response.privileges()) + .map(p -> p.getActions()) + .flatMap(Collection::stream) + .filter(s -> s.indexOf('*') == -1) + .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); + cache.put(applicationName, fixedActions); + listener.onResponse(fixedActions); + }, + listener::onFailure + )); + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ServiceProviderPrivileges.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ServiceProviderPrivileges.java index c4b9debc5a8..537a30e5130 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ServiceProviderPrivileges.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ServiceProviderPrivileges.java @@ -9,20 +9,20 @@ package org.elasticsearch.xpack.idp.privileges; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; -import java.util.Collections; -import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.function.Function; public class ServiceProviderPrivileges { private final String applicationName; private final String resource; - private final Map roles; + private final Function> roleMapping; - public ServiceProviderPrivileges(String applicationName, String resource, Map roles) { + public ServiceProviderPrivileges(String applicationName, String resource, Function> roleMapping) { this.applicationName = Objects.requireNonNull(applicationName, "Application name cannot be null"); this.resource = Objects.requireNonNull(resource, "Resource cannot be null"); - this.roles = Collections.unmodifiableMap(roles); + this.roleMapping = Objects.requireNonNull(roleMapping, "Role Mapping cannot be null"); } /** @@ -42,7 +42,7 @@ public class ServiceProviderPrivileges { } /** - * Returns a mapping from "role name" (key) to "{@link ApplicationPrivilegeDescriptor#getActions() action name}" (value) + * Returns a mapping from "{@link ApplicationPrivilegeDescriptor#getActions() action name}" (input) to "role name" (output) * that represents the roles that should be exposed to this Service Provider. * The "role name" (but not the action name) will be provided to the service provider. * These roles have no semantic meaning within the IdP, they are simply metadata that we pass to the Service Provider. They may not @@ -50,7 +50,7 @@ public class ServiceProviderPrivileges { * terminology (e.g. "groups"). * The actions will be resolved as application privileges from the IdP's security cluster. */ - public Map getRoleActions() { - return roles; + public Function> getRoleMapping() { + return roleMapping; } } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolver.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolver.java index 073ecf7764a..9e36c63a9a4 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolver.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolver.java @@ -21,7 +21,6 @@ import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges import java.util.Collections; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -38,7 +37,7 @@ public class UserPrivilegeResolver { public UserPrivileges(String principal, boolean hasAccess, Set roles) { this.principal = Objects.requireNonNull(principal, "principal may not be null"); if (hasAccess == false && roles.isEmpty() == false) { - throw new IllegalArgumentException("a user without access ([" + hasAccess + "]) may not have roles ([" + roles + "])"); + throw new IllegalArgumentException("a user without access may not have roles ([" + roles + "])"); } this.hasAccess = hasAccess; this.roles = Collections.unmodifiableSet(Objects.requireNonNull(roles, "roles may not be null")); @@ -67,10 +66,12 @@ public class UserPrivilegeResolver { private final Logger logger = LogManager.getLogger(); private final Client client; private final SecurityContext securityContext; + private final ApplicationActionsResolver actionsResolver; - public UserPrivilegeResolver(Client client, SecurityContext securityContext) { + public UserPrivilegeResolver(Client client, SecurityContext securityContext, ApplicationActionsResolver actionsResolver) { this.client = client; this.securityContext = securityContext; + this.actionsResolver = actionsResolver; } /** @@ -78,22 +79,29 @@ public class UserPrivilegeResolver { * Requires that the active user is set in the {@link org.elasticsearch.xpack.core.security.SecurityContext}. */ public void resolve(ServiceProviderPrivileges service, ActionListener listener) { - HasPrivilegesRequest request = new HasPrivilegesRequest(); - final String username = securityContext.requireUser().principal(); - request.username(username); - request.applicationPrivileges(buildResourcePrivilege(service)); - request.clusterPrivileges(Strings.EMPTY_ARRAY); - request.indexPrivileges(new RoleDescriptor.IndicesPrivileges[0]); - client.execute(HasPrivilegesAction.INSTANCE, request, ActionListener.wrap( - response -> { - logger.debug("Checking access for user [{}] to application [{}] resource [{}]", - username, service.getApplicationName(), service.getResource()); - UserPrivileges privileges = buildResult(response, service); - logger.debug("Resolved service privileges [{}]", privileges); - listener.onResponse(privileges); - }, - listener::onFailure - )); + buildResourcePrivilege(service, ActionListener.wrap(resourcePrivilege -> { + final String username = securityContext.requireUser().principal(); + if (resourcePrivilege == null) { + listener.onResponse(UserPrivileges.noAccess(username)); + return; + } + HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.username(username); + request.clusterPrivileges(Strings.EMPTY_ARRAY); + request.indexPrivileges(new RoleDescriptor.IndicesPrivileges[0]); + request.applicationPrivileges(resourcePrivilege); + client.execute(HasPrivilegesAction.INSTANCE, request, ActionListener.wrap( + response -> { + logger.debug("Checking access for user [{}] to application [{}] resource [{}]", + username, service.getApplicationName(), service.getResource()); + UserPrivileges privileges = buildResult(response, service); + logger.debug("Resolved service privileges [{}]", privileges); + listener.onResponse(privileges); + }, + listener::onFailure + )); + }, listener::onFailure)); + } private UserPrivileges buildResult(HasPrivilegesResponse response, ServiceProviderPrivileges service) { @@ -101,28 +109,35 @@ public class UserPrivilegeResolver { if (appPrivileges == null || appPrivileges.isEmpty()) { return UserPrivileges.noAccess(response.getUsername()); } - final Set roles = service.getRoleActions().entrySet().stream() - .filter(entry -> checkAccess(appPrivileges, entry.getValue(), service.getResource())) + + final Set roles = appPrivileges.stream() + .filter(rp -> rp.getResource().equals(service.getResource())) + .map(rp -> rp.getPrivileges().entrySet()) + .flatMap(Set::stream) + .filter(Map.Entry::getValue) .map(Map.Entry::getKey) + .map(service.getRoleMapping()) + .filter(Objects::nonNull) + .flatMap(Set::stream) .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); final boolean hasAccess = roles.isEmpty() == false; return new UserPrivileges(response.getUsername(), hasAccess, roles); } - private boolean checkAccess(Set userPrivileges, String action, String resource) { - final Optional match = userPrivileges.stream() - .filter(rp -> rp.getResource().equals(resource)) - .filter(rp -> rp.isAllowed(action)) - .findAny(); - match.ifPresent(rp -> logger.debug("User has access to [{} on {}] via [{}]", action, resource, rp)); - return match.isPresent(); - } - - private RoleDescriptor.ApplicationResourcePrivileges buildResourcePrivilege(ServiceProviderPrivileges service) { - final RoleDescriptor.ApplicationResourcePrivileges.Builder builder = RoleDescriptor.ApplicationResourcePrivileges.builder(); - builder.application(service.getApplicationName()); - builder.resources(service.getResource()); - builder.privileges(service.getRoleActions().values()); - return builder.build(); + private void buildResourcePrivilege(ServiceProviderPrivileges service, + ActionListener listener) { + actionsResolver.getActions(service.getApplicationName(), ActionListener.wrap(actions -> { + if (actions == null || actions.isEmpty()) { + logger.warn("No application-privilege actions defined for application [{}]", service.getApplicationName()); + listener.onResponse(null); + } else { + logger.debug("Using actions [{}] for application [{}]", actions, service.getApplicationName()); + final RoleDescriptor.ApplicationResourcePrivileges.Builder builder = RoleDescriptor.ApplicationResourcePrivileges.builder(); + builder.application(service.getApplicationName()); + builder.resources(service.getResource()); + builder.privileges(actions); + listener.onResponse(builder.build()); + } + }, listener::onFailure)); } } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocument.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocument.java index 64dd6696812..da6d6dba6d9 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocument.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocument.java @@ -39,12 +39,13 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; import java.util.stream.Collectors; +import static org.elasticsearch.common.collect.Set.copyOf; + /** * This class models the storage of a {@link SamlServiceProvider} as an Elasticsearch document. */ @@ -56,14 +57,14 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable public static class Privileges { public String resource; - public Map roleActions = Collections.emptyMap(); + public Set rolePatterns = Collections.emptySet(); public void setResource(String resource) { this.resource = resource; } - public void setRoleActions(Map roleActions) { - this.roleActions = roleActions; + public void setRolePatterns(Collection rolePatterns) { + this.rolePatterns = copyOf(rolePatterns); } @Override @@ -72,12 +73,12 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable if (o == null || getClass() != o.getClass()) return false; final Privileges that = (Privileges) o; return Objects.equals(resource, that.resource) && - Objects.equals(roleActions, that.roleActions); + Objects.equals(rolePatterns, that.rolePatterns); } @Override public int hashCode() { - return Objects.hash(resource, roleActions); + return Objects.hash(resource, rolePatterns); } } @@ -267,7 +268,7 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable authenticationExpiryMillis = in.readOptionalVLong(); privileges.resource = in.readString(); - privileges.roleActions = in.readMap(StreamInput::readString, StreamInput::readString); + privileges.rolePatterns = in.readSet(StreamInput::readString); attributeNames.principal = in.readString(); attributeNames.email = in.readOptionalString(); @@ -292,8 +293,7 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable out.writeOptionalVLong(authenticationExpiryMillis); out.writeString(privileges.resource); - out.writeMap(privileges.roleActions == null ? Collections.emptyMap() : privileges.roleActions, - StreamOutput::writeString, StreamOutput::writeString); + out.writeStringCollection(privileges.rolePatterns == null ? Collections.emptySet() : privileges.rolePatterns); out.writeString(attributeNames.principal); out.writeOptionalString(attributeNames.email); @@ -415,9 +415,7 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable DOC_PARSER.declareObject(NULL_CONSUMER, (parser, doc) -> PRIVILEGES_PARSER.parse(parser, doc.privileges, null), Fields.PRIVILEGES); PRIVILEGES_PARSER.declareString(Privileges::setResource, Fields.Privileges.RESOURCE); - PRIVILEGES_PARSER.declareField(Privileges::setRoleActions, - (parser, ignore) -> parser.currentToken() == XContentParser.Token.VALUE_NULL ? null : parser.mapStrings(), - Fields.Privileges.ROLES, ObjectParser.ValueType.OBJECT_OR_NULL); + PRIVILEGES_PARSER.declareStringArray(Privileges::setRolePatterns, Fields.Privileges.ROLES); DOC_PARSER.declareObject(NULL_CONSUMER, (p, doc) -> ATTRIBUTES_PARSER.parse(p, doc.attributeNames, null), Fields.ATTRIBUTES); ATTRIBUTES_PARSER.declareString(AttributeNames::setPrincipal, Fields.Attributes.PRINCIPAL); @@ -491,7 +489,7 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable builder.startObject(Fields.PRIVILEGES.getPreferredName()); builder.field(Fields.Privileges.RESOURCE.getPreferredName(), privileges.resource); - builder.field(Fields.Privileges.ROLES.getPreferredName(), privileges.roleActions); + builder.field(Fields.Privileges.ROLES.getPreferredName(), privileges.rolePatterns); builder.endObject(); builder.startObject(Fields.ATTRIBUTES.getPreferredName()); diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderFactory.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderFactory.java index 03db3e0c480..daae54b2d08 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderFactory.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderFactory.java @@ -14,9 +14,11 @@ import org.opensaml.security.x509.X509Credential; import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; -import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -58,8 +60,20 @@ public final class SamlServiceProviderFactory { private ServiceProviderPrivileges buildPrivileges(SamlServiceProviderDocument.Privileges configuredPrivileges) { final String resource = configuredPrivileges.resource; - final Map roles = Optional.ofNullable(configuredPrivileges.roleActions).orElse(Collections.emptyMap()); - return new ServiceProviderPrivileges(defaults.applicationName, resource, roles); + final Function> roleMapping; + if (configuredPrivileges.rolePatterns == null || configuredPrivileges.rolePatterns.isEmpty()) { + roleMapping = in -> Collections.emptySet(); + } else { + final Set patterns = configuredPrivileges.rolePatterns.stream() + .map(Pattern::compile) + .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); + roleMapping = action -> patterns.stream() + .map(p -> p.matcher(action)) + .filter(Matcher::matches) + .map(m -> m.group(1)) + .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); + } + return new ServiceProviderPrivileges(defaults.applicationName, resource, roleMapping); } private URL parseUrl(SamlServiceProviderDocument document) { diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/PutSamlServiceProviderRequestTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/PutSamlServiceProviderRequestTests.java index 0a8b150762d..75596bbfd7a 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/PutSamlServiceProviderRequestTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/PutSamlServiceProviderRequestTests.java @@ -28,7 +28,6 @@ import java.util.HashMap; import java.util.Map; import static org.elasticsearch.common.xcontent.XContentHelper.convertToMap; -import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyIterable; @@ -97,11 +96,9 @@ public class PutSamlServiceProviderRequestTests extends ESTestCase { attributeMap.put("email", "urn:oid:0.2." + randomLongBetween(1001, 2000)); attributeMap.put("name", "urn:oid:0.3." + randomLongBetween(2001, 3000)); attributeMap.put("roles", "urn:oid:0.4." + randomLongBetween(3001, 4000)); - final Map roleMap = new HashMap<>(); - roleMap.put("role_name", "role:" + randomAlphaOfLengthBetween(4, 8)); final Map privilegeMap = new HashMap<>(); privilegeMap.put("resource", "ece:deployment:" + randomLongBetween(1_000_000, 999_999_999)); - privilegeMap.put("roles", roleMap); + privilegeMap.put("roles", Collections.singletonList("role:(.*)")); final Map fields = new HashMap<>(); fields.put("name", randomAlphaOfLengthBetween(3, 30)); fields.put("acs", "https://www." + randomAlphaOfLengthBetween(3, 30) + ".fake/saml/acs"); @@ -116,8 +113,7 @@ public class PutSamlServiceProviderRequestTests extends ESTestCase { assertThat(request.getDocument().acs, equalTo(fields.get("acs"))); assertThat(request.getDocument().enabled, equalTo(fields.get("enabled"))); assertThat(request.getDocument().privileges.resource, notNullValue()); - assertThat(request.getDocument().privileges.roleActions, aMapWithSize(1)); - assertThat(request.getDocument().privileges.roleActions.keySet(), contains("role_name")); + assertThat(request.getDocument().privileges.rolePatterns, contains("role:(.*)")); assertThat(request.getDocument().attributeNames.principal, startsWith("urn:oid:0.1")); assertThat(request.getDocument().attributeNames.email, startsWith("urn:oid:0.2")); assertThat(request.getDocument().attributeNames.name, startsWith("urn:oid:0.3")); diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java index 19fbbf87ab4..0fd0185a1c0 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java @@ -41,6 +41,8 @@ import org.opensaml.xmlsec.crypto.XMLSigningUtil; import org.opensaml.xmlsec.signature.support.SignatureConstants; import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLEncoder; @@ -49,6 +51,7 @@ import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; @@ -73,6 +76,7 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase { String acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs"; String entityId = SP_ENTITY_ID; registerServiceProvider(entityId, acsUrl); + registerApplicationPrivileges(); ensureGreen(SamlServiceProviderIndex.INDEX_NAME); // User login a.k.a exchange the user credentials for an API Key @@ -96,13 +100,16 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase { Map serviceProvider = objectPath.evaluate("service_provider"); assertThat(serviceProvider, hasKey("entity_id")); assertThat(serviceProvider.get("entity_id"), equalTo(entityId)); + assertContainsAttributeWithValue(body, "principal", SAMPLE_IDPUSER_NAME); + assertContainsAttributeWithValue(body, "roles", "superuser"); } public void testIdPInitiatedSsoFailsForUnknownSP() throws Exception { String acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs"; String entityId = SP_ENTITY_ID; registerServiceProvider(entityId, acsUrl); + registerApplicationPrivileges(); ensureGreen(SamlServiceProviderIndex.INDEX_NAME); // User login a.k.a exchange the user credentials for an API Key final String apiKeyCredentials = getApiKeyFromCredentials(SAMPLE_IDPUSER_NAME, @@ -124,6 +131,7 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase { String acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs"; String entityId = SP_ENTITY_ID; registerServiceProvider(entityId, acsUrl); + registerApplicationPrivileges(); ensureGreen(SamlServiceProviderIndex.INDEX_NAME); // Make a request to init an SSO flow with the API Key as secondary authentication Request request = new Request("POST", "/_idp/saml/init"); @@ -137,6 +145,7 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase { String acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs"; String entityId = SP_ENTITY_ID; registerServiceProvider(entityId, acsUrl); + registerApplicationPrivileges(); ensureGreen(SamlServiceProviderIndex.INDEX_NAME); // Validate incoming authentication request Request validateRequest = new Request("POST", "/_idp/saml/validate"); @@ -182,6 +191,7 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase { Response initResponse = getRestClient().performRequest(initRequest); ObjectPath initResponseObject = ObjectPath.createFromResponse(initResponse); assertThat(initResponseObject.evaluate("post_url").toString(), equalTo(acsUrl)); + final String body = initResponseObject.evaluate("saml_response").toString(); assertThat(body, containsString("")); assertThat(body, containsString("Destination=\"" + acsUrl + "\"")); @@ -192,6 +202,7 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase { assertThat(sp, hasKey("entity_id")); assertThat(sp.get("entity_id"), equalTo(entityId)); assertContainsAttributeWithValue(body, "principal", SAMPLE_IDPUSER_NAME); + assertContainsAttributeWithValue(body, "roles", "superuser"); } public void testSpInitiatedSsoFailsForUserWithNoAccess() throws Exception { @@ -254,6 +265,7 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase { String acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs"; String entityId = SP_ENTITY_ID; registerServiceProvider(entityId, acsUrl); + registerApplicationPrivileges(); ensureGreen(SamlServiceProviderIndex.INDEX_NAME); // Validate incoming authentication request Request validateRequest = new Request("POST", "/_idp/saml/validate"); @@ -275,6 +287,7 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase { String acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs"; String entityId = SP_ENTITY_ID; registerServiceProvider(entityId, acsUrl); + registerApplicationPrivileges(); ensureGreen(SamlServiceProviderIndex.INDEX_NAME); // Validate incoming authentication request Request validateRequest = new Request("POST", "/_idp/saml/validate"); @@ -309,11 +322,10 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase { spFields.put(SamlServiceProviderDocument.Fields.NAME.getPreferredName(), "Dummy SP"); Map attributeMap = new HashMap<>(); attributeMap.put("principal", "https://saml.elasticsearch.org/attributes/principal"); + attributeMap.put("roles", "https://saml.elasticsearch.org/attributes/roles"); Map privilegeMap = new HashMap<>(); - Map roleMap = new HashMap<>(); - roleMap.put("superuser", "sso:superuser"); privilegeMap.put("resource", entityId); - privilegeMap.put("roles", roleMap); + privilegeMap.put("roles", Collections.singleton("sso:(\\w+)")); spFields.put("attributes", attributeMap); spFields.put("privileges", privilegeMap); Request request = @@ -335,6 +347,39 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase { assertThat(serviceProvider.get("enabled"), equalTo(true)); } + private void registerApplicationPrivileges() throws IOException { + registerApplicationPrivileges( + org.elasticsearch.common.collect.Map.of( + "deployment_admin", + org.elasticsearch.common.collect.Set.of("sso:superuser"), + "deployment_viewer", + org.elasticsearch.common.collect.Set.of("sso:viewer") + )); + } + + private void registerApplicationPrivileges(Map> privileges) throws IOException { + Request request = new Request("PUT", "/_security/privilege?refresh=" + WriteRequest.RefreshPolicy.IMMEDIATE.getValue()); + request.setOptions(REQUEST_OPTIONS_AS_CONSOLE_USER); + final XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + builder.startObject("elastic-cloud"); // app-name + privileges.forEach((privName, actions) -> { + try { + builder.startObject(privName); + builder.field("actions", actions); + builder.endObject(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + builder.endObject(); // app-name + builder.endObject(); // root + request.setJsonEntity(Strings.toString(builder)); + + Response response = getRestClient().performRequest(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + } + private String getApiKeyFromCredentials(String username, SecureString password) { Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue(username, password))); diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolverTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolverTests.java index 5eb8fb428df..542c482fcfc 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolverTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolverTests.java @@ -28,13 +28,19 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; +import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.same; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; public class UserPrivilegeResolverTests extends ESTestCase { @@ -44,9 +50,18 @@ public class UserPrivilegeResolverTests extends ESTestCase { @Before public void setupTest() { - client = Mockito.mock(Client.class); + client = mock(Client.class); securityContext = new SecurityContext(Settings.EMPTY, new ThreadContext(Settings.EMPTY)); - resolver = new UserPrivilegeResolver(client, securityContext); + final ApplicationActionsResolver actionsResolver = mock(ApplicationActionsResolver.class); + doAnswer(inv -> { + final Object[] args = inv.getArguments(); + assertThat(args, arrayWithSize(2)); + ActionListener> listener = (ActionListener>) args[args.length - 1]; + listener.onResponse(org.elasticsearch.common.collect.Set.of( + "role:cluster:view", "role:cluster:admin", "role:cluster:operator", "role:cluster:monitor")); + return null; + }).when(actionsResolver).getActions(anyString(), any(ActionListener.class)); + resolver = new UserPrivilegeResolver(client, securityContext, actionsResolver); } public void testResolveZeroAccess() throws Exception { @@ -55,11 +70,11 @@ public class UserPrivilegeResolverTests extends ESTestCase { setupUser(username); setupHasPrivileges(username, app); final PlainActionFuture future = new PlainActionFuture<>(); - final HashMap roles = new HashMap<>(); - roles.put("viewer", "role:cluster:view"); - roles.put("admin", "role:cluster:admin"); - resolver.resolve(service(app, "cluster:" + randomLong(), - roles), future); + final Function> roleMapping = org.elasticsearch.common.collect.Map.of( + "role:cluster:view", org.elasticsearch.common.collect.Set.of("viewer"), + "role:cluster:admin", org.elasticsearch.common.collect.Set.of("admin") + )::get; + resolver.resolve(service(app, "cluster:" + randomLong(), roleMapping), future); final UserPrivilegeResolver.UserPrivileges privileges = future.get(); assertThat(privileges.principal, equalTo(username)); assertThat(privileges.hasAccess, equalTo(false)); @@ -76,10 +91,11 @@ public class UserPrivilegeResolverTests extends ESTestCase { setupUser(username); setupHasPrivileges(username, app, access(resource, viewerAction, false), access(resource, adminAction, false)); final PlainActionFuture future = new PlainActionFuture<>(); - final HashMap roles = new HashMap<>(); - roles.put("viewer", viewerAction); - roles.put("admin", adminAction); - resolver.resolve(service(app, resource, roles), future); + final Function> roleMapping = org.elasticsearch.common.collect.Map.of( + viewerAction, Collections.singleton("viewer"), + adminAction, Collections.singleton("admin") + )::get; + resolver.resolve(service(app, resource, roleMapping), future); final UserPrivilegeResolver.UserPrivileges privileges = future.get(); assertThat(privileges.principal, equalTo(username)); assertThat(privileges.hasAccess, equalTo(false)); @@ -97,10 +113,11 @@ public class UserPrivilegeResolverTests extends ESTestCase { setupHasPrivileges(username, app, access(resource, viewerAction, true), access(resource, adminAction, false)); final PlainActionFuture future = new PlainActionFuture<>(); - final HashMap roles = new HashMap<>(); - roles.put("viewer", viewerAction); - roles.put("admin", adminAction); - resolver.resolve(service(app, resource, roles), future); + final Function> roleMapping = org.elasticsearch.common.collect.Map.of( + viewerAction, Collections.singleton("viewer"), + adminAction, Collections.singleton("admin") + )::get; + resolver.resolve(service(app, resource, roleMapping), future); final UserPrivilegeResolver.UserPrivileges privileges = future.get(); assertThat(privileges.principal, equalTo(username)); assertThat(privileges.hasAccess, equalTo(true)); @@ -125,20 +142,28 @@ public class UserPrivilegeResolverTests extends ESTestCase { ); final PlainActionFuture future = new PlainActionFuture<>(); - final HashMap roles = new HashMap<>(); - roles.put("viewer", viewerAction); - roles.put("admin", adminAction); - roles.put("operator", operatorAction); - roles.put("monitor", monitorAction); - resolver.resolve(service(app, resource, roles), future); + Function> roleMapping = action -> { + switch (action) { + case viewerAction: + return Collections.singleton("viewer"); + case adminAction: + return Collections.singleton("admin"); + case operatorAction: + return Collections.singleton("operator"); + case monitorAction: + return Collections.singleton("monitor"); + } + return Collections.emptySet(); + }; + resolver.resolve(service(app, resource, roleMapping), future); final UserPrivilegeResolver.UserPrivileges privileges = future.get(); assertThat(privileges.principal, equalTo(username)); assertThat(privileges.hasAccess, equalTo(true)); assertThat(privileges.roles, containsInAnyOrder("operator", "monitor")); } - private ServiceProviderPrivileges service(String appName, String resource, Map roles) { - return new ServiceProviderPrivileges(appName, resource, roles); + private ServiceProviderPrivileges service(String appName, String resource, Function> roleMapping) { + return new ServiceProviderPrivileges(appName, resource, roleMapping); } private HasPrivilegesResponse setupHasPrivileges(String username, String appName, diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocumentTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocumentTests.java index 23abff6494a..0b506fcbcb8 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocumentTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocumentTests.java @@ -26,9 +26,9 @@ import java.io.IOException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Collections; -import java.util.HashMap; +import java.util.HashSet; import java.util.List; -import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; @@ -110,11 +110,11 @@ public class SamlServiceProviderDocumentTests extends IdpSamlTestCase { doc1.certificates.setIdentityProviderX509MetadataSigningCertificates(idpMetadataCertificates); doc1.privileges.setResource("service:" + randomAlphaOfLength(12) + ":" + randomAlphaOfLength(12)); - final Map roleActions = new HashMap<>(); + final Set rolePatterns = new HashSet<>(); for (int i = randomIntBetween(1, 6); i > 0; i--) { - roleActions.put(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLength(6) + ":" + randomAlphaOfLength(6)); + rolePatterns.add(randomAlphaOfLength(6) + ":(" + randomAlphaOfLength(6) + ")"); } - doc1.privileges.setRoleActions(roleActions); + doc1.privileges.setRolePatterns(rolePatterns); doc1.attributeNames.setPrincipal("urn:" + randomAlphaOfLengthBetween(4, 8) + "." + randomAlphaOfLengthBetween(4, 8)); doc1.attributeNames.setEmail("urn:" + randomAlphaOfLengthBetween(4, 8) + "." + randomAlphaOfLengthBetween(4, 8)); diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderIndexTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderIndexTests.java index e04781b72cd..e0cebae898d 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderIndexTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderIndexTests.java @@ -32,10 +32,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -274,11 +273,11 @@ public class SamlServiceProviderIndexTests extends ESSingleNodeTestCase { document.privileges.setResource("app:" + randomAlphaOfLengthBetween(3, 6) + ":" + Math.abs(randomLong())); final int roleCount = randomIntBetween(0, 4); - final Map roles = new HashMap<>(); + final Set roles = new HashSet<>(); for (int i = 0; i < roleCount; i++) { - roles.put(randomAlphaOfLengthBetween(4, 8), randomAlphaOfLengthBetween(3, 6) + ":" + randomAlphaOfLengthBetween(3, 6)); + roles.add(randomAlphaOfLengthBetween(3, 6) + ":(" + randomAlphaOfLengthBetween(3, 6) + ")"); } - document.privileges.setRoleActions(roles); + document.privileges.setRolePatterns(roles); document.attributeNames.setPrincipal(randomUri()); if (randomBoolean()) { diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderResolverTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderResolverTests.java index 01529b363f4..1f4546c6870 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderResolverTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderResolverTests.java @@ -19,10 +19,9 @@ import org.opensaml.saml.saml2.core.NameID; import java.net.URL; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Set; +import java.util.function.Function; import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; @@ -59,8 +58,7 @@ public class SamlServiceProviderResolverTests extends ESTestCase { final String principalAttribute = randomAlphaOfLengthBetween(6, 36); final String rolesAttribute = randomAlphaOfLengthBetween(6, 36); final String resource = "ece:" + randomAlphaOfLengthBetween(6, 12); - final Map rolePrivileges = new HashMap<>(); - rolePrivileges.put(randomAlphaOfLengthBetween(3, 6), "role:" + randomAlphaOfLengthBetween(4, 8)); + final Set rolePrivileges = Collections.singleton("role:(.*)"); final DocumentVersion docVersion = new DocumentVersion( randomAlphaOfLength(12), randomNonNegativeLong(), randomNonNegativeLong()); @@ -69,7 +67,7 @@ public class SamlServiceProviderResolverTests extends ESTestCase { document.setAuthenticationExpiry(null); document.setAcs(acs.toString()); document.privileges.setResource(resource); - document.privileges.setRoleActions(rolePrivileges); + document.privileges.setRolePatterns(rolePrivileges); document.attributeNames.setPrincipal(principalAttribute); document.attributeNames.setRoles(rolesAttribute); @@ -93,7 +91,10 @@ public class SamlServiceProviderResolverTests extends ESTestCase { assertThat(serviceProvider.getPrivileges(), notNullValue()); assertThat(serviceProvider.getPrivileges().getApplicationName(), equalTo(serviceProviderDefaults.applicationName)); assertThat(serviceProvider.getPrivileges().getResource(), equalTo(resource)); - assertThat(serviceProvider.getPrivileges().getRoleActions(), equalTo(rolePrivileges)); + final Function> roleMapping = serviceProvider.getPrivileges().getRoleMapping(); + assertThat(roleMapping, notNullValue()); + assertThat(roleMapping.apply("role:foo"), equalTo(org.elasticsearch.common.collect.Set.of("foo"))); + assertThat(roleMapping.apply("foo:bar"), equalTo(org.elasticsearch.common.collect.Set.of())); } public void testResolveReturnsCachedObject() throws Exception { diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/WildcardServiceProviderResolverTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/WildcardServiceProviderResolverTests.java index 314d4e951bc..63a95519a93 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/WildcardServiceProviderResolverTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/WildcardServiceProviderResolverTests.java @@ -37,7 +37,8 @@ public class WildcardServiceProviderResolverTests extends IdpSamlTestCase { + " \"template\": { " + " \"name\": \"{{service}} at example.com (A)\"," + " \"privileges\": {" - + " \"resource\": \"service1:example:{{service}}\"" + + " \"resource\": \"service1:example:{{service}}\"," + + " \"roles\": [ \"sso:(.*)\" ]" + " }," + " \"attributes\": {" + " \"principal\": \"http://cloud.elastic.co/saml/principal\"," @@ -54,7 +55,8 @@ public class WildcardServiceProviderResolverTests extends IdpSamlTestCase { + " \"template\": { " + " \"name\": \"{{service}} at example.com (B)\"," + " \"privileges\": {" - + " \"resource\": \"service1:example:{{service}}\"" + + " \"resource\": \"service1:example:{{service}}\"," + + " \"roles\": [ \"sso:(.*)\" ]" + " }," + " \"attributes\": {" + " \"principal\": \"http://cloud.elastic.co/saml/principal\"," @@ -71,7 +73,8 @@ public class WildcardServiceProviderResolverTests extends IdpSamlTestCase { + " \"template\": { " + " \"name\": \"{{id}} at example.net\"," + " \"privileges\": {" - + " \"resource\": \"service2:example:{{id}}\"" + + " \"resource\": \"service2:example:{{id}}\"," + + " \"roles\": [ \"sso:(.*)\" ]" + " }," + " \"attributes\": {" + " \"principal\": \"http://cloud.elastic.co/saml/principal\"," diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/test/IdentityProviderIntegTestCase.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/test/IdentityProviderIntegTestCase.java index ab7a48b142c..a3cd7006788 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/test/IdentityProviderIntegTestCase.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/test/IdentityProviderIntegTestCase.java @@ -227,9 +227,9 @@ public abstract class IdentityProviderIntegTestCase extends ESIntegTestCase { " resources: [ '" + SP_ENTITY_ID + "' ]\n" + " privileges: [ 'sso:superuser' ]\n" + "\n" + - // Console user should be able to call all IDP related endpoints + // Console user should be able to call all IDP related endpoints and register application privileges CONSOLE_USER_ROLE + ":\n" + - " cluster: ['cluster:admin/idp/*']\n" + + " cluster: ['cluster:admin/idp/*', 'cluster:admin/xpack/security/privilege/*' ]\n" + " indices: []\n"; }