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
This commit is contained in:
Tim Vernum 2020-04-06 14:10:30 +10:00 committed by GitHub
parent b939b47b77
commit 30b01fe00d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 398 additions and 138 deletions

View File

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

View File

@ -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<String, Object> privilegeMap = new HashMap<>();
privilegeMap.put("resource", SP_ENTITY_ID);
final Map<String, String> 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<String, String> 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<String, Object> privilegeMap = new HashMap<>();
privilegeMap.put("resource", SP_ENTITY_ID);
final Map<String, String> 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<String, String> attributeMap = new HashMap<>();
attributeMap.put("principal", "https://idp.test.es.elasticsearch.org/attribute/principal");

View File

@ -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<String, Collection<String>> privileges) throws IOException {
final RestHighLevelClient client = getHighLevelAdminClient();
final List<ApplicationPrivilege> 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);

View File

@ -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<String, Object> 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<String, Object> privilegeMap = new HashMap<>();
privilegeMap.put("resource", entityId);
final Map<String, String> 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<String, String> attributeMap = new HashMap<>();
attributeMap.put("principal", "https://idp.test.es.elasticsearch.org/attribute/principal");

View File

@ -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);

View File

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

View File

@ -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",

View File

@ -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);

View File

@ -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<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.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));
cache.put(applicationName, fixedActions);
listener.onResponse(fixedActions);
},
listener::onFailure
));
}
}

View File

@ -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<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.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<String, String> getRoleActions() {
return roles;
public Function<String, Set<String>> getRoleMapping() {
return roleMapping;
}
}

View File

@ -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<String> 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<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) {
@ -101,28 +109,35 @@ public class UserPrivilegeResolver {
if (appPrivileges == null || appPrivileges.isEmpty()) {
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(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<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));
}
}

View File

@ -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<String, String> roleActions = Collections.emptyMap();
public Set<String> rolePatterns = Collections.emptySet();
public void setResource(String resource) {
this.resource = resource;
}
public void setRoleActions(Map<String, String> roleActions) {
this.roleActions = roleActions;
public void setRolePatterns(Collection<String> 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());

View File

@ -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<String, String> roles = Optional.ofNullable(configuredPrivileges.roleActions).orElse(Collections.emptyMap());
return new ServiceProviderPrivileges(defaults.applicationName, resource, roles);
final Function<String, Set<String>> roleMapping;
if (configuredPrivileges.rolePatterns == null || configuredPrivileges.rolePatterns.isEmpty()) {
roleMapping = in -> Collections.emptySet();
} else {
final Set<Pattern> 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) {

View File

@ -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<String, String> roleMap = new HashMap<>();
roleMap.put("role_name", "role:" + randomAlphaOfLengthBetween(4, 8));
final Map<String, Object> 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<String, Object> 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"));

View File

@ -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<String, String> 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("<saml2p:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>"));
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<String, String> attributeMap = new HashMap<>();
attributeMap.put("principal", "https://saml.elasticsearch.org/attributes/principal");
attributeMap.put("roles", "https://saml.elasticsearch.org/attributes/roles");
Map<String, Object> privilegeMap = new HashMap<>();
Map<String, String> 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<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) {
Client client = client().filterWithHeader(Collections.singletonMap("Authorization",
UsernamePasswordToken.basicAuthHeaderValue(username, password)));

View File

@ -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<Set<String>> listener = (ActionListener<Set<String>>) 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<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
final HashMap<String, String> 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<String, Set<String>> 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<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
final HashMap<String, String> roles = new HashMap<>();
roles.put("viewer", viewerAction);
roles.put("admin", adminAction);
resolver.resolve(service(app, resource, roles), future);
final Function<String, Set<String>> 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<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
final HashMap<String, String> roles = new HashMap<>();
roles.put("viewer", viewerAction);
roles.put("admin", adminAction);
resolver.resolve(service(app, resource, roles), future);
final Function<String, Set<String>> 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<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
final HashMap<String, String> 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<String, Set<String>> 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<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,

View File

@ -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<String, String> roleActions = new HashMap<>();
final Set<String> 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));

View File

@ -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<String, String> roles = new HashMap<>();
final Set<String> 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()) {

View File

@ -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<String, String> rolePrivileges = new HashMap<>();
rolePrivileges.put(randomAlphaOfLengthBetween(3, 6), "role:" + randomAlphaOfLengthBetween(4, 8));
final Set<String> 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<String, Set<String>> 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 {

View File

@ -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\","

View File

@ -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";
}