Browse Source

Resolve SSO roles by pattern (#54440)

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").
Tim Vernum 5 years ago
parent
commit
90da8c9970
21 changed files with 376 additions and 111 deletions
  1. 1 2
      x-pack/plugin/core/src/main/resources/org/elasticsearch/xpack/idp/saml-service-provider-template.json
  2. 8 3
      x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/IdentityProviderAuthenticationIT.java
  3. 12 0
      x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/IdpRestTestCase.java
  4. 11 1
      x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/ManageServiceProviderRestIT.java
  5. 10 0
      x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/WildcardServiceProviderRestIT.java
  6. 1 1
      x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/roles.yml
  7. 2 6
      x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/wildcard_services.json
  8. 5 2
      x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java
  9. 135 0
      x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ApplicationActionsResolver.java
  10. 8 7
      x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ServiceProviderPrivileges.java
  11. 51 36
      x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolver.java
  12. 9 13
      x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocument.java
  13. 17 3
      x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderFactory.java
  14. 2 4
      x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/PutSamlServiceProviderRequestTests.java
  15. 44 2
      x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java
  16. 38 11
      x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolverTests.java
  17. 5 6
      x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocumentTests.java
  18. 4 5
      x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderIndexTests.java
  19. 5 4
      x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderResolverTests.java
  20. 6 3
      x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/WildcardServiceProviderResolverTests.java
  21. 2 2
      x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/test/IdentityProviderIntegTestCase.java

+ 1 - 2
x-pack/plugin/core/src/main/resources/org/elasticsearch/xpack/idp/saml-service-provider-template.json

@@ -57,8 +57,7 @@
               "type": "keyword"
               "type": "keyword"
             },
             },
             "roles": {
             "roles": {
-              "type": "object",
-              "dynamic": false
+              "type": "keyword"
             }
             }
           }
           }
         },
         },

+ 8 - 3
x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/IdentityProviderAuthenticationIT.java

@@ -27,6 +27,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.Base64;
 import java.util.Base64;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.Set;
 
 
 import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
 import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.contains;
@@ -43,8 +44,12 @@ public class IdentityProviderAuthenticationIT extends IdpRestTestCase {
     private final String REALM_NAME = "cloud-saml";
     private final String REALM_NAME = "cloud-saml";
 
 
     @Before
     @Before
-    public void createUsers() throws IOException {
+    public void setupSecurityData() throws IOException {
         setUserPassword("kibana", new SecureString("kibana".toCharArray()));
         setUserPassword("kibana", new SecureString("kibana".toCharArray()));
+        createApplicationPrivileges("elastic-cloud", Map.ofEntries(
+            Map.entry("deployment_admin", Set.of("sso:admin")),
+            Map.entry("deployment_viewer", Set.of("sso:viewer"))
+        ));
     }
     }
 
 
     public void testRegistrationAndIdpInitiatedSso() throws Exception {
     public void testRegistrationAndIdpInitiatedSso() throws Exception {
@@ -53,7 +58,7 @@ public class IdentityProviderAuthenticationIT extends IdpRestTestCase {
             Map.entry("acs", SP_ACS),
             Map.entry("acs", SP_ACS),
             Map.entry("privileges", Map.ofEntries(
             Map.entry("privileges", Map.ofEntries(
                 Map.entry("resource", SP_ENTITY_ID),
                 Map.entry("resource", SP_ENTITY_ID),
-                Map.entry("roles", Map.of("superuser", "role:superuser", "viewer", "role:viewer"))
+                Map.entry("roles", List.of("sso:(\\w+)"))
             )),
             )),
             Map.entry("attributes", Map.ofEntries(
             Map.entry("attributes", Map.ofEntries(
                 Map.entry("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"),
                 Map.entry("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"),
@@ -74,7 +79,7 @@ public class IdentityProviderAuthenticationIT extends IdpRestTestCase {
             Map.entry("acs", SP_ACS),
             Map.entry("acs", SP_ACS),
             Map.entry("privileges", Map.ofEntries(
             Map.entry("privileges", Map.ofEntries(
                 Map.entry("resource", SP_ENTITY_ID),
                 Map.entry("resource", SP_ENTITY_ID),
-                Map.entry("roles", Map.of("superuser", "role:superuser", "viewer", "role:viewer"))
+                Map.entry("roles", List.of("sso:(\\w+)"))
             )),
             )),
             Map.entry("attributes", Map.ofEntries(
             Map.entry("attributes", Map.ofEntries(
                 Map.entry("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"),
                 Map.entry("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"),

+ 12 - 0
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.ChangePasswordRequest;
 import org.elasticsearch.client.security.DeleteRoleRequest;
 import org.elasticsearch.client.security.DeleteRoleRequest;
 import org.elasticsearch.client.security.DeleteUserRequest;
 import org.elasticsearch.client.security.DeleteUserRequest;
+import org.elasticsearch.client.security.PutPrivilegesRequest;
 import org.elasticsearch.client.security.PutRoleRequest;
 import org.elasticsearch.client.security.PutRoleRequest;
 import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.RefreshPolicy;
 import org.elasticsearch.client.security.RefreshPolicy;
 import org.elasticsearch.client.security.user.User;
 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.ApplicationResourcePrivileges;
 import org.elasticsearch.client.security.user.privileges.IndicesPrivileges;
 import org.elasticsearch.client.security.user.privileges.IndicesPrivileges;
 import org.elasticsearch.client.security.user.privileges.Role;
 import org.elasticsearch.client.security.user.privileges.Role;
@@ -34,6 +36,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 
 import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
 import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.equalTo;
@@ -103,6 +106,15 @@ public abstract class IdpRestTestCase extends ESRestTestCase {
         client.security().deleteRole(request, RequestOptions.DEFAULT);
         client.security().deleteRole(request, RequestOptions.DEFAULT);
     }
     }
 
 
+    protected void createApplicationPrivileges(String applicationName, Map<String, Collection<String>> privileges) throws IOException {
+        final RestHighLevelClient client = getHighLevelAdminClient();
+        final List<ApplicationPrivilege> applicationPrivileges = privileges.entrySet().stream()
+            .map(e -> new ApplicationPrivilege(applicationName, e.getKey(), List.copyOf(e.getValue()), null))
+            .collect(Collectors.toUnmodifiableList());
+        final PutPrivilegesRequest request = new PutPrivilegesRequest(applicationPrivileges, RefreshPolicy.IMMEDIATE);
+        client.security().putPrivileges(request, RequestOptions.DEFAULT);
+    }
+
     protected void setUserPassword(String username, SecureString password) throws IOException {
     protected void setUserPassword(String username, SecureString password) throws IOException {
         final RestHighLevelClient client = getHighLevelAdminClient();
         final RestHighLevelClient client = getHighLevelAdminClient();
         final ChangePasswordRequest request = new ChangePasswordRequest(username, password.getChars(), RefreshPolicy.NONE);
         final ChangePasswordRequest request = new ChangePasswordRequest(username, password.getChars(), RefreshPolicy.NONE);

+ 11 - 1
x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/ManageServiceProviderRestIT.java

@@ -12,9 +12,11 @@ import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.common.xcontent.ObjectPath;
 import org.elasticsearch.common.xcontent.ObjectPath;
 import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderIndex;
 import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderIndex;
 import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderIndex.DocumentVersion;
 import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderIndex.DocumentVersion;
+import org.junit.Before;
 
 
 import java.io.IOException;
 import java.io.IOException;
 import java.util.Map;
 import java.util.Map;
+import java.util.Set;
 
 
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.containsString;
@@ -30,6 +32,14 @@ public class ManageServiceProviderRestIT extends IdpRestTestCase {
     // From SAMLConstants
     // From SAMLConstants
     private final String REDIRECT_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect";
     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", Map.ofEntries(
+            Map.entry("deployment_admin", Set.of("sso:superuser")),
+            Map.entry("deployment_viewer", Set.of("sso:viewer"))
+        ));
+    }
+
     public void testCreateAndDeleteServiceProvider() throws Exception {
     public void testCreateAndDeleteServiceProvider() throws Exception {
         final String entityId = "ec:" + randomAlphaOfLength(8) + ":" + randomAlphaOfLength(12);
         final String entityId = "ec:" + randomAlphaOfLength(8) + ":" + randomAlphaOfLength(12);
         final Map<String, Object> request = Map.ofEntries(
         final Map<String, Object> request = Map.ofEntries(
@@ -37,7 +47,7 @@ public class ManageServiceProviderRestIT extends IdpRestTestCase {
             Map.entry("acs", "https://sp1.test.es.elasticsearch.org/saml/acs"),
             Map.entry("acs", "https://sp1.test.es.elasticsearch.org/saml/acs"),
             Map.entry("privileges", Map.ofEntries(
             Map.entry("privileges", Map.ofEntries(
                 Map.entry("resource", entityId),
                 Map.entry("resource", entityId),
-                Map.entry("roles", Map.of("superuser", "role:superuser", "viewer", "role:viewer"))
+                Map.entry("roles", Set.of("role:(\\w+)"))
             )),
             )),
             Map.entry("attributes", Map.ofEntries(
             Map.entry("attributes", Map.ofEntries(
                 Map.entry("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"),
                 Map.entry("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"),

+ 10 - 0
x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/java/org/elasticsearch/xpack/idp/WildcardServiceProviderRestIT.java

@@ -14,10 +14,12 @@ import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
 import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
+import org.junit.Before;
 
 
 import java.io.IOException;
 import java.io.IOException;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.Set;
 
 
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.containsString;
@@ -32,6 +34,14 @@ public class WildcardServiceProviderRestIT extends IdpRestTestCase {
     // From SAMLConstants
     // From SAMLConstants
     private final String REDIRECT_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect";
     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", Map.ofEntries(
+            Map.entry("deployment_admin", Set.of("sso:admin")),
+            Map.entry("deployment_viewer", Set.of("sso:viewer"))
+        ));
+    }
+
     public void testGetWildcardServiceProviderMetadata() throws Exception {
     public void testGetWildcardServiceProviderMetadata() throws Exception {
         final String owner = randomAlphaOfLength(8);
         final String owner = randomAlphaOfLength(8);
         final String service = randomAlphaOfLength(8);
         final String service = randomAlphaOfLength(8);

+ 1 - 1
x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/roles.yml

@@ -8,4 +8,4 @@ idp_user:
   applications:
   applications:
     - application: elastic-cloud
     - application: elastic-cloud
       resources: ["ec:123456:abcdefg"]
       resources: ["ec:123456:abcdefg"]
-      privileges: ["role:viewer"]
+      privileges: ["sso:viewer"]

+ 2 - 6
x-pack/plugin/identity-provider/qa/idp-rest-tests/src/test/resources/wildcard_services.json

@@ -8,9 +8,7 @@
         "name": "Application 1 ({{service}})",
         "name": "Application 1 ({{service}})",
         "privileges": {
         "privileges": {
           "resource": "sso:{{entity_id}}",
           "resource": "sso:{{entity_id}}",
-          "roles": {
-            "admin": "sso:admin"
-          }
+          "roles": [ "sso:(.*)" ]
         },
         },
         "attributes": {
         "attributes": {
           "principal": "saml:attribute:principal",
           "principal": "saml:attribute:principal",
@@ -28,9 +26,7 @@
         "name": "Application 2 ({{service}})",
         "name": "Application 2 ({{service}})",
         "privileges": {
         "privileges": {
           "resource": "sso:{{entity_id}}",
           "resource": "sso:{{entity_id}}",
-          "roles": {
-            "admin": "sso:admin"
-          }
+          "roles": [ "sso:(.*)" ]
         },
         },
         "attributes": {
         "attributes": {
           "principal": "saml:attribute:principal",
           "principal": "saml:attribute:principal",

+ 5 - 2
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.TransportSamlInitiateSingleSignOnAction;
 import org.elasticsearch.xpack.idp.action.TransportSamlMetadataAction;
 import org.elasticsearch.xpack.idp.action.TransportSamlMetadataAction;
 import org.elasticsearch.xpack.idp.action.TransportSamlValidateAuthnRequestAction;
 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.privileges.UserPrivilegeResolver;
 import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider;
 import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider;
 import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProviderBuilder;
 import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProviderBuilder;
@@ -94,10 +95,11 @@ public class IdentityProviderPlugin extends Plugin implements ActionPlugin {
         SamlInit.initialize();
         SamlInit.initialize();
         final SamlServiceProviderIndex index = new SamlServiceProviderIndex(client, clusterService);
         final SamlServiceProviderIndex index = new SamlServiceProviderIndex(client, clusterService);
         final SecurityContext securityContext = new SecurityContext(settings, threadPool.getThreadContext());
         final SecurityContext securityContext = new SecurityContext(settings, threadPool.getThreadContext());
-        final UserPrivilegeResolver userPrivilegeResolver = new UserPrivilegeResolver(client, securityContext);
-
 
 
         final ServiceProviderDefaults serviceProviderDefaults = ServiceProviderDefaults.forSettings(settings);
         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 SamlServiceProviderFactory serviceProviderFactory = new SamlServiceProviderFactory(serviceProviderDefaults);
         final SamlServiceProviderResolver registeredServiceProviderResolver
         final SamlServiceProviderResolver registeredServiceProviderResolver
             = new SamlServiceProviderResolver(settings, index, serviceProviderFactory);
             = new SamlServiceProviderResolver(settings, index, serviceProviderFactory);
@@ -157,6 +159,7 @@ public class IdentityProviderPlugin extends Plugin implements ActionPlugin {
         settings.addAll(ServiceProviderCacheSettings.getSettings());
         settings.addAll(ServiceProviderCacheSettings.getSettings());
         settings.addAll(ServiceProviderDefaults.getSettings());
         settings.addAll(ServiceProviderDefaults.getSettings());
         settings.addAll(WildcardServiceProviderResolver.getSettings());
         settings.addAll(WildcardServiceProviderResolver.getSettings());
+        settings.addAll(ApplicationActionsResolver.getSettings());
         settings.addAll(X509KeyPairSettings.withPrefix("xpack.idp.signing.", false).getAllSettings());
         settings.addAll(X509KeyPairSettings.withPrefix("xpack.idp.signing.", false).getAllSettings());
         settings.addAll(X509KeyPairSettings.withPrefix("xpack.idp.metadata_signing.", false).getAllSettings());
         settings.addAll(X509KeyPairSettings.withPrefix("xpack.idp.metadata_signing.", false).getAllSettings());
         return Collections.unmodifiableList(settings);
         return Collections.unmodifiableList(settings);

+ 135 - 0
x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ApplicationActionsResolver.java

@@ -0,0 +1,135 @@
+/*
+ * 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.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.List;
+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<Integer> CACHE_SIZE
+        = Setting.intSetting("xpack.idp.privileges.cache.size", CACHE_SIZE_DEFAULT, Setting.Property.NodeScope);
+    public static final Setting<TimeValue> 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<String, Set<String>> 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.<String, Set<String>>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<? extends Setting<?>> 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<Set<String>> listener) {
+        final Set<String> actions = this.cache.get(application);
+        if (actions == null || actions.isEmpty()) {
+            loadActions(application, listener);
+        } else {
+            listener.onResponse(actions);
+        }
+    }
+
+    private void loadActions(String applicationName, ActionListener<Set<String>> listener) {
+        final GetPrivilegesRequest request = new GetPrivilegesRequest();
+        request.application(applicationName);
+        this.client.execute(GetPrivilegesAction.INSTANCE, request, ActionListener.wrap(
+            response -> {
+                final Set<String> fixedActions = Stream.of(response.privileges())
+                    .map(p -> p.getActions())
+                    .flatMap(Collection::stream)
+                    .filter(s -> s.indexOf('*') == -1)
+                    .collect(Collectors.toUnmodifiableSet());
+                cache.put(applicationName, fixedActions);
+                listener.onResponse(fixedActions);
+            },
+            listener::onFailure
+        ));
+    }
+}

+ 8 - 7
x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/ServiceProviderPrivileges.java

@@ -9,19 +9,20 @@ package org.elasticsearch.xpack.idp.privileges;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
 import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
 import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
 
 
-import java.util.Map;
 import java.util.Objects;
 import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
 
 
 public class ServiceProviderPrivileges {
 public class ServiceProviderPrivileges {
 
 
     private final String applicationName;
     private final String applicationName;
     private final String resource;
     private final String resource;
-    private final Map<String, String> roles;
+    private final Function<String, Set<String>> roleMapping;
 
 
-    public ServiceProviderPrivileges(String applicationName, String resource, Map<String, String> roles) {
+    public ServiceProviderPrivileges(String applicationName, String resource, Function<String, Set<String>> roleMapping) {
         this.applicationName = Objects.requireNonNull(applicationName, "Application name cannot be null");
         this.applicationName = Objects.requireNonNull(applicationName, "Application name cannot be null");
         this.resource = Objects.requireNonNull(resource, "Resource cannot be null");
         this.resource = Objects.requireNonNull(resource, "Resource cannot be null");
-        this.roles = Map.copyOf(roles);
+        this.roleMapping = Objects.requireNonNull(roleMapping, "Role Mapping cannot be null");
     }
     }
 
 
     /**
     /**
@@ -41,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.
      * 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.
      * 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
      * These roles have no semantic meaning within the IdP, they are simply metadata that we pass to the Service Provider. They may not
@@ -49,7 +50,7 @@ public class ServiceProviderPrivileges {
      * terminology (e.g. "groups").
      * terminology (e.g. "groups").
      * The actions will be resolved as application privileges from the IdP's security cluster.
      * The actions will be resolved as application privileges from the IdP's security cluster.
      */
      */
-    public Map<String, String> getRoleActions() {
-        return roles;
+    public Function<String, Set<String>> getRoleMapping() {
+        return roleMapping;
     }
     }
 }
 }

+ 51 - 36
x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolver.java

@@ -20,7 +20,6 @@ import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges
 
 
 import java.util.Map;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
@@ -37,7 +36,7 @@ public class UserPrivilegeResolver {
         public UserPrivileges(String principal, boolean hasAccess, Set<String> roles) {
         public UserPrivileges(String principal, boolean hasAccess, Set<String> roles) {
             this.principal = Objects.requireNonNull(principal, "principal may not be null");
             this.principal = Objects.requireNonNull(principal, "principal may not be null");
             if (hasAccess == false && roles.isEmpty() == false) {
             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.hasAccess = hasAccess;
             this.roles = Set.copyOf(Objects.requireNonNull(roles, "roles may not be null"));
             this.roles = Set.copyOf(Objects.requireNonNull(roles, "roles may not be null"));
@@ -66,10 +65,12 @@ public class UserPrivilegeResolver {
     private final Logger logger = LogManager.getLogger();
     private final Logger logger = LogManager.getLogger();
     private final Client client;
     private final Client client;
     private final SecurityContext securityContext;
     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.client = client;
         this.securityContext = securityContext;
         this.securityContext = securityContext;
+        this.actionsResolver = actionsResolver;
     }
     }
 
 
     /**
     /**
@@ -77,22 +78,29 @@ public class UserPrivilegeResolver {
      * Requires that the active user is set in the {@link org.elasticsearch.xpack.core.security.SecurityContext}.
      * Requires that the active user is set in the {@link org.elasticsearch.xpack.core.security.SecurityContext}.
      */
      */
     public void resolve(ServiceProviderPrivileges service, ActionListener<UserPrivileges> listener) {
     public void resolve(ServiceProviderPrivileges service, ActionListener<UserPrivileges> 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) {
     private UserPrivileges buildResult(HasPrivilegesResponse response, ServiceProviderPrivileges service) {
@@ -100,28 +108,35 @@ public class UserPrivilegeResolver {
         if (appPrivileges == null || appPrivileges.isEmpty()) {
         if (appPrivileges == null || appPrivileges.isEmpty()) {
             return UserPrivileges.noAccess(response.getUsername());
             return UserPrivileges.noAccess(response.getUsername());
         }
         }
-        final Set<String> roles = service.getRoleActions().entrySet().stream()
-            .filter(entry -> checkAccess(appPrivileges, entry.getValue(), service.getResource()))
+
+        final Set<String> 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(Map.Entry::getKey)
+            .map(service.getRoleMapping())
+            .filter(Objects::nonNull)
+            .flatMap(Set::stream)
             .collect(Collectors.toUnmodifiableSet());
             .collect(Collectors.toUnmodifiableSet());
         final boolean hasAccess = roles.isEmpty() == false;
         final boolean hasAccess = roles.isEmpty() == false;
         return new UserPrivileges(response.getUsername(), hasAccess, roles);
         return new UserPrivileges(response.getUsername(), hasAccess, roles);
     }
     }
 
 
-    private boolean checkAccess(Set<ResourcePrivileges> userPrivileges, String action, String resource) {
-        final Optional<ResourcePrivileges> 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<RoleDescriptor.ApplicationResourcePrivileges> 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));
     }
     }
 }
 }

+ 9 - 13
x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocument.java

@@ -36,7 +36,6 @@ import java.time.Instant;
 import java.util.Base64;
 import java.util.Base64;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.List;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Objects;
 import java.util.Set;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.BiConsumer;
@@ -53,14 +52,14 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable
 
 
     public static class Privileges {
     public static class Privileges {
         public String resource;
         public String resource;
-        public Map<String, String> roleActions = Map.of();
+        public Set<String> rolePatterns = Set.of();
 
 
         public void setResource(String resource) {
         public void setResource(String resource) {
             this.resource = resource;
             this.resource = resource;
         }
         }
 
 
-        public void setRoleActions(Map<String, String> roleActions) {
-            this.roleActions = roleActions;
+        public void setRolePatterns(Collection<String> rolePatterns) {
+            this.rolePatterns = Set.copyOf(rolePatterns);
         }
         }
 
 
         @Override
         @Override
@@ -69,12 +68,12 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable
             if (o == null || getClass() != o.getClass()) return false;
             if (o == null || getClass() != o.getClass()) return false;
             final Privileges that = (Privileges) o;
             final Privileges that = (Privileges) o;
             return Objects.equals(resource, that.resource) &&
             return Objects.equals(resource, that.resource) &&
-                Objects.equals(roleActions, that.roleActions);
+                Objects.equals(rolePatterns, that.rolePatterns);
         }
         }
 
 
         @Override
         @Override
         public int hashCode() {
         public int hashCode() {
-            return Objects.hash(resource, roleActions);
+            return Objects.hash(resource, rolePatterns);
         }
         }
     }
     }
 
 
@@ -259,7 +258,7 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable
         authenticationExpiryMillis = in.readOptionalVLong();
         authenticationExpiryMillis = in.readOptionalVLong();
 
 
         privileges.resource = in.readString();
         privileges.resource = in.readString();
-        privileges.roleActions = in.readMap(StreamInput::readString, StreamInput::readString);
+        privileges.rolePatterns = in.readSet(StreamInput::readString);
 
 
         attributeNames.principal = in.readString();
         attributeNames.principal = in.readString();
         attributeNames.email = in.readOptionalString();
         attributeNames.email = in.readOptionalString();
@@ -284,8 +283,7 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable
         out.writeOptionalVLong(authenticationExpiryMillis);
         out.writeOptionalVLong(authenticationExpiryMillis);
 
 
         out.writeString(privileges.resource);
         out.writeString(privileges.resource);
-        out.writeMap(privileges.roleActions == null ? Map.of() : privileges.roleActions,
-            StreamOutput::writeString, StreamOutput::writeString);
+        out.writeStringCollection(privileges.rolePatterns == null ? Set.of() : privileges.rolePatterns);
 
 
         out.writeString(attributeNames.principal);
         out.writeString(attributeNames.principal);
         out.writeOptionalString(attributeNames.email);
         out.writeOptionalString(attributeNames.email);
@@ -406,9 +404,7 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable
 
 
         DOC_PARSER.declareObject(NULL_CONSUMER, (parser, doc) -> PRIVILEGES_PARSER.parse(parser, doc.privileges, null), Fields.PRIVILEGES);
         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.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);
         DOC_PARSER.declareObject(NULL_CONSUMER, (p, doc) -> ATTRIBUTES_PARSER.parse(p, doc.attributeNames, null), Fields.ATTRIBUTES);
         ATTRIBUTES_PARSER.declareString(AttributeNames::setPrincipal, Fields.Attributes.PRINCIPAL);
         ATTRIBUTES_PARSER.declareString(AttributeNames::setPrincipal, Fields.Attributes.PRINCIPAL);
@@ -482,7 +478,7 @@ public class SamlServiceProviderDocument implements ToXContentObject, Writeable
 
 
         builder.startObject(Fields.PRIVILEGES.getPreferredName());
         builder.startObject(Fields.PRIVILEGES.getPreferredName());
         builder.field(Fields.Privileges.RESOURCE.getPreferredName(), privileges.resource);
         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.endObject();
 
 
         builder.startObject(Fields.ATTRIBUTES.getPreferredName());
         builder.startObject(Fields.ATTRIBUTES.getPreferredName());

+ 17 - 3
x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderFactory.java

@@ -13,9 +13,11 @@ import org.opensaml.security.x509.X509Credential;
 
 
 import java.net.MalformedURLException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URL;
-import java.util.Map;
 import java.util.Optional;
 import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
 /**
 /**
@@ -57,8 +59,20 @@ public final class SamlServiceProviderFactory {
 
 
     private ServiceProviderPrivileges buildPrivileges(SamlServiceProviderDocument.Privileges configuredPrivileges) {
     private ServiceProviderPrivileges buildPrivileges(SamlServiceProviderDocument.Privileges configuredPrivileges) {
         final String resource = configuredPrivileges.resource;
         final String resource = configuredPrivileges.resource;
-        final Map<String, String> roles = Optional.ofNullable(configuredPrivileges.roleActions).orElse(Map.of());
-        return new ServiceProviderPrivileges(defaults.applicationName, resource, roles);
+        final Function<String, Set<String>> roleMapping;
+        if (configuredPrivileges.rolePatterns == null || configuredPrivileges.rolePatterns.isEmpty()) {
+            roleMapping = in -> Set.of();
+        } else {
+            final Set<Pattern> patterns = configuredPrivileges.rolePatterns.stream()
+                .map(Pattern::compile)
+                .collect(Collectors.toUnmodifiableSet());
+            roleMapping = action -> patterns.stream()
+                .map(p -> p.matcher(action))
+                .filter(Matcher::matches)
+                .map(m -> m.group(1))
+                .collect(Collectors.toUnmodifiableSet());
+        }
+        return new ServiceProviderPrivileges(defaults.applicationName, resource, roleMapping);
     }
     }
 
 
     private URL parseUrl(SamlServiceProviderDocument document) {
     private URL parseUrl(SamlServiceProviderDocument document) {

+ 2 - 4
x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/PutSamlServiceProviderRequestTests.java

@@ -28,7 +28,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
 import static org.elasticsearch.common.xcontent.XContentHelper.convertToMap;
 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.contains;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.emptyIterable;
 import static org.hamcrest.Matchers.emptyIterable;
@@ -104,7 +103,7 @@ public class PutSamlServiceProviderRequestTests extends ESTestCase {
         ));
         ));
         fields.put("privileges", Map.of(
         fields.put("privileges", Map.of(
             "resource", "ece:deployment:" + randomLongBetween(1_000_000, 999_999_999),
             "resource", "ece:deployment:" + randomLongBetween(1_000_000, 999_999_999),
-            "roles", Map.of("role_name", "role:" + randomAlphaOfLengthBetween(4, 8))
+            "roles", List.of("role:(.*)")
         ));
         ));
         fields.put("certificates", Map.of());
         fields.put("certificates", Map.of());
         final String entityId = "https://www." + randomAlphaOfLengthBetween(5, 12) + ".app/";
         final String entityId = "https://www." + randomAlphaOfLengthBetween(5, 12) + ".app/";
@@ -114,8 +113,7 @@ public class PutSamlServiceProviderRequestTests extends ESTestCase {
         assertThat(request.getDocument().acs, equalTo(fields.get("acs")));
         assertThat(request.getDocument().acs, equalTo(fields.get("acs")));
         assertThat(request.getDocument().enabled, equalTo(fields.get("enabled")));
         assertThat(request.getDocument().enabled, equalTo(fields.get("enabled")));
         assertThat(request.getDocument().privileges.resource, notNullValue());
         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.principal, startsWith("urn:oid:0.1"));
         assertThat(request.getDocument().attributeNames.email, startsWith("urn:oid:0.2"));
         assertThat(request.getDocument().attributeNames.email, startsWith("urn:oid:0.2"));
         assertThat(request.getDocument().attributeNames.name, startsWith("urn:oid:0.3"));
         assertThat(request.getDocument().attributeNames.name, startsWith("urn:oid:0.3"));

+ 44 - 2
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 org.opensaml.xmlsec.signature.support.SignatureConstants;
 
 
 import java.io.ByteArrayOutputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.io.UnsupportedEncodingException;
 import java.io.UnsupportedEncodingException;
 import java.net.URL;
 import java.net.URL;
 import java.net.URLEncoder;
 import java.net.URLEncoder;
@@ -49,6 +51,7 @@ import java.util.Base64;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
 import java.util.zip.Deflater;
 import java.util.zip.Deflater;
 import java.util.zip.DeflaterOutputStream;
 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 acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs";
         String entityId = SP_ENTITY_ID;
         String entityId = SP_ENTITY_ID;
         registerServiceProvider(entityId, acsUrl);
         registerServiceProvider(entityId, acsUrl);
+        registerApplicationPrivileges();
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
 
 
         // User login a.k.a exchange the user credentials for an API Key
         // User login a.k.a exchange the user credentials for an API Key
@@ -96,13 +100,16 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase {
         Map<String, String> serviceProvider = objectPath.evaluate("service_provider");
         Map<String, String> serviceProvider = objectPath.evaluate("service_provider");
         assertThat(serviceProvider, hasKey("entity_id"));
         assertThat(serviceProvider, hasKey("entity_id"));
         assertThat(serviceProvider.get("entity_id"), equalTo(entityId));
         assertThat(serviceProvider.get("entity_id"), equalTo(entityId));
+
         assertContainsAttributeWithValue(body, "principal", SAMPLE_IDPUSER_NAME);
         assertContainsAttributeWithValue(body, "principal", SAMPLE_IDPUSER_NAME);
+        assertContainsAttributeWithValue(body, "roles", "superuser");
     }
     }
 
 
     public void testIdPInitiatedSsoFailsForUnknownSP() throws Exception {
     public void testIdPInitiatedSsoFailsForUnknownSP() throws Exception {
         String acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs";
         String acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs";
         String entityId = SP_ENTITY_ID;
         String entityId = SP_ENTITY_ID;
         registerServiceProvider(entityId, acsUrl);
         registerServiceProvider(entityId, acsUrl);
+        registerApplicationPrivileges();
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
         // User login a.k.a exchange the user credentials for an API Key
         // User login a.k.a exchange the user credentials for an API Key
         final String apiKeyCredentials = getApiKeyFromCredentials(SAMPLE_IDPUSER_NAME,
         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 acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs";
         String entityId = SP_ENTITY_ID;
         String entityId = SP_ENTITY_ID;
         registerServiceProvider(entityId, acsUrl);
         registerServiceProvider(entityId, acsUrl);
+        registerApplicationPrivileges();
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
         // Make a request to init an SSO flow with the API Key as secondary authentication
         // Make a request to init an SSO flow with the API Key as secondary authentication
         Request request = new Request("POST", "/_idp/saml/init");
         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 acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs";
         String entityId = SP_ENTITY_ID;
         String entityId = SP_ENTITY_ID;
         registerServiceProvider(entityId, acsUrl);
         registerServiceProvider(entityId, acsUrl);
+        registerApplicationPrivileges();
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
         // Validate incoming authentication request
         // Validate incoming authentication request
         Request validateRequest = new Request("POST", "/_idp/saml/validate");
         Request validateRequest = new Request("POST", "/_idp/saml/validate");
@@ -182,6 +191,7 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase {
         Response initResponse = getRestClient().performRequest(initRequest);
         Response initResponse = getRestClient().performRequest(initRequest);
         ObjectPath initResponseObject = ObjectPath.createFromResponse(initResponse);
         ObjectPath initResponseObject = ObjectPath.createFromResponse(initResponse);
         assertThat(initResponseObject.evaluate("post_url").toString(), equalTo(acsUrl));
         assertThat(initResponseObject.evaluate("post_url").toString(), equalTo(acsUrl));
+
         final String body = initResponseObject.evaluate("saml_response").toString();
         final String body = initResponseObject.evaluate("saml_response").toString();
         assertThat(body, containsString("<saml2p:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>"));
         assertThat(body, containsString("<saml2p:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>"));
         assertThat(body, containsString("Destination=\"" + acsUrl + "\""));
         assertThat(body, containsString("Destination=\"" + acsUrl + "\""));
@@ -192,6 +202,7 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase {
         assertThat(sp, hasKey("entity_id"));
         assertThat(sp, hasKey("entity_id"));
         assertThat(sp.get("entity_id"), equalTo(entityId));
         assertThat(sp.get("entity_id"), equalTo(entityId));
         assertContainsAttributeWithValue(body, "principal", SAMPLE_IDPUSER_NAME);
         assertContainsAttributeWithValue(body, "principal", SAMPLE_IDPUSER_NAME);
+        assertContainsAttributeWithValue(body, "roles", "superuser");
     }
     }
 
 
     public void testSpInitiatedSsoFailsForUserWithNoAccess() throws Exception {
     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 acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs";
         String entityId = SP_ENTITY_ID;
         String entityId = SP_ENTITY_ID;
         registerServiceProvider(entityId, acsUrl);
         registerServiceProvider(entityId, acsUrl);
+        registerApplicationPrivileges();
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
         // Validate incoming authentication request
         // Validate incoming authentication request
         Request validateRequest = new Request("POST", "/_idp/saml/validate");
         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 acsUrl = "https://" + randomAlphaOfLength(12) + ".elastic-cloud.com/saml/acs";
         String entityId = SP_ENTITY_ID;
         String entityId = SP_ENTITY_ID;
         registerServiceProvider(entityId, acsUrl);
         registerServiceProvider(entityId, acsUrl);
+        registerApplicationPrivileges();
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
         ensureGreen(SamlServiceProviderIndex.INDEX_NAME);
         // Validate incoming authentication request
         // Validate incoming authentication request
         Request validateRequest = new Request("POST", "/_idp/saml/validate");
         Request validateRequest = new Request("POST", "/_idp/saml/validate");
@@ -308,10 +321,12 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase {
         spFields.put(SamlServiceProviderDocument.Fields.NAME_ID.getPreferredName(), TRANSIENT);
         spFields.put(SamlServiceProviderDocument.Fields.NAME_ID.getPreferredName(), TRANSIENT);
         spFields.put(SamlServiceProviderDocument.Fields.NAME.getPreferredName(), "Dummy SP");
         spFields.put(SamlServiceProviderDocument.Fields.NAME.getPreferredName(), "Dummy SP");
         spFields.put("attributes", Map.of(
         spFields.put("attributes", Map.of(
-            "principal", "https://saml.elasticsearch.org/attributes/principal"));
+            "principal", "https://saml.elasticsearch.org/attributes/principal",
+            "roles", "https://saml.elasticsearch.org/attributes/roles"
+        ));
         spFields.put("privileges", Map.of(
         spFields.put("privileges", Map.of(
             "resource", entityId,
             "resource", entityId,
-            "roles", Map.of("superuser", "sso:superuser")
+            "roles", Set.of("sso:(\\w+)")
         ));
         ));
         Request request =
         Request request =
             new Request("PUT", "/_idp/saml/sp/" + urlEncode(entityId) + "?refresh=" + WriteRequest.RefreshPolicy.IMMEDIATE.getValue());
             new Request("PUT", "/_idp/saml/sp/" + urlEncode(entityId) + "?refresh=" + WriteRequest.RefreshPolicy.IMMEDIATE.getValue());
@@ -332,6 +347,33 @@ public class SamlIdentityProviderTests extends IdentityProviderIntegTestCase {
         assertThat(serviceProvider.get("enabled"), equalTo(true));
         assertThat(serviceProvider.get("enabled"), equalTo(true));
     }
     }
 
 
+    private void registerApplicationPrivileges() throws IOException {
+        registerApplicationPrivileges(Map.of("deployment_admin", Set.of("sso:superuser"), "deployment_viewer", Set.of("sso:viewer")));
+    }
+
+    private void registerApplicationPrivileges(Map<String, Set<String>> 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) {
     private String getApiKeyFromCredentials(String username, SecureString password) {
         Client client = client().filterWithHeader(Collections.singletonMap("Authorization",
         Client client = client().filterWithHeader(Collections.singletonMap("Authorization",
             UsernamePasswordToken.basicAuthHeaderValue(username, password)));
             UsernamePasswordToken.basicAuthHeaderValue(username, password)));

+ 38 - 11
x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolverTests.java

@@ -28,13 +28,18 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
+import static org.hamcrest.Matchers.arrayWithSize;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.emptyIterable;
 import static org.hamcrest.Matchers.emptyIterable;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.equalTo;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.same;
 import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
 
 
 public class UserPrivilegeResolverTests extends ESTestCase {
 public class UserPrivilegeResolverTests extends ESTestCase {
 
 
@@ -44,9 +49,17 @@ public class UserPrivilegeResolverTests extends ESTestCase {
 
 
     @Before
     @Before
     public void setupTest() {
     public void setupTest() {
-        client = Mockito.mock(Client.class);
+        client = mock(Client.class);
         securityContext = new SecurityContext(Settings.EMPTY, new ThreadContext(Settings.EMPTY));
         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<Set<String>> listener = (ActionListener<Set<String>>) args[args.length - 1];
+            listener.onResponse(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 {
     public void testResolveZeroAccess() throws Exception {
@@ -55,8 +68,9 @@ public class UserPrivilegeResolverTests extends ESTestCase {
         setupUser(username);
         setupUser(username);
         setupHasPrivileges(username, app);
         setupHasPrivileges(username, app);
         final PlainActionFuture<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
         final PlainActionFuture<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
-        resolver.resolve(service(app, "cluster:" + randomLong(),
-            Map.of("viewer", "role:cluster:view", "admin", "role:cluster:admin")), future);
+        final Function<String, Set<String>> roleMapping =
+            Map.of("role:cluster:view", Set.of("viewer"), "role:cluster:admin", Set.of("admin"))::get;
+        resolver.resolve(service(app, "cluster:" + randomLong(), roleMapping), future);
         final UserPrivilegeResolver.UserPrivileges privileges = future.get();
         final UserPrivilegeResolver.UserPrivileges privileges = future.get();
         assertThat(privileges.principal, equalTo(username));
         assertThat(privileges.principal, equalTo(username));
         assertThat(privileges.hasAccess, equalTo(false));
         assertThat(privileges.hasAccess, equalTo(false));
@@ -74,7 +88,8 @@ public class UserPrivilegeResolverTests extends ESTestCase {
         setupHasPrivileges(username, app, access(resource, viewerAction, false), access(resource, adminAction, false));
         setupHasPrivileges(username, app, access(resource, viewerAction, false), access(resource, adminAction, false));
 
 
         final PlainActionFuture<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
         final PlainActionFuture<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
-        resolver.resolve(service(app, resource, Map.of("viewer", viewerAction, "admin", adminAction)), future);
+        final Function<String, Set<String>> roleMapping = Map.of(viewerAction, Set.of("viewer"), adminAction, Set.of("admin"))::get;
+        resolver.resolve(service(app, resource, roleMapping), future);
         final UserPrivilegeResolver.UserPrivileges privileges = future.get();
         final UserPrivilegeResolver.UserPrivileges privileges = future.get();
         assertThat(privileges.principal, equalTo(username));
         assertThat(privileges.principal, equalTo(username));
         assertThat(privileges.hasAccess, equalTo(false));
         assertThat(privileges.hasAccess, equalTo(false));
@@ -92,7 +107,8 @@ public class UserPrivilegeResolverTests extends ESTestCase {
         setupHasPrivileges(username, app, access(resource, viewerAction, true), access(resource, adminAction, false));
         setupHasPrivileges(username, app, access(resource, viewerAction, true), access(resource, adminAction, false));
 
 
         final PlainActionFuture<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
         final PlainActionFuture<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
-        resolver.resolve(service(app, resource, Map.of("viewer", viewerAction, "admin", adminAction)), future);
+        final Function<String, Set<String>> roleMapping = Map.of(viewerAction, Set.of("viewer"), adminAction, Set.of("admin"))::get;
+        resolver.resolve(service(app, resource, roleMapping), future);
         final UserPrivilegeResolver.UserPrivileges privileges = future.get();
         final UserPrivilegeResolver.UserPrivileges privileges = future.get();
         assertThat(privileges.principal, equalTo(username));
         assertThat(privileges.principal, equalTo(username));
         assertThat(privileges.hasAccess, equalTo(true));
         assertThat(privileges.hasAccess, equalTo(true));
@@ -117,17 +133,28 @@ public class UserPrivilegeResolverTests extends ESTestCase {
         );
         );
 
 
         final PlainActionFuture<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
         final PlainActionFuture<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
-        resolver.resolve(service(app, resource,
-            Map.of("viewer", viewerAction, "admin", adminAction, "operator", operatorAction, "monitor", monitorAction)),
-            future);
+        Function<String, Set<String>> roleMapping = action -> {
+            switch (action) {
+                case viewerAction:
+                    return Set.of("viewer");
+                case adminAction:
+                    return Set.of("admin");
+                case operatorAction:
+                    return Set.of("operator");
+                case monitorAction:
+                    return Set.of("monitor");
+            }
+            return Set.of();
+        };
+        resolver.resolve(service(app, resource, roleMapping), future);
         final UserPrivilegeResolver.UserPrivileges privileges = future.get();
         final UserPrivilegeResolver.UserPrivileges privileges = future.get();
         assertThat(privileges.principal, equalTo(username));
         assertThat(privileges.principal, equalTo(username));
         assertThat(privileges.hasAccess, equalTo(true));
         assertThat(privileges.hasAccess, equalTo(true));
         assertThat(privileges.roles, containsInAnyOrder("operator", "monitor"));
         assertThat(privileges.roles, containsInAnyOrder("operator", "monitor"));
     }
     }
 
 
-    private ServiceProviderPrivileges service(String appName, String resource, Map<String, String> roles) {
-        return new ServiceProviderPrivileges(appName, resource, roles);
+    private ServiceProviderPrivileges service(String appName, String resource, Function<String, Set<String>> roleMapping) {
+        return new ServiceProviderPrivileges(appName, resource, roleMapping);
     }
     }
 
 
     private HasPrivilegesResponse setupHasPrivileges(String username, String appName,
     private HasPrivilegesResponse setupHasPrivileges(String username, String appName,

+ 5 - 6
x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocumentTests.java

@@ -25,12 +25,11 @@ import org.opensaml.security.x509.X509Credential;
 import java.io.IOException;
 import java.io.IOException;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
-import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.List;
-import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
-import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.emptyIterable;
 import static org.hamcrest.Matchers.emptyIterable;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.not;
@@ -109,11 +108,11 @@ public class SamlServiceProviderDocumentTests extends IdpSamlTestCase {
         doc1.certificates.setIdentityProviderX509MetadataSigningCertificates(idpMetadataCertificates);
         doc1.certificates.setIdentityProviderX509MetadataSigningCertificates(idpMetadataCertificates);
 
 
         doc1.privileges.setResource("service:" + randomAlphaOfLength(12) + ":" + randomAlphaOfLength(12));
         doc1.privileges.setResource("service:" + randomAlphaOfLength(12) + ":" + randomAlphaOfLength(12));
-        final Map<String, String> roleActions = new HashMap<>();
+        final Set<String> rolePatterns = new HashSet<>();
         for (int i = randomIntBetween(1, 6); i > 0; i--) {
         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.setPrincipal("urn:" + randomAlphaOfLengthBetween(4, 8) + "." + randomAlphaOfLengthBetween(4, 8));
         doc1.attributeNames.setEmail("urn:" + randomAlphaOfLengthBetween(4, 8) + "." + randomAlphaOfLengthBetween(4, 8));
         doc1.attributeNames.setEmail("urn:" + randomAlphaOfLengthBetween(4, 8) + "." + randomAlphaOfLengthBetween(4, 8));

+ 4 - 5
x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderIndexTests.java

@@ -30,10 +30,9 @@ import org.junit.Before;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collection;
-import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
-import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
@@ -270,11 +269,11 @@ public class SamlServiceProviderIndexTests extends ESSingleNodeTestCase {
 
 
         document.privileges.setResource("app:" + randomAlphaOfLengthBetween(3, 6) + ":" + Math.abs(randomLong()));
         document.privileges.setResource("app:" + randomAlphaOfLengthBetween(3, 6) + ":" + Math.abs(randomLong()));
         final int roleCount = randomIntBetween(0, 4);
         final int roleCount = randomIntBetween(0, 4);
-        final Map<String, String> roles = new HashMap<>();
+        final Set<String> roles = new HashSet<>();
         for (int i = 0; i < roleCount; i++) {
         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());
         document.attributeNames.setPrincipal(randomUri());
         if (randomBoolean()) {
         if (randomBoolean()) {

+ 5 - 4
x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderResolverTests.java

@@ -18,7 +18,6 @@ import org.junit.Before;
 import org.opensaml.saml.saml2.core.NameID;
 import org.opensaml.saml.saml2.core.NameID;
 
 
 import java.net.URL;
 import java.net.URL;
-import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 
 
 import static org.hamcrest.Matchers.emptyIterable;
 import static org.hamcrest.Matchers.emptyIterable;
@@ -56,7 +55,7 @@ public class SamlServiceProviderResolverTests extends ESTestCase {
         final String principalAttribute = randomAlphaOfLengthBetween(6, 36);
         final String principalAttribute = randomAlphaOfLengthBetween(6, 36);
         final String rolesAttribute = randomAlphaOfLengthBetween(6, 36);
         final String rolesAttribute = randomAlphaOfLengthBetween(6, 36);
         final String resource = "ece:" + randomAlphaOfLengthBetween(6, 12);
         final String resource = "ece:" + randomAlphaOfLengthBetween(6, 12);
-        final Map<String, String> rolePrivileges = Map.of(randomAlphaOfLengthBetween(3, 6), "role:" + randomAlphaOfLengthBetween(4, 8));
+        final Set<String> rolePrivileges = Set.of("role:(.*)");
 
 
         final DocumentVersion docVersion = new DocumentVersion(
         final DocumentVersion docVersion = new DocumentVersion(
             randomAlphaOfLength(12), randomNonNegativeLong(), randomNonNegativeLong());
             randomAlphaOfLength(12), randomNonNegativeLong(), randomNonNegativeLong());
@@ -65,7 +64,7 @@ public class SamlServiceProviderResolverTests extends ESTestCase {
         document.setAuthenticationExpiry(null);
         document.setAuthenticationExpiry(null);
         document.setAcs(acs.toString());
         document.setAcs(acs.toString());
         document.privileges.setResource(resource);
         document.privileges.setResource(resource);
-        document.privileges.setRoleActions(rolePrivileges);
+        document.privileges.setRolePatterns(rolePrivileges);
         document.attributeNames.setPrincipal(principalAttribute);
         document.attributeNames.setPrincipal(principalAttribute);
         document.attributeNames.setRoles(rolesAttribute);
         document.attributeNames.setRoles(rolesAttribute);
 
 
@@ -89,7 +88,9 @@ public class SamlServiceProviderResolverTests extends ESTestCase {
         assertThat(serviceProvider.getPrivileges(), notNullValue());
         assertThat(serviceProvider.getPrivileges(), notNullValue());
         assertThat(serviceProvider.getPrivileges().getApplicationName(), equalTo(serviceProviderDefaults.applicationName));
         assertThat(serviceProvider.getPrivileges().getApplicationName(), equalTo(serviceProviderDefaults.applicationName));
         assertThat(serviceProvider.getPrivileges().getResource(), equalTo(resource));
         assertThat(serviceProvider.getPrivileges().getResource(), equalTo(resource));
-        assertThat(serviceProvider.getPrivileges().getRoleActions(), equalTo(rolePrivileges));
+        assertThat(serviceProvider.getPrivileges().getRoleMapping(), notNullValue());
+        assertThat(serviceProvider.getPrivileges().getRoleMapping().apply("role:foo"), equalTo(Set.of("foo")));
+        assertThat(serviceProvider.getPrivileges().getRoleMapping().apply("foo:bar"), equalTo(Set.of()));
     }
     }
 
 
     public void testResolveReturnsCachedObject() throws Exception {
     public void testResolveReturnsCachedObject() throws Exception {

+ 6 - 3
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\": { "
         + "      \"template\": { "
         + "         \"name\": \"{{service}} at example.com (A)\","
         + "         \"name\": \"{{service}} at example.com (A)\","
         + "         \"privileges\": {"
         + "         \"privileges\": {"
-        + "           \"resource\": \"service1:example:{{service}}\""
+        + "           \"resource\": \"service1:example:{{service}}\","
+        + "           \"roles\": [ \"sso:(.*)\" ]"
         + "         },"
         + "         },"
         + "         \"attributes\": {"
         + "         \"attributes\": {"
         + "           \"principal\": \"http://cloud.elastic.co/saml/principal\","
         + "           \"principal\": \"http://cloud.elastic.co/saml/principal\","
@@ -54,7 +55,8 @@ public class WildcardServiceProviderResolverTests extends IdpSamlTestCase {
         + "      \"template\": { "
         + "      \"template\": { "
         + "         \"name\": \"{{service}} at example.com (B)\","
         + "         \"name\": \"{{service}} at example.com (B)\","
         + "         \"privileges\": {"
         + "         \"privileges\": {"
-        + "           \"resource\": \"service1:example:{{service}}\""
+        + "           \"resource\": \"service1:example:{{service}}\","
+        + "           \"roles\": [ \"sso:(.*)\" ]"
         + "         },"
         + "         },"
         + "         \"attributes\": {"
         + "         \"attributes\": {"
         + "           \"principal\": \"http://cloud.elastic.co/saml/principal\","
         + "           \"principal\": \"http://cloud.elastic.co/saml/principal\","
@@ -71,7 +73,8 @@ public class WildcardServiceProviderResolverTests extends IdpSamlTestCase {
         + "      \"template\": { "
         + "      \"template\": { "
         + "         \"name\": \"{{id}} at example.net\","
         + "         \"name\": \"{{id}} at example.net\","
         + "         \"privileges\": {"
         + "         \"privileges\": {"
-        + "           \"resource\": \"service2:example:{{id}}\""
+        + "           \"resource\": \"service2:example:{{id}}\","
+        + "           \"roles\": [ \"sso:(.*)\" ]"
         + "         },"
         + "         },"
         + "         \"attributes\": {"
         + "         \"attributes\": {"
         + "           \"principal\": \"http://cloud.elastic.co/saml/principal\","
         + "           \"principal\": \"http://cloud.elastic.co/saml/principal\","

+ 2 - 2
x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/test/IdentityProviderIntegTestCase.java

@@ -214,9 +214,9 @@ public abstract class IdentityProviderIntegTestCase extends ESIntegTestCase {
             "       resources: [ '" + SP_ENTITY_ID + "' ]\n" +
             "       resources: [ '" + SP_ENTITY_ID + "' ]\n" +
             "       privileges: [ 'sso:superuser' ]\n" +
             "       privileges: [ 'sso:superuser' ]\n" +
             "\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" +
             CONSOLE_USER_ROLE + ":\n" +
-            "  cluster: ['cluster:admin/idp/*']\n" +
+            "  cluster: ['cluster:admin/idp/*', 'cluster:admin/xpack/security/privilege/*' ]\n" +
             "  indices: []\n";
             "  indices: []\n";
     }
     }