diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationContext.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationContext.java new file mode 100644 index 0000000000000..a99b3f740d639 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationContext.java @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authc; + +import org.elasticsearch.Version; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.Map; + +import static org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType; +import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; + +public class AuthenticationContext { + + private final Version version; + private final Subject authenticatingSubject; + private final Subject effectiveSubject; + // TODO: Rename to AuthenticationMethod + private final AuthenticationType type; + + private AuthenticationContext( + Version version, + Subject authenticatingSubject, + Subject effectiveSubject, + AuthenticationType authenticationType + ) { + this.version = version; + this.authenticatingSubject = authenticatingSubject; + this.effectiveSubject = effectiveSubject; + this.type = authenticationType; + } + + public boolean isRunAs() { + assert authenticatingSubject != null && effectiveSubject != null; + return authenticatingSubject != effectiveSubject; + } + + public Subject getAuthenticatingSubject() { + return authenticatingSubject; + } + + public Subject getEffectiveSubject() { + return effectiveSubject; + } + + public Authentication toAuthentication() { + return new Authentication( + effectiveSubject.getUser(), + authenticatingSubject.getRealm(), + effectiveSubject.getRealm(), + version, + type, + authenticatingSubject.getMetadata() + ); + } + + public static AuthenticationContext fromAuthentication(Authentication authentication) { + final Builder builder = new Builder(authentication.getVersion()); + builder.authenticationType(authentication.getAuthenticationType()); + final User user = authentication.getUser(); + if (user.isRunAs()) { + builder.authenticatingSubject(user.authenticatedUser(), authentication.getAuthenticatedBy(), authentication.getMetadata()); + // The lookup user for run-as currently don't have authentication metadata associated with them because + // lookupUser only returns the User object. The lookup user for authorization delegation does have + // authentication metadata, but the realm does not expose this difference between authenticatingUser and + // delegateUser so effectively this is handled together with the authenticatingSubject not effectiveSubject. + builder.effectiveSubject(user, authentication.getLookedUpBy(), Map.of()); + } else { + builder.authenticatingSubject(user, authentication.getAuthenticatedBy(), authentication.getMetadata()); + } + return builder.build(); + } + + public static class Builder { + private final Version version; + private AuthenticationType authenticationType; + private Subject authenticatingSubject; + private Subject effectiveSubject; + + public Builder() { + this(Version.CURRENT); + } + + public Builder(Version version) { + this.version = version; + } + + public Builder authenticationType(AuthenticationType authenticationType) { + this.authenticationType = authenticationType; + return this; + } + + public Builder authenticatingSubject(User authenticatingUser, RealmRef authenticatingRealmRef, Map metadata) { + this.authenticatingSubject = new Subject(authenticatingUser, authenticatingRealmRef, version, metadata); + return this; + } + + public Builder effectiveSubject(User effectiveUser, RealmRef lookupRealmRef, Map metadata) { + this.effectiveSubject = new Subject(effectiveUser, lookupRealmRef, version, metadata); + return this; + } + + public AuthenticationContext build() { + if (effectiveSubject == null) { + effectiveSubject = authenticatingSubject; + } + return new AuthenticationContext(version, authenticatingSubject, effectiveSubject, authenticationType); + } + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java index 7a8ab3b5df237..4b48a922da856 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java @@ -24,5 +24,8 @@ public final class AuthenticationField { public static final String API_KEY_ROLE_DESCRIPTORS_KEY = "_security_api_key_role_descriptors"; public static final String API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY = "_security_api_key_limited_by_role_descriptors"; + public static final String ANONYMOUS_REALM_NAME = "__anonymous"; + public static final String ANONYMOUS_REALM_TYPE = "__anonymous"; + private AuthenticationField() {} } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java new file mode 100644 index 0000000000000..d328ff795c559 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authc; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.Version; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.util.ArrayUtils; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference; +import org.elasticsearch.xpack.core.security.user.AnonymousUser; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.xpack.core.security.authc.Authentication.VERSION_API_KEY_ROLES_AS_BYTES; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY; + +/** + * A subject is a more generic concept similar to user and associated to the current authentication. + * It is more generic than user because it can also represent API keys and service accounts. + * It also contains authentication level information, e.g. realm and metadata so that it can answer + * queries in a better encapsulated way. + */ +public class Subject { + + public enum Type { + USER, + API_KEY, + SERVICE_ACCOUNT, + } + + private final Version version; + private final User user; + private final Authentication.RealmRef realm; + private final Type type; + private final Map metadata; + + public Subject(User user, Authentication.RealmRef realm) { + this(user, realm, Version.CURRENT, Map.of()); + } + + public Subject(User user, Authentication.RealmRef realm, Version version, Map metadata) { + this.version = version; + this.user = user; + this.realm = realm; + // Realm can be null for run-as user if it does not exist. Pretend it is a user and it will be rejected later in authorization + // This is to be consistent with existing behaviour. + if (realm == null) { + this.type = Type.USER; + } else if (AuthenticationField.API_KEY_REALM_TYPE.equals(realm.getType())) { + assert AuthenticationField.API_KEY_REALM_NAME.equals(realm.getName()) : "api key realm name mismatch"; + this.type = Type.API_KEY; + } else if (ServiceAccountSettings.REALM_TYPE.equals(realm.getType())) { + assert ServiceAccountSettings.REALM_NAME.equals(realm.getName()) : "service account realm name mismatch"; + this.type = Type.SERVICE_ACCOUNT; + } else { + this.type = Type.USER; + } + this.metadata = metadata; + } + + public User getUser() { + return user; + } + + public Authentication.RealmRef getRealm() { + return realm; + } + + public Type getType() { + return type; + } + + public Map getMetadata() { + return metadata; + } + + /** + * Return a List of RoleReferences that represents role definitions associated to the subject. + * The final role of this subject should be the intersection of all role references in the list. + */ + public List getRoleReferences(@Nullable AnonymousUser anonymousUser) { + switch (type) { + case USER: + return buildRoleReferencesForUser(anonymousUser); + case API_KEY: + return buildRoleReferencesForApiKey(); + case SERVICE_ACCOUNT: + return List.of(new RoleReference.ServiceAccountRoleReference(user.principal())); + default: + assert false : "unknown subject type: [" + type + "]"; + throw new IllegalStateException("unknown subject type: [" + type + "]"); + } + } + + private List buildRoleReferencesForUser(AnonymousUser anonymousUser) { + if (user.equals(anonymousUser)) { + return List.of(new RoleReference.NamedRoleReference(user.roles())); + } + final String[] allRoleNames; + if (anonymousUser == null || false == anonymousUser.enabled()) { + allRoleNames = user.roles(); + } else { + // TODO: should we validate enable status and length of role names on instantiation time of anonymousUser? + if (anonymousUser.roles().length == 0) { + throw new IllegalStateException("anonymous is only enabled when the anonymous user has roles"); + } + allRoleNames = ArrayUtils.concat(user.roles(), anonymousUser.roles()); + } + return List.of(new RoleReference.NamedRoleReference(allRoleNames)); + } + + private List buildRoleReferencesForApiKey() { + if (version.before(VERSION_API_KEY_ROLES_AS_BYTES)) { + return buildRolesReferenceForApiKeyBwc(); + } + final String apiKeyId = (String) metadata.get(AuthenticationField.API_KEY_ID_KEY); + final BytesReference roleDescriptorsBytes = (BytesReference) metadata.get(API_KEY_ROLE_DESCRIPTORS_KEY); + final BytesReference limitedByRoleDescriptorsBytes = getLimitedByRoleDescriptorsBytes(); + if (roleDescriptorsBytes == null && limitedByRoleDescriptorsBytes == null) { + throw new ElasticsearchSecurityException("no role descriptors found for API key"); + } + final RoleReference.ApiKeyRoleReference limitedByRoleReference = new RoleReference.ApiKeyRoleReference( + apiKeyId, + limitedByRoleDescriptorsBytes, + "apikey_limited_role" + ); + if (isEmptyRoleDescriptorsBytes(roleDescriptorsBytes)) { + return List.of(limitedByRoleReference); + } + return List.of(new RoleReference.ApiKeyRoleReference(apiKeyId, roleDescriptorsBytes, "apikey_role"), limitedByRoleReference); + } + + private boolean isEmptyRoleDescriptorsBytes(BytesReference roleDescriptorsBytes) { + return roleDescriptorsBytes == null || (roleDescriptorsBytes.length() == 2 && "{}".equals(roleDescriptorsBytes.utf8ToString())); + } + + private List buildRolesReferenceForApiKeyBwc() { + final String apiKeyId = (String) metadata.get(AuthenticationField.API_KEY_ID_KEY); + final Map roleDescriptorsMap = getRoleDescriptorMap(API_KEY_ROLE_DESCRIPTORS_KEY); + final Map limitedByRoleDescriptorsMap = getRoleDescriptorMap(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY); + if (roleDescriptorsMap == null && limitedByRoleDescriptorsMap == null) { + throw new ElasticsearchSecurityException("no role descriptors found for API key"); + } else { + final RoleReference.BwcApiKeyRoleReference limitedByRoleReference = new RoleReference.BwcApiKeyRoleReference( + apiKeyId, + limitedByRoleDescriptorsMap, + "_limited_role_desc" + ); + if (roleDescriptorsMap == null || roleDescriptorsMap.isEmpty()) { + return List.of(limitedByRoleReference); + } else { + return List.of( + new RoleReference.BwcApiKeyRoleReference(apiKeyId, roleDescriptorsMap, "_role_desc"), + limitedByRoleReference + ); + } + } + } + + @SuppressWarnings("unchecked") + private Map getRoleDescriptorMap(String key) { + return (Map) metadata.get(key); + } + + // This following fixed role descriptor is for fleet-server BWC on and before 7.14. + // It is fixed and must NOT be updated when the fleet-server service account updates. + // Package private for testing + static final BytesArray FLEET_SERVER_ROLE_DESCRIPTOR_BYTES_V_7_14 = new BytesArray( + "{\"elastic/fleet-server\":{\"cluster\":[\"monitor\",\"manage_own_api_key\"]," + + "\"indices\":[{\"names\":[\"logs-*\",\"metrics-*\",\"traces-*\",\"synthetics-*\"," + + "\".logs-endpoint.diagnostic.collection-*\"]," + + "\"privileges\":[\"write\",\"create_index\",\"auto_configure\"],\"allow_restricted_indices\":false}," + + "{\"names\":[\".fleet-*\"],\"privileges\":[\"read\",\"write\",\"monitor\",\"create_index\",\"auto_configure\"]," + + "\"allow_restricted_indices\":false}],\"applications\":[],\"run_as\":[],\"metadata\":{}," + + "\"transient_metadata\":{\"enabled\":true}}}" + ); + + private BytesReference getLimitedByRoleDescriptorsBytes() { + final BytesReference bytesReference = (BytesReference) metadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY); + // Unfortunate BWC bug fix code + if (bytesReference.length() == 2 && "{}".equals(bytesReference.utf8ToString())) { + if (ServiceAccountSettings.REALM_NAME.equals(metadata.get(AuthenticationField.API_KEY_CREATOR_REALM_NAME)) + && "elastic/fleet-server".equals(user.principal())) { + return FLEET_SERVER_ROLE_DESCRIPTOR_BYTES_V_7_14; + } + } + return bytesReference; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java index 43e2e47e78c7b..e7d7f1ad57fca 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java @@ -22,6 +22,7 @@ import java.util.Set; import java.util.function.Predicate; +// TODO: extract a Role interface so limitedRole can be more than 2 levels /** * A {@link Role} limited by another role.
* The effective permissions returned on {@link #authorize(String, Set, Map, FieldPermissionsCache)} call would be limited by the diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleKey.java new file mode 100644 index 0000000000000..9d550b0a4fd76 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleKey.java @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authz.store; + +import java.util.Objects; +import java.util.Set; + +/** + * A unique identifier that can be associated to a Role. It can be used as cache key for role caching. + */ +public final class RoleKey { + + public static final String ROLES_STORE_SOURCE = "roles_stores"; + public static final RoleKey ROLE_KEY_EMPTY = new RoleKey(Set.of(), "__empty_role"); + public static final RoleKey ROLE_KEY_SUPERUSER = new RoleKey( + Set.of(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()), + RoleKey.ROLES_STORE_SOURCE + ); + + private final Set names; + private final String source; + + public RoleKey(Set names, String source) { + this.names = Objects.requireNonNull(names); + this.source = Objects.requireNonNull(source); + } + + public Set getNames() { + return names; + } + + public String getSource() { + return source; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RoleKey roleKey = (RoleKey) o; + return names.equals(roleKey.names) && source.equals(roleKey.source); + } + + @Override + public int hashCode() { + return Objects.hash(names, source); + } + + @Override + public String toString() { + return "RoleKey{" + "names=" + names + ", source='" + source + '\'' + '}'; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReference.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReference.java new file mode 100644 index 0000000000000..ec8868304b3f2 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReference.java @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authz.store; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.hash.MessageDigests; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * RoleReference is a handle to the actual role definitions (role descriptors). + * It has different sub-types depending on how the role descriptors should be resolved. + */ +public interface RoleReference { + + /** + * Unique ID of the instance. Instances that have equal ID means they are equivalent + * in terms of authorization. + * It is currently used as cache key for role caching purpose. + * Callers can use this value to determine whether it should skip + * resolving the role descriptors and subsequently building the role. + */ + RoleKey id(); + + /** + * Resolve concrete role descriptors for the roleReference. + */ + void resolve(RoleReferenceResolver resolver, ActionListener listener); + + /** + * Referencing a collection of role descriptors by their names + */ + final class NamedRoleReference implements RoleReference { + private final String[] roleNames; + + public NamedRoleReference(String[] roleNames) { + this.roleNames = roleNames; + } + + public String[] getRoleNames() { + return roleNames; + } + + @Override + public RoleKey id() { + if (roleNames.length == 0) { + return RoleKey.ROLE_KEY_EMPTY; + } else if (Arrays.asList(roleNames).contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName())) { + return RoleKey.ROLE_KEY_SUPERUSER; + } else { + return new RoleKey(Set.copyOf(new HashSet<>(List.of(roleNames))), RoleKey.ROLES_STORE_SOURCE); + } + } + + @Override + public void resolve(RoleReferenceResolver resolver, ActionListener listener) { + resolver.resolveNamedRoleReference(this, listener); + } + } + + /** + * Referencing API Key role descriptors. Can be either the assigned (key) role descriptors or the limited-by (owner's) role descriptors + */ + final class ApiKeyRoleReference implements RoleReference { + + private final String apiKeyId; + private final BytesReference roleDescriptorsBytes; + private final String roleKeySource; + private RoleKey id = null; + + public ApiKeyRoleReference(String apiKeyId, BytesReference roleDescriptorsBytes, String roleKeySource) { + this.apiKeyId = apiKeyId; + this.roleDescriptorsBytes = roleDescriptorsBytes; + this.roleKeySource = roleKeySource; + } + + @Override + public RoleKey id() { + // Hashing can be expensive. memorize the result in case the method is called multiple times. + if (id == null) { + final String roleDescriptorsHash = MessageDigests.toHexString( + MessageDigests.digest(roleDescriptorsBytes, MessageDigests.sha256()) + ); + id = new RoleKey(Set.of("apikey:" + roleDescriptorsHash), roleKeySource); + } + return id; + } + + @Override + public void resolve(RoleReferenceResolver resolver, ActionListener listener) { + resolver.resolveApiKeyRoleReference(this, listener); + } + + public String getApiKeyId() { + return apiKeyId; + } + + public BytesReference getRoleDescriptorsBytes() { + return roleDescriptorsBytes; + } + } + + /** + * Same as {@link ApiKeyRoleReference} but for BWC purpose (prior to v7.9.0) + */ + final class BwcApiKeyRoleReference implements RoleReference { + private final String apiKeyId; + private final Map roleDescriptorsMap; + private final String roleKeySourceSuffix; + + public BwcApiKeyRoleReference(String apiKeyId, Map roleDescriptorsMap, String roleKeySourceSuffix) { + this.apiKeyId = apiKeyId; + this.roleDescriptorsMap = roleDescriptorsMap; + this.roleKeySourceSuffix = roleKeySourceSuffix; + } + + @Override + public RoleKey id() { + // Since api key id is unique, it is sufficient and more correct to use it as the names + return new RoleKey(Set.of(apiKeyId), "bwc_api_key" + roleKeySourceSuffix); + } + + @Override + public void resolve(RoleReferenceResolver resolver, ActionListener listener) { + resolver.resolveBwcApiKeyRoleReference(this, listener); + } + + public String getApiKeyId() { + return apiKeyId; + } + + public Map getRoleDescriptorsMap() { + return roleDescriptorsMap; + } + } + + /** + * Referencing role descriptors by the service account principal + */ + final class ServiceAccountRoleReference implements RoleReference { + private final String principal; + + public ServiceAccountRoleReference(String principal) { + this.principal = principal; + } + + public String getPrincipal() { + return principal; + } + + @Override + public RoleKey id() { + return new RoleKey(Set.of(principal), "service_account"); + } + + @Override + public void resolve(RoleReferenceResolver resolver, ActionListener listener) { + resolver.resolveServiceAccountRoleReference(this, listener); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReferenceResolver.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReferenceResolver.java new file mode 100644 index 0000000000000..44522b5884521 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReferenceResolver.java @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authz.store; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference.ServiceAccountRoleReference; + +/** + * Implementation of this interface knows how to turn different subtypes of {@link RoleReference} into concrete role descriptors. + */ +public interface RoleReferenceResolver { + + void resolveNamedRoleReference(RoleReference.NamedRoleReference namedRoleReference, ActionListener listener); + + void resolveApiKeyRoleReference(RoleReference.ApiKeyRoleReference apiKeyRoleReference, ActionListener listener); + + void resolveBwcApiKeyRoleReference( + RoleReference.BwcApiKeyRoleReference bwcApiKeyRoleReference, + ActionListener listener + ); + + void resolveServiceAccountRoleReference(ServiceAccountRoleReference roleReference, ActionListener listener); +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RolesRetrievalResult.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RolesRetrievalResult.java new file mode 100644 index 0000000000000..7d77f29977c25 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RolesRetrievalResult.java @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authz.store; + +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public final class RolesRetrievalResult { + + public static final RolesRetrievalResult EMPTY = new RolesRetrievalResult(); + public static final RolesRetrievalResult SUPERUSER; + + static { + SUPERUSER = new RolesRetrievalResult(); + SUPERUSER.addDescriptors(Set.of(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR)); + } + + private final Set roleDescriptors = new HashSet<>(); + private Set missingRoles = Collections.emptySet(); + private boolean success = true; + + public void addDescriptors(Set descriptors) { + roleDescriptors.addAll(descriptors); + } + + public Set getRoleDescriptors() { + return roleDescriptors; + } + + public void setFailure() { + success = false; + } + + public boolean isSuccess() { + return success; + } + + public void setMissingRoles(Set missingRoles) { + this.missingRoles = missingRoles; + } + + public Set getMissingRoles() { + return missingRoles; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java index d9ee948badc24..e3f1bf4530fb9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java @@ -72,7 +72,7 @@ private User( /** * @return The principal of this user - effectively serving as the - * unique identity of of the user. + * unique identity of the user (within a given realm). */ public String principal() { return this.username; @@ -116,14 +116,20 @@ public boolean enabled() { } /** + * @deprecated We are transitioning to AuthenticationContext which frees User from managing the run-as information. * @return The user that was originally authenticated. * This may be the user itself, or a different user which used runAs. */ + @Deprecated public User authenticatedUser() { return authenticatedUser == null ? this : authenticatedUser; } - /** Return true if this user was not the originally authenticated user, false otherwise. */ + /** + * @deprecated We are transitioning to AuthenticationContext which frees User from managing the run-as information. + * Return true if this user was not the originally authenticated user, false otherwise. + * */ + @Deprecated public boolean isRunAs() { return authenticatedUser != null; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/SubjectTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/SubjectTests.java new file mode 100644 index 0000000000000..2b0f5dc351a50 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/SubjectTests.java @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authc; + +import org.elasticsearch.Version; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.ArrayUtils; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference.ApiKeyRoleReference; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference.BwcApiKeyRoleReference; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference.NamedRoleReference; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference.ServiceAccountRoleReference; +import org.elasticsearch.xpack.core.security.user.AnonymousUser; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_REALM_NAME; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_REALM_TYPE; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY; +import static org.elasticsearch.xpack.core.security.authc.Subject.FLEET_SERVER_ROLE_DESCRIPTOR_BYTES_V_7_14; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.isA; + +public class SubjectTests extends ESTestCase { + + public void testGetRoleReferencesForRegularUser() { + final User user = new User("joe", "role_a", "role_b"); + final Subject subject = new Subject( + user, + new Authentication.RealmRef(randomAlphaOfLength(5), randomAlphaOfLength(5), "node"), + Version.CURRENT, + Map.of() + ); + + final AnonymousUser anonymousUser = randomFrom(getAnonymousUser(), null); + + final List roleReferences = subject.getRoleReferences(anonymousUser); + assertThat(roleReferences, hasSize(1)); + assertThat(roleReferences.get(0), instanceOf(NamedRoleReference.class)); + final NamedRoleReference namedRoleReference = (NamedRoleReference) roleReferences.get(0); + assertThat( + namedRoleReference.getRoleNames(), + arrayContaining(ArrayUtils.concat(user.roles(), anonymousUser == null ? Strings.EMPTY_ARRAY : anonymousUser.roles())) + ); + } + + public void testGetRoleReferencesForAnonymousUser() { + final AnonymousUser anonymousUser = getAnonymousUser(); + + final Subject subject = new Subject( + anonymousUser, + new Authentication.RealmRef(randomAlphaOfLength(5), randomAlphaOfLength(5), "node"), + Version.CURRENT, + Map.of() + ); + + final List roleReferences = subject.getRoleReferences(anonymousUser); + assertThat(roleReferences, hasSize(1)); + assertThat(roleReferences.get(0), instanceOf(NamedRoleReference.class)); + final NamedRoleReference namedRoleReference = (NamedRoleReference) roleReferences.get(0); + // Anonymous roles do not get applied again + assertThat(namedRoleReference.getRoleNames(), equalTo(anonymousUser.roles())); + } + + public void testGetRoleReferencesForServiceAccount() { + final User serviceUser = new User(randomAlphaOfLength(5) + "/" + randomAlphaOfLength(5)); + final Subject subject = new Subject( + serviceUser, + new Authentication.RealmRef(ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, "node"), + Version.CURRENT, + Map.of() + ); + + final List roleReferences = subject.getRoleReferences(getAnonymousUser()); + assertThat(roleReferences, hasSize(1)); + assertThat(roleReferences.get(0), instanceOf(ServiceAccountRoleReference.class)); + final ServiceAccountRoleReference serviceAccountRoleReference = (ServiceAccountRoleReference) roleReferences.get(0); + assertThat(serviceAccountRoleReference.getPrincipal(), equalTo(serviceUser.principal())); + } + + public void testGetRoleReferencesForApiKey() { + Map authMetadata = new HashMap<>(); + final String apiKeyId = randomAlphaOfLength(12); + authMetadata.put(AuthenticationField.API_KEY_ID_KEY, apiKeyId); + authMetadata.put(AuthenticationField.API_KEY_NAME_KEY, randomBoolean() ? null : randomAlphaOfLength(12)); + final BytesReference roleBytes = new BytesArray("{\"a role\": {\"cluster\": [\"all\"]}}"); + final BytesReference limitedByRoleBytes = new BytesArray("{\"limitedBy role\": {\"cluster\": [\"all\"]}}"); + + final boolean emptyRoleBytes = randomBoolean(); + + authMetadata.put( + AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, + emptyRoleBytes ? randomFrom(Arrays.asList(null, new BytesArray("{}"))) : roleBytes + ); + authMetadata.put(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, limitedByRoleBytes); + + final Subject subject = new Subject( + new User("joe"), + new Authentication.RealmRef(API_KEY_REALM_NAME, API_KEY_REALM_TYPE, "node"), + Version.CURRENT, + authMetadata + ); + + final List roleReferences = subject.getRoleReferences(getAnonymousUser()); + if (emptyRoleBytes) { + assertThat(roleReferences, contains(isA(ApiKeyRoleReference.class))); + final ApiKeyRoleReference roleReference = (ApiKeyRoleReference) roleReferences.get(0); + assertThat(roleReference.getApiKeyId(), equalTo(apiKeyId)); + assertThat(roleReference.getRoleDescriptorsBytes(), equalTo(authMetadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY))); + } else { + assertThat(roleReferences, contains(isA(ApiKeyRoleReference.class), isA(ApiKeyRoleReference.class))); + final ApiKeyRoleReference roleReference = (ApiKeyRoleReference) roleReferences.get(0); + assertThat(roleReference.getApiKeyId(), equalTo(apiKeyId)); + assertThat(roleReference.getRoleDescriptorsBytes(), equalTo(authMetadata.get(API_KEY_ROLE_DESCRIPTORS_KEY))); + + final ApiKeyRoleReference limitedByRoleReference = (ApiKeyRoleReference) roleReferences.get(1); + assertThat(limitedByRoleReference.getApiKeyId(), equalTo(apiKeyId)); + assertThat(limitedByRoleReference.getRoleDescriptorsBytes(), equalTo(authMetadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY))); + } + } + + public void testGetRoleReferencesForApiKeyBwc() { + Map authMetadata = new HashMap<>(); + final String apiKeyId = randomAlphaOfLength(12); + authMetadata.put(AuthenticationField.API_KEY_ID_KEY, apiKeyId); + authMetadata.put(AuthenticationField.API_KEY_NAME_KEY, randomBoolean() ? null : randomAlphaOfLength(12)); + boolean emptyApiKeyRoleDescriptor = randomBoolean(); + Map roleARDMap = Map.of("cluster", List.of("monitor")); + authMetadata.put( + API_KEY_ROLE_DESCRIPTORS_KEY, + (emptyApiKeyRoleDescriptor) + ? randomFrom(Arrays.asList(null, Collections.emptyMap())) + : Collections.singletonMap("a role", roleARDMap) + ); + + Map limitedRdMap = Map.of("cluster", List.of("all")); + authMetadata.put(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, Collections.singletonMap("limited role", limitedRdMap)); + + final Subject subject = new Subject( + new User("joe"), + new Authentication.RealmRef(API_KEY_REALM_NAME, API_KEY_REALM_TYPE, "node"), + VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.V_7_8_1), + authMetadata + ); + + final List roleReferences = subject.getRoleReferences(getAnonymousUser()); + + if (emptyApiKeyRoleDescriptor) { + assertThat(roleReferences, contains(isA(BwcApiKeyRoleReference.class))); + final BwcApiKeyRoleReference limitedByRoleReference = (BwcApiKeyRoleReference) roleReferences.get(0); + assertThat(limitedByRoleReference.getApiKeyId(), equalTo(apiKeyId)); + assertThat(limitedByRoleReference.getRoleDescriptorsMap(), equalTo(authMetadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY))); + } else { + assertThat(roleReferences, contains(isA(BwcApiKeyRoleReference.class), isA(BwcApiKeyRoleReference.class))); + final BwcApiKeyRoleReference roleReference = (BwcApiKeyRoleReference) roleReferences.get(0); + assertThat(roleReference.getApiKeyId(), equalTo(apiKeyId)); + assertThat(roleReference.getRoleDescriptorsMap(), equalTo(authMetadata.get(API_KEY_ROLE_DESCRIPTORS_KEY))); + + final BwcApiKeyRoleReference limitedByRoleReference = (BwcApiKeyRoleReference) roleReferences.get(1); + assertThat(limitedByRoleReference.getApiKeyId(), equalTo(apiKeyId)); + assertThat(limitedByRoleReference.getRoleDescriptorsMap(), equalTo(authMetadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY))); + } + } + + public void testGetFleetApiKeyRoleReferenceBwcBugFix() { + final BytesReference roleBytes = new BytesArray("{\"a role\": {\"cluster\": [\"all\"]}}"); + final BytesReference limitedByRoleBytes = new BytesArray("{}"); + final Subject subject = new Subject( + new User("elastic/fleet-server"), + new Authentication.RealmRef(API_KEY_REALM_NAME, API_KEY_REALM_TYPE, "node"), + Version.CURRENT, + Map.of( + AuthenticationField.API_KEY_CREATOR_REALM_NAME, + ServiceAccountSettings.REALM_NAME, + AuthenticationField.API_KEY_ID_KEY, + randomAlphaOfLength(20), + AuthenticationField.API_KEY_NAME_KEY, + randomAlphaOfLength(12), + AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, + roleBytes, + AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, + limitedByRoleBytes + ) + ); + + final List roleReferences = subject.getRoleReferences(getAnonymousUser()); + assertThat(roleReferences, contains(isA(ApiKeyRoleReference.class), isA(ApiKeyRoleReference.class))); + final ApiKeyRoleReference limitedByRoleReference = (ApiKeyRoleReference) roleReferences.get(1); + assertThat(limitedByRoleReference.getRoleDescriptorsBytes(), equalTo(FLEET_SERVER_ROLE_DESCRIPTOR_BYTES_V_7_14)); + } + + private AnonymousUser getAnonymousUser() { + final List anonymousRoles = randomList(0, 2, () -> "role_anonymous_" + randomAlphaOfLength(8)); + return new AnonymousUser(Settings.builder().putList("xpack.security.authc.anonymous.roles", anonymousRoles).build()); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/RoleReferenceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/RoleReferenceTests.java new file mode 100644 index 0000000000000..972115a9ee2ab --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/RoleReferenceTests.java @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authz.store; + +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.hash.MessageDigests; +import org.elasticsearch.test.ESTestCase; + +import java.util.Set; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; + +public class RoleReferenceTests extends ESTestCase { + + public void testNamedRoleReference() { + final String[] roleNames = randomArray(0, 2, String[]::new, () -> randomAlphaOfLength(8)); + + final boolean hasSuperUserRole = roleNames.length > 0 && randomBoolean(); + if (hasSuperUserRole) { + roleNames[randomIntBetween(0, roleNames.length - 1)] = "superuser"; + } + final RoleReference.NamedRoleReference namedRoleReference = new RoleReference.NamedRoleReference(roleNames); + + if (hasSuperUserRole) { + assertThat(namedRoleReference.id(), is(RoleKey.ROLE_KEY_SUPERUSER)); + } else if (roleNames.length == 0) { + assertThat(namedRoleReference.id(), is(RoleKey.ROLE_KEY_EMPTY)); + } else { + final RoleKey roleKey = namedRoleReference.id(); + assertThat(roleKey.getNames(), equalTo(Set.of(roleNames))); + assertThat(roleKey.getSource(), equalTo(RoleKey.ROLES_STORE_SOURCE)); + } + } + + public void testApiKeyRoleReference() { + final String apiKeyId = randomAlphaOfLength(20); + final BytesArray roleDescriptorsBytes = new BytesArray(randomAlphaOfLength(50)); + final String roleKeySource = randomAlphaOfLength(8); + final RoleReference.ApiKeyRoleReference apiKeyRoleReference = new RoleReference.ApiKeyRoleReference( + apiKeyId, + roleDescriptorsBytes, + roleKeySource + ); + + final RoleKey roleKey = apiKeyRoleReference.id(); + assertThat( + roleKey.getNames(), + hasItem("apikey:" + MessageDigests.toHexString(MessageDigests.digest(roleDescriptorsBytes, MessageDigests.sha256()))) + ); + assertThat(roleKey.getSource(), equalTo(roleKeySource)); + } + + public void testServiceAccountRoleReference() { + final String principal = randomAlphaOfLength(8) + "/" + randomAlphaOfLength(8); + final RoleReference.ServiceAccountRoleReference serviceAccountRoleReference = new RoleReference.ServiceAccountRoleReference( + principal + ); + final RoleKey roleKey = serviceAccountRoleReference.id(); + assertThat(roleKey.getNames(), hasItem(principal)); + assertThat(roleKey.getSource(), equalTo("service_account")); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index e45dc1fa2722e..8a2cdc37f272e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -90,7 +90,6 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; @@ -137,7 +136,6 @@ import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; import static org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType; -import static org.elasticsearch.xpack.core.security.authc.Authentication.VERSION_API_KEY_ROLES_AS_BYTES; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS; import static org.elasticsearch.xpack.security.Security.SECURITY_CRYPTO_THREAD_POOL_NAME; @@ -533,87 +531,7 @@ void loadApiKeyAndValidateCredentials( }), client::get); } - /** - * This method is kept for BWC and should only be used for authentication objects created before v7.9.0. - * For authentication of newer versions, use {@link #getApiKeyIdAndRoleBytes} - * - * The current request has been authenticated by an API key and this method enables the - * retrieval of role descriptors that are associated with the api key - */ - public void getRoleForApiKey(Authentication authentication, ActionListener listener) { - if (authentication.getAuthenticationType() != AuthenticationType.API_KEY) { - throw new IllegalStateException("authentication type must be api key but is " + authentication.getAuthenticationType()); - } - assert authentication.getVersion().before(VERSION_API_KEY_ROLES_AS_BYTES) - : "This method only applies to authentication objects created before v7.9.0"; - - final Map metadata = authentication.getMetadata(); - final String apiKeyId = (String) metadata.get(AuthenticationField.API_KEY_ID_KEY); - @SuppressWarnings("unchecked") - final Map roleDescriptors = (Map) metadata.get(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY); - @SuppressWarnings("unchecked") - final Map authnRoleDescriptors = (Map) metadata.get( - AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY - ); - - if (roleDescriptors == null && authnRoleDescriptors == null) { - listener.onFailure(new ElasticsearchSecurityException("no role descriptors found for API key")); - } else if (roleDescriptors == null || roleDescriptors.isEmpty()) { - final List authnRoleDescriptorsList = parseRoleDescriptors(apiKeyId, authnRoleDescriptors); - listener.onResponse(new ApiKeyRoleDescriptors(apiKeyId, authnRoleDescriptorsList, null)); - } else { - final List roleDescriptorList = parseRoleDescriptors(apiKeyId, roleDescriptors); - final List authnRoleDescriptorsList = parseRoleDescriptors(apiKeyId, authnRoleDescriptors); - listener.onResponse(new ApiKeyRoleDescriptors(apiKeyId, roleDescriptorList, authnRoleDescriptorsList)); - } - } - - public Tuple getApiKeyIdAndRoleBytes(Authentication authentication, boolean limitedBy) { - if (authentication.getAuthenticationType() != AuthenticationType.API_KEY) { - throw new IllegalStateException("authentication type must be api key but is " + authentication.getAuthenticationType()); - } - assert authentication.getVersion().onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES) - : "This method only applies to authentication objects created on or after v7.9.0"; - - final Map metadata = authentication.getMetadata(); - final BytesReference bytesReference = (BytesReference) metadata.get( - limitedBy ? AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY : AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY - ); - if (limitedBy && bytesReference.length() == 2 && "{}".equals(bytesReference.utf8ToString())) { - if (ServiceAccountSettings.REALM_NAME.equals(metadata.get(AuthenticationField.API_KEY_CREATOR_REALM_NAME)) - && "elastic/fleet-server".equals(authentication.getUser().principal())) { - return new Tuple<>((String) metadata.get(AuthenticationField.API_KEY_ID_KEY), FLEET_SERVER_ROLE_DESCRIPTOR_BYTES_V_7_14); - } - } - return new Tuple<>((String) metadata.get(AuthenticationField.API_KEY_ID_KEY), bytesReference); - } - - public static class ApiKeyRoleDescriptors { - - private final String apiKeyId; - private final List roleDescriptors; - private final List limitedByRoleDescriptors; - - public ApiKeyRoleDescriptors(String apiKeyId, List roleDescriptors, List limitedByDescriptors) { - this.apiKeyId = apiKeyId; - this.roleDescriptors = roleDescriptors; - this.limitedByRoleDescriptors = limitedByDescriptors; - } - - public String getApiKeyId() { - return apiKeyId; - } - - public List getRoleDescriptors() { - return roleDescriptors; - } - - public List getLimitedByRoleDescriptors() { - return limitedByRoleDescriptors; - } - } - - private List parseRoleDescriptors(final String apiKeyId, final Map roleDescriptors) { + public List parseRoleDescriptors(final String apiKeyId, final Map roleDescriptors) { if (roleDescriptors == null) { return null; } @@ -639,7 +557,7 @@ private List parseRoleDescriptors(final String apiKeyId, final M }).collect(Collectors.toList()); } - public List parseRoleDescriptors(final String apiKeyId, BytesReference bytesReference) { + public List parseRoleDescriptorsBytes(final String apiKeyId, BytesReference bytesReference) { if (bytesReference == null) { return Collections.emptyList(); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java index 87cadf53dff5b..ee0164257a319 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java @@ -35,6 +35,9 @@ import java.util.function.Consumer; import java.util.function.Function; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_NAME; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_TYPE; + class AuthenticatorChain { private static final Logger logger = LogManager.getLogger(AuthenticatorChain.class); @@ -313,7 +316,7 @@ void handleNullToken(Authenticator.Context context, ActionListener listener) { assert authentication.isAuthenticatedWithServiceAccount() : "authentication is not for service account: " + authentication; final String principal = authentication.getUser().principal(); + getRoleDescriptorForPrincipal(principal, listener); + } + + public void getRoleDescriptorForPrincipal(String principal, ActionListener listener) { final ServiceAccount account = ACCOUNTS.get(principal); if (account == null) { listener.onFailure( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 187618b31b914..50e7a2a7e4ed0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -74,7 +74,6 @@ import org.elasticsearch.xpack.core.security.authz.privilege.NamedClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.support.StringMatcher; -import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.sql.SqlAsyncActionNames; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; @@ -124,24 +123,13 @@ public RBACEngine(Settings settings, CompositeRolesStore rolesStore) { @Override public void resolveAuthorizationInfo(RequestInfo requestInfo, ActionListener listener) { final Authentication authentication = requestInfo.getAuthentication(); - getRoles(authentication.getUser(), authentication, ActionListener.wrap(role -> { - if (authentication.getUser().isRunAs()) { - getRoles( - authentication.getUser().authenticatedUser(), - authentication, - ActionListener.wrap( - authenticatedUserRole -> listener.onResponse(new RBACAuthorizationInfo(role, authenticatedUserRole)), - listener::onFailure - ) - ); - } else { - listener.onResponse(new RBACAuthorizationInfo(role, role)); - } - }, listener::onFailure)); - } - - private void getRoles(User user, Authentication authentication, ActionListener listener) { - rolesStore.getRoles(user, authentication, listener); + rolesStore.getRoles( + authentication, + ActionListener.wrap( + roleTuple -> listener.onResponse(new RBACAuthorizationInfo(roleTuple.v1(), roleTuple.v2())), + listener::onFailure + ) + ); } @Override diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 047a15e826a0a..7c7928cf2898e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -8,19 +8,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.lucene.util.automaton.Automaton; -import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.support.ContextPreservingActionListener; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; -import org.elasticsearch.common.hash.MessageDigests; -import org.elasticsearch.common.logging.DeprecationCategory; -import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; @@ -30,8 +24,9 @@ import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Tuple; import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.xpack.core.common.IteratingActionListener; import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationContext; +import org.elasticsearch.xpack.core.security.authc.Subject; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.accesscontrol.DocumentSubsetBitsetCache; @@ -45,9 +40,10 @@ import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; -import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; +import org.elasticsearch.xpack.core.security.authz.store.RoleKey; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference; +import org.elasticsearch.xpack.core.security.authz.store.RolesRetrievalResult; import org.elasticsearch.xpack.core.security.support.CacheIteratorHelper; -import org.elasticsearch.xpack.core.security.support.MetadataUtils; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; import org.elasticsearch.xpack.core.security.user.SystemUser; @@ -61,7 +57,6 @@ 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; @@ -69,16 +64,10 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.BiConsumer; import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; import java.util.stream.Collectors; -import static java.util.function.Predicate.not; import static org.elasticsearch.common.util.set.Sets.newHashSet; -import static org.elasticsearch.xpack.core.security.SecurityField.DOCUMENT_LEVEL_SECURITY_FEATURE; -import static org.elasticsearch.xpack.core.security.authc.Authentication.VERSION_API_KEY_ROLES_AS_BYTES; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.isIndexDeleted; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.isMoveFromRedToNonRed; @@ -88,36 +77,28 @@ */ public class CompositeRolesStore { - private static final String ROLES_STORE_SOURCE = "roles_stores"; - private static final Setting CACHE_SIZE_SETTING = Setting.intSetting( - "xpack.security.authz.store.roles.cache.max_size", + static final Setting NEGATIVE_LOOKUP_CACHE_SIZE_SETTING = Setting.intSetting( + "xpack.security.authz.store.roles.negative_lookup_cache.max_size", 10000, Property.NodeScope ); - private static final Setting NEGATIVE_LOOKUP_CACHE_SIZE_SETTING = Setting.intSetting( - "xpack.security.authz.store.roles.negative_lookup_cache.max_size", + private static final Setting CACHE_SIZE_SETTING = Setting.intSetting( + "xpack.security.authz.store.roles.cache.max_size", 10000, Property.NodeScope ); private static final Logger logger = LogManager.getLogger(CompositeRolesStore.class); - private final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(CompositeRolesStore.class); - private final RoleProviders roleProviders; private final NativePrivilegeStore privilegeStore; - private final XPackLicenseState licenseState; - private final Consumer> effectiveRoleDescriptorsConsumer; private final FieldPermissionsCache fieldPermissionsCache; private final Cache roleCache; private final CacheIteratorHelper roleCacheHelper; private final Cache negativeLookupCache; private final DocumentSubsetBitsetCache dlsBitsetCache; - private final ThreadContext threadContext; - private final AtomicLong numInvalidation = new AtomicLong(); private final AnonymousUser anonymousUser; - private final ApiKeyService apiKeyService; - private final ServiceAccountService serviceAccountService; - private final boolean isAnonymousEnabled; + private final AtomicLong numInvalidation = new AtomicLong(); + private final RoleDescriptorStore roleReferenceResolver; private final Role superuserRole; private final Role xpackUserRole; private final Role asyncSearchUserRole; @@ -151,11 +132,7 @@ public void providersChanged() { this.privilegeStore = Objects.requireNonNull(privilegeStore); this.dlsBitsetCache = Objects.requireNonNull(dlsBitsetCache); - this.licenseState = Objects.requireNonNull(licenseState); this.fieldPermissionsCache = Objects.requireNonNull(fieldPermissionsCache); - this.apiKeyService = Objects.requireNonNull(apiKeyService); - this.serviceAccountService = Objects.requireNonNull(serviceAccountService); - this.effectiveRoleDescriptorsConsumer = Objects.requireNonNull(effectiveRoleDescriptorsConsumer); CacheBuilder builder = CacheBuilder.builder(); final int cacheSize = CACHE_SIZE_SETTING.get(settings); if (cacheSize >= 0) { @@ -163,105 +140,83 @@ public void providersChanged() { } this.roleCache = builder.build(); this.roleCacheHelper = new CacheIteratorHelper<>(roleCache); - this.threadContext = threadContext; CacheBuilder nlcBuilder = CacheBuilder.builder(); final int nlcCacheSize = NEGATIVE_LOOKUP_CACHE_SIZE_SETTING.get(settings); if (nlcCacheSize >= 0) { nlcBuilder.setMaximumWeight(nlcCacheSize); } this.negativeLookupCache = nlcBuilder.build(); - this.anonymousUser = new AnonymousUser(settings); - this.isAnonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); this.restrictedIndicesAutomaton = resolver.getSystemNameAutomaton(); this.superuserRole = Role.builder(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, fieldPermissionsCache, restrictedIndicesAutomaton) .build(); xpackUserRole = Role.builder(XPackUser.ROLE_DESCRIPTOR, fieldPermissionsCache, restrictedIndicesAutomaton).build(); asyncSearchUserRole = Role.builder(AsyncSearchUser.ROLE_DESCRIPTOR, fieldPermissionsCache, restrictedIndicesAutomaton).build(); + + this.roleReferenceResolver = new RoleDescriptorStore( + roleProviders, + apiKeyService, + serviceAccountService, + negativeLookupCache, + licenseState, + threadContext, + effectiveRoleDescriptorsConsumer + ); + this.anonymousUser = new AnonymousUser(settings); } - public void roles(Set roleNames, ActionListener roleActionListener) { - final RoleKey roleKey = new RoleKey(roleNames, ROLES_STORE_SOURCE); - Role existing = roleCache.get(roleKey); - if (existing != null) { - roleActionListener.onResponse(existing); - } else { - final long invalidationCounter = numInvalidation.get(); - roleDescriptors(roleNames, ActionListener.wrap(rolesRetrievalResult -> { - logDeprecatedRoles(rolesRetrievalResult.roleDescriptors); - final boolean missingRoles = rolesRetrievalResult.getMissingRoles().isEmpty() == false; - if (missingRoles) { - logger.debug( - () -> new ParameterizedMessage("Could not find roles with names {}", rolesRetrievalResult.getMissingRoles()) - ); - } - final Set effectiveDescriptors; - Set roleDescriptors = rolesRetrievalResult.getRoleDescriptors(); - if (roleDescriptors.stream().anyMatch(RoleDescriptor::isUsingDocumentOrFieldLevelSecurity) - && DOCUMENT_LEVEL_SECURITY_FEATURE.checkWithoutTracking(licenseState) == false) { - effectiveDescriptors = roleDescriptors.stream() - .filter(not(RoleDescriptor::isUsingDocumentOrFieldLevelSecurity)) - .collect(Collectors.toSet()); - } else { - effectiveDescriptors = roleDescriptors; - } - logger.trace( - () -> new ParameterizedMessage( - "Exposing effective role descriptors [{}] for role names [{}]", - effectiveDescriptors, - roleNames + public void getRoles(Authentication authentication, ActionListener> roleActionListener) { + final AuthenticationContext authenticationContext = AuthenticationContext.fromAuthentication(authentication); + getRole(authenticationContext.getEffectiveSubject(), ActionListener.wrap(role -> { + if (authenticationContext.isRunAs()) { + getRole( + authenticationContext.getAuthenticatingSubject(), + ActionListener.wrap( + authenticatingRole -> roleActionListener.onResponse(new Tuple<>(role, authenticatingRole)), + roleActionListener::onFailure ) ); - effectiveRoleDescriptorsConsumer.accept(Collections.unmodifiableCollection(effectiveDescriptors)); - logger.trace( - () -> new ParameterizedMessage( - "Building role from descriptors [{}] for role names [{}]", - effectiveDescriptors, - roleNames - ) - ); - buildThenMaybeCacheRole( - roleKey, - effectiveDescriptors, - rolesRetrievalResult.getMissingRoles(), - rolesRetrievalResult.isSuccess(), - invalidationCounter, - roleActionListener - ); - }, roleActionListener::onFailure)); - } + } else { + roleActionListener.onResponse(new Tuple<>(role, role)); + } + }, roleActionListener::onFailure)); } - void logDeprecatedRoles(Set roleDescriptors) { - roleDescriptors.stream() - .filter(rd -> Boolean.TRUE.equals(rd.getMetadata().get(MetadataUtils.DEPRECATED_METADATA_KEY))) - .forEach(rd -> { - String reason = Objects.toString( - rd.getMetadata().get(MetadataUtils.DEPRECATED_REASON_METADATA_KEY), - "Please check the documentation" - ); - deprecationLogger.warn( - DeprecationCategory.SECURITY, - "deprecated_role-" + rd.getName(), - "The role [" + rd.getName() + "] is deprecated and will be removed in a future version of Elasticsearch. " + reason - ); - }); - } + public void getRole(Subject subject, ActionListener roleActionListener) { + final Role internalUserRole = tryGetRoleForInternalUser(subject); + if (internalUserRole != null) { + roleActionListener.onResponse(internalUserRole); + return; + } - // for testing - Role getXpackUserRole() { - return xpackUserRole; - } + assert false == User.isInternal(subject.getUser()) : "Internal user should not pass here"; - // for testing - Role getAsyncSearchUserRole() { - return asyncSearchUserRole; + final List roleReferences = subject.getRoleReferences(anonymousUser); + // TODO: Two levels of nesting can be relaxed in future + assert roleReferences.size() <= 2 : "only support up to one level of limiting"; + assert false == roleReferences.isEmpty() : "role references cannot be empty"; + + buildRoleFromRoleReference(roleReferences.get(0), ActionListener.wrap(role -> { + if (roleReferences.size() == 1) { + roleActionListener.onResponse(role); + } else { + buildRoleFromRoleReference( + roleReferences.get(1), + ActionListener.wrap( + limitedByRole -> roleActionListener.onResponse(LimitedRole.createLimitedRole(role, limitedByRole)), + roleActionListener::onFailure + ) + ); + } + + }, roleActionListener::onFailure)); } - public void getRoles(User user, Authentication authentication, ActionListener roleActionListener) { + private Role tryGetRoleForInternalUser(Subject subject) { // we need to special case the internal users in this method, if we apply the anonymous roles to every user including these system // user accounts then we run into the chance of a deadlock because then we need to get a role that we may be trying to get as the // internal user. The SystemUser is special cased as it has special privileges to execute internal actions and should never be - // passed into this method. The XPackUser has the Superuser role and we can simply return that + // passed into this method. The XPackSecurityUser has the Superuser role and we can simply return that + final User user = subject.getUser(); if (SystemUser.is(user)) { throw new IllegalArgumentException( "the user [" + user.principal() + "] is the system user and we should never try to get its" + " roles" @@ -269,121 +224,65 @@ public void getRoles(User user, Authentication authentication, ActionListener roleActionListener) { - Set roleNames = new HashSet<>(Arrays.asList(user.roles())); - if (isAnonymousEnabled && anonymousUser.equals(user) == false) { - if (anonymousUser.roles().length == 0) { - throw new IllegalStateException("anonymous is only enabled when the anonymous user has roles"); - } - Collections.addAll(roleNames, anonymousUser.roles()); + public void buildRoleFromRoleReference(RoleReference roleReference, ActionListener roleActionListener) { + final RoleKey roleKey = roleReference.id(); + if (roleKey == RoleKey.ROLE_KEY_SUPERUSER) { + roleActionListener.onResponse(superuserRole); + return; } - - if (roleNames.isEmpty()) { + if (roleKey == RoleKey.ROLE_KEY_EMPTY) { roleActionListener.onResponse(Role.EMPTY); - } else if (roleNames.contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName())) { - roleActionListener.onResponse(superuserRole); - } else { - roles(roleNames, roleActionListener); + return; } - } - private void getRolesForServiceAccount(Authentication authentication, ActionListener roleActionListener) { - serviceAccountService.getRoleDescriptor(authentication, ActionListener.wrap(roleDescriptor -> { - final RoleKey roleKey = new RoleKey(Set.of(roleDescriptor.getName()), "service_account"); - final Role existing = roleCache.get(roleKey); - if (existing == null) { - final long invalidationCounter = numInvalidation.get(); - buildThenMaybeCacheRole(roleKey, List.of(roleDescriptor), Set.of(), true, invalidationCounter, roleActionListener); - } else { - roleActionListener.onResponse(existing); - } - }, roleActionListener::onFailure)); - } - - private void getRolesForApiKey(Authentication authentication, ActionListener roleActionListener) { - if (authentication.getVersion().onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES)) { - buildAndCacheRoleForApiKey(authentication, false, ActionListener.wrap(role -> { - if (role == Role.EMPTY) { - buildAndCacheRoleForApiKey(authentication, true, roleActionListener); + final Role existing = roleCache.get(roleKey); + if (existing == null) { + final long invalidationCounter = numInvalidation.get(); + roleReference.resolve(roleReferenceResolver, ActionListener.wrap(rolesRetrievalResult -> { + if (RolesRetrievalResult.EMPTY == rolesRetrievalResult) { + roleActionListener.onResponse(Role.EMPTY); + } else if (RolesRetrievalResult.SUPERUSER == rolesRetrievalResult) { + roleActionListener.onResponse(superuserRole); } else { - buildAndCacheRoleForApiKey( - authentication, - true, - ActionListener.wrap( - limitedByRole -> roleActionListener.onResponse(LimitedRole.createLimitedRole(role, limitedByRole)), - roleActionListener::onFailure - ) + buildThenMaybeCacheRole( + roleKey, + rolesRetrievalResult.getRoleDescriptors(), + rolesRetrievalResult.getMissingRoles(), + rolesRetrievalResult.isSuccess(), + invalidationCounter, + roleActionListener ); } }, roleActionListener::onFailure)); } else { - apiKeyService.getRoleForApiKey(authentication, ActionListener.wrap(apiKeyRoleDescriptors -> { - final List descriptors = apiKeyRoleDescriptors.getRoleDescriptors(); - if (descriptors == null) { - roleActionListener.onFailure(new IllegalStateException("missing role descriptors")); - } else if (apiKeyRoleDescriptors.getLimitedByRoleDescriptors() == null) { - buildAndCacheRoleFromDescriptors(descriptors, apiKeyRoleDescriptors.getApiKeyId() + "_role_desc", roleActionListener); - } else { - buildAndCacheRoleFromDescriptors( - descriptors, - apiKeyRoleDescriptors.getApiKeyId() + "_role_desc", - ActionListener.wrap( - role -> buildAndCacheRoleFromDescriptors( - apiKeyRoleDescriptors.getLimitedByRoleDescriptors(), - apiKeyRoleDescriptors.getApiKeyId() + "_limited_role_desc", - ActionListener.wrap( - limitedBy -> roleActionListener.onResponse(LimitedRole.createLimitedRole(role, limitedBy)), - roleActionListener::onFailure - ) - ), - roleActionListener::onFailure - ) - ); - } - }, roleActionListener::onFailure)); + roleActionListener.onResponse(existing); } } - public void buildAndCacheRoleFromDescriptors(Collection roleDescriptors, String source, ActionListener listener) { - if (ROLES_STORE_SOURCE.equals(source)) { - throw new IllegalArgumentException("source [" + ROLES_STORE_SOURCE + "] is reserved for internal use"); - } - RoleKey roleKey = new RoleKey(roleDescriptors.stream().map(RoleDescriptor::getName).collect(Collectors.toSet()), source); - Role existing = roleCache.get(roleKey); - if (existing != null) { - listener.onResponse(existing); - } else { - final long invalidationCounter = numInvalidation.get(); - buildThenMaybeCacheRole(roleKey, roleDescriptors, Collections.emptySet(), true, invalidationCounter, listener); - } + // package private for testing + RoleDescriptorStore getRoleReferenceResolver() { + return roleReferenceResolver; + } + + // for testing + Role getXpackUserRole() { + return xpackUserRole; + } + + // for testing + Role getAsyncSearchUserRole() { + return asyncSearchUserRole; } private void buildThenMaybeCacheRole( @@ -394,7 +293,12 @@ private void buildThenMaybeCacheRole( long invalidationCounter, ActionListener listener ) { - logger.trace("Building role from descriptors [{}] for names [{}] from source [{}]", roleDescriptors, roleKey.names, roleKey.source); + logger.trace( + "Building role from descriptors [{}] for names [{}] from source [{}]", + roleDescriptors, + roleKey.getNames(), + roleKey.getSource() + ); buildRoleFromDescriptors( roleDescriptors, fieldPermissionsCache, @@ -424,81 +328,9 @@ private void buildThenMaybeCacheRole( ); } - private void buildAndCacheRoleForApiKey(Authentication authentication, boolean limitedBy, ActionListener roleActionListener) { - final Tuple apiKeyIdAndBytes = apiKeyService.getApiKeyIdAndRoleBytes(authentication, limitedBy); - final String roleDescriptorsHash = MessageDigests.toHexString( - MessageDigests.digest(apiKeyIdAndBytes.v2(), MessageDigests.sha256()) - ); - final RoleKey roleKey = new RoleKey(Set.of("apikey:" + roleDescriptorsHash), limitedBy ? "apikey_limited_role" : "apikey_role"); - final Role existing = roleCache.get(roleKey); - if (existing == null) { - final long invalidationCounter = numInvalidation.get(); - final List roleDescriptors = apiKeyService.parseRoleDescriptors(apiKeyIdAndBytes.v1(), apiKeyIdAndBytes.v2()); - buildThenMaybeCacheRole(roleKey, roleDescriptors, Collections.emptySet(), true, invalidationCounter, roleActionListener); - } else { - roleActionListener.onResponse(existing); - } - } - + // TODO: Temporary to fill the gap public void getRoleDescriptors(Set roleNames, ActionListener> listener) { - roleDescriptors(roleNames, ActionListener.wrap(rolesRetrievalResult -> { - if (rolesRetrievalResult.isSuccess()) { - listener.onResponse(rolesRetrievalResult.getRoleDescriptors()); - } else { - listener.onFailure(new ElasticsearchException("role retrieval had one or more failures")); - } - }, listener::onFailure)); - } - - private void roleDescriptors(Set roleNames, ActionListener rolesResultListener) { - final Set filteredRoleNames = roleNames.stream().filter((s) -> { - if (negativeLookupCache.get(s) != null) { - logger.debug(() -> new ParameterizedMessage("Requested role [{}] does not exist (cached)", s)); - return false; - } else { - return true; - } - }).collect(Collectors.toSet()); - - loadRoleDescriptorsAsync(filteredRoleNames, rolesResultListener); - } - - private void loadRoleDescriptorsAsync(Set roleNames, ActionListener listener) { - final RolesRetrievalResult rolesResult = new RolesRetrievalResult(); - final List, ActionListener>> asyncRoleProviders = roleProviders.getProviders(); - final ActionListener descriptorsListener = ContextPreservingActionListener.wrapPreservingContext( - ActionListener.wrap(ignore -> { - rolesResult.setMissingRoles(roleNames); - listener.onResponse(rolesResult); - }, listener::onFailure), - threadContext - ); - - final Predicate iterationPredicate = result -> roleNames.isEmpty() == false; - new IteratingActionListener<>(descriptorsListener, (rolesProvider, providerListener) -> { - // try to resolve descriptors with role provider - rolesProvider.accept(roleNames, ActionListener.wrap(result -> { - if (result.isSuccess()) { - logger.debug( - () -> new ParameterizedMessage("Roles [{}] were resolved by [{}]", names(result.getDescriptors()), rolesProvider) - ); - final Set resolvedDescriptors = result.getDescriptors(); - rolesResult.addDescriptors(resolvedDescriptors); - // remove resolved descriptors from the set of roles still needed to be resolved - for (RoleDescriptor descriptor : resolvedDescriptors) { - roleNames.remove(descriptor.getName()); - } - } else { - logger.warn(new ParameterizedMessage("role retrieval failed from [{}]", rolesProvider), result.getFailure()); - rolesResult.setFailure(); - } - providerListener.onResponse(result); - }, providerListener::onFailure)); - }, asyncRoleProviders, threadContext, Function.identity(), iterationPredicate).run(); - } - - private String names(Collection descriptors) { - return descriptors.stream().map(RoleDescriptor::getName).collect(Collectors.joining(",")); + roleReferenceResolver.getRoleDescriptors(roleNames, listener); } public static void buildRoleFromDescriptors( @@ -602,13 +434,13 @@ public void invalidateAll() { public void invalidate(String role) { numInvalidation.incrementAndGet(); - roleCacheHelper.removeKeysIf(key -> key.names.contains(role)); + roleCacheHelper.removeKeysIf(key -> key.getNames().contains(role)); negativeLookupCache.invalidate(role); } public void invalidate(Set roles) { numInvalidation.incrementAndGet(); - roleCacheHelper.removeKeysIf(key -> Sets.haveEmptyIntersection(key.names, roles) == false); + roleCacheHelper.removeKeysIf(key -> Sets.haveEmptyIntersection(key.getNames(), roles) == false); roles.forEach(negativeLookupCache::invalidate); } @@ -714,61 +546,6 @@ private static void collatePrivilegesByIndices( } } - private static final class RolesRetrievalResult { - - private final Set roleDescriptors = new HashSet<>(); - private Set missingRoles = Collections.emptySet(); - private boolean success = true; - - private void addDescriptors(Set descriptors) { - roleDescriptors.addAll(descriptors); - } - - private Set getRoleDescriptors() { - return roleDescriptors; - } - - private void setFailure() { - success = false; - } - - private boolean isSuccess() { - return success; - } - - private void setMissingRoles(Set missingRoles) { - this.missingRoles = missingRoles; - } - - private Set getMissingRoles() { - return missingRoles; - } - } - - private static final class RoleKey { - - private final Set names; - private final String source; - - private RoleKey(Set names, String source) { - this.names = Objects.requireNonNull(names); - this.source = Objects.requireNonNull(source); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RoleKey roleKey = (RoleKey) o; - return names.equals(roleKey.names) && source.equals(roleKey.source); - } - - @Override - public int hashCode() { - return Objects.hash(names, source); - } - } - public static List> getSettings() { return Arrays.asList(CACHE_SIZE_SETTING, NEGATIVE_LOOKUP_CACHE_SIZE_SETTING); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleDescriptorStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleDescriptorStore.java new file mode 100644 index 0000000000000..777ac09bc714e --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleDescriptorStore.java @@ -0,0 +1,249 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authz.store; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ContextPreservingActionListener; +import org.elasticsearch.common.cache.Cache; +import org.elasticsearch.common.logging.DeprecationCategory; +import org.elasticsearch.common.logging.DeprecationLogger; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.xpack.core.common.IteratingActionListener; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference; +import org.elasticsearch.xpack.core.security.authz.store.RoleReferenceResolver; +import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; +import org.elasticsearch.xpack.core.security.authz.store.RolesRetrievalResult; +import org.elasticsearch.xpack.core.security.support.MetadataUtils; +import org.elasticsearch.xpack.security.authc.ApiKeyService; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.function.Predicate.not; +import static org.elasticsearch.xpack.core.security.SecurityField.DOCUMENT_LEVEL_SECURITY_FEATURE; + +public class RoleDescriptorStore implements RoleReferenceResolver { + + private static final Logger logger = LogManager.getLogger(RoleDescriptorStore.class); + private final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RoleDescriptorStore.class); + + private final RoleProviders roleProviders; + private final ApiKeyService apiKeyService; + private final ServiceAccountService serviceAccountService; + private final XPackLicenseState licenseState; + private final ThreadContext threadContext; + private final Consumer> effectiveRoleDescriptorsConsumer; + private final Cache negativeLookupCache; + + public RoleDescriptorStore( + RoleProviders roleProviders, + ApiKeyService apiKeyService, + ServiceAccountService serviceAccountService, + Cache negativeLookupCache, + XPackLicenseState licenseState, + ThreadContext threadContext, + Consumer> effectiveRoleDescriptorsConsumer + ) { + this.roleProviders = roleProviders; + this.apiKeyService = Objects.requireNonNull(apiKeyService); + this.serviceAccountService = Objects.requireNonNull(serviceAccountService); + this.licenseState = Objects.requireNonNull(licenseState); + this.threadContext = threadContext; + this.effectiveRoleDescriptorsConsumer = Objects.requireNonNull(effectiveRoleDescriptorsConsumer); + this.negativeLookupCache = negativeLookupCache; + } + + @Override + public void resolveNamedRoleReference( + RoleReference.NamedRoleReference namedRoleReference, + ActionListener listener + ) { + final Set roleNames = Set.copyOf(new HashSet<>(List.of(namedRoleReference.getRoleNames()))); + if (roleNames.isEmpty()) { + assert false : "empty role names should have short circuited earlier"; + listener.onResponse(RolesRetrievalResult.EMPTY); + } else if (roleNames.contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName())) { + assert false : "superuser role should have short circuited earlier"; + listener.onResponse(RolesRetrievalResult.SUPERUSER); + } else { + resolveRoleNames(roleNames, listener); + } + } + + @Override + public void resolveApiKeyRoleReference( + RoleReference.ApiKeyRoleReference apiKeyRoleReference, + ActionListener listener + ) { + final List roleDescriptors = apiKeyService.parseRoleDescriptorsBytes( + apiKeyRoleReference.getApiKeyId(), + apiKeyRoleReference.getRoleDescriptorsBytes() + ); + final RolesRetrievalResult rolesRetrievalResult = new RolesRetrievalResult(); + rolesRetrievalResult.addDescriptors(Set.copyOf(roleDescriptors)); + listener.onResponse(rolesRetrievalResult); + } + + @Override + public void resolveBwcApiKeyRoleReference( + RoleReference.BwcApiKeyRoleReference bwcApiKeyRoleReference, + ActionListener listener + ) { + final List roleDescriptors = apiKeyService.parseRoleDescriptors( + bwcApiKeyRoleReference.getApiKeyId(), + bwcApiKeyRoleReference.getRoleDescriptorsMap() + ); + final RolesRetrievalResult rolesRetrievalResult = new RolesRetrievalResult(); + rolesRetrievalResult.addDescriptors(Set.copyOf(roleDescriptors)); + listener.onResponse(rolesRetrievalResult); + } + + @Override + public void resolveServiceAccountRoleReference( + RoleReference.ServiceAccountRoleReference roleReference, + ActionListener listener + ) { + serviceAccountService.getRoleDescriptorForPrincipal(roleReference.getPrincipal(), listener.map(roleDescriptor -> { + final RolesRetrievalResult rolesRetrievalResult = new RolesRetrievalResult(); + rolesRetrievalResult.addDescriptors(Set.of(roleDescriptor)); + return rolesRetrievalResult; + })); + } + + private void resolveRoleNames(Set roleNames, ActionListener listener) { + roleDescriptors(roleNames, ActionListener.wrap(rolesRetrievalResult -> { + logDeprecatedRoles(rolesRetrievalResult.getRoleDescriptors()); + final boolean missingRoles = rolesRetrievalResult.getMissingRoles().isEmpty() == false; + if (missingRoles) { + logger.debug(() -> new ParameterizedMessage("Could not find roles with names {}", rolesRetrievalResult.getMissingRoles())); + } + final Set effectiveDescriptors; + Set roleDescriptors = rolesRetrievalResult.getRoleDescriptors(); + if (roleDescriptors.stream().anyMatch(RoleDescriptor::isUsingDocumentOrFieldLevelSecurity) + && DOCUMENT_LEVEL_SECURITY_FEATURE.checkWithoutTracking(licenseState) == false) { + effectiveDescriptors = roleDescriptors.stream() + .filter(not(RoleDescriptor::isUsingDocumentOrFieldLevelSecurity)) + .collect(Collectors.toSet()); + } else { + effectiveDescriptors = roleDescriptors; + } + logger.trace( + () -> new ParameterizedMessage( + "Exposing effective role descriptors [{}] for role names [{}]", + effectiveDescriptors, + roleNames + ) + ); + effectiveRoleDescriptorsConsumer.accept(Collections.unmodifiableCollection(effectiveDescriptors)); + // TODO: why not populate negativeLookupCache here with missing roles? + + // TODO: replace with a class that better represent the result, e.g. carry info for disabled role + final RolesRetrievalResult finalResult = new RolesRetrievalResult(); + finalResult.addDescriptors(effectiveDescriptors); + finalResult.setMissingRoles(rolesRetrievalResult.getMissingRoles()); + if (false == rolesRetrievalResult.isSuccess()) { + finalResult.setFailure(); + } + listener.onResponse(finalResult); + }, listener::onFailure)); + } + + public void getRoleDescriptors(Set roleNames, ActionListener> listener) { + roleDescriptors(roleNames, ActionListener.wrap(rolesRetrievalResult -> { + if (rolesRetrievalResult.isSuccess()) { + listener.onResponse(rolesRetrievalResult.getRoleDescriptors()); + } else { + listener.onFailure(new ElasticsearchException("role retrieval had one or more failures")); + } + }, listener::onFailure)); + } + + private void roleDescriptors(Set roleNames, ActionListener rolesResultListener) { + final Set filteredRoleNames = roleNames.stream().filter((s) -> { + if (negativeLookupCache.get(s) != null) { + logger.debug(() -> new ParameterizedMessage("Requested role [{}] does not exist (cached)", s)); + return false; + } else { + return true; + } + }).collect(Collectors.toSet()); + + loadRoleDescriptorsAsync(filteredRoleNames, rolesResultListener); + } + + void logDeprecatedRoles(Set roleDescriptors) { + roleDescriptors.stream() + .filter(rd -> Boolean.TRUE.equals(rd.getMetadata().get(MetadataUtils.DEPRECATED_METADATA_KEY))) + .forEach(rd -> { + String reason = Objects.toString( + rd.getMetadata().get(MetadataUtils.DEPRECATED_REASON_METADATA_KEY), + "Please check the documentation" + ); + deprecationLogger.critical( + DeprecationCategory.SECURITY, + "deprecated_role-" + rd.getName(), + "The role [" + rd.getName() + "] is deprecated and will be removed in a future version of Elasticsearch. " + reason + ); + }); + } + + private void loadRoleDescriptorsAsync(Set roleNames, ActionListener listener) { + final RolesRetrievalResult rolesResult = new RolesRetrievalResult(); + final List, ActionListener>> asyncRoleProviders = roleProviders.getProviders(); + final ActionListener descriptorsListener = ContextPreservingActionListener.wrapPreservingContext( + ActionListener.wrap(ignore -> { + rolesResult.setMissingRoles(roleNames); + listener.onResponse(rolesResult); + }, listener::onFailure), + threadContext + ); + + final Predicate iterationPredicate = result -> roleNames.isEmpty() == false; + new IteratingActionListener<>(descriptorsListener, (rolesProvider, providerListener) -> { + // try to resolve descriptors with role provider + rolesProvider.accept(roleNames, ActionListener.wrap(result -> { + if (result.isSuccess()) { + logger.debug( + () -> new ParameterizedMessage( + "Roles [{}] were resolved by [{}]", + result.getDescriptors().stream().map(RoleDescriptor::getName).collect(Collectors.joining(",")), + rolesProvider + ) + ); + final Set resolvedDescriptors = result.getDescriptors(); + rolesResult.addDescriptors(resolvedDescriptors); + // remove resolved descriptors from the set of roles still needed to be resolved + for (RoleDescriptor descriptor : resolvedDescriptors) { + roleNames.remove(descriptor.getName()); + } + } else { + logger.warn(new ParameterizedMessage("role retrieval failed from [{}]", rolesProvider), result.getFailure()); + rolesResult.setFailure(); + } + providerListener.onResponse(result); + }, providerListener::onFailure)); + }, asyncRoleProviders, threadContext, Function.identity(), iterationPredicate).run(); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index ba9b0a3a5327f..0caa4cff53802 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -38,13 +38,11 @@ import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.core.Tuple; import org.elasticsearch.index.get.GetResult; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.MockLogAppender; -import org.elasticsearch.test.VersionUtils; import org.elasticsearch.test.XContentTestUtils; import org.elasticsearch.threadpool.FixedExecutorBuilder; import org.elasticsearch.threadpool.TestThreadPool; @@ -71,7 +69,6 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.ApiKeyService.ApiKeyCredentials; import org.elasticsearch.xpack.security.authc.ApiKeyService.ApiKeyDoc; -import org.elasticsearch.xpack.security.authc.ApiKeyService.ApiKeyRoleDescriptors; import org.elasticsearch.xpack.security.authc.ApiKeyService.CachedApiKeyHashResult; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; @@ -88,7 +85,6 @@ import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Arrays; import java.util.Base64; import java.util.Collection; import java.util.Collections; @@ -119,7 +115,9 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyArray; +import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThanOrEqualTo; @@ -565,50 +563,24 @@ public void testValidateApiKey() throws Exception { assertNull(service.getApiKeyAuthCache().get(apiKeyId)); } - public void testGetRolesForApiKeyNotInContext() throws Exception { - Map superUserRdMap; - try (XContentBuilder builder = JsonXContent.contentBuilder()) { - superUserRdMap = XContentHelper.convertToMap( - XContentType.JSON.xContent(), - BytesReference.bytes(SUPERUSER_ROLE_DESCRIPTOR.toXContent(builder, ToXContent.EMPTY_PARAMS, true)).streamInput(), - false - ); - } - Map authMetadata = new HashMap<>(); - authMetadata.put(AuthenticationField.API_KEY_ID_KEY, randomAlphaOfLength(12)); - authMetadata.put(AuthenticationField.API_KEY_NAME_KEY, randomBoolean() ? null : randomAlphaOfLength(12)); - authMetadata.put( - AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, - Collections.singletonMap(SUPERUSER_ROLE_DESCRIPTOR.getName(), superUserRdMap) - ); - authMetadata.put( - AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, - Collections.singletonMap(SUPERUSER_ROLE_DESCRIPTOR.getName(), superUserRdMap) - ); + @SuppressWarnings("unchecked") + public void testParseRoleDescriptorsMap() throws Exception { + final String apiKeyId = randomAlphaOfLength(12); - final Authentication authentication = new Authentication( - new User("joe"), - new RealmRef("apikey", "apikey", "node"), - null, - VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.V_7_8_1), - AuthenticationType.API_KEY, - authMetadata - ); + final NativePrivilegeStore privilegesStore = mock(NativePrivilegeStore.class); + doAnswer(i -> { + assertThat(i.getArguments().length, equalTo(3)); + final Object arg2 = i.getArguments()[2]; + assertThat(arg2, instanceOf(ActionListener.class)); + ActionListener> listener = (ActionListener>) arg2; + listener.onResponse(Collections.emptyList()); + return null; + }).when(privilegesStore).getPrivileges(any(Collection.class), any(Collection.class), anyActionListener()); ApiKeyService service = createApiKeyService(Settings.EMPTY); - PlainActionFuture roleFuture = new PlainActionFuture<>(); - service.getRoleForApiKey(authentication, roleFuture); - ApiKeyRoleDescriptors result = roleFuture.get(); - assertThat(result.getRoleDescriptors().size(), is(1)); - assertThat(result.getRoleDescriptors().get(0).getName(), is("superuser")); - } + assertThat(service.parseRoleDescriptors(apiKeyId, null), nullValue()); + assertThat(service.parseRoleDescriptors(apiKeyId, Collections.emptyMap()), emptyIterable()); - @SuppressWarnings("unchecked") - public void testGetRolesForApiKey() throws Exception { - Map authMetadata = new HashMap<>(); - authMetadata.put(AuthenticationField.API_KEY_ID_KEY, randomAlphaOfLength(12)); - authMetadata.put(AuthenticationField.API_KEY_NAME_KEY, randomBoolean() ? null : randomAlphaOfLength(12)); - boolean emptyApiKeyRoleDescriptor = randomBoolean(); final RoleDescriptor roleARoleDescriptor = new RoleDescriptor( "a role", new String[] { "monitor" }, @@ -624,100 +596,32 @@ public void testGetRolesForApiKey() throws Exception { false ); } - authMetadata.put( - AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, - (emptyApiKeyRoleDescriptor) - ? randomFrom(Arrays.asList(null, Collections.emptyMap())) - : Collections.singletonMap("a role", roleARDMap) - ); - final RoleDescriptor limitedRoleDescriptor = new RoleDescriptor( - "limited role", - new String[] { "all" }, - new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").build() }, - null - ); - Map limitedRdMap; + List roleDescriptors = service.parseRoleDescriptors(apiKeyId, Map.of("a role", roleARDMap)); + assertThat(roleDescriptors, hasSize(1)); + assertThat(roleDescriptors.get(0), equalTo(roleARoleDescriptor)); + + Map superUserRdMap; try (XContentBuilder builder = JsonXContent.contentBuilder()) { - limitedRdMap = XContentHelper.convertToMap( + superUserRdMap = XContentHelper.convertToMap( XContentType.JSON.xContent(), - BytesReference.bytes(limitedRoleDescriptor.toXContent(builder, ToXContent.EMPTY_PARAMS, true)).streamInput(), + BytesReference.bytes(SUPERUSER_ROLE_DESCRIPTOR.toXContent(builder, ToXContent.EMPTY_PARAMS, true)).streamInput(), false ); } - authMetadata.put(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, Collections.singletonMap("limited role", limitedRdMap)); - - final Authentication authentication = new Authentication( - new User("joe"), - new RealmRef("apikey", "apikey", "node"), - null, - VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.V_7_8_1), - AuthenticationType.API_KEY, - authMetadata - ); - - final NativePrivilegeStore privilegesStore = mock(NativePrivilegeStore.class); - doAnswer(i -> { - assertThat(i.getArguments().length, equalTo(3)); - final Object arg2 = i.getArguments()[2]; - assertThat(arg2, instanceOf(ActionListener.class)); - ActionListener> listener = (ActionListener>) arg2; - listener.onResponse(Collections.emptyList()); - return null; - }).when(privilegesStore).getPrivileges(any(Collection.class), any(Collection.class), anyActionListener()); - ApiKeyService service = createApiKeyService(Settings.EMPTY); - - PlainActionFuture roleFuture = new PlainActionFuture<>(); - service.getRoleForApiKey(authentication, roleFuture); - ApiKeyRoleDescriptors result = roleFuture.get(); - if (emptyApiKeyRoleDescriptor) { - assertNull(result.getLimitedByRoleDescriptors()); - assertThat(result.getRoleDescriptors().size(), is(1)); - assertThat(result.getRoleDescriptors().get(0).getName(), is("limited role")); - } else { - assertThat(result.getRoleDescriptors().size(), is(1)); - assertThat(result.getLimitedByRoleDescriptors().size(), is(1)); - assertThat(result.getRoleDescriptors().get(0).getName(), is("a role")); - assertThat(result.getLimitedByRoleDescriptors().get(0).getName(), is("limited role")); - } - } - - public void testGetApiKeyIdAndRoleBytes() { - Map authMetadata = new HashMap<>(); - final String apiKeyId = randomAlphaOfLength(12); - authMetadata.put(AuthenticationField.API_KEY_ID_KEY, apiKeyId); - authMetadata.put(AuthenticationField.API_KEY_NAME_KEY, randomBoolean() ? null : randomAlphaOfLength(12)); - final BytesReference roleBytes = new BytesArray("{\"a role\": {\"cluster\": [\"all\"]}}"); - final BytesReference limitedByRoleBytes = new BytesArray("{\"limitedBy role\": {\"cluster\": [\"all\"]}}"); - authMetadata.put(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, roleBytes); - authMetadata.put(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, limitedByRoleBytes); - - final Authentication authentication = new Authentication( - new User("joe"), - new RealmRef("apikey", "apikey", "node"), - null, - Version.CURRENT, - AuthenticationType.API_KEY, - authMetadata - ); - ApiKeyService service = createApiKeyService(Settings.EMPTY); - - Tuple apiKeyIdAndRoleBytes = service.getApiKeyIdAndRoleBytes(authentication, false); - assertEquals(apiKeyId, apiKeyIdAndRoleBytes.v1()); - assertEquals(roleBytes, apiKeyIdAndRoleBytes.v2()); - apiKeyIdAndRoleBytes = service.getApiKeyIdAndRoleBytes(authentication, true); - assertEquals(apiKeyId, apiKeyIdAndRoleBytes.v1()); - assertEquals(limitedByRoleBytes, apiKeyIdAndRoleBytes.v2()); + roleDescriptors = service.parseRoleDescriptors(apiKeyId, Map.of(SUPERUSER_ROLE_DESCRIPTOR.getName(), superUserRdMap)); + assertThat(roleDescriptors, hasSize(1)); + assertThat(roleDescriptors.get(0), equalTo(SUPERUSER_ROLE_DESCRIPTOR)); } public void testParseRoleDescriptors() { ApiKeyService service = createApiKeyService(Settings.EMPTY); final String apiKeyId = randomAlphaOfLength(12); - List roleDescriptors = service.parseRoleDescriptors(apiKeyId, null); + List roleDescriptors = service.parseRoleDescriptorsBytes(apiKeyId, null); assertTrue(roleDescriptors.isEmpty()); BytesReference roleBytes = new BytesArray("{\"a role\": {\"cluster\": [\"all\"]}}"); - roleDescriptors = service.parseRoleDescriptors(apiKeyId, roleBytes); + roleDescriptors = service.parseRoleDescriptorsBytes(apiKeyId, roleBytes); assertEquals(1, roleDescriptors.size()); assertEquals("a role", roleDescriptors.get(0).getName()); assertArrayEquals(new String[] { "all" }, roleDescriptors.get(0).getClusterPrivileges()); @@ -731,7 +635,7 @@ public void testParseRoleDescriptors() { + "\"privileges\":[\"*\"],\"resources\":[\"*\"]}],\"run_as\":[\"*\"],\"metadata\":{\"_reserved\":true}," + "\"transient_metadata\":{}}}\n" ); - roleDescriptors = service.parseRoleDescriptors(apiKeyId, roleBytes); + roleDescriptors = service.parseRoleDescriptorsBytes(apiKeyId, roleBytes); assertEquals(2, roleDescriptors.size()); assertEquals( Set.of("reporting_user", "superuser"), @@ -1133,7 +1037,7 @@ public void testApiKeyDocCache() throws IOException, ExecutionException, Interru final BytesReference limitedByRoleDescriptorsBytes = service.getRoleDescriptorsBytesCache() .get(cachedApiKeyDoc.limitedByRoleDescriptorsHash); assertNotNull(limitedByRoleDescriptorsBytes); - final List limitedByRoleDescriptors = service.parseRoleDescriptors(docId, limitedByRoleDescriptorsBytes); + final List limitedByRoleDescriptors = service.parseRoleDescriptorsBytes(docId, limitedByRoleDescriptorsBytes); assertEquals(1, limitedByRoleDescriptors.size()); assertEquals(SUPERUSER_ROLE_DESCRIPTOR, limitedByRoleDescriptors.get(0)); if (metadata == null) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 7ebb5dfb7491e..39ac65d0f05eb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -130,6 +130,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler; +import org.elasticsearch.xpack.core.security.authc.Subject; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; @@ -267,12 +268,18 @@ public void setup() { }).when(privilegesStore).getPrivileges(any(Collection.class), any(Collection.class), anyActionListener()); final Map, Role> roleCache = new HashMap<>(); + final AnonymousUser anonymousUser = mock(AnonymousUser.class); + when(anonymousUser.enabled()).thenReturn(false); + doAnswer(i -> { + i.callRealMethod(); + return null; + }).when(rolesStore).getRoles(any(Authentication.class), anyActionListener()); doAnswer((i) -> { - ActionListener callback = (ActionListener) i.getArguments()[2]; - User user = (User) i.getArguments()[0]; + ActionListener callback = (ActionListener) i.getArguments()[1]; + User user = ((Subject) i.getArguments()[0]).getUser(); buildRole(user, privilegesStore, fieldPermissionsCache, roleCache, callback); return null; - }).when(rolesStore).getRoles(any(User.class), any(Authentication.class), anyActionListener()); + }).when(rolesStore).getRole(any(Subject.class), anyActionListener()); roleMap.put(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName(), ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); operatorPrivilegesService = mock(OperatorPrivileges.OperatorPrivilegesService.class); authorizationService = new AuthorizationService( @@ -894,7 +901,6 @@ public void testServiceAccountDenial() { Authentication.AuthenticationType.TOKEN, Map.of() ); - Mockito.reset(rolesStore); final Role role; if (canRunAs) { role = Role.builder(RESTRICTED_INDICES_AUTOMATON, "can_run_as") @@ -905,10 +911,10 @@ public void testServiceAccountDenial() { } doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") - ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; listener.onResponse(role); return null; - }).when(rolesStore).getRoles(any(User.class), any(Authentication.class), anyActionListener()); + }).when(rolesStore).getRole(any(Subject.class), anyActionListener()); ElasticsearchSecurityException securityException = expectThrows( ElasticsearchSecurityException.class, @@ -1099,12 +1105,12 @@ public void testSearchAgainstIndex() throws Exception { ); this.setFakeOriginatingAction = false; authorize(authentication, SearchAction.NAME, searchRequest, true, () -> { - verify(rolesStore).getRoles(Mockito.same(user), Mockito.same(authentication), Mockito.any()); + verify(rolesStore).getRoles(Mockito.same(authentication), Mockito.any()); IndicesAccessControl iac = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); // Within the action handler, execute a child action (the query phase of search) authorize(authentication, SearchTransportService.QUERY_ACTION_NAME, shardRequest, false, () -> { // This child action triggers a second interaction with the role store (which is cached) - verify(rolesStore, times(2)).getRoles(Mockito.same(user), Mockito.same(authentication), Mockito.any()); + verify(rolesStore, times(2)).getRoles(Mockito.same(authentication), Mockito.any()); // But it does not create a new IndicesAccessControl assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), sameInstance(iac)); }); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index 33189917f9140..ef8fbb3654de8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -55,8 +55,8 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.graph.action.GraphExploreAction; -import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; +import org.elasticsearch.xpack.core.security.authc.Subject; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; @@ -64,6 +64,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference; import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; @@ -108,7 +109,6 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.oneOf; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -127,7 +127,6 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { private String tomorrowSuffix; @Before - // @SuppressWarnings("unchecked") public void setup() { Settings settings = Settings.builder() .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) @@ -334,11 +333,9 @@ public void setup() { doAnswer((i) -> { @SuppressWarnings("unchecked") ActionListener callback = (ActionListener) i.getArguments()[1]; - @SuppressWarnings("unchecked") - Set names = (Set) i.getArguments()[0]; - assertNotNull(names); + final RoleReference.NamedRoleReference namedRoleReference = (RoleReference.NamedRoleReference) i.getArguments()[0]; Set roleDescriptors = new HashSet<>(); - for (String name : names) { + for (String name : namedRoleReference.getRoleNames()) { RoleDescriptor descriptor = roleMap.get(name); if (descriptor != null) { roleDescriptors.add(descriptor); @@ -357,12 +354,12 @@ public void setup() { ); } return Void.TYPE; - }).when(rolesStore).roles(anySet(), anyActionListener()); + }).when(rolesStore).buildRoleFromRoleReference(any(RoleReference.NamedRoleReference.class), anyActionListener()); doAnswer(i -> { - User user = (User) i.getArguments()[0]; + User user = ((Subject) i.getArguments()[0]).getUser(); @SuppressWarnings("unchecked") - ActionListener listener = (ActionListener) i.getArguments()[2]; + ActionListener listener = (ActionListener) i.getArguments()[1]; if (XPackUser.is(user)) { listener.onResponse(Role.builder(XPackUser.ROLE_DESCRIPTOR, fieldPermissionsCache, RESTRICTED_INDICES_AUTOMATON).build()); return Void.TYPE; @@ -379,9 +376,10 @@ public void setup() { ); return Void.TYPE; } + i.callRealMethod(); return Void.TYPE; - }).when(rolesStore).getRoles(any(User.class), any(Authentication.class), anyActionListener()); + }).when(rolesStore).getRole(any(Subject.class), anyActionListener()); ClusterService clusterService = mock(ClusterService.class); when(clusterService.getClusterSettings()).thenReturn(new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); @@ -2250,12 +2248,8 @@ private Set buildAuthorizedIndices(User user, String action) { private Set buildAuthorizedIndices(User user, String action, TransportRequest request) { PlainActionFuture rolesListener = new PlainActionFuture<>(); - final Authentication authentication = new Authentication( - user, - new RealmRef("test", "indices-aliases-resolver-tests", "node"), - null - ); - rolesStore.getRoles(user, authentication, rolesListener); + final Subject subject = new Subject(user, new RealmRef("test", "indices-aliases-resolver-tests", "node")); + rolesStore.getRole(subject, rolesListener); return RBACEngine.resolveAuthorizedIndicesFromRole( rolesListener.actionGet(), getRequestInfo(request, action), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index df361828028bd..84533d147d4b6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -34,7 +34,6 @@ import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Nullable; -import org.elasticsearch.core.Tuple; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.license.LicenseStateListener; import org.elasticsearch.license.MockLicenseState; @@ -53,7 +52,9 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; +import org.elasticsearch.xpack.core.security.authc.AuthenticationContext; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; +import org.elasticsearch.xpack.core.security.authc.Subject; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.accesscontrol.DocumentSubsetBitsetCache; @@ -68,6 +69,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference; import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.core.security.index.IndexAuditTrailField; import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames; @@ -114,8 +116,11 @@ import static org.elasticsearch.test.ActionListenerUtils.anyActionListener; import static org.elasticsearch.xpack.core.security.SecurityField.DOCUMENT_LEVEL_SECURITY_FEATURE; import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_ID_KEY; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY; import static org.elasticsearch.xpack.security.authc.ApiKeyServiceTests.Utils.createApiKeyAuthentication; import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -124,9 +129,10 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doAnswer; @@ -208,25 +214,25 @@ public void testRolesWhenDlsFlsUnlicensed() throws IOException { ); PlainActionFuture roleFuture = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton("fls"), roleFuture); + getRoleForRoleNames(compositeRolesStore, Collections.singleton("fls"), roleFuture); assertEquals(Role.EMPTY, roleFuture.actionGet()); assertThat(effectiveRoleDescriptors.get().isEmpty(), is(true)); effectiveRoleDescriptors.set(null); roleFuture = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton("dls"), roleFuture); + getRoleForRoleNames(compositeRolesStore, Collections.singleton("dls"), roleFuture); assertEquals(Role.EMPTY, roleFuture.actionGet()); assertThat(effectiveRoleDescriptors.get().isEmpty(), is(true)); effectiveRoleDescriptors.set(null); roleFuture = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton("fls_dls"), roleFuture); + getRoleForRoleNames(compositeRolesStore, Collections.singleton("fls_dls"), roleFuture); assertEquals(Role.EMPTY, roleFuture.actionGet()); assertThat(effectiveRoleDescriptors.get().isEmpty(), is(true)); effectiveRoleDescriptors.set(null); roleFuture = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton("no_fls_dls"), roleFuture); + getRoleForRoleNames(compositeRolesStore, Collections.singleton("no_fls_dls"), roleFuture); assertNotEquals(Role.EMPTY, roleFuture.actionGet()); assertThat(effectiveRoleDescriptors.get(), containsInAnyOrder(noFlsDlsRole)); effectiveRoleDescriptors.set(null); @@ -289,25 +295,25 @@ public void testRolesWhenDlsFlsLicensed() throws IOException { ); PlainActionFuture roleFuture = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton("fls"), roleFuture); + getRoleForRoleNames(compositeRolesStore, Collections.singleton("fls"), roleFuture); assertNotEquals(Role.EMPTY, roleFuture.actionGet()); assertThat(effectiveRoleDescriptors.get(), containsInAnyOrder(flsRole)); effectiveRoleDescriptors.set(null); roleFuture = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton("dls"), roleFuture); + getRoleForRoleNames(compositeRolesStore, Collections.singleton("dls"), roleFuture); assertNotEquals(Role.EMPTY, roleFuture.actionGet()); assertThat(effectiveRoleDescriptors.get(), containsInAnyOrder(dlsRole)); effectiveRoleDescriptors.set(null); roleFuture = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton("fls_dls"), roleFuture); + getRoleForRoleNames(compositeRolesStore, Collections.singleton("fls_dls"), roleFuture); assertNotEquals(Role.EMPTY, roleFuture.actionGet()); assertThat(effectiveRoleDescriptors.get(), containsInAnyOrder(flsDlsRole)); effectiveRoleDescriptors.set(null); roleFuture = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton("no_fls_dls"), roleFuture); + getRoleForRoleNames(compositeRolesStore, Collections.singleton("no_fls_dls"), roleFuture); assertNotEquals(Role.EMPTY, roleFuture.actionGet()); assertThat(effectiveRoleDescriptors.get(), containsInAnyOrder(noFlsDlsRole)); effectiveRoleDescriptors.set(null); @@ -353,7 +359,7 @@ public void testNegativeLookupsAreCached() { final String roleName = randomAlphaOfLengthBetween(1, 10); PlainActionFuture future = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton(roleName), future); + getRoleForRoleNames(compositeRolesStore, Collections.singleton(roleName), future); final Role role = future.actionGet(); assertThat(effectiveRoleDescriptors.get().isEmpty(), is(true)); effectiveRoleDescriptors.set(null); @@ -372,20 +378,12 @@ public void testNegativeLookupsAreCached() { : Collections.singleton(roleName); for (int i = 0; i < numberOfTimesToCall; i++) { future = new PlainActionFuture<>(); - compositeRolesStore.roles(names, future); - future.actionGet(); - if (getSuperuserRole && i == 0) { - assertThat(effectiveRoleDescriptors.get(), containsInAnyOrder(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR)); - effectiveRoleDescriptors.set(null); - } else { - assertThat(effectiveRoleDescriptors.get(), is(nullValue())); + getRoleForRoleNames(compositeRolesStore, names, future); + final Role role1 = future.actionGet(); + if (getSuperuserRole) { + assertThat(role1.names(), arrayContaining("superuser")); } - } - - if (getSuperuserRole && numberOfTimesToCall > 0) { - // the superuser role was requested so we get the role descriptors again - verify(reservedRolesStore, times(2)).accept(anySet(), anyActionListener()); - verify(nativePrivilegeStore).getPrivileges(isASet(), isASet(), anyActionListener()); + assertThat(effectiveRoleDescriptors.get(), is(nullValue())); } verifyNoMoreInteractions(fileRolesStore, reservedRolesStore, nativeRolesStore, nativePrivilegeStore); } @@ -425,7 +423,7 @@ public void testNegativeLookupsCacheDisabled() { final String roleName = randomAlphaOfLengthBetween(1, 10); PlainActionFuture future = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton(roleName), future); + getRoleForRoleNames(compositeRolesStore, Collections.singleton(roleName), future); final Role role = future.actionGet(); assertThat(effectiveRoleDescriptors.get().isEmpty(), is(true)); effectiveRoleDescriptors.set(null); @@ -475,7 +473,7 @@ public void testNegativeLookupsAreNotCachedWithFailures() { final String roleName = randomAlphaOfLengthBetween(1, 10); PlainActionFuture future = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton(roleName), future); + getRoleForRoleNames(compositeRolesStore, Collections.singleton(roleName), future); final Role role = future.actionGet(); assertThat(effectiveRoleDescriptors.get().isEmpty(), is(true)); effectiveRoleDescriptors.set(null); @@ -490,7 +488,7 @@ public void testNegativeLookupsAreNotCachedWithFailures() { final Set names = Collections.singleton(roleName); for (int i = 0; i < numberOfTimesToCall; i++) { future = new PlainActionFuture<>(); - compositeRolesStore.roles(names, future); + getRoleForRoleNames(compositeRolesStore, names, future); future.actionGet(); assertThat(effectiveRoleDescriptors.get().isEmpty(), is(true)); effectiveRoleDescriptors.set(null); @@ -581,7 +579,7 @@ public void testCustomRolesProviders() { final Set roleNames = Sets.newHashSet("roleA", "roleB", "unknown"); PlainActionFuture future = new PlainActionFuture<>(); - compositeRolesStore.roles(roleNames, future); + getRoleForRoleNames(compositeRolesStore, roleNames, future); final Role role = future.actionGet(); assertThat(effectiveRoleDescriptors.get(), containsInAnyOrder(roleAProvider1, roleBProvider2)); effectiveRoleDescriptors.set(null); @@ -600,7 +598,7 @@ public void testCustomRolesProviders() { final int numberOfTimesToCall = scaledRandomIntBetween(1, 8); for (int i = 0; i < numberOfTimesToCall; i++) { future = new PlainActionFuture<>(); - compositeRolesStore.roles(Collections.singleton("unknown"), future); + getRoleForRoleNames(compositeRolesStore, Collections.singleton("unknown"), future); future.actionGet(); if (i == 0) { assertThat(effectiveRoleDescriptors.get().isEmpty(), is(true)); @@ -857,7 +855,7 @@ public void testCustomRolesProviderFailures() throws Exception { final Set roleNames = Sets.newHashSet("roleA", "roleB", "unknown"); PlainActionFuture future = new PlainActionFuture<>(); - compositeRolesStore.roles(roleNames, future); + getRoleForRoleNames(compositeRolesStore, roleNames, future); try { future.get(); fail("provider should have thrown a failure"); @@ -922,7 +920,7 @@ public void testCustomRolesProvidersLicensing() { Set roleNames = Sets.newHashSet("roleA"); PlainActionFuture future = new PlainActionFuture<>(); - compositeRolesStore.roles(roleNames, future); + getRoleForRoleNames(compositeRolesStore, roleNames, future); Role role = future.actionGet(); assertThat(effectiveRoleDescriptors.get(), hasSize(0)); effectiveRoleDescriptors.set(null); @@ -936,7 +934,7 @@ public void testCustomRolesProvidersLicensing() { roleNames = Sets.newHashSet("roleA"); future = new PlainActionFuture<>(); - compositeRolesStore.roles(roleNames, future); + getRoleForRoleNames(compositeRolesStore, roleNames, future); role = future.actionGet(); assertThat(effectiveRoleDescriptors.get(), containsInAnyOrder(roleA)); effectiveRoleDescriptors.set(null); @@ -950,7 +948,7 @@ public void testCustomRolesProvidersLicensing() { roleNames = Sets.newHashSet("roleA"); future = new PlainActionFuture<>(); - compositeRolesStore.roles(roleNames, future); + getRoleForRoleNames(compositeRolesStore, roleNames, future); role = future.actionGet(); assertEquals(0, role.indices().groups().length); assertThat(effectiveRoleDescriptors.get(), hasSize(0)); @@ -1096,8 +1094,7 @@ public void testDefaultRoleUserWithoutRoles() { PlainActionFuture rolesFuture = new PlainActionFuture<>(); final User user = new User("no role user"); - Authentication auth = new Authentication(user, new RealmRef("name", "type", "node"), null); - compositeRolesStore.getRoles(user, auth, rolesFuture); + compositeRolesStore.getRole(new Subject(user, new RealmRef("name", "type", "node")), rolesFuture); final Role roles = rolesFuture.actionGet(); assertEquals(Role.EMPTY, roles); } @@ -1144,8 +1141,8 @@ public void testAnonymousUserEnabledRoleAdded() { PlainActionFuture rolesFuture = new PlainActionFuture<>(); final User user = new User("no role user"); - Authentication auth = new Authentication(user, new RealmRef("name", "type", "node"), null); - compositeRolesStore.getRoles(user, auth, rolesFuture); + Subject subject = new Subject(user, new RealmRef("name", "type", "node")); + compositeRolesStore.getRole(subject, rolesFuture); final Role roles = rolesFuture.actionGet(); assertThat(Arrays.asList(roles.names()), hasItem("anonymous_user_role")); } @@ -1181,8 +1178,8 @@ public void testDoesNotUseRolesStoreForXPacAndAsyncSearchUser() { // test Xpack user short circuits to its own reserved role PlainActionFuture rolesFuture = new PlainActionFuture<>(); - Authentication auth = new Authentication(XPackUser.INSTANCE, new RealmRef("name", "type", "node"), null); - compositeRolesStore.getRoles(XPackUser.INSTANCE, auth, rolesFuture); + Subject subject = new Subject(XPackUser.INSTANCE, new RealmRef("name", "type", "node")); + compositeRolesStore.getRole(subject, rolesFuture); Role roles = rolesFuture.actionGet(); assertThat(roles, equalTo(compositeRolesStore.getXpackUserRole())); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); @@ -1190,8 +1187,8 @@ public void testDoesNotUseRolesStoreForXPacAndAsyncSearchUser() { // test AyncSearch user short circuits to its own reserved role rolesFuture = new PlainActionFuture<>(); - auth = new Authentication(AsyncSearchUser.INSTANCE, new RealmRef("name", "type", "node"), null); - compositeRolesStore.getRoles(AsyncSearchUser.INSTANCE, auth, rolesFuture); + subject = new Subject(AsyncSearchUser.INSTANCE, new RealmRef("name", "type", "node")); + compositeRolesStore.getRole(subject, rolesFuture); roles = rolesFuture.actionGet(); assertThat(roles, equalTo(compositeRolesStore.getAsyncSearchUserRole())); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); @@ -1228,7 +1225,10 @@ public void testGetRolesForSystemUserThrowsException() { verify(fileRolesStore).addListener(anyConsumer()); // adds a listener in ctor IllegalArgumentException iae = expectThrows( IllegalArgumentException.class, - () -> compositeRolesStore.getRoles(SystemUser.INSTANCE, null, null) + () -> compositeRolesStore.getRole( + new Subject(SystemUser.INSTANCE, new RealmRef("__attach", "__attach", randomAlphaOfLengthBetween(3, 8))), + null + ) ); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); assertEquals("the user [_system] is the system user and we should never try to get its roles", iae.getMessage()); @@ -1292,19 +1292,20 @@ public void testApiKeyAuthUsesApiKeyService() throws Exception { ); PlainActionFuture roleFuture = new PlainActionFuture<>(); - compositeRolesStore.getRoles(authentication.getUser(), authentication, roleFuture); + compositeRolesStore.getRole(AuthenticationContext.fromAuthentication(authentication).getEffectiveSubject(), roleFuture); Role role = roleFuture.actionGet(); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); if (version == Version.CURRENT) { - verify(apiKeyService, times(2)).getApiKeyIdAndRoleBytes(eq(authentication), anyBoolean()); + verify(apiKeyService, times(1)).parseRoleDescriptorsBytes(anyString(), any(BytesReference.class)); } else { - verify(apiKeyService).getRoleForApiKey(eq(authentication), anyActionListener()); + verify(apiKeyService, times(1)).parseRoleDescriptors(anyString(), anyMap()); } assertThat(role.names().length, is(1)); assertThat(role.names()[0], containsString("user_role_")); } + @SuppressWarnings("unchecked") public void testApiKeyAuthUsesApiKeyServiceWithScopedRole() throws Exception { final FileRolesStore fileRolesStore = mock(FileRolesStore.class); doCallRealMethod().when(fileRolesStore).accept(anySet(), anyActionListener()); @@ -1362,17 +1363,31 @@ public void testApiKeyAuthUsesApiKeyServiceWithScopedRole() throws Exception { Collections.singletonList(new RoleDescriptor("key_role_" + randomAlphaOfLength(8), new String[] { "monitor" }, null, null)), version ); + final String apiKeyId = (String) authentication.getMetadata().get(API_KEY_ID_KEY); PlainActionFuture roleFuture = new PlainActionFuture<>(); - compositeRolesStore.getRoles(authentication.getUser(), authentication, roleFuture); + compositeRolesStore.getRole(AuthenticationContext.fromAuthentication(authentication).getEffectiveSubject(), roleFuture); Role role = roleFuture.actionGet(); assertThat(role.checkClusterAction("cluster:admin/foo", Empty.INSTANCE, mock(Authentication.class)), is(false)); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); if (version == Version.CURRENT) { - verify(apiKeyService).getApiKeyIdAndRoleBytes(eq(authentication), eq(false)); - verify(apiKeyService).getApiKeyIdAndRoleBytes(eq(authentication), eq(true)); + verify(apiKeyService).parseRoleDescriptorsBytes( + apiKeyId, + (BytesReference) authentication.getMetadata().get(API_KEY_ROLE_DESCRIPTORS_KEY) + ); + verify(apiKeyService).parseRoleDescriptorsBytes( + apiKeyId, + (BytesReference) authentication.getMetadata().get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY) + ); } else { - verify(apiKeyService).getRoleForApiKey(eq(authentication), anyActionListener()); + verify(apiKeyService).parseRoleDescriptors( + apiKeyId, + (Map) authentication.getMetadata().get(API_KEY_ROLE_DESCRIPTORS_KEY) + ); + verify(apiKeyService).parseRoleDescriptors( + apiKeyId, + (Map) authentication.getMetadata().get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY) + ); } assertThat(role.names().length, is(1)); assertThat(role.names()[0], containsString("user_role_")); @@ -1396,6 +1411,8 @@ public void testGetRolesForRunAs() { // API key run as final String apiKeyId = randomAlphaOfLength(20); + final BytesReference roleDescriptorBytes = new BytesArray("{}"); + final BytesReference limitedByRoleDescriptorBytes = new BytesArray("{\"a\":{\"cluster\":[\"all\"]}}"); final User authenticatedUser1 = new User("authenticated_user"); final Authentication authentication1 = new Authentication( @@ -1404,15 +1421,19 @@ public void testGetRolesForRunAs() { new RealmRef(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8), randomAlphaOfLength(8)), Version.CURRENT, AuthenticationType.API_KEY, - Map.of(API_KEY_ID_KEY, randomAlphaOfLength(20)) - ); - when(apiKeyService.getApiKeyIdAndRoleBytes(eq(authentication1), anyBoolean())).thenReturn( - new Tuple<>(apiKeyId, new BytesArray("{}")) + Map.of( + API_KEY_ID_KEY, + apiKeyId, + API_KEY_ROLE_DESCRIPTORS_KEY, + roleDescriptorBytes, + API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, + limitedByRoleDescriptorBytes + ) ); final PlainActionFuture future1 = new PlainActionFuture<>(); - compositeRolesStore.getRoles(authenticatedUser1, authentication1, future1); + compositeRolesStore.getRole(AuthenticationContext.fromAuthentication(authentication1).getAuthenticatingSubject(), future1); future1.actionGet(); - verify(apiKeyService, times(2)).getApiKeyIdAndRoleBytes(eq(authentication1), anyBoolean()); + verify(apiKeyService).parseRoleDescriptorsBytes(apiKeyId, limitedByRoleDescriptorBytes); // Service account run as final User authenticatedUser2 = new User("elastic/some-service"); @@ -1430,10 +1451,10 @@ public void testGetRolesForRunAs() { final ActionListener listener = (ActionListener) invocation.getArguments()[1]; listener.onResponse(new RoleDescriptor(authenticatedUser2.principal(), null, null, null)); return null; - }).when(serviceAccountService).getRoleDescriptor(eq(authentication2), anyActionListener()); - compositeRolesStore.getRoles(authenticatedUser2, authentication2, future2); + }).when(serviceAccountService).getRoleDescriptorForPrincipal(eq(authenticatedUser2.principal()), anyActionListener()); + compositeRolesStore.getRole(AuthenticationContext.fromAuthentication(authentication2).getAuthenticatingSubject(), future2); future2.actionGet(); - verify(serviceAccountService, times(1)).getRoleDescriptor(eq(authentication2), anyActionListener()); + verify(serviceAccountService).getRoleDescriptorForPrincipal(eq(authenticatedUser2.principal()), anyActionListener()); } public void testUsageStats() { @@ -1528,7 +1549,7 @@ public void testLoggingOfDeprecatedRoles() { ); // Use a LHS so that the random-shufle-order of the list is preserved - compositeRolesStore.logDeprecatedRoles(new LinkedHashSet<>(descriptors)); + compositeRolesStore.getRoleReferenceResolver().logDeprecatedRoles(new LinkedHashSet<>(descriptors)); assertWarnings( "The role [" @@ -1599,15 +1620,13 @@ public void testCacheEntryIsReusedForIdenticalApiKeyRoles() { AuthenticationType.API_KEY, metadata ); - doCallRealMethod().when(apiKeyService).getApiKeyIdAndRoleBytes(eq(authentication), anyBoolean()); PlainActionFuture roleFuture = new PlainActionFuture<>(); - compositeRolesStore.getRoles(authentication.getUser(), authentication, roleFuture); + compositeRolesStore.getRole(AuthenticationContext.fromAuthentication(authentication).getEffectiveSubject(), roleFuture); roleFuture.actionGet(); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); - verify(apiKeyService, times(2)).getApiKeyIdAndRoleBytes(eq(authentication), anyBoolean()); - verify(apiKeyService).parseRoleDescriptors("key-id-1", roleBytes); - verify(apiKeyService).parseRoleDescriptors("key-id-1", limitedByRoleBytes); + verify(apiKeyService).parseRoleDescriptorsBytes("key-id-1", roleBytes); + verify(apiKeyService).parseRoleDescriptorsBytes("key-id-1", limitedByRoleBytes); // Different API key with the same roles should read from cache final Map metadata2 = new HashMap<>(); @@ -1623,13 +1642,11 @@ public void testCacheEntryIsReusedForIdenticalApiKeyRoles() { AuthenticationType.API_KEY, metadata2 ); - doCallRealMethod().when(apiKeyService).getApiKeyIdAndRoleBytes(eq(authentication), anyBoolean()); roleFuture = new PlainActionFuture<>(); - compositeRolesStore.getRoles(authentication.getUser(), authentication, roleFuture); + compositeRolesStore.getRole(AuthenticationContext.fromAuthentication(authentication).getEffectiveSubject(), roleFuture); roleFuture.actionGet(); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); - verify(apiKeyService, times(2)).getApiKeyIdAndRoleBytes(eq(authentication), anyBoolean()); - verify(apiKeyService, never()).parseRoleDescriptors(eq("key-id-2"), any(BytesReference.class)); + verify(apiKeyService, never()).parseRoleDescriptorsBytes(eq("key-id-2"), any(BytesReference.class)); // Different API key with the same limitedBy role should read from cache, new role should be built final BytesArray anotherRoleBytes = new BytesArray("{\"b role\": {\"cluster\": [\"manage_security\"]}}"); @@ -1646,13 +1663,11 @@ public void testCacheEntryIsReusedForIdenticalApiKeyRoles() { AuthenticationType.API_KEY, metadata3 ); - doCallRealMethod().when(apiKeyService).getApiKeyIdAndRoleBytes(eq(authentication), anyBoolean()); roleFuture = new PlainActionFuture<>(); - compositeRolesStore.getRoles(authentication.getUser(), authentication, roleFuture); + compositeRolesStore.getRole(AuthenticationContext.fromAuthentication(authentication).getEffectiveSubject(), roleFuture); roleFuture.actionGet(); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); - verify(apiKeyService).getApiKeyIdAndRoleBytes(eq(authentication), eq(false)); - verify(apiKeyService).parseRoleDescriptors("key-id-3", anotherRoleBytes); + verify(apiKeyService).parseRoleDescriptorsBytes("key-id-3", anotherRoleBytes); } private Authentication createAuthentication() { @@ -1781,6 +1796,12 @@ public void testXpackUserHasClusterPrivileges() { } } + private void getRoleForRoleNames(CompositeRolesStore rolesStore, Collection roleNames, ActionListener listener) { + final Subject subject = mock(Subject.class); + when(subject.getRoleReferences(any())).thenReturn(List.of(new RoleReference.NamedRoleReference(roleNames.toArray(String[]::new)))); + rolesStore.getRole(subject, listener); + } + private Role getXPackUserRole() { CompositeRolesStore compositeRolesStore = buildCompositeRolesStore( SECURITY_ENABLED_SETTINGS,