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:
parent
b939b47b77
commit
30b01fe00d
|
@ -57,8 +57,7 @@
|
|||
"type": "keyword"
|
||||
},
|
||||
"roles": {
|
||||
"type": "object",
|
||||
"dynamic": false
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -8,4 +8,4 @@ idp_user:
|
|||
applications:
|
||||
- application: elastic-cloud
|
||||
resources: ["ec:123456:abcdefg"]
|
||||
privileges: ["role:viewer"]
|
||||
privileges: ["sso:viewer"]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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\","
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue