diff --git a/docs/changelog/82639.yaml b/docs/changelog/82639.yaml new file mode 100644 index 0000000000000..ecc9acaabd54e --- /dev/null +++ b/docs/changelog/82639.yaml @@ -0,0 +1,5 @@ +pr: 82639 +summary: Authentication under domains +area: Authentication +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java index a7c9071ef9c25..58a12017af6b6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java @@ -10,41 +10,29 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; -import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext; -import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Nullable; import org.elasticsearch.node.Node; -import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType; -import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; import org.elasticsearch.xpack.core.security.authc.support.SecondaryAuthentication; +import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; import java.io.UncheckedIOException; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; -import static org.elasticsearch.xpack.core.security.authc.Authentication.VERSION_API_KEY_ROLES_AS_BYTES; -import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_NAME; -import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_TYPE; - /** * A lightweight utility that can find the current user and authentication information for the local thread. */ public class SecurityContext { - private final Logger logger = LogManager.getLogger(SecurityContext.class); + private static final Logger logger = LogManager.getLogger(SecurityContext.class); private final ThreadContext threadContext; private final AuthenticationContextSerializer authenticationSerializer; @@ -107,41 +95,32 @@ public ThreadContext getThreadContext() { * Sets the user forcefully to the provided user. There must not be an existing user in the ThreadContext otherwise an exception * will be thrown. This method is package private for testing. */ - public void setUser(User user, Version version) { - Objects.requireNonNull(user); - final Authentication.RealmRef authenticatedBy = new Authentication.RealmRef(ATTACH_REALM_NAME, ATTACH_REALM_TYPE, nodeName); - final Authentication.RealmRef lookedUpBy; - if (user.isRunAs()) { - lookedUpBy = authenticatedBy; - } else { - lookedUpBy = null; - } - setAuthentication( - new Authentication(user, authenticatedBy, lookedUpBy, version, AuthenticationType.INTERNAL, Collections.emptyMap()) - ); - } - - /** Writes the authentication to the thread context */ - private void setAuthentication(Authentication authentication) { - try { - authentication.writeToContext(threadContext); - } catch (IOException e) { - throw new AssertionError("how can we have a IOException with a user we set", e); - } + public void setInternalUser(User internalUser, Version version) { + assert User.isInternal(internalUser); + setAuthentication(Authentication.newInternalAuthentication(internalUser, version, nodeName)); } /** * Runs the consumer in a new context as the provided user. The original context is provided to the consumer. When this method * returns, the original context is restored. */ - public void executeAsUser(User user, Consumer consumer, Version version) { + public void executeAsInternalUser(User internalUser, Version version, Consumer consumer) { + assert User.isInternal(internalUser); final StoredContext original = threadContext.newStoredContext(true); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { - setUser(user, version); + setInternalUser(internalUser, version); consumer.accept(original); } } + public void executeAsSystemUser(Consumer consumer) { + executeAsSystemUser(Version.CURRENT, consumer); + } + + public void executeAsSystemUser(Version version, Consumer consumer) { + executeAsInternalUser(SystemUser.INSTANCE, version, consumer); + } + /** * Runs the consumer in a new context as the provided user. The original context is provided to the consumer. When this method * returns, the original context is restored. @@ -164,16 +143,7 @@ public void executeAfterRewritingAuthentication(Consumer consumer final StoredContext original = threadContext.newStoredContext(true); final Authentication authentication = getAuthentication(); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { - setAuthentication( - new Authentication( - authentication.getUser(), - authentication.getAuthenticatedBy(), - authentication.getLookedUpBy(), - version, - authentication.getAuthenticationType(), - rewriteMetadataForApiKeyRoleDescriptors(version, authentication) - ) - ); + setAuthentication(authentication.maybeRewriteForOlderVersion(version)); existingRequestHeaders.forEach((k, v) -> { if (threadContext.getHeader(k) == null) { threadContext.putHeader(k, v); @@ -183,54 +153,12 @@ public void executeAfterRewritingAuthentication(Consumer consumer } } - @SuppressWarnings("unchecked") - private Map rewriteMetadataForApiKeyRoleDescriptors(Version streamVersion, Authentication authentication) { - Map metadata = authentication.getMetadata(); - // If authentication type is API key, regardless whether it has run-as, the metadata must contain API key role descriptors - if (authentication.isAuthenticatedWithApiKey()) { - if (authentication.getVersion().onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES) - && streamVersion.before(VERSION_API_KEY_ROLES_AS_BYTES)) { - metadata = new HashMap<>(metadata); - metadata.put( - AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, - convertRoleDescriptorsBytesToMap((BytesReference) metadata.get(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY)) - ); - metadata.put( - AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, - convertRoleDescriptorsBytesToMap( - (BytesReference) metadata.get(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY) - ) - ); - } else if (authentication.getVersion().before(VERSION_API_KEY_ROLES_AS_BYTES) - && streamVersion.onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES)) { - metadata = new HashMap<>(metadata); - metadata.put( - AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, - convertRoleDescriptorsMapToBytes( - (Map) metadata.get(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY) - ) - ); - metadata.put( - AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, - convertRoleDescriptorsMapToBytes( - (Map) metadata.get(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY) - ) - ); - } - } - return metadata; - } - - private Map convertRoleDescriptorsBytesToMap(BytesReference roleDescriptorsBytes) { - return XContentHelper.convertToMap(roleDescriptorsBytes, false, XContentType.JSON).v2(); - } - - private BytesReference convertRoleDescriptorsMapToBytes(Map roleDescriptorsMap) { - try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { - builder.map(roleDescriptorsMap); - return BytesReference.bytes(builder); + /** Writes the authentication to the thread context */ + private void setAuthentication(Authentication authentication) { + try { + authentication.writeToContext(threadContext); } catch (IOException e) { - throw new UncheckedIOException(e); + throw new AssertionError("how can we have a IOException with a user we set", e); } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index e47cd0bc8d3c8..edc01bfe27211 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -6,38 +6,59 @@ */ package org.elasticsearch.xpack.core.security.authc; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.Assertions; import org.elasticsearch.Version; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings; import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; +import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.InternalUserSerializationHelper; import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Base64; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; -import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newAnonymousRealmRef; +import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newApiKeyRealmRef; +import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newInternalAttachRealmRef; +import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newInternalFallbackRealmRef; +import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newServiceAccountRealmRef; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_NAME; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_TYPE; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_NAME; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_TYPE; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_NAME; +import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_TYPE; // TODO(hub-cap) Clean this up after moving User over - This class can re-inherit its field AUTHENTICATION_KEY in AuthenticationField. // That interface can be removed public class Authentication implements ToXContentObject { + private static final Logger logger = LogManager.getLogger(Authentication.class); + public static final Version VERSION_API_KEY_ROLES_AS_BYTES = Version.V_7_9_0; + public static final Version VERSION_REALM_DOMAINS = Version.V_8_2_0; private final User user; private final RealmRef authenticatedBy; @@ -47,11 +68,7 @@ public class Authentication implements ToXContentObject { private final Map metadata; // authentication contains metadata, includes api_key details (including api_key metadata) public Authentication(User user, RealmRef authenticatedBy, RealmRef lookedUpBy) { - this(user, authenticatedBy, lookedUpBy, Version.CURRENT); - } - - public Authentication(User user, RealmRef authenticatedBy, RealmRef lookedUpBy, Version version) { - this(user, authenticatedBy, lookedUpBy, version, AuthenticationType.REALM, Collections.emptyMap()); + this(user, authenticatedBy, lookedUpBy, Version.CURRENT, AuthenticationType.REALM, Collections.emptyMap()); } public Authentication( @@ -69,6 +86,7 @@ public Authentication( this.type = type; this.metadata = metadata; this.assertApiKeyMetadata(); + this.assertDomainAssignment(); } public Authentication(StreamInput in) throws IOException { @@ -83,6 +101,7 @@ public Authentication(StreamInput in) throws IOException { type = AuthenticationType.values()[in.readVInt()]; metadata = in.readMap(); this.assertApiKeyMetadata(); + this.assertDomainAssignment(); } public User getUser() { @@ -105,6 +124,14 @@ public RealmRef getSourceRealm() { return lookedUpBy == null ? authenticatedBy : lookedUpBy; } + /** + * Returns the authentication version. + * Nodes can only interpret authentications from current or older versions as the node's. + * + * Authentication is serialized and travels across the cluster nodes as the sub-requests are handled, + * and can also be cached by long-running jobs that continue to act on behalf of the user, beyond + * the lifetime of the original request. + */ public Version getVersion() { return version; } @@ -117,6 +144,81 @@ public Map getMetadata() { return metadata; } + /** + * Returns a new {@code Authentication}, like this one, but which is compatible with older version nodes. + * This is commonly employed when the {@code Authentication} is serialized across cluster nodes with mixed versions. + */ + public Authentication maybeRewriteForOlderVersion(Version olderVersion) { + // TODO how can this not be true + // assert olderVersion.onOrBefore(getVersion()); + Authentication newAuthentication = new Authentication( + getUser(), + maybeRewriteRealmRef(olderVersion, getAuthenticatedBy()), + maybeRewriteRealmRef(olderVersion, getLookedUpBy()), + olderVersion, + getAuthenticationType(), + maybeRewriteMetadataForApiKeyRoleDescriptors(olderVersion, this) + ); + if (isAssignedToDomain() && false == newAuthentication.isAssignedToDomain()) { + logger.info("Rewriting authentication [" + this + "] without domain"); + } + return newAuthentication; + } + + /** + * Returns a new {@code Authentication} that reflects a "run as another user" action under the current {@code Authentication}. + * The security {@code RealmRef#Domain} of the resulting {@code Authentication} is that of the run-as user's realm. + */ + public Authentication runAs(User runAs, @Nullable RealmRef lookupRealmRef) { + Objects.requireNonNull(runAs); + assert false == runAs.isRunAs(); + assert false == getUser().isRunAs(); + return new Authentication( + new User(runAs, getUser()), + getAuthenticatedBy(), + lookupRealmRef, + getVersion(), + getAuthenticationType(), + getMetadata() + ); + } + + /** Returns a new {@code Authentication} for tokens created by the current {@code Authentication}, which is used when + * authenticating using the token credential. + */ + public Authentication token() { + assert false == isServiceAccount(); + final Authentication newTokenAuthentication = new Authentication( + getUser(), + getAuthenticatedBy(), + getLookedUpBy(), + Version.CURRENT, + AuthenticationType.TOKEN, + getMetadata() + ); + assert Objects.equals(getDomain(), newTokenAuthentication.getDomain()); + return newTokenAuthentication; + } + + /** + * Returns {@code true} if the effective user belongs to a realm under a domain. + * See also {@link #getDomain()} and {@link #getSourceRealm()}. + */ + public boolean isAssignedToDomain() { + return getDomain() != null; + } + + /** + * Returns the {@link RealmDomain} that the effective user belongs to. + * A user belongs to a realm which in turn belongs to a domain. + * + * The same username can be authenticated by different realms (e.g. with different credential types), + * but resources created across realms cannot be accessed unless the realms are also part of the same domain. + */ + public @Nullable RealmDomain getDomain() { + return getSourceRealm().getDomain(); + } + public boolean isAuthenticatedWithServiceAccount() { return ServiceAccountSettings.REALM_TYPE.equals(getAuthenticatedBy().getType()); } @@ -125,6 +227,14 @@ public boolean isAuthenticatedWithApiKey() { return AuthenticationType.API_KEY.equals(getAuthenticationType()); } + public boolean isAuthenticatedAnonymously() { + return AuthenticationType.ANONYMOUS.equals(getAuthenticationType()); + } + + public boolean isAuthenticatedInternally() { + return AuthenticationType.INTERNAL.equals(getAuthenticationType()); + } + /** * Authenticate with a service account and no run-as */ @@ -268,14 +378,25 @@ public void toXContentFragment(XContentBuilder builder) throws IOException { builder.startObject(User.Fields.AUTHENTICATION_REALM.getPreferredName()); builder.field(User.Fields.REALM_NAME.getPreferredName(), getAuthenticatedBy().getName()); builder.field(User.Fields.REALM_TYPE.getPreferredName(), getAuthenticatedBy().getType()); + // domain name is generally ambiguous, because it can change during the lifetime of the authentication, + // but it is good enough for display purposes (including auditing) + if (getAuthenticatedBy().getDomain() != null) { + builder.field(User.Fields.REALM_DOMAIN.getPreferredName(), getAuthenticatedBy().getDomain().name()); + } builder.endObject(); builder.startObject(User.Fields.LOOKUP_REALM.getPreferredName()); if (getLookedUpBy() != null) { builder.field(User.Fields.REALM_NAME.getPreferredName(), getLookedUpBy().getName()); builder.field(User.Fields.REALM_TYPE.getPreferredName(), getLookedUpBy().getType()); + if (getLookedUpBy().getDomain() != null) { + builder.field(User.Fields.REALM_DOMAIN.getPreferredName(), getLookedUpBy().getDomain().name()); + } } else { builder.field(User.Fields.REALM_NAME.getPreferredName(), getAuthenticatedBy().getName()); builder.field(User.Fields.REALM_TYPE.getPreferredName(), getAuthenticatedBy().getType()); + if (getAuthenticatedBy().getDomain() != null) { + builder.field(User.Fields.REALM_DOMAIN.getPreferredName(), getAuthenticatedBy().getDomain().name()); + } } builder.endObject(); builder.field(User.Fields.AUTHENTICATION_TYPE.getPreferredName(), getAuthenticationType().name().toLowerCase(Locale.ROOT)); @@ -296,6 +417,17 @@ private void assertApiKeyMetadata() { : "API KEY authentication requires metadata to contain API KEY id, and the value must be non-null."; } + private void assertDomainAssignment() { + if (Assertions.ENABLED) { + if (isAssignedToDomain()) { + assert false == isApiKey(); + assert false == isServiceAccount(); + assert false == isAuthenticatedAnonymously(); + assert false == isAuthenticatedInternally(); + } + } + } + @Override public String toString() { StringBuilder builder = new StringBuilder("Authentication[").append(user) @@ -310,28 +442,43 @@ public String toString() { return builder.toString(); } - public static class RealmRef { + public static class RealmRef implements Writeable { private final String nodeName; private final String name; private final String type; + private final @Nullable RealmDomain domain; public RealmRef(String name, String type, String nodeName) { - this.nodeName = nodeName; - this.name = name; - this.type = type; + this(name, type, nodeName, null); + } + + public RealmRef(String name, String type, String nodeName, @Nullable RealmDomain domain) { + this.nodeName = Objects.requireNonNull(nodeName, "node name cannot be null"); + this.name = Objects.requireNonNull(name, "realm name cannot be null"); + this.type = Objects.requireNonNull(type, "realm type cannot be null"); + this.domain = domain; } public RealmRef(StreamInput in) throws IOException { this.nodeName = in.readString(); this.name = in.readString(); this.type = in.readString(); + if (in.getVersion().onOrAfter(VERSION_REALM_DOMAINS)) { + this.domain = in.readOptionalWriteable(RealmDomain::readFrom); + } else { + this.domain = null; + } } - void writeTo(StreamOutput out) throws IOException { + @Override + public void writeTo(StreamOutput out) throws IOException { out.writeString(nodeName); out.writeString(name); out.writeString(type); + if (out.getVersion().onOrAfter(VERSION_REALM_DOMAINS)) { + out.writeOptionalWriteable(domain); + } } public String getNodeName() { @@ -346,6 +493,14 @@ public String getType() { return type; } + /** + * Returns the domain assignment for the realm, if one assigned, or {@code null} otherwise, as per the + * {@code RealmSettings#DOMAIN_TO_REALM_ASSOC_SETTING} setting. + */ + public @Nullable RealmDomain getDomain() { + return domain; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -354,8 +509,8 @@ public boolean equals(Object o) { RealmRef realmRef = (RealmRef) o; if (nodeName.equals(realmRef.nodeName) == false) return false; - if (name.equals(realmRef.name) == false) return false; - return type.equals(realmRef.type); + if (type.equals(realmRef.type) == false) return false; + return Objects.equals(domain, realmRef.domain); } @Override @@ -363,25 +518,203 @@ public int hashCode() { int result = nodeName.hashCode(); result = 31 * result + name.hashCode(); result = 31 * result + type.hashCode(); + if (domain != null) { + result = 31 * result + domain.hashCode(); + } return result; } @Override public String toString() { - return "{Realm[" + type + "." + name + "] on Node[" + nodeName + "]}"; + if (domain != null) { + return "{Realm[" + type + "." + name + "] under Domain[" + domain.name() + "] on Node[" + nodeName + "]}"; + } else { + return "{Realm[" + type + "." + name + "] on Node[" + nodeName + "]}"; + } + } + + static RealmRef newInternalAttachRealmRef(String nodeName) { + // the "attach" internal realm is not part of any realm domain + return new Authentication.RealmRef(ATTACH_REALM_NAME, ATTACH_REALM_TYPE, nodeName, null); + } + + static RealmRef newInternalFallbackRealmRef(String nodeName) { + // the "fallback" internal realm is not part of any realm domain + RealmRef realmRef = new RealmRef(FALLBACK_REALM_NAME, FALLBACK_REALM_TYPE, nodeName, null); + return realmRef; + } + + static RealmRef newAnonymousRealmRef(String nodeName) { + // the "anonymous" internal realm is not part of any realm domain + return new Authentication.RealmRef(ANONYMOUS_REALM_NAME, ANONYMOUS_REALM_TYPE, nodeName, null); + } + + static RealmRef newServiceAccountRealmRef(String nodeName) { + // no domain for service account tokens + return new Authentication.RealmRef(ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, nodeName, null); + } + + static RealmRef newApiKeyRealmRef(String nodeName) { + // no domain for API Key tokens + return new RealmRef(AuthenticationField.API_KEY_REALM_NAME, AuthenticationField.API_KEY_REALM_TYPE, nodeName, null); + } + } + + // TODO is a newer version than the node's a valid value? + public static Authentication newInternalAuthentication(User internalUser, Version version, String nodeName) { + // TODO create a system user class, so that the type system guarantees that this is only invoked for internal users + assert User.isInternal(internalUser); + final Authentication.RealmRef authenticatedBy = newInternalAttachRealmRef(nodeName); + Authentication authentication = new Authentication( + internalUser, + authenticatedBy, + null, + version, + AuthenticationType.INTERNAL, + Collections.emptyMap() + ); + assert false == authentication.isAssignedToDomain(); + return authentication; + } + + public static Authentication newInternalFallbackAuthentication(User fallbackUser, String nodeName) { + // TODO assert SystemUser.is(fallbackUser); + final Authentication.RealmRef authenticatedBy = newInternalFallbackRealmRef(nodeName); + Authentication authentication = new Authentication( + fallbackUser, + authenticatedBy, + null, + Version.CURRENT, + Authentication.AuthenticationType.INTERNAL, + Collections.emptyMap() + ); + assert false == authentication.isAssignedToDomain(); + return authentication; + } + + public static Authentication newAnonymousAuthentication(AnonymousUser anonymousUser, String nodeName) { + final Authentication.RealmRef authenticatedBy = newAnonymousRealmRef(nodeName); + Authentication authentication = new Authentication( + anonymousUser, + authenticatedBy, + null, + Version.CURRENT, + Authentication.AuthenticationType.ANONYMOUS, + Collections.emptyMap() + ); + assert false == authentication.isAssignedToDomain(); + return authentication; + } + + public static Authentication newServiceAccountAuthentication(User serviceAccountUser, String nodeName, Map metadata) { + // TODO make the service account user a separate class/interface + assert false == serviceAccountUser.isRunAs(); + final Authentication.RealmRef authenticatedBy = newServiceAccountRealmRef(nodeName); + Authentication authentication = new Authentication( + serviceAccountUser, + authenticatedBy, + null, + Version.CURRENT, + AuthenticationType.TOKEN, + metadata + ); + assert false == authentication.isAssignedToDomain(); + return authentication; + } + + public static Authentication newRealmAuthentication(User user, RealmRef realmRef) { + // TODO make the type system ensure that this is not a run-as user + assert false == user.isRunAs(); + Authentication authentication = new Authentication(user, realmRef, null, Version.CURRENT, AuthenticationType.REALM, Map.of()); + assert false == authentication.isServiceAccount(); + assert false == authentication.isApiKey(); + assert false == authentication.isAuthenticatedInternally(); + assert false == authentication.isAuthenticatedAnonymously(); + return authentication; + } + + public static Authentication newApiKeyAuthentication(AuthenticationResult authResult, String nodeName) { + assert authResult.isAuthenticated() : "API Key authn result must be successful"; + final User apiKeyUser = authResult.getValue(); + assert false == apiKeyUser.isRunAs(); + final Authentication.RealmRef authenticatedBy = newApiKeyRealmRef(nodeName); + Authentication authentication = new Authentication( + apiKeyUser, + authenticatedBy, + null, + Version.CURRENT, + AuthenticationType.API_KEY, + authResult.getMetadata() + ); + assert false == authentication.isAssignedToDomain(); + return authentication; + } + + private static RealmRef maybeRewriteRealmRef(Version streamVersion, RealmRef realmRef) { + if (realmRef != null && realmRef.getDomain() != null && streamVersion.before(VERSION_REALM_DOMAINS)) { + // security domain erasure + new RealmRef(realmRef.getName(), realmRef.getType(), realmRef.getNodeName(), null); + } + return realmRef; + } + + @SuppressWarnings("unchecked") + private static Map maybeRewriteMetadataForApiKeyRoleDescriptors(Version streamVersion, Authentication authentication) { + Map metadata = authentication.getMetadata(); + // If authentication type is API key, regardless whether it has run-as, the metadata must contain API key role descriptors + if (authentication.isAuthenticatedWithApiKey()) { + if (authentication.getVersion().onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES) + && streamVersion.before(VERSION_API_KEY_ROLES_AS_BYTES)) { + metadata = new HashMap<>(metadata); + if (metadata.containsKey(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY)) { + metadata.put( + AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, + convertRoleDescriptorsBytesToMap((BytesReference) metadata.get(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY)) + ); + } + if (metadata.containsKey(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)) { + metadata.put( + AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, + convertRoleDescriptorsBytesToMap( + (BytesReference) metadata.get(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY) + ) + ); + } + } else if (authentication.getVersion().before(VERSION_API_KEY_ROLES_AS_BYTES) + && streamVersion.onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES)) { + metadata = new HashMap<>(metadata); + if (metadata.containsKey(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY)) { + metadata.put( + AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY, + convertRoleDescriptorsMapToBytes( + (Map) metadata.get(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY) + ) + ); + } + if (metadata.containsKey(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)) { + metadata.put( + AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, + convertRoleDescriptorsMapToBytes( + (Map) metadata.get(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY) + ) + ); + } + } } + return metadata; } - public static ConstructingObjectParser REALM_REF_PARSER = new ConstructingObjectParser<>( - "realm_ref", - false, - (args, v) -> new RealmRef((String) args[0], (String) args[1], (String) args[2]) - ); + private static Map convertRoleDescriptorsBytesToMap(BytesReference roleDescriptorsBytes) { + return XContentHelper.convertToMap(roleDescriptorsBytes, false, XContentType.JSON).v2(); + } - static { - REALM_REF_PARSER.declareString(constructorArg(), new ParseField("name")); - REALM_REF_PARSER.declareString(constructorArg(), new ParseField("type")); - REALM_REF_PARSER.declareString(constructorArg(), new ParseField("node_name")); + private static BytesReference convertRoleDescriptorsMapToBytes(Map roleDescriptorsMap) { + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.map(roleDescriptorsMap); + return BytesReference.bytes(builder); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } public enum AuthenticationType { 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 index a99b3f740d639..f9a5ca926170e 100644 --- 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 @@ -48,17 +48,6 @@ 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()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java index 9761605e92cfe..398ce6962dcbc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java @@ -8,10 +8,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Nullable; import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.node.Node; import org.elasticsearch.xpack.core.XPackField; +import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; import org.elasticsearch.xpack.core.security.user.User; @@ -29,7 +33,8 @@ public abstract class Realm implements Comparable { protected final Logger logger = LogManager.getLogger(getClass()); - protected RealmConfig config; + protected final RealmConfig config; + private final SetOnce realmRef = new SetOnce<>(); public Realm(RealmConfig config) { this.config = config; @@ -53,11 +58,7 @@ public String name() { * @return The order of this realm within the executing realm chain. */ public int order() { - return config.order; - } - - public String domain() { - return config.domain(); + return config.order(); } /** @@ -75,11 +76,11 @@ public Map> getAuthenticationFailureHeaders() { } @Override - public int compareTo(Realm other) { - int result = Integer.compare(config.order, other.config.order); + public final int compareTo(Realm other) { + int result = Integer.compare(order(), other.order()); if (result == 0) { // If same order, compare based on the realm name - result = config.name().compareTo(other.config.name()); + result = name().compareTo(other.name()); } return result; } @@ -144,13 +145,30 @@ public void usageStats(ActionListener> listener) { listener.onResponse(stats); } + public void initRealmRef(@Nullable RealmDomain domain) { + final String nodeName = Node.NODE_NAME_SETTING.get(config.settings()); + this.realmRef.set(new RealmRef(config.name(), config.type(), nodeName, domain)); + } + + public RealmRef realmRef() { + RealmRef realmRef = this.realmRef.get(); + if (realmRef == null) { + throw new IllegalStateException("Realm [" + this + "] not fully configured"); + } + return realmRef; + } + @Override public String toString() { - return config.type() + "/" + config.name(); + if (realmRef.get() != null && realmRef.get().getDomain() != null) { + return config.type() + "/" + config.name() + "/" + realmRef.get().getDomain().name(); + } else { + return config.type() + "/" + config.name(); + } } /** - * This is no-op in the base class, but allows realms to be aware of what other realms are configured + * This allows realms to be aware of what other realms are configured. * * @see DelegatedAuthorizationSettings */ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmConfig.java index fba9947cbcb8a..017c87ae983eb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmConfig.java @@ -6,12 +6,15 @@ */ package org.elasticsearch.xpack.core.security.authc; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.core.Nullable; import org.elasticsearch.env.Environment; +import java.io.IOException; import java.util.Objects; import java.util.function.Function; import java.util.function.Supplier; @@ -19,7 +22,6 @@ public class RealmConfig { final RealmIdentifier identifier; - final @Nullable String domain; final boolean enabled; final int order; private final Environment env; @@ -28,7 +30,6 @@ public class RealmConfig { public RealmConfig(RealmIdentifier identifier, Settings settings, Environment env, ThreadContext threadContext) { this.identifier = identifier; - this.domain = RealmSettings.getDomainForRealm(settings, identifier); this.settings = settings; this.env = env; this.threadContext = threadContext; @@ -49,10 +50,6 @@ public RealmIdentifier identifier() { return identifier; } - public @Nullable String domain() { - return domain; - } - public String name() { return identifier.name; } @@ -186,7 +183,7 @@ public boolean hasSetting(Setting.AffixSetting setting) { * (e.g. {@code xpack.security.authc.realms.native.native_realm.order}), it is often necessary to be able to * pass this pair of variables as a single type (e.g. in method parameters, or return values). */ - public static class RealmIdentifier { + public static class RealmIdentifier implements Writeable { private final String type; private final String name; @@ -195,6 +192,11 @@ public RealmIdentifier(String type, String name) { this.name = Objects.requireNonNull(name, "Realm name cannot be null"); } + public RealmIdentifier(StreamInput in) throws IOException { + this.type = in.readString(); + this.name = in.readString(); + } + public String getType() { return type; } @@ -227,5 +229,11 @@ public int hashCode() { public String toString() { return type + '/' + name; } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(type); + out.writeString(name); + } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmDomain.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmDomain.java new file mode 100644 index 0000000000000..14d044de9c343 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmDomain.java @@ -0,0 +1,30 @@ +/* + * 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.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; + +import java.io.IOException; +import java.util.Set; + +public record RealmDomain(String name, Set realms) implements Writeable { + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeCollection(realms); + } + + static RealmDomain readFrom(StreamInput in) throws IOException { + String domainName = in.readString(); + Set realms = in.readSet(RealmConfig.RealmIdentifier::new); + return new RealmDomain(domainName, realms); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java index b0e65725e7c11..0d04da1c230a2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RealmSettings.java @@ -11,15 +11,13 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsException; -import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings; import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -27,13 +25,6 @@ import java.util.function.Function; import java.util.stream.Collectors; -import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_NAME; -import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_TYPE; -import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_NAME; -import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_TYPE; -import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_NAME; -import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_TYPE; - /** * Provides a number of utility methods for interacting with {@link Settings} and {@link Setting} inside a {@link Realm}. * Settings for realms use an {@link Setting#affixKeySetting(String, String, Function, Setting.AffixSettingDependency[]) affix} style, @@ -125,41 +116,11 @@ public static Map getRealmSettings(Settin } /** - * Returns the domain name that the given realm is assigned to. - * Assumes {@code #verifyRealmNameToDomainNameAssociation} successfully verified the configuration. - */ - public static @Nullable String getDomainForRealm(Settings globalSettings, RealmConfig.RealmIdentifier realmIdentifier) { - // TODO reserved realm settings need to be pulled into core - if (realmIdentifier.equals(new RealmConfig.RealmIdentifier("reserved", "reserved")) - || realmIdentifier.equals( - new RealmConfig.RealmIdentifier(AuthenticationField.API_KEY_REALM_NAME, AuthenticationField.API_KEY_REALM_TYPE) - ) - || realmIdentifier.equals(new RealmConfig.RealmIdentifier(FALLBACK_REALM_NAME, FALLBACK_REALM_TYPE)) - || realmIdentifier.equals(new RealmConfig.RealmIdentifier(ANONYMOUS_REALM_NAME, ANONYMOUS_REALM_TYPE)) - || realmIdentifier.equals(new RealmConfig.RealmIdentifier(ATTACH_REALM_NAME, ATTACH_REALM_TYPE)) - || realmIdentifier.equals( - new RealmConfig.RealmIdentifier(ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE) - )) { - return null; - } - // file and native realms can be referred to by their default names too - for (String domainName : DOMAIN_TO_REALM_ASSOC_SETTING.getNamespaces(globalSettings)) { - Setting> realmsByDomainSetting = DOMAIN_TO_REALM_ASSOC_SETTING.getConcreteSettingForNamespace(domainName); - if (realmsByDomainSetting.get(globalSettings).contains(realmIdentifier.getName())) { - return domainName; - } - } - return null; - } - - /** - * Verifies that realms are assigned to at most one domain and that domains do not refer to undefined realms. - * Must be invoked once on node start-up (and usually not by cmd line tools). + * Computes the realm name to domain name association. + * Also verifies that realms are assigned to at most one domain and that domains do not refer to undefined realms. */ - public static void verifyRealmNameToDomainNameAssociation( - Settings globalSettings, - Collection allRealmIdentifiers - ) { + public static Map computeRealmNameToDomainNameAssociation(Settings globalSettings) { + final Set allRealmIdentifiers = RealmSettings.getRealmSettings(globalSettings).keySet(); final Map> realmToDomainsMap = new HashMap<>(); for (String domainName : DOMAIN_TO_REALM_ASSOC_SETTING.getNamespaces(globalSettings)) { if (domainName.startsWith(RESERVED_REALM_AND_DOMAIN_NAME_PREFIX)) { @@ -194,8 +155,9 @@ public static void verifyRealmNameToDomainNameAssociation( // default file and native realm names can be used in domain association boolean fileRealmConfigured = false; boolean nativeRealmConfigured = false; + Set unknownRealms = new HashSet<>(realmToDomainsMap.keySet()); for (RealmConfig.RealmIdentifier identifier : allRealmIdentifiers) { - realmToDomainsMap.remove(identifier.getName()); + unknownRealms.remove(identifier.getName()); if (identifier.getType().equals(FileRealmSettings.TYPE)) { fileRealmConfigured = true; } @@ -204,18 +166,21 @@ public static void verifyRealmNameToDomainNameAssociation( } } if (false == fileRealmConfigured) { - realmToDomainsMap.remove(FileRealmSettings.DEFAULT_NAME); + unknownRealms.remove(FileRealmSettings.DEFAULT_NAME); } if (false == nativeRealmConfigured) { - realmToDomainsMap.remove(NativeRealmSettings.DEFAULT_NAME); + unknownRealms.remove(NativeRealmSettings.DEFAULT_NAME); } // verify that domain assignment does not refer to unknown realms - if (false == realmToDomainsMap.isEmpty()) { - final StringBuilder undefinedRealmsErrorMessageBuilder = new StringBuilder("Undefined realms ").append( - realmToDomainsMap.keySet() - ).append(" cannot be assigned to domains"); + if (false == unknownRealms.isEmpty()) { + final StringBuilder undefinedRealmsErrorMessageBuilder = new StringBuilder("Undefined realms ").append(unknownRealms) + .append(" cannot be assigned to domains"); throw new IllegalArgumentException(undefinedRealmsErrorMessageBuilder.toString()); } + return realmToDomainsMap.entrySet() + .stream() + .map(e -> Map.entry(e.getKey(), e.getValue().stream().findAny().get())) + .collect(Collectors.toUnmodifiableMap(e -> e.getKey(), e -> e.getValue())); } /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/AsyncSearchUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/AsyncSearchUser.java index 48633e5a56bb4..9153d580f4e52 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/AsyncSearchUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/AsyncSearchUser.java @@ -33,6 +33,11 @@ public class AsyncSearchUser extends User { private AsyncSearchUser() { super(NAME, ROLE_NAME); + // the following traits, and especially the run-as one, go with all the internal users + // TODO abstract in a base `InternalUser` class + assert false == isRunAs() : "cannot run-as the system user"; + assert enabled(); + assert roles() != null && roles().length == 1; } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SystemUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SystemUser.java index bc2f99562b8e1..a4eafb640b693 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SystemUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SystemUser.java @@ -24,6 +24,11 @@ public class SystemUser extends User { private SystemUser() { super(NAME, ROLE_NAME); + // the following traits, and especially the run-as one, go with all the internal users + // TODO abstract in a base `InternalUser` class + assert false == isRunAs() : "cannot run-as the system user"; + assert enabled(); + assert roles() != null && roles().length == 1; } @Override 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 e3f1bf4530fb9..ce306396bbe85 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 @@ -263,6 +263,7 @@ public interface Fields { ParseField LOOKUP_REALM = new ParseField("lookup_realm"); ParseField REALM_TYPE = new ParseField("type"); ParseField REALM_NAME = new ParseField("name"); + ParseField REALM_DOMAIN = new ParseField("domain"); ParseField AUTHENTICATION_TYPE = new ParseField("authentication_type"); ParseField TOKEN = new ParseField("token"); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackSecurityUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackSecurityUser.java index f331aedf6a7ca..4787e613536d7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackSecurityUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackSecurityUser.java @@ -33,6 +33,11 @@ public class XPackSecurityUser extends User { private XPackSecurityUser() { super(NAME, ROLE_NAME); + // the following traits, and especially the run-as one, go with all the internal users + // TODO abstract in a base `InternalUser` class + assert false == isRunAs() : "cannot run-as the system user"; + assert enabled(); + assert roles() != null && roles().length == 1; } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackUser.java index 6f7f5c9ee92f0..bcc80e05043f4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackUser.java @@ -34,6 +34,11 @@ public class XPackUser extends User { private XPackUser() { super(NAME, ROLE_NAME); + // the following traits, and especially the run-as one, go with all the internal users + // TODO abstract in a base `InternalUser` class + assert false == isRunAs() : "cannot run-as the system user"; + assert enabled(); + assert roles() != null && roles().length == 1; } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java index c9076ea01f7f9..e4358a484566d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java @@ -8,6 +8,9 @@ package org.elasticsearch.xpack.core.security.authc; import org.elasticsearch.Version; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.xpack.core.security.action.service.TokenInfo; @@ -15,8 +18,18 @@ import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; +import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; +import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; +import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; +import org.elasticsearch.xpack.core.security.authc.oidc.OpenIdConnectRealmSettings; +import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings; +import org.elasticsearch.xpack.core.security.user.AnonymousUser; +import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; +import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; +import org.elasticsearch.xpack.core.security.user.XPackUser; import java.util.Arrays; import java.util.EnumSet; @@ -24,9 +37,11 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; public class AuthenticationTests extends ESTestCase { @@ -48,13 +63,14 @@ public void testWillGetAuthenticateByWhenLookupIsNull() { public void testCanAccessResourcesOf() { // Same user is the same final User user1 = randomUser(); - final RealmRef realm1 = randomRealm(); + final RealmRef realm1 = randomRealmRef(false); checkCanAccessResources(randomAuthentication(user1, realm1), randomAuthentication(user1, realm1)); // Different username is different no matter which realm it is from final User user2 = randomValueOtherThanMany(u -> u.principal().equals(user1.principal()), AuthenticationTests::randomUser); // user 2 can be from either the same realm or a different realm - final RealmRef realm2 = randomFrom(realm1, randomRealm()); + final RealmRef realm2 = randomFrom(realm1, randomRealmRef(false)); + assertCannotAccessResources(randomAuthentication(user1, realm2), randomAuthentication(user2, realm2)); // Same username but different realm is different @@ -96,7 +112,6 @@ public void testCanAccessResourcesOf() { randomApiKeyAuthentication(randomFrom(user1, user2), apiKeyId1), randomApiKeyAuthentication(randomFrom(user1, user2), apiKeyId2) ); - final User user3 = randomValueOtherThanMany( u -> u.principal().equals(user1.principal()) || u.principal().equals(user2.principal()), AuthenticationTests::randomUser @@ -104,18 +119,18 @@ public void testCanAccessResourcesOf() { // Same API key but run-as different user are not the same owner assertCannotAccessResources( - randomApiKeyAuthentication(new User(user2, user1), apiKeyId1), - randomApiKeyAuthentication(new User(user3, user1), apiKeyId1) + randomApiKeyAuthentication(user1, apiKeyId1).runAs(user2, realm2), + randomApiKeyAuthentication(user1, apiKeyId1).runAs(user3, realm2) ); // Same or different API key run-as the same user are the same owner checkCanAccessResources( - randomApiKeyAuthentication(new User(user3, user1), apiKeyId1), - randomApiKeyAuthentication(new User(user3, user1), apiKeyId1) + randomApiKeyAuthentication(user1, apiKeyId1).runAs(user3, realm2), + randomApiKeyAuthentication(user1, apiKeyId1).runAs(user3, realm2) ); checkCanAccessResources( - randomApiKeyAuthentication(new User(user3, user1), apiKeyId1), - randomApiKeyAuthentication(new User(user3, user2), apiKeyId2) + randomApiKeyAuthentication(user1, apiKeyId1).runAs(user3, realm2), + randomApiKeyAuthentication(user2, apiKeyId2).runAs(user3, realm2) ); } @@ -156,6 +171,147 @@ public void testIsServiceAccount() { } } + public void testNonRealmAuthenticationsNoDomain() { + final String apiKeyId = randomAlphaOfLengthBetween(10, 20); + Authentication apiAuthentication = randomApiKeyAuthentication(randomUser(), apiKeyId); + assertThat(apiAuthentication.isAssignedToDomain(), is(false)); + assertThat(apiAuthentication.getDomain(), nullValue()); + apiAuthentication = apiAuthentication.token(); + assertThat(apiAuthentication.isAssignedToDomain(), is(false)); + assertThat(apiAuthentication.getDomain(), nullValue()); + Authentication anonAuthentication = randomAnonymousAuthentication(); + assertThat(anonAuthentication.isAssignedToDomain(), is(false)); + assertThat(anonAuthentication.getDomain(), nullValue()); + Authentication serviceAccountAuthentication = randomServiceAccountAuthentication(); + assertThat(serviceAccountAuthentication.isAssignedToDomain(), is(false)); + assertThat(serviceAccountAuthentication.getDomain(), nullValue()); + Authentication internalAuthentication = randomInternalAuthentication(); + assertThat(internalAuthentication.isAssignedToDomain(), is(false)); + assertThat(internalAuthentication.getDomain(), nullValue()); + } + + public void testRealmAuthenticationIsAssignedToDomain() { + Authentication realmAuthn = randomRealmAuthentication(true); + assertThat(realmAuthn.isAssignedToDomain(), is(true)); + realmAuthn = realmAuthn.token(); + assertThat(realmAuthn.isAssignedToDomain(), is(true)); + realmAuthn = randomRealmAuthentication(false); + assertThat(realmAuthn.isAssignedToDomain(), is(false)); + realmAuthn = realmAuthn.token(); + assertThat(realmAuthn.isAssignedToDomain(), is(false)); + } + + public void testRunAsAuthenticationWithDomain() { + RealmRef authnRealmRef = randomRealmRef(true); + RealmRef lookupRealmRef = randomRealmRef(true); + // realm/token run-as + Authentication test = Authentication.newRealmAuthentication(randomUser(), authnRealmRef); + if (randomBoolean()) { + test = test.token(); + } + test = test.runAs(randomUser(), lookupRealmRef); + if (randomBoolean()) { + test = test.token(); + } + assertThat(test.isAssignedToDomain(), is(true)); + assertThat(test.getDomain(), is(lookupRealmRef.getDomain())); + test = Authentication.newRealmAuthentication(randomUser(), randomRealmRef(false)); + if (randomBoolean()) { + test = test.token(); + } + test = test.runAs(randomUser(), lookupRealmRef); + if (randomBoolean()) { + test = test.token(); + } + assertThat(test.isAssignedToDomain(), is(true)); + assertThat(test.getDomain(), is(lookupRealmRef.getDomain())); + test = Authentication.newRealmAuthentication(randomUser(), authnRealmRef); + if (randomBoolean()) { + test = test.token(); + } + test = test.runAs(randomUser(), randomRealmRef(false)); + if (randomBoolean()) { + test = test.token(); + } + assertThat(test.isAssignedToDomain(), is(false)); + assertThat(test.getDomain(), nullValue()); + test = Authentication.newRealmAuthentication(randomUser(), randomRealmRef(false)); + if (randomBoolean()) { + test = test.token(); + } + test = test.runAs(randomUser(), lookupRealmRef); + if (randomBoolean()) { + test = test.token(); + } + assertThat(test.isAssignedToDomain(), is(true)); + assertThat(test.getDomain(), is(lookupRealmRef.getDomain())); + // api key run-as + test = randomApiKeyAuthentication(randomUser(), randomAlphaOfLengthBetween(10, 20), Version.CURRENT); + if (randomBoolean()) { + test = test.token(); + } + assertThat(test.isAssignedToDomain(), is(false)); + assertThat(test.getDomain(), nullValue()); + if (randomBoolean()) { + test = test.runAs(randomUser(), lookupRealmRef); + if (randomBoolean()) { + test = test.token(); + } + assertThat(test.isAssignedToDomain(), is(true)); + assertThat(test.getDomain(), is(lookupRealmRef.getDomain())); + } else { + test = test.runAs(randomUser(), randomRealmRef(false)); + if (randomBoolean()) { + test = test.token(); + } + assertThat(test.isAssignedToDomain(), is(false)); + assertThat(test.getDomain(), nullValue()); + } + // service account run-as + test = randomServiceAccountAuthentication(); + assertThat(test.isAssignedToDomain(), is(false)); + assertThat(test.getDomain(), nullValue()); + if (randomBoolean()) { + test = test.runAs(randomUser(), lookupRealmRef); + if (randomBoolean()) { + test = test.token(); + } + assertThat(test.isAssignedToDomain(), is(true)); + assertThat(test.getDomain(), is(lookupRealmRef.getDomain())); + } else { + test = test.runAs(randomUser(), randomRealmRef(false)); + if (randomBoolean()) { + test = test.token(); + } + assertThat(test.isAssignedToDomain(), is(false)); + assertThat(test.getDomain(), nullValue()); + } + } + + public void testDomainSerialize() throws Exception { + Authentication test = randomRealmAuthentication(true); + boolean runAs = randomBoolean(); + if (runAs) { + test = test.runAs(randomUser(), randomRealmRef(true)); + } + try (BytesStreamOutput out = new BytesStreamOutput()) { + test.writeTo(out); + StreamInput in = out.bytes().streamInput(); + Authentication testBack = new Authentication(in); + assertThat(test.getDomain(), is(testBack.getDomain())); + } + + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.setVersion(Version.V_8_0_0); + test.writeTo(out); + StreamInput in = out.bytes().streamInput(); + in.setVersion(Version.V_8_0_0); + Authentication testBack = new Authentication(in); + assertThat(testBack.getDomain(), nullValue()); + assertThat(testBack.isAssignedToDomain(), is(false)); + } + } + private void checkCanAccessResources(Authentication authentication0, Authentication authentication1) { if (authentication0.getAuthenticationType() == authentication1.getAuthenticationType() || EnumSet.of(AuthenticationType.REALM, AuthenticationType.TOKEN) @@ -176,12 +332,36 @@ public static User randomUser() { return new User(randomAlphaOfLengthBetween(3, 8), randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 8))); } - public static RealmRef randomRealm() { - return new RealmRef( - randomAlphaOfLengthBetween(3, 8), - randomFrom(FileRealmSettings.TYPE, NativeRealmSettings.TYPE, randomAlphaOfLengthBetween(3, 8)), + public static RealmRef randomRealmRef(boolean underDomain) { + final Supplier randomRealmTypeSupplier = () -> randomFrom( + FileRealmSettings.TYPE, + NativeRealmSettings.TYPE, + LdapRealmSettings.AD_TYPE, + LdapRealmSettings.LDAP_TYPE, + JwtRealmSettings.TYPE, + OpenIdConnectRealmSettings.TYPE, + SamlRealmSettings.TYPE, + KerberosRealmSettings.TYPE, randomAlphaOfLengthBetween(3, 8) ); + if (underDomain) { + final Set domainRealms = Set.of( + randomArray( + 1, + 4, + RealmConfig.RealmIdentifier[]::new, + () -> new RealmConfig.RealmIdentifier( + randomRealmTypeSupplier.get(), + randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT) + ) + ) + ); + RealmDomain domain = new RealmDomain("domain", domainRealms); + RealmConfig.RealmIdentifier realmIdentifier = randomFrom(domainRealms); + return new RealmRef(realmIdentifier.getName(), realmIdentifier.getType(), randomAlphaOfLengthBetween(3, 8), domain); + } else { + return new RealmRef(randomAlphaOfLengthBetween(3, 8), randomRealmTypeSupplier.get(), randomAlphaOfLengthBetween(3, 8), null); + } } private RealmRef mutateRealm(RealmRef original, String name, String type) { @@ -197,7 +377,7 @@ public static Authentication randomAuthentication(User user, RealmRef realmRef) user = randomUser(); } if (realmRef == null) { - realmRef = randomRealm(); + realmRef = randomRealmRef(false); } final Version version = VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.CURRENT); final AuthenticationType authenticationType = randomValueOtherThan( @@ -215,7 +395,7 @@ public static Authentication randomAuthentication(User user, RealmRef realmRef) if (randomBoolean()) { // run-as return new Authentication( new User(user.principal(), user.roles(), randomUser()), - randomRealm(), + randomRealmRef(false), realmRef, version, authenticationType, @@ -227,28 +407,21 @@ public static Authentication randomAuthentication(User user, RealmRef realmRef) } public static Authentication randomApiKeyAuthentication(User user, String apiKeyId) { - final RealmRef apiKeyRealm = new RealmRef("_es_api_key", "_es_api_key", randomAlphaOfLengthBetween(3, 8)); + return randomApiKeyAuthentication(user, apiKeyId, VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.CURRENT)); + } + + public static Authentication randomApiKeyAuthentication(User user, String apiKeyId, Version version) { final HashMap metadata = new HashMap<>(); metadata.put(AuthenticationField.API_KEY_ID_KEY, apiKeyId); metadata.put(AuthenticationField.API_KEY_NAME_KEY, randomBoolean() ? null : randomAlphaOfLengthBetween(1, 16)); - return new Authentication( - user, - apiKeyRealm, - user.isRunAs() ? new RealmRef("lookup_realm", "lookup_realm", randomAlphaOfLength(5)) : null, - VersionUtils.randomVersionBetween(random(), Version.V_7_0_0, Version.CURRENT), - AuthenticationType.API_KEY, - metadata - ); + return Authentication.newApiKeyAuthentication(AuthenticationResult.success(user, metadata), randomAlphaOfLengthBetween(3, 8)) + .maybeRewriteForOlderVersion(version); } public static Authentication randomServiceAccountAuthentication() { - final RealmRef realmRef = new RealmRef("_service_account", "_service_account", randomAlphaOfLengthBetween(3, 8)); - return new Authentication( + return Authentication.newServiceAccountAuthentication( new User(randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8)), - realmRef, - null, - Version.CURRENT, - AuthenticationType.TOKEN, + randomAlphaOfLengthBetween(3, 8), Map.of( "_token_name", randomAlphaOfLength(8), @@ -258,6 +431,28 @@ public static Authentication randomServiceAccountAuthentication() { ); } + public static Authentication randomRealmAuthentication(boolean underDomain) { + return Authentication.newRealmAuthentication(randomUser(), randomRealmRef(underDomain)); + } + + public static Authentication randomInternalAuthentication() { + String nodeName = randomAlphaOfLengthBetween(3, 8); + return randomFrom( + Authentication.newInternalAuthentication( + randomFrom(SystemUser.INSTANCE, XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, AsyncSearchUser.INSTANCE), + Version.CURRENT, + nodeName + ), + Authentication.newInternalFallbackAuthentication(SystemUser.INSTANCE, nodeName) + ); + } + + public static Authentication randomAnonymousAuthentication() { + Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "anon_role").build(); + String nodeName = randomAlphaOfLengthBetween(3, 8); + return Authentication.newAnonymousAuthentication(new AnonymousUser(settings), nodeName); + } + private boolean realmIsSingleton(RealmRef realmRef) { return Set.of(FileRealmSettings.TYPE, NativeRealmSettings.TYPE).contains(realmRef.getType()); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/SecondaryAuthenticationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/SecondaryAuthenticationTests.java index eef3b2e4993e3..6657afa53ebb0 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/SecondaryAuthenticationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/SecondaryAuthenticationTests.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.core.security.authc.support; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ContextPreservingActionListener; import org.elasticsearch.common.settings.Settings; @@ -29,6 +28,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Mockito.mock; public class SecondaryAuthenticationTests extends ESTestCase { @@ -52,113 +52,136 @@ public void testCannotCreateObjectWithNullAuthentication() { public void testSynchronousExecuteInSecondaryContext() { final User user1 = new User("u1", "role1"); - securityContext.setUser(user1, Version.CURRENT); - assertThat(securityContext.getUser().principal(), equalTo("u1")); + setUser(user1, () -> { + assertThat(securityContext.getUser().principal(), equalTo("u1")); - final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm()); - final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2); + final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm()); + final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2); - assertThat(securityContext.getUser().principal(), equalTo("u1")); - var result = secondaryAuth.execute(original -> { - assertThat(securityContext.getUser().principal(), equalTo("u2")); - assertThat(securityContext.getAuthentication(), sameInstance(authentication2)); - return "xyzzy"; + assertThat(securityContext.getUser().principal(), equalTo("u1")); + var result = secondaryAuth.execute(original -> { + assertThat(securityContext.getUser().principal(), equalTo("u2")); + assertThat(securityContext.getAuthentication(), sameInstance(authentication2)); + return "xyzzy"; + }); + assertThat(securityContext.getUser().principal(), equalTo("u1")); + assertThat(result, equalTo("xyzzy")); }); - assertThat(securityContext.getUser().principal(), equalTo("u1")); - assertThat(result, equalTo("xyzzy")); } public void testSecondaryContextCanBeRestored() { final User user1 = new User("u1", "role1"); - securityContext.setUser(user1, Version.CURRENT); - assertThat(securityContext.getUser().principal(), equalTo("u1")); + setUser(user1, () -> { + assertThat(securityContext.getUser().principal(), equalTo("u1")); - final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm()); - final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2); + final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm()); + final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2); - assertThat(securityContext.getUser().principal(), equalTo("u1")); - final AtomicReference secondaryContext = new AtomicReference<>(); - secondaryAuth.execute(storedContext -> { - assertThat(securityContext.getUser().principal(), equalTo("u2")); - assertThat(securityContext.getAuthentication(), sameInstance(authentication2)); - secondaryContext.set(threadPool.getThreadContext().newStoredContext(false)); - storedContext.restore(); assertThat(securityContext.getUser().principal(), equalTo("u1")); - return null; + final AtomicReference secondaryContext = new AtomicReference<>(); + secondaryAuth.execute(storedContext -> { + assertThat(securityContext.getUser().principal(), equalTo("u2")); + assertThat(securityContext.getAuthentication(), sameInstance(authentication2)); + secondaryContext.set(threadPool.getThreadContext().newStoredContext(false)); + storedContext.restore(); + assertThat(securityContext.getUser().principal(), equalTo("u1")); + return null; + }); + assertThat(securityContext.getUser().principal(), equalTo("u1")); + secondaryContext.get().restore(); + assertThat(securityContext.getUser().principal(), equalTo("u2")); }); - assertThat(securityContext.getUser().principal(), equalTo("u1")); - secondaryContext.get().restore(); - assertThat(securityContext.getUser().principal(), equalTo("u2")); } public void testWrapRunnable() throws Exception { final User user1 = new User("u1", "role1"); - securityContext.setUser(user1, Version.CURRENT); - assertThat(securityContext.getUser().principal(), equalTo("u1")); + setUser(user1, () -> { + assertThat(securityContext.getUser().principal(), equalTo("u1")); - final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm()); - final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2); + final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm()); + final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2); - assertThat(securityContext.getUser().principal(), equalTo("u1")); - final Semaphore semaphore = new Semaphore(0); - final Future future = threadPool.generic().submit(secondaryAuth.wrap(() -> { + assertThat(securityContext.getUser().principal(), equalTo("u1")); + final Semaphore semaphore = new Semaphore(0); + final Future future = threadPool.generic().submit(secondaryAuth.wrap(() -> { + try { + assertThat(securityContext.getUser().principal(), equalTo("u2")); + semaphore.acquire(); + assertThat(securityContext.getUser().principal(), equalTo("u2")); + semaphore.acquire(); + assertThat(securityContext.getUser().principal(), equalTo("u2")); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + })); + assertThat(securityContext.getUser().principal(), equalTo("u1")); + semaphore.release(); + assertThat(securityContext.getUser().principal(), equalTo("u1")); + semaphore.release(); + assertThat(securityContext.getUser().principal(), equalTo("u1")); + + // ensure that the runnable didn't throw any exceptions / assertions try { - assertThat(securityContext.getUser().principal(), equalTo("u2")); - semaphore.acquire(); - assertThat(securityContext.getUser().principal(), equalTo("u2")); - semaphore.acquire(); - assertThat(securityContext.getUser().principal(), equalTo("u2")); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + future.get(1, TimeUnit.SECONDS); + } catch (Exception e) { throw new RuntimeException(e); } - })); - assertThat(securityContext.getUser().principal(), equalTo("u1")); - semaphore.release(); - assertThat(securityContext.getUser().principal(), equalTo("u1")); - semaphore.release(); - assertThat(securityContext.getUser().principal(), equalTo("u1")); - - // ensure that the runnable didn't throw any exceptions / assertions - future.get(1, TimeUnit.SECONDS); + }); } - public void testPreserveSecondaryContextAcrossThreads() throws Exception { + public void testPreserveSecondaryContextAcrossThreads() { final User user1 = new User("u1", "role1"); - securityContext.setUser(user1, Version.CURRENT); - assertThat(securityContext.getUser().principal(), equalTo("u1")); + setUser(user1, () -> { + assertThat(securityContext.getUser().principal(), equalTo("u1")); - final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm()); - final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2); + final Authentication authentication2 = new Authentication(new User("u2", "role2"), realm(), realm()); + final SecondaryAuthentication secondaryAuth = new SecondaryAuthentication(securityContext, authentication2); - assertThat(securityContext.getUser().principal(), equalTo("u1")); + assertThat(securityContext.getUser().principal(), equalTo("u1")); - final AtomicReference threadUser = new AtomicReference<>(); - final AtomicReference listenerUser = new AtomicReference<>(); + final AtomicReference threadUser = new AtomicReference<>(); + final AtomicReference listenerUser = new AtomicReference<>(); - final ThreadContext threadContext = threadPool.getThreadContext(); - secondaryAuth.execute(originalContext -> { - assertThat(securityContext.getUser().principal(), equalTo("u2")); - ActionListener listener = new ContextPreservingActionListener<>( - threadContext.newRestorableContext(false), - ActionListener.wrap(() -> listenerUser.set(securityContext.getUser())) - ); - originalContext.restore(); - threadPool.generic().execute(() -> { - threadUser.set(securityContext.getUser()); - listener.onResponse(null); + final ThreadContext threadContext = threadPool.getThreadContext(); + secondaryAuth.execute(originalContext -> { + assertThat(securityContext.getUser().principal(), equalTo("u2")); + ActionListener listener = new ContextPreservingActionListener<>( + threadContext.newRestorableContext(false), + ActionListener.wrap(() -> listenerUser.set(securityContext.getUser())) + ); + originalContext.restore(); + threadPool.generic().execute(() -> { + threadUser.set(securityContext.getUser()); + listener.onResponse(null); + }); + return null; }); - return null; + assertThat(securityContext.getUser().principal(), equalTo("u1")); + try { + assertBusy(() -> assertThat(listenerUser.get(), notNullValue()), 1, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException(e); + } + assertThat(threadUser.get(), notNullValue()); + assertThat(threadUser.get().principal(), equalTo("u1")); + assertThat(listenerUser.get().principal(), equalTo("u2")); }); - assertThat(securityContext.getUser().principal(), equalTo("u1")); - assertBusy(() -> assertThat(listenerUser.get(), notNullValue()), 1, TimeUnit.SECONDS); - assertThat(threadUser.get(), notNullValue()); - assertThat(threadUser.get().principal(), equalTo("u1")); - assertThat(listenerUser.get().principal(), equalTo("u2")); } private Authentication.RealmRef realm() { return new Authentication.RealmRef(randomAlphaOfLengthBetween(4, 8), randomAlphaOfLengthBetween(2, 4), randomAlphaOfLength(12)); } + private void setUser(User user, Runnable runnable) { + final Authentication authentication = new Authentication( + user, + mock(Authentication.RealmRef.class), + mock(Authentication.RealmRef.class) + ); + securityContext.executeWithAuthentication(authentication, ignored -> { + runnable.run(); + return null; + }); + } } diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolverTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolverTests.java index 891b9fa9f1822..0ebfdf36a038c 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolverTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolverTests.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.idp.privileges; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.client.internal.Client; @@ -21,6 +20,8 @@ import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges; import org.elasticsearch.xpack.core.security.user.User; import org.junit.Before; @@ -68,20 +69,26 @@ public void setupTest() { public void testResolveZeroAccess() throws Exception { final String username = randomAlphaOfLengthBetween(4, 12); final String app = randomAlphaOfLengthBetween(3, 8); - setupUser(username); - setupHasPrivileges(username, app); - final PlainActionFuture future = new PlainActionFuture<>(); - final Function> roleMapping = Map.of( - "role:cluster:view", - Set.of("viewer"), - "role:cluster:admin", - Set.of("admin") - )::get; - resolver.resolve(service(app, "cluster:" + randomLong(), roleMapping), future); - final UserPrivilegeResolver.UserPrivileges privileges = future.get(); - assertThat(privileges.principal, equalTo(username)); - assertThat(privileges.hasAccess, equalTo(false)); - assertThat(privileges.roles, emptyIterable()); + setupUser(username, () -> { + setupHasPrivileges(username, app); + final PlainActionFuture future = new PlainActionFuture<>(); + final Function> roleMapping = Map.of( + "role:cluster:view", + Set.of("viewer"), + "role:cluster:admin", + Set.of("admin") + )::get; + resolver.resolve(service(app, "cluster:" + randomLong(), roleMapping), future); + final UserPrivilegeResolver.UserPrivileges privileges; + try { + privileges = future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + assertThat(privileges.principal, equalTo(username)); + assertThat(privileges.hasAccess, equalTo(false)); + assertThat(privileges.roles, emptyIterable()); + }); } public void testResolveSsoWithNoRoleAccess() throws Exception { @@ -91,16 +98,22 @@ public void testResolveSsoWithNoRoleAccess() throws Exception { final String viewerAction = "role:cluster:view"; final String adminAction = "role:cluster:admin"; - setupUser(username); - setupHasPrivileges(username, app, access(resource, viewerAction, false), access(resource, adminAction, false)); - - final PlainActionFuture future = new PlainActionFuture<>(); - final Function> roleMapping = Map.of(viewerAction, Set.of("viewer"), adminAction, Set.of("admin"))::get; - resolver.resolve(service(app, resource, roleMapping), future); - final UserPrivilegeResolver.UserPrivileges privileges = future.get(); - assertThat(privileges.principal, equalTo(username)); - assertThat(privileges.hasAccess, equalTo(false)); - assertThat(privileges.roles, emptyIterable()); + setupUser(username, () -> { + setupHasPrivileges(username, app, access(resource, viewerAction, false), access(resource, adminAction, false)); + + final PlainActionFuture future = new PlainActionFuture<>(); + final Function> roleMapping = Map.of(viewerAction, Set.of("viewer"), adminAction, Set.of("admin"))::get; + resolver.resolve(service(app, resource, roleMapping), future); + final UserPrivilegeResolver.UserPrivileges privileges; + try { + privileges = future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + assertThat(privileges.principal, equalTo(username)); + assertThat(privileges.hasAccess, equalTo(false)); + assertThat(privileges.roles, emptyIterable()); + }); } public void testResolveSsoWithSingleRole() throws Exception { @@ -110,16 +123,22 @@ public void testResolveSsoWithSingleRole() throws Exception { final String viewerAction = "role:cluster:view"; final String adminAction = "role:cluster:admin"; - setupUser(username); - setupHasPrivileges(username, app, access(resource, viewerAction, true), access(resource, adminAction, false)); - - final PlainActionFuture future = new PlainActionFuture<>(); - final Function> roleMapping = Map.of(viewerAction, Set.of("viewer"), adminAction, Set.of("admin"))::get; - resolver.resolve(service(app, resource, roleMapping), future); - final UserPrivilegeResolver.UserPrivileges privileges = future.get(); - assertThat(privileges.principal, equalTo(username)); - assertThat(privileges.hasAccess, equalTo(true)); - assertThat(privileges.roles, containsInAnyOrder("viewer")); + setupUser(username, () -> { + setupHasPrivileges(username, app, access(resource, viewerAction, true), access(resource, adminAction, false)); + + final PlainActionFuture future = new PlainActionFuture<>(); + final Function> roleMapping = Map.of(viewerAction, Set.of("viewer"), adminAction, Set.of("admin"))::get; + resolver.resolve(service(app, resource, roleMapping), future); + final UserPrivilegeResolver.UserPrivileges privileges; + try { + privileges = future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + assertThat(privileges.principal, equalTo(username)); + assertThat(privileges.hasAccess, equalTo(true)); + assertThat(privileges.roles, containsInAnyOrder("viewer")); + }); } public void testResolveSsoWithMultipleRoles() throws Exception { @@ -131,31 +150,37 @@ public void testResolveSsoWithMultipleRoles() throws Exception { final String operatorAction = "role:cluster:operator"; final String monitorAction = "role:cluster:monitor"; - setupUser(username); - setupHasPrivileges( - username, - app, - access(resource, viewerAction, false), - access(resource, adminAction, false), - access(resource, operatorAction, true), - access(resource, monitorAction, true) - ); - - final PlainActionFuture future = new PlainActionFuture<>(); - Function> roleMapping = action -> { - return switch (action) { - case viewerAction -> Set.of("viewer"); - case adminAction -> Set.of("admin"); - case operatorAction -> Set.of("operator"); - case monitorAction -> Set.of("monitor"); - default -> Set.of(); + setupUser(username, () -> { + setupHasPrivileges( + username, + app, + access(resource, viewerAction, false), + access(resource, adminAction, false), + access(resource, operatorAction, true), + access(resource, monitorAction, true) + ); + + final PlainActionFuture future = new PlainActionFuture<>(); + Function> roleMapping = action -> { + return switch (action) { + case viewerAction -> Set.of("viewer"); + case adminAction -> Set.of("admin"); + case operatorAction -> Set.of("operator"); + case monitorAction -> Set.of("monitor"); + default -> Set.of(); + }; }; - }; - resolver.resolve(service(app, resource, roleMapping), future); - final UserPrivilegeResolver.UserPrivileges privileges = future.get(); - assertThat(privileges.principal, equalTo(username)); - assertThat(privileges.hasAccess, equalTo(true)); - assertThat(privileges.roles, containsInAnyOrder("operator", "monitor")); + resolver.resolve(service(app, resource, roleMapping), future); + final UserPrivilegeResolver.UserPrivileges privileges; + try { + privileges = future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + assertThat(privileges.principal, equalTo(username)); + assertThat(privileges.hasAccess, equalTo(true)); + assertThat(privileges.roles, containsInAnyOrder("operator", "monitor")); + }); } private ServiceProviderPrivileges service(String appName, String resource, Function> roleMapping) { @@ -198,9 +223,16 @@ private Tuple> access(String resource, String act return new Tuple<>(resource, new Tuple<>(action, access)); } - private void setupUser(String principal) { - final User user = new User(principal, randomAlphaOfLengthBetween(6, 12)); - securityContext.setUser(user, Version.CURRENT); + private void setupUser(String principal, Runnable runnable) { + final Authentication authentication = new Authentication( + new User(principal, randomAlphaOfLengthBetween(6, 12)), + mock(RealmRef.class), + mock(RealmRef.class) + ); + securityContext.executeWithAuthentication(authentication, ignored -> { + runnable.run(); + return null; + }); } } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateActionTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateActionTests.java index e4ce8c76edbe9..f92bcaaac958d 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateActionTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateActionTests.java @@ -22,20 +22,28 @@ import java.util.List; +import static org.elasticsearch.xpack.core.security.authc.RealmSettings.DOMAIN_TO_REALM_ASSOC_SETTING; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; public class RestAuthenticateActionTests extends SecurityIntegTestCase { private static boolean anonymousEnabled; + private static String domainName; @BeforeClass public static void maybeEnableAnonymous() { anonymousEnabled = randomBoolean(); } + @BeforeClass + public static void maybeSetDomain() { + domainName = randomFrom(randomAlphaOfLengthBetween(3, 5), null); + } + @Override protected boolean addMockHttpTransport() { return false; // enable http @@ -50,6 +58,9 @@ protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { .putList(AnonymousUser.ROLES_SETTING.getKey(), SecuritySettingsSource.TEST_ROLE, "foo") .put(AuthorizationService.ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.getKey(), false); } + if (domainName != null) { + builder.put(DOMAIN_TO_REALM_ASSOC_SETTING.getConcreteSettingForNamespace(domainName).getKey(), "file"); + } return builder.build(); } @@ -69,8 +80,18 @@ public void testAuthenticateApi() throws Exception { assertThat(objectPath.evaluate("username").toString(), equalTo(SecuritySettingsSource.TEST_USER_NAME)); assertThat(objectPath.evaluate("authentication_realm.name").toString(), equalTo("file")); assertThat(objectPath.evaluate("authentication_realm.type").toString(), equalTo("file")); + if (domainName != null) { + assertThat(objectPath.evaluate("authentication_realm.domain").toString(), equalTo(domainName)); + } else { + assertThat(objectPath.evaluate("lookup_realm.domain"), nullValue()); + } assertThat(objectPath.evaluate("lookup_realm.name").toString(), equalTo("file")); assertThat(objectPath.evaluate("lookup_realm.type").toString(), equalTo("file")); + if (domainName != null) { + assertThat(objectPath.evaluate("lookup_realm.domain").toString(), equalTo(domainName)); + } else { + assertThat(objectPath.evaluate("lookup_realm.domain"), nullValue()); + } assertThat(objectPath.evaluate("authentication_type").toString(), equalTo("realm")); List roles = objectPath.evaluate("roles"); if (anonymousEnabled) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java index 9356c850c6468..40169067fe9a5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java @@ -8,7 +8,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionResponse; @@ -102,11 +101,7 @@ operations are blocked on license expiration. All data operations (read and writ final boolean useSystemUser = AuthorizationUtils.shouldReplaceUserWithSystem(threadContext, action); try { if (useSystemUser) { - securityContext.executeAsUser( - SystemUser.INSTANCE, - (original) -> { applyInternal(task, chain, action, request, contextPreservingListener); }, - Version.CURRENT - ); + securityContext.executeAsSystemUser(original -> applyInternal(task, chain, action, request, contextPreservingListener)); } else if (AuthorizationUtils.shouldSetUserBasedOnActionOrigin(threadContext)) { AuthorizationUtils.switchUserBasedOnActionOriginAndExecute( threadContext, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java index 1f9ed4ba2c1bc..8e713dbbf9935 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java @@ -62,6 +62,8 @@ protected void doExecute(Task task, AuthenticateRequest request, ActionListener< .toArray(String[]::new); listener.onResponse( new AuthenticateResponse( + // TODO do not rebuild the authentication for display purposes, instead make the authentication service construct + // the user so it includes the anonymous roles as well new Authentication( new User( new User(user.principal(), allRoleNames, user.fullName(), user.email(), user.metadata(), user.enabled()), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyAuthenticator.java index 506bde7c0d59a..86fbc0771efa9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyAuthenticator.java @@ -49,7 +49,7 @@ public void authenticate(Context context, ActionListener { if (authResult.isAuthenticated()) { - final Authentication authentication = apiKeyService.createApiKeyAuthentication(authResult, nodeName); + final Authentication authentication = Authentication.newApiKeyAuthentication(authResult, nodeName); listener.onResponse(AuthenticationResult.success(authentication)); } else if (authResult.getStatus() == AuthenticationResult.Status.TERMINATE) { Exception e = (authResult.getException() != null) 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 6690b46eac94a..68735f1e0a5ea 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 @@ -86,7 +86,6 @@ import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyResponse; import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; @@ -432,26 +431,6 @@ void tryAuthenticate(ThreadContext ctx, ApiKeyCredentials credentials, ActionLis })); } - public Authentication createApiKeyAuthentication(AuthenticationResult authResult, String nodeName) { - if (false == authResult.isAuthenticated()) { - throw new IllegalArgumentException("API Key authn result must be successful"); - } - final User user = authResult.getValue(); - final RealmRef authenticatedBy = new RealmRef( - AuthenticationField.API_KEY_REALM_NAME, - AuthenticationField.API_KEY_REALM_TYPE, - nodeName - ); - return new Authentication( - user, - authenticatedBy, - null, - Version.CURRENT, - Authentication.AuthenticationType.API_KEY, - authResult.getMetadata() - ); - } - void loadApiKeyAndValidateCredentials( ThreadContext ctx, ApiKeyCredentials credentials, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 6c9b4c00852d1..f9e3ed08f464a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -111,7 +111,7 @@ public AuthenticationService( new ServiceAccountAuthenticator(serviceAccountService, nodeName), new OAuth2TokenAuthenticator(tokenService), new ApiKeyAuthenticator(apiKeyService, nodeName), - new RealmsAuthenticator(nodeName, numInvalidation, lastSuccessfulAuthCache) + new RealmsAuthenticator(numInvalidation, lastSuccessfulAuthCache) ); } 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 9703490aee16f..ad46afc6ade9c 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 @@ -11,7 +11,6 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchSecurityException; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; @@ -29,17 +28,12 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; -import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.function.BiConsumer; 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; -import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_NAME; -import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_TYPE; - class AuthenticatorChain { private static final Logger logger = LogManager.getLogger(AuthenticatorChain.class); @@ -211,42 +205,19 @@ private void maybeLookupRunAsUser( return; } - final User user = authentication.getUser(); - if (runAsUsername.isEmpty()) { - logger.debug("user [{}] attempted to runAs with an empty username", user.principal()); - listener.onFailure( - context.getRequest() - .runAsDenied( - new Authentication(new User(runAsUsername, null, user), authentication.getAuthenticatedBy(), null), - context.getMostRecentAuthenticationToken() - ) - ); - return; - } - // Now we have a valid runAsUsername realmsAuthenticator.lookupRunAsUser(context, authentication, ActionListener.wrap(tuple -> { final Authentication finalAuth; if (tuple == null) { - logger.debug("Cannot find run-as user [{}] for authenticated user [{}]", runAsUsername, user.principal()); - // the user does not exist, but we still create a User object, which will later be rejected by authz - finalAuth = new Authentication( - new User(runAsUsername, null, user), - authentication.getAuthenticatedBy(), - null, - authentication.getVersion(), - authentication.getAuthenticationType(), - authentication.getMetadata() + logger.debug( + "Cannot find run-as user [{}] for authenticated user [{}]", + runAsUsername, + authentication.getUser().principal() ); + // the user does not exist, but we still create a User object, which will later be rejected by authz + finalAuth = authentication.runAs(new User(runAsUsername, null, null, null, Map.of(), true), null); } else { - finalAuth = new Authentication( - new User(tuple.v1(), user), - authentication.getAuthenticatedBy(), - tuple.v2(), - authentication.getVersion(), - authentication.getAuthenticationType(), - authentication.getMetadata() - ); + finalAuth = authentication.runAs(tuple.v1(), tuple.v2().realmRef()); } finishAuthentication(context, finalAuth, listener); }, listener::onFailure)); @@ -302,30 +273,14 @@ void handleNullToken(Authenticator.Context context, ActionListener domainForRealmMap = RealmSettings.computeRealmNameToDomainNameAssociation(settings); final List realmConfigs = buildRealmConfigs(); final List initialRealms = initRealms(realmConfigs); + configureRealmRef(initialRealms, realmConfigs, domainForRealmMap); this.allConfiguredRealms = initialRealms; this.allConfiguredRealms.forEach(r -> r.initialize(this.allConfiguredRealms, licenseState)); assert this.allConfiguredRealms.get(0) == reservedRealm : "the first realm must be reserved realm"; @@ -94,6 +98,23 @@ public Realms( licenseState.addListener(this::recomputeActiveRealms); } + static void configureRealmRef(Collection realms, Collection realmConfigs, Map domainForRealm) { + for (Realm realm : realms) { + String domainName = domainForRealm.get(realm.name()); + if (domainName != null) { + Set domainIdentifiers = new HashSet<>(); + for (RealmConfig realmConfig : realmConfigs) { + if (domainName.equals(domainForRealm.get(realmConfig.name()))) { + domainIdentifiers.add(realmConfig.identifier()); + } + } + realm.initRealmRef(new RealmDomain(domainName, domainIdentifiers)); + } else { + realm.initRealmRef(null); + } + } + } + protected void recomputeActiveRealms() { final XPackLicenseState licenseStateSnapshot = licenseState.copyCurrentLicenseState(); final List licensedRealms = calculateLicensedRealms(licenseStateSnapshot); @@ -229,7 +250,7 @@ protected List initRealms(List realmConfigs) throws Exceptio checkUniqueOrders(orderToRealmName); Collections.sort(realms); - maybeAddBasicRealms(realms, findDisabledBasicRealmTypes(realmConfigs)); + maybeAddBasicRealms(realms, realmConfigs); // always add built in first! realms.add(0, reservedRealm); String duplicateRealms = nameToRealmIdentifier.entrySet() @@ -307,26 +328,31 @@ public void usageStats(ActionListener> listener) { } } - private void maybeAddBasicRealms(List realms, Set disabledBasicRealmTypes) throws Exception { + private void maybeAddBasicRealms(List realms, List realmConfigs) throws Exception { + final Set disabledBasicRealmTypes = findDisabledBasicRealmTypes(realmConfigs); final Set realmTypes = realms.stream().map(Realm::type).collect(Collectors.toUnmodifiableSet()); // Add native realm first so that file realm will be in the beginning if (false == disabledBasicRealmTypes.contains(NativeRealmSettings.TYPE) && false == realmTypes.contains(NativeRealmSettings.TYPE)) { var nativeRealmId = new RealmConfig.RealmIdentifier(NativeRealmSettings.TYPE, NativeRealmSettings.DEFAULT_NAME); - realms.add( - 0, - factories.get(NativeRealmSettings.TYPE) - .create( - new RealmConfig(nativeRealmId, ensureOrderSetting(settings, nativeRealmId, Integer.MIN_VALUE), env, threadContext) - ) + var realmConfig = new RealmConfig( + nativeRealmId, + ensureOrderSetting(settings, nativeRealmId, Integer.MIN_VALUE), + env, + threadContext ); + realmConfigs.add(realmConfig); + realms.add(0, factories.get(NativeRealmSettings.TYPE).create(realmConfig)); } if (false == disabledBasicRealmTypes.contains(FileRealmSettings.TYPE) && false == realmTypes.contains(FileRealmSettings.TYPE)) { var fileRealmId = new RealmConfig.RealmIdentifier(FileRealmSettings.TYPE, FileRealmSettings.DEFAULT_NAME); - realms.add( - 0, - factories.get(FileRealmSettings.TYPE) - .create(new RealmConfig(fileRealmId, ensureOrderSetting(settings, fileRealmId, Integer.MIN_VALUE), env, threadContext)) + var realmConfig = new RealmConfig( + fileRealmId, + ensureOrderSetting(settings, fileRealmId, Integer.MIN_VALUE), + env, + threadContext ); + realmConfigs.add(realmConfig); + realms.add(0, factories.get(FileRealmSettings.TYPE).create(realmConfig)); } } @@ -348,7 +374,6 @@ private void checkUniqueOrders(Map> orderToRealmName) { private List buildRealmConfigs() { final Map realmsSettings = RealmSettings.getRealmSettings(settings); - RealmSettings.verifyRealmNameToDomainNameAssociation(settings, realmsSettings.keySet()); final Set internalTypes = new HashSet<>(); final List kerberosRealmNames = new ArrayList<>(); final List realmConfigs = new ArrayList<>(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/RealmsAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/RealmsAuthenticator.java index a8240ec04ab74..0c13bfa5e4b76 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/RealmsAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/RealmsAuthenticator.java @@ -41,13 +41,11 @@ class RealmsAuthenticator implements Authenticator { private static final Logger logger = LogManager.getLogger(RealmsAuthenticator.class); - private final String nodeName; private final AtomicLong numInvalidation; private final Cache lastSuccessfulAuthCache; private boolean authenticationTokenExtracted = false; - RealmsAuthenticator(String nodeName, AtomicLong numInvalidation, Cache lastSuccessfulAuthCache) { - this.nodeName = nodeName; + RealmsAuthenticator(AtomicLong numInvalidation, Cache lastSuccessfulAuthCache) { this.numInvalidation = numInvalidation; this.lastSuccessfulAuthCache = lastSuccessfulAuthCache; } @@ -134,7 +132,7 @@ private void consumeToken(Context context, ActionListener> messages = new LinkedHashMap<>(); - final AtomicReference authenticatedByRef = new AtomicReference<>(); + final AtomicReference authenticatedByRef = new AtomicReference<>(); final AtomicReference> authenticationResultRef = new AtomicReference<>(); final BiConsumer> realmAuthenticatingConsumer = (realm, userListener) -> { @@ -156,7 +154,7 @@ private void consumeToken(Context context, ActionListener result = authenticationResultRef.get(); assert result != null : "authentication result must not be null when user is not null"; context.getThreadContext().putTransient(AuthenticationResult.THREAD_CONTEXT_KEY, result); - listener.onResponse(AuthenticationResult.success(new Authentication(user, authenticatedByRef.get(), null))); + listener.onResponse( + AuthenticationResult.success(Authentication.newRealmAuthentication(user, authenticatedByRef.get().realmRef())) + ); } }, e -> { if (e != null) { @@ -274,11 +274,7 @@ private void consumeNullUser( * if the user is found or not, with a non-null user. We do not fail requests if the run as user is not found as that can leak the * names of users that exist using a timing attack */ - public void lookupRunAsUser( - Context context, - Authentication authentication, - ActionListener> listener - ) { + public void lookupRunAsUser(Context context, Authentication authentication, ActionListener> listener) { assert authentication.getLookedUpBy() == null : "authentication already has a lookup realm"; final String runAsUsername = context.getThreadContext().getHeader(AuthenticationServiceField.RUN_AS_USER_HEADER); if (runAsUsername != null && runAsUsername.isEmpty() == false) { @@ -302,9 +298,7 @@ public void lookupRunAsUser( lastSuccessfulAuthCache.computeIfAbsent(runAsUsername, s -> realm); } logger.trace("Using run-as user [{}] with authenticated user [{}]", foundUser, authentication.getUser().principal()); - listener.onResponse( - new Tuple<>(tuple.v1(), new Authentication.RealmRef(tuple.v2().name(), tuple.v2().type(), nodeName)) - ); + listener.onResponse(tuple); } }, e -> listener.onFailure(context.getRequest().exceptionProcessingRequest(e, context.getMostRecentAuthenticationToken())))); } else if (runAsUsername == null) { @@ -313,14 +307,7 @@ public void lookupRunAsUser( logger.debug("user [{}] attempted to runAs with an empty username", authentication.getUser().principal()); listener.onFailure( context.getRequest() - .runAsDenied( - new Authentication( - new User(runAsUsername, null, authentication.getUser()), - authentication.getAuthenticatedBy(), - null - ), - context.getMostRecentAuthenticationToken() - ) + .runAsDenied(authentication.runAs(new User(runAsUsername), null), context.getMostRecentAuthenticationToken()) ); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index 0bb8bc385fdd4..694cf53c3f5aa 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -90,7 +90,6 @@ import org.elasticsearch.xpack.core.security.ScrollHelper; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType; import org.elasticsearch.xpack.core.security.authc.KeyAndTimestamp; import org.elasticsearch.xpack.core.security.authc.TokenMetadata; import org.elasticsearch.xpack.core.security.authc.support.Hasher; @@ -368,14 +367,7 @@ private void createOAuth2Tokens( traceLog("create token", new IllegalArgumentException("originating client authentication must be provided")) ); } else { - final Authentication tokenAuth = new Authentication( - authentication.getUser(), - authentication.getAuthenticatedBy(), - authentication.getLookedUpBy(), - tokenVersion, - AuthenticationType.TOKEN, - authentication.getMetadata() - ); + final Authentication tokenAuth = authentication.token().maybeRewriteForOlderVersion(tokenVersion); final String storedAccessToken; final String storedRefreshToken; if (tokenVersion.onOrAfter(VERSION_HASHED_TOKENS)) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java index 6b555f77cd924..4d10bb13b0c92 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java @@ -10,7 +10,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchSecurityException; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.client.internal.Client; import org.elasticsearch.common.settings.SecureString; @@ -25,7 +24,6 @@ import org.elasticsearch.xpack.core.security.action.service.TokenInfo; import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource; import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; @@ -188,17 +186,9 @@ private Authentication createAuthentication( String nodeName ) { final User user = account.asUser(); - final Authentication.RealmRef authenticatedBy = new Authentication.RealmRef( - ServiceAccountSettings.REALM_NAME, - ServiceAccountSettings.REALM_TYPE, - nodeName - ); - return new Authentication( + return Authentication.newServiceAccountAuthentication( user, - authenticatedBy, - null, - Version.CURRENT, - Authentication.AuthenticationType.TOKEN, + nodeName, Map.of(TOKEN_NAME_FIELD, token.getTokenName(), TOKEN_SOURCE_FIELD, tokenSource.name().toLowerCase(Locale.ROOT)) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java index 6bae75dd5b326..7b3f44e9b984b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java @@ -115,7 +115,7 @@ public static void switchUserBasedOnActionOriginAndExecute( switch (actionOrigin) { case SECURITY_ORIGIN: - securityContext.executeAsUser(XPackSecurityUser.INSTANCE, consumer, Version.CURRENT); + securityContext.executeAsInternalUser(XPackSecurityUser.INSTANCE, Version.CURRENT, consumer); break; case WATCHER_ORIGIN: case ML_ORIGIN: @@ -133,10 +133,10 @@ public static void switchUserBasedOnActionOriginAndExecute( case LOGSTASH_MANAGEMENT_ORIGIN: case FLEET_ORIGIN: case TASKS_ORIGIN: // TODO use a more limited user for tasks - securityContext.executeAsUser(XPackUser.INSTANCE, consumer, Version.CURRENT); + securityContext.executeAsInternalUser(XPackUser.INSTANCE, Version.CURRENT, consumer); break; case ASYNC_SEARCH_ORIGIN: - securityContext.executeAsUser(AsyncSearchUser.INSTANCE, consumer, Version.CURRENT); + securityContext.executeAsInternalUser(AsyncSearchUser.INSTANCE, Version.CURRENT, consumer); break; default: assert false : "action.origin [" + actionOrigin + "] is unknown!"; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java index 36edbf3c5f082..dfc225cab5f97 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java @@ -35,7 +35,6 @@ import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.transport.ProfileConfigurations; -import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authz.AuthorizationService; @@ -98,17 +97,16 @@ public void sendRequest( // Sometimes a system action gets executed like a internal create index request or update mappings request // which means that the user is copied over to system actions so we need to change the user if (AuthorizationUtils.shouldReplaceUserWithSystem(threadPool.getThreadContext(), action)) { - securityContext.executeAsUser( - SystemUser.INSTANCE, - (original) -> sendWithUser( + securityContext.executeAsSystemUser( + minVersion, + original -> sendWithUser( connection, action, request, options, new ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original), handler), sender - ), - minVersion + ) ); } else if (AuthorizationUtils.shouldSetUserBasedOnActionOrigin(threadPool.getThreadContext())) { AuthorizationUtils.switchUserBasedOnActionOriginAndExecute( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java index 82655e30e2c6d..ac8928f2d179e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java @@ -105,10 +105,10 @@ requests from all the nodes are attached with a user (either a serialize authcService.authenticate(securityAction, request, true, ActionListener.wrap((authentication) -> { if (authentication != null) { if (securityAction.equals(TransportService.HANDSHAKE_ACTION_NAME) && SystemUser.is(authentication.getUser()) == false) { - securityContext.executeAsUser(SystemUser.INSTANCE, (ctx) -> { + securityContext.executeAsSystemUser(version, original -> { final Authentication replaced = securityContext.getAuthentication(); authzService.authorize(replaced, securityAction, request, listener); - }, version); + }); } else { authzService.authorize(authentication, securityAction, request, listener); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java index 061bc22c28fe8..4195dd969ace5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java @@ -20,8 +20,11 @@ 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.AuthenticationField; +import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; +import org.elasticsearch.xpack.core.security.user.XPackUser; import org.junit.Before; import java.io.EOFException; @@ -71,17 +74,17 @@ public void testGetAuthenticationDoesNotSwallowIOException() { assertThat(e.getCause(), instanceOf(EOFException.class)); } - public void testSetUser() { - final User user = new User("test"); + public void testSetInternalUser() { + final User internalUser = randomFrom(SystemUser.INSTANCE, XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, AsyncSearchUser.INSTANCE); assertNull(securityContext.getAuthentication()); assertNull(securityContext.getUser()); - securityContext.setUser(user, Version.CURRENT); - assertEquals(user, securityContext.getUser()); + securityContext.setInternalUser(internalUser, Version.CURRENT); + assertEquals(internalUser, securityContext.getUser()); assertEquals(AuthenticationType.INTERNAL, securityContext.getAuthentication().getAuthenticationType()); IllegalStateException e = expectThrows( IllegalStateException.class, - () -> securityContext.setUser(randomFrom(user, SystemUser.INSTANCE), Version.CURRENT) + () -> securityContext.setInternalUser(randomFrom(internalUser, SystemUser.INSTANCE), Version.CURRENT) ); assertEquals("authentication ([_xpack_security_authentication]) is already present in the context", e.getMessage()); } @@ -96,13 +99,18 @@ public void testExecuteAsUser() throws IOException { original = null; } - final User executionUser = new User("executor"); + final User executionUser = randomFrom( + SystemUser.INSTANCE, + XPackUser.INSTANCE, + XPackSecurityUser.INSTANCE, + AsyncSearchUser.INSTANCE + ); final AtomicReference contextAtomicReference = new AtomicReference<>(); - securityContext.executeAsUser(executionUser, (originalCtx) -> { + securityContext.executeAsInternalUser(executionUser, Version.CURRENT, (originalCtx) -> { assertEquals(executionUser, securityContext.getUser()); assertEquals(AuthenticationType.INTERNAL, securityContext.getAuthentication().getAuthenticationType()); contextAtomicReference.set(originalCtx); - }, Version.CURRENT); + }); final User userAfterExecution = securityContext.getUser(); assertEquals(original, userAfterExecution); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java index 9e21519bea3a7..ac62b75c2df32 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java @@ -217,8 +217,8 @@ public void testAnonymousAccess() throws Exception { public void testAccessGranted() throws Exception { Authentication authentication = new Authentication( new User("_username", "r1"), - new RealmRef(null, null, null), - new RealmRef(null, null, null) + new RealmRef("_realm", "_type", "node", null), + new RealmRef("_look", "_type", "node", null) ); AuthorizationInfo authzInfo = () -> Collections.singletonMap( PRINCIPAL_ROLES_FIELD_NAME, @@ -239,8 +239,8 @@ public void testAccessGranted() throws Exception { public void testAccessDenied() throws Exception { Authentication authentication = new Authentication( new User("_username", "r1"), - new RealmRef(null, null, null), - new RealmRef(null, null, null) + new RealmRef("_realm", "_type", "node", null), + new RealmRef("_look", "_type", "node", null) ); AuthorizationInfo authzInfo = () -> Collections.singletonMap( PRINCIPAL_ROLES_FIELD_NAME, @@ -289,8 +289,8 @@ public void testConnectionDenied() throws Exception { public void testAuthenticationSuccessRest() throws Exception { Authentication authentication = new Authentication( new User("_username", "r1"), - new RealmRef("_realm", null, null), - new RealmRef(null, null, null) + new RealmRef("_realm", "_type", "node", null), + new RealmRef("_look", "_type", "node", null) ); final String requestId = randomAlphaOfLengthBetween(6, 12); service.get().authenticationSuccess(requestId, authentication, restRequest); @@ -307,8 +307,8 @@ public void testAuthenticationSuccessRest() throws Exception { public void testAuthenticationSuccessTransport() throws Exception { Authentication authentication = new Authentication( new User("_username", "r1"), - new RealmRef("_realm", null, null), - new RealmRef(null, null, null) + new RealmRef("_realm", "_type", "node", null), + new RealmRef("_look", "_type", "node", null) ); final String requestId = randomAlphaOfLengthBetween(6, 12); service.get().authenticationSuccess(requestId, authentication, "_action", request); 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 c57fc3956f6ed..256678c8aa375 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 @@ -1518,7 +1518,7 @@ public static Authentication createApiKeyAuthentication( final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); final SecurityContext securityContext = new SecurityContext(Settings.EMPTY, threadContext); authenticationContextSerializer.writeToContext( - apiKeyService.createApiKeyAuthentication(authenticationResult, "node01"), + Authentication.newApiKeyAuthentication(authenticationResult, "node01"), threadContext ); final CompletableFuture authFuture = new CompletableFuture<>(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index bb2097fc32e17..761b8f0a390c1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -79,6 +79,7 @@ import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.Realm.Factory; import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.RealmDomain; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; @@ -120,6 +121,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; @@ -175,7 +177,9 @@ public class AuthenticationServiceTests extends ESTestCase { private TransportRequest transportRequest; private RestRequest restRequest; private Realms realms; + private RealmDomain firstDomain; private Realm firstRealm; + private RealmDomain secondDomain; private Realm secondRealm; private AuditTrail auditTrail; private AuditTrailService auditTrailService; @@ -207,14 +211,18 @@ public void init() throws Exception { restRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withRemoteAddress(remoteAddress).build(); threadContext = new ThreadContext(Settings.EMPTY); + firstDomain = randomFrom(new RealmDomain("firstDomain", Set.of()), null); firstRealm = mock(Realm.class); when(firstRealm.type()).thenReturn(FIRST_REALM_TYPE); when(firstRealm.name()).thenReturn(FIRST_REALM_NAME); when(firstRealm.toString()).thenReturn(FIRST_REALM_NAME + "/" + FIRST_REALM_TYPE); + when(firstRealm.realmRef()).thenReturn(new RealmRef(FIRST_REALM_NAME, FIRST_REALM_TYPE, "authc_test", firstDomain)); + secondDomain = randomFrom(new RealmDomain("secondDomain", Set.of()), null); secondRealm = mock(Realm.class); when(secondRealm.type()).thenReturn(SECOND_REALM_TYPE); when(secondRealm.name()).thenReturn(SECOND_REALM_NAME); when(secondRealm.toString()).thenReturn(SECOND_REALM_NAME + "/" + SECOND_REALM_TYPE); + when(secondRealm.realmRef()).thenReturn(new RealmRef(SECOND_REALM_NAME, SECOND_REALM_TYPE, "authc_test", secondDomain)); Settings settings = Settings.builder() .put("path.home", createTempDir()) .put("node.name", "authc_test") @@ -384,6 +392,7 @@ public void testTokenFirstMissingSecondFound() throws Exception { service.authenticate("action", transportRequest, true, ActionListener.wrap(authentication -> { assertThat(threadContext.getTransient(AuthenticationResult.THREAD_CONTEXT_KEY), is(authenticationResult)); assertThat(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY), is(authentication)); + assertThat(authentication.getDomain(), is(secondDomain)); verify(auditTrail).authenticationSuccess(anyString(), eq(authentication), eq("action"), eq(transportRequest)); setCompletedToTrue(completed); }, this::logAndFail)); @@ -519,6 +528,7 @@ public void testAuthenticateSmartRealmOrdering() { assertThat(result.getAuthenticatedBy(), is(notNullValue())); // TODO implement equals assertThat(result.getAuthenticatedBy().getName(), is(SECOND_REALM_NAME)); assertThat(result.getAuthenticatedBy().getType(), is(SECOND_REALM_TYPE)); + assertThat(result.getAuthenticatedBy().getDomain(), is(secondDomain)); assertThreadContextContainsAuthentication(result); verify(auditTrail).authenticationSuccess(reqId.get(), result, "_action", transportRequest); setCompletedToTrue(completed); @@ -539,6 +549,7 @@ public void testAuthenticateSmartRealmOrdering() { assertThat(result.getAuthenticatedBy(), is(notNullValue())); // TODO implement equals assertThat(result.getAuthenticatedBy().getName(), is(SECOND_REALM_NAME)); assertThat(result.getAuthenticatedBy().getType(), is(SECOND_REALM_TYPE)); + assertThat(result.getAuthenticatedBy().getDomain(), is(secondDomain)); assertThreadContextContainsAuthentication(result); verify(auditTrail, times(2)).authenticationSuccess(reqId.get(), result, "_action", transportRequest); setCompletedToTrue(completed); @@ -547,8 +558,7 @@ public void testAuthenticateSmartRealmOrdering() { verify(auditTrail).authenticationFailed(reqId.get(), firstRealm.name(), token, "_action", transportRequest); verify(firstRealm, times(2)).name(); // used above one time - verify(secondRealm, Mockito.atLeast(2)).name(); // also used in license tracking - verify(secondRealm, Mockito.atLeast(2)).type(); // used to create realm ref, and license tracking + verify(secondRealm, times(2)).realmRef(); // also used in license tracking verify(firstRealm, times(2)).token(threadContext); verify(secondRealm, times(2)).token(threadContext); verify(firstRealm).supports(token); @@ -573,6 +583,7 @@ public void testAuthenticateSmartRealmOrdering() { assertThat(result.getAuthenticatedBy(), is(notNullValue())); assertThat(result.getAuthenticatedBy().getName(), is(FIRST_REALM_NAME)); assertThat(result.getAuthenticatedBy().getType(), is(FIRST_REALM_TYPE)); + assertThat(result.getAuthenticatedBy().getDomain(), is(firstDomain)); assertThreadContextContainsAuthentication(result); verify(auditTrail).authenticationSuccess(reqId.get(), result, "_action", transportRequest); setCompletedToTrue(completed); @@ -658,6 +669,7 @@ public void testAuthenticateSmartRealmOrderingDisabled() { assertThat(result.getUser(), is(user)); assertThat(result.getLookedUpBy(), is(nullValue())); assertThat(result.getAuthenticatedBy().getName(), is(SECOND_REALM_NAME)); // TODO implement equals + assertThat(result.getAuthenticatedBy().getDomain(), is(secondDomain)); assertThreadContextContainsAuthentication(result); verify(auditTrail).authenticationSuccess(reqId.get(), result, "_action", transportRequest); setCompletedToTrue(completed); @@ -673,6 +685,7 @@ public void testAuthenticateSmartRealmOrderingDisabled() { assertThat(result.getUser(), is(user)); assertThat(result.getLookedUpBy(), is(nullValue())); assertThat(result.getAuthenticatedBy().getName(), is(SECOND_REALM_NAME)); // TODO implement equals + assertThat(result.getAuthenticatedBy().getDomain(), is(secondDomain)); assertThreadContextContainsAuthentication(result); verify(auditTrail, times(2)).authenticationSuccess(reqId.get(), result, "_action", transportRequest); setCompletedToTrue(completed); @@ -680,8 +693,7 @@ public void testAuthenticateSmartRealmOrderingDisabled() { }, this::logAndFail)); verify(auditTrail, times(2)).authenticationFailed(reqId.get(), firstRealm.name(), token, "_action", transportRequest); verify(firstRealm, times(3)).name(); // used above one time - verify(secondRealm, Mockito.atLeast(2)).name(); - verify(secondRealm, Mockito.atLeast(2)).type(); // used to create realm ref + verify(secondRealm, times(2)).realmRef(); verify(firstRealm, times(2)).token(threadContext); verify(secondRealm, times(2)).token(threadContext); verify(firstRealm, times(2)).supports(token); @@ -714,6 +726,7 @@ public void testAuthenticateFirstNotSupportingSecondSucceeds() throws Exception assertThat(result.getUser(), is(user)); assertThat(result.getAuthenticationType(), is(AuthenticationType.REALM)); assertThat(result.getAuthenticatedBy().getName(), is(secondRealm.name())); // TODO implement equals + assertThat(result.getAuthenticatedBy().getDomain(), is(secondDomain)); assertThat(result.getAuthenticationType(), is(AuthenticationType.REALM)); assertThreadContextContainsAuthentication(result); verify(auditTrail).authenticationSuccess(reqId.get(), result, "_action", transportRequest); @@ -726,7 +739,11 @@ public void testAuthenticateFirstNotSupportingSecondSucceeds() throws Exception } public void testAuthenticateCached() throws Exception { - final Authentication authentication = new Authentication(new User("_username", "r1"), new RealmRef("test", "cached", "foo"), null); + final Authentication authentication = new Authentication( + new User("_username", "r1"), + new RealmRef("test", "cached", "foo", randomFrom(new RealmDomain("", Set.of()), null)), + null + ); authentication.writeToContext(threadContext); boolean requestIdAlreadyPresent = randomBoolean(); SetOnce reqId = new SetOnce<>(); @@ -921,6 +938,7 @@ public void testAuthenticateTransportSuccess() throws Exception { assertThat(result, notNullValue()); assertThat(result.getUser(), sameInstance(user)); assertThat(result.getAuthenticationType(), is(AuthenticationType.REALM)); + assertThat(result.getAuthenticatedBy().getDomain(), is(firstDomain)); assertThat(result.getAuthenticatedBy().getName(), is(firstRealm.name())); // TODO implement equals assertThreadContextContainsAuthentication(result); verify(auditTrail).authenticationSuccess(reqId.get(), result, "_action", transportRequest); @@ -944,6 +962,7 @@ public void testAuthenticateRestSuccess() throws Exception { assertThat(authentication.getUser(), sameInstance(user1)); assertThat(authentication.getAuthenticationType(), is(AuthenticationType.REALM)); assertThat(authentication.getAuthenticatedBy().getName(), is(firstRealm.name())); // TODO implement equals + assertThat(authentication.getAuthenticatedBy().getDomain(), is(firstDomain)); // TODO implement equals assertThreadContextContainsAuthentication(authentication); String reqId = expectAuditRequestId(threadContext); verify(auditTrail).authenticationSuccess(reqId, authentication, restRequest); @@ -977,6 +996,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { assertThat(authentication, notNullValue()); assertThat(authentication.getUser(), sameInstance(user1)); assertThat(authentication.getAuthenticationType(), is(AuthenticationType.REALM)); + assertThat(authentication.getDomain(), is(firstDomain)); assertThreadContextContainsAuthentication(authentication); authRef.set(authentication); authHeaderRef.set(threadContext.getHeader(AuthenticationField.AUTHENTICATION_KEY)); @@ -1254,6 +1274,7 @@ public void testAnonymousUserRest() throws Exception { assertThat(result, notNullValue()); assertThat(result.v1().getUser(), sameInstance((Object) anonymousUser)); assertThat(result.v1().getAuthenticationType(), is(AuthenticationType.ANONYMOUS)); + assertThat(result.v1().getDomain(), nullValue()); assertThreadContextContainsAuthentication(result.v1()); assertThat(expectAuditRequestId(threadContext), is(result.v2())); verify(auditTrail).authenticationSuccess(result.v2(), result.v1(), request); @@ -1328,6 +1349,7 @@ public void testAnonymousUserTransportNoDefaultUser() throws Exception { assertThat(expectAuditRequestId(threadContext), is(result.v2())); assertThat(result.v1().getUser(), sameInstance(anonymousUser)); assertThat(result.v1().getAuthenticationType(), is(AuthenticationType.ANONYMOUS)); + assertThat(result.v1().getDomain(), nullValue()); assertThreadContextContainsAuthentication(result.v1()); verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result.v1()), eq(threadContext)); }); @@ -1364,6 +1386,7 @@ public void testAnonymousUserTransportWithDefaultUser() throws Exception { assertThat(expectAuditRequestId(threadContext), is(result.v2())); assertThat(result.v1().getUser(), sameInstance(SystemUser.INSTANCE)); assertThat(result.v1().getAuthenticationType(), is(AuthenticationType.INTERNAL)); + assertThat(result.v1().getDomain(), nullValue()); assertThreadContextContainsAuthentication(result.v1()); verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result.v1()), eq(threadContext)); }); @@ -1889,7 +1912,11 @@ public void testInvalidToken() throws Exception { final byte[] randomBytes = new byte[numBytes]; random().nextBytes(randomBytes); final CountDownLatch latch = new CountDownLatch(1); - final Authentication expected = new Authentication(user, new RealmRef(firstRealm.name(), firstRealm.type(), "authc_test"), null); + final Authentication expected = new Authentication( + user, + new RealmRef(firstRealm.name(), firstRealm.type(), "authc_test", firstDomain), + null + ); AtomicBoolean success = new AtomicBoolean(false); boolean requestIdAlreadyPresent = randomBoolean(); SetOnce reqId = new SetOnce<>(); @@ -2097,6 +2124,7 @@ public void testApiKeyAuth() { assertThat(result.v1().getUser().fullName(), is("john doe")); assertThat(result.v1().getUser().email(), is("john@doe.com")); assertThat(result.v1().getAuthenticationType(), is(AuthenticationType.API_KEY)); + assertThat(result.v1().getDomain(), nullValue()); verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result.v1()), eq(threadContext)); }); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsAuthenticatorTests.java index b3a4642dfd203..3eba690b5d5dd 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsAuthenticatorTests.java @@ -25,10 +25,12 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authc.RealmDomain; import org.elasticsearch.xpack.core.security.user.User; import org.junit.Before; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import static org.hamcrest.Matchers.equalTo; @@ -46,8 +48,11 @@ public class RealmsAuthenticatorTests extends ESTestCase { private ThreadContext threadContext; private Realms realms; + private RealmDomain domain1; private Realm realm1; + private RealmDomain domain2; private Realm realm2; + private RealmDomain domain3; private Realm realm3; private AuthenticationService.AuditableRequest request; private AuthenticationToken authenticationToken; @@ -62,20 +67,28 @@ public class RealmsAuthenticatorTests extends ESTestCase { @Before public void init() throws Exception { threadContext = new ThreadContext(Settings.EMPTY); + nodeName = randomAlphaOfLength(8); realms = mock(Realms.class); + + domain1 = randomFrom(new RealmDomain("domain1", Set.of()), null); realm1 = mock(Realm.class); when(realm1.name()).thenReturn("realm1"); when(realm1.type()).thenReturn("realm1"); when(realm1.toString()).thenReturn("realm1/realm1"); + when(realm1.realmRef()).thenReturn(new Authentication.RealmRef("realm1", "realm1", nodeName, domain1)); + domain2 = randomFrom(new RealmDomain("domain2", Set.of()), null); realm2 = mock(Realm.class); when(realm2.name()).thenReturn("realm2"); when(realm2.type()).thenReturn("realm2"); when(realm2.toString()).thenReturn("realm2/realm2"); + when(realm2.realmRef()).thenReturn(new Authentication.RealmRef("realm2", "realm2", nodeName, domain2)); + domain3 = randomFrom(new RealmDomain("domain3", Set.of()), null); realm3 = mock(Realm.class); when(realm3.toString()).thenReturn("realm3/realm3"); when(realms.getActiveRealms()).thenReturn(List.of(realm1, realm2)); when(realms.getUnlicensedRealms()).thenReturn(List.of(realm3)); + when(realm3.realmRef()).thenReturn(new Authentication.RealmRef("realm3", "realm3", nodeName, domain3)); request = randomBoolean() ? mock(AuthenticationService.AuditableRestRequest.class) @@ -85,10 +98,9 @@ public void init() throws Exception { when(authenticationToken.principal()).thenReturn(username); user = new User(username); - nodeName = randomAlphaOfLength(8); numInvalidation = new AtomicLong(); lastSuccessfulAuthCache = mock(Cache.class); - realmsAuthenticator = new RealmsAuthenticator(nodeName, numInvalidation, lastSuccessfulAuthCache); + realmsAuthenticator = new RealmsAuthenticator(numInvalidation, lastSuccessfulAuthCache); } public void testExtractCredentials() { @@ -151,7 +163,14 @@ public void testAuthenticate() { assertThat(authentication.getUser(), is(user)); assertThat( authentication.getAuthenticatedBy(), - equalTo(new Authentication.RealmRef(successfulRealm.name(), successfulRealm.type(), nodeName)) + is( + new Authentication.RealmRef( + successfulRealm.name(), + successfulRealm.type(), + nodeName, + successfulRealm.realmRef().getDomain() + ) + ) ); } @@ -211,20 +230,18 @@ public void testLookupRunAsUser() { } final Realm authRealm = randomFrom(realm1, realm2); - final Authentication authentication = new Authentication( - user, - new Authentication.RealmRef(authRealm.name(), authRealm.type(), nodeName), - null - ); - final PlainActionFuture> future = new PlainActionFuture<>(); + + final Authentication authentication = Authentication.newRealmAuthentication(user, authRealm.realmRef()); + final PlainActionFuture> future = new PlainActionFuture<>(); realmsAuthenticator.lookupRunAsUser(createAuthenticatorContext(), authentication, future); - final Tuple tuple = future.actionGet(); + final Tuple tuple = future.actionGet(); assertThat(tuple.v1(), equalTo(new User(runAsUsername))); - assertThat(tuple.v2().getName(), is(lookupByRealm1 ? realm1.name() : realm2.name())); + assertThat(tuple.v2().name(), is(lookupByRealm1 ? realm1.name() : realm2.name())); + assertThat(tuple.v2().realmRef(), is(lookupByRealm1 ? realm1.realmRef() : realm2.realmRef())); } public void testNullRunAsUser() { - final PlainActionFuture> future = new PlainActionFuture<>(); + final PlainActionFuture> future = new PlainActionFuture<>(); realmsAuthenticator.lookupRunAsUser(createAuthenticatorContext(), mock(Authentication.class), future); assertThat(future.actionGet(), nullValue()); } @@ -232,12 +249,8 @@ public void testNullRunAsUser() { public void testEmptyRunAsUsernameWillFail() { threadContext.putHeader(AuthenticationServiceField.RUN_AS_USER_HEADER, ""); final Realm authRealm = randomFrom(realm1, realm2); - final Authentication authentication = new Authentication( - user, - new Authentication.RealmRef(authRealm.name(), authRealm.type(), nodeName), - null - ); - final PlainActionFuture> future = new PlainActionFuture<>(); + final Authentication authentication = Authentication.newRealmAuthentication(user, authRealm.realmRef()); + final PlainActionFuture> future = new PlainActionFuture<>(); final ElasticsearchSecurityException e = new ElasticsearchSecurityException("fail"); when(request.runAsDenied(any(), any())).thenReturn(e); realmsAuthenticator.lookupRunAsUser(createAuthenticatorContext(), authentication, future); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java index 31c7e27c3a3b6..ae6fbd1ec8f6a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java @@ -22,13 +22,17 @@ import org.elasticsearch.license.LicenseStateListener; import org.elasticsearch.license.LicensedFeature; import org.elasticsearch.license.MockLicenseState; +import org.elasticsearch.node.Node; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.MockLogAppender; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.security.authc.Authentication; 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.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.RealmDomain; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; @@ -37,8 +41,10 @@ import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings; +import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.Security; +import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.junit.Before; import org.mockito.Mockito; @@ -114,11 +120,18 @@ public void init() throws Exception { }).when(licenseState).addListener(Mockito.any(LicenseStateListener.class)); threadContext = new ThreadContext(Settings.EMPTY); - reservedRealm = mock(ReservedRealm.class); + + ThreadPool threadPool = mock(ThreadPool.class); + when(threadPool.getThreadContext()).thenReturn(threadContext); + + reservedRealm = new ReservedRealm( + mock(Environment.class), + Settings.EMPTY, + mock(NativeUsersStore.class), + new AnonymousUser(Settings.EMPTY), + threadPool + ); allowAllRealms(); - when(reservedRealm.type()).thenReturn(ReservedRealm.TYPE); - when(reservedRealm.name()).thenReturn("reserved"); - when(reservedRealm.order()).thenReturn(Integer.MIN_VALUE); } private void allowAllRealms() { @@ -214,7 +227,6 @@ public void testWithSettings() throws Exception { int index = orderToIndex.get(i); assertThat(realm.type(), equalTo("type_" + index)); assertThat(realm.name(), equalTo("realm_" + index)); - assertThat(realm.domain(), nullValue()); i++; if (i == randomRealmTypesCount) { break; @@ -253,29 +265,34 @@ public void testDomainAssignment() throws Exception { } final String nativeRealmDomain = randomFrom(domains); + Map> realmsForDomain = new HashMap<>(); for (String domain : domains) { if (domain != null) { - List realmsForDomain = new ArrayList<>(); + realmsForDomain.computeIfAbsent(domain, k -> new HashSet<>()); for (Map.Entry indexAndDomain : indexToDomain.entrySet()) { if (domain.equals(indexAndDomain.getValue())) { - realmsForDomain.add("realm_" + indexAndDomain.getKey()); + realmsForDomain.get(domain) + .add(new RealmConfig.RealmIdentifier("type_" + indexAndDomain.getKey(), "realm_" + indexAndDomain.getKey())); } } if (domain.equals(fileRealmDomain)) { - realmsForDomain.add("default_file"); + realmsForDomain.get(domain).add(new RealmConfig.RealmIdentifier("file", "default_file")); } if (domain.equals(nativeRealmDomain)) { - realmsForDomain.add("default_native"); + realmsForDomain.get(domain).add(new RealmConfig.RealmIdentifier("native", "default_native")); } - if (false == realmsForDomain.isEmpty() || randomBoolean()) { + if (false == realmsForDomain.get(domain).isEmpty() || randomBoolean()) { builder.put( "xpack.security.authc.domains." + domain + ".realms", - realmsForDomain.stream().collect(Collectors.joining(", ")) + realmsForDomain.get(domain).stream().map(RealmConfig.RealmIdentifier::getName).collect(Collectors.joining(", ")) ); } } } + String nodeName = randomAlphaOfLength(10); + builder.put(Node.NODE_NAME_SETTING.getKey(), nodeName); + Settings settings = builder.build(); Environment env = TestEnvironment.newEnvironment(settings); Realms realms = new Realms(settings, env, factories, licenseState, threadContext, reservedRealm); @@ -284,30 +301,20 @@ public void testDomainAssignment() throws Exception { assertThat(iterator.hasNext(), is(true)); Realm realm = iterator.next(); assertThat(realm, is(reservedRealm)); - assertThat(realm.domain(), nullValue()); - if (false == fileRealmDisabled && false == nativeRealmDisabled) { + assertThat(reservedRealm.realmRef().getDomain(), nullValue()); + if (false == fileRealmDisabled) { assertTrue(iterator.hasNext()); realm = iterator.next(); assertThat(realm.type(), is(FileRealmSettings.TYPE)); assertThat(realm.name(), is(FileRealmSettings.DEFAULT_NAME)); - assertThat(realm.domain(), is(fileRealmDomain)); - assertTrue(iterator.hasNext()); - realm = iterator.next(); - assertThat(realm.type(), is(NativeRealmSettings.TYPE)); - assertThat(realm.name(), is(NativeRealmSettings.DEFAULT_NAME)); - assertThat(realm.domain(), is(nativeRealmDomain)); - } else if (false == fileRealmDisabled) { - assertTrue(iterator.hasNext()); - realm = iterator.next(); - assertThat(realm.type(), is(FileRealmSettings.TYPE)); - assertThat(realm.name(), is(FileRealmSettings.DEFAULT_NAME)); - assertThat(realm.domain(), is(fileRealmDomain)); - } else if (false == nativeRealmDisabled) { + assertDomainForRealm(realm, nodeName, realmsForDomain); + } + if (false == nativeRealmDisabled) { assertTrue(iterator.hasNext()); realm = iterator.next(); assertThat(realm.type(), is(NativeRealmSettings.TYPE)); assertThat(realm.name(), is(NativeRealmSettings.DEFAULT_NAME)); - assertThat(realm.domain(), is(nativeRealmDomain)); + assertDomainForRealm(realm, nodeName, realmsForDomain); } while (iterator.hasNext()) { @@ -317,8 +324,19 @@ public void testDomainAssignment() throws Exception { int index = orderToIndex.get(realm.order()); assertThat(realm.type(), is("type_" + index)); assertThat(realm.name(), is("realm_" + index)); - assertThat(realm.domain(), is(indexToDomain.get(index))); + assertDomainForRealm(realm, nodeName, realmsForDomain); + } + } + + private void assertDomainForRealm(Realm realm, String nodeName, Map> realmsByDomainName) { + for (var domainRealmIds : realmsByDomainName.entrySet()) { + if (domainRealmIds.getValue().contains(new RealmConfig.RealmIdentifier(realm.type(), realm.name()))) { + RealmDomain domain = new RealmDomain(domainRealmIds.getKey(), domainRealmIds.getValue()); + assertThat(realm.realmRef(), is(new Authentication.RealmRef(realm.name(), realm.type(), nodeName, domain))); + return; + } } + assertThat(realm.realmRef(), is(new Authentication.RealmRef(realm.name(), realm.type(), nodeName, null))); } public void testRealmAssignedToMultipleDomainsNotPermitted() { @@ -362,11 +380,10 @@ public void testReservedRealmIsAlwaysDomainless() throws Exception { assertThat(iterator.hasNext(), is(true)); Realm realm = iterator.next(); assertThat(realm, is(reservedRealm)); - assertThat(realm.domain(), nullValue()); realm = iterator.next(); assertThat(realm.type(), is("type_" + realmId)); assertThat(realm.name(), is("reserved")); - assertThat(realm.domain(), is("domain_reserved")); + assertThat(realm.realmRef().getDomain().name(), is("domain_reserved")); } public void testDomainWithUndefinedRealms() { @@ -470,17 +487,17 @@ public void testWithEmptySettings() throws Exception { assertThat(iter.hasNext(), is(true)); Realm realm = iter.next(); assertThat(realm, is(reservedRealm)); - assertThat(realm.domain(), nullValue()); + assertThat(realm.realmRef().getDomain(), nullValue()); assertThat(iter.hasNext(), is(true)); realm = iter.next(); assertThat(realm.type(), equalTo(FileRealmSettings.TYPE)); assertThat(realm.name(), equalTo("default_" + FileRealmSettings.TYPE)); - assertThat(realm.domain(), nullValue()); + assertThat(realm.realmRef().getDomain(), nullValue()); assertThat(iter.hasNext(), is(true)); realm = iter.next(); assertThat(realm.type(), equalTo(NativeRealmSettings.TYPE)); assertThat(realm.name(), equalTo("default_" + NativeRealmSettings.TYPE)); - assertThat(realm.domain(), nullValue()); + assertThat(realm.realmRef().getDomain(), nullValue()); assertThat(iter.hasNext(), is(false)); assertThat(realms.getUnlicensedRealms(), empty()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DummyUsernamePasswordRealm.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DummyUsernamePasswordRealm.java index 8704f990c05e4..b110552788eba 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DummyUsernamePasswordRealm.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DummyUsernamePasswordRealm.java @@ -25,6 +25,7 @@ public class DummyUsernamePasswordRealm extends UsernamePasswordRealm { public DummyUsernamePasswordRealm(RealmConfig config) { super(config); + initRealmRef(null); this.users = new HashMap<>(); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java index db807211187b5..82fc5a4a6cba4 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java @@ -84,7 +84,6 @@ public class SecondaryAuthenticatorTests extends ESTestCase { private SecurityContext securityContext; private TokenService tokenService; private Client client; - private OperatorPrivileges operatorPrivileges; @Before public void setupMocks() throws Exception { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptorTests.java index cee8270874278..372b06833633d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptorTests.java @@ -57,8 +57,8 @@ public void testInterceptorThrowsWhenFLSDLSEnabled() { AuditTrailService auditTrailService = new AuditTrailService(Collections.emptyList(), licenseState); Authentication authentication = new Authentication( new User("john", "role"), - new RealmRef(null, null, null), - new RealmRef(null, null, null) + new RealmRef("auth_name", "auth_type", "node"), + new RealmRef("look_name", "look_type", "node") ); final FieldPermissions fieldPermissions; final boolean useFls = randomBoolean(); @@ -127,8 +127,8 @@ public void testInterceptorThrowsWhenTargetHasGreaterPermissions() throws Except AuditTrailService auditTrailService = new AuditTrailService(Collections.emptyList(), licenseState); Authentication authentication = new Authentication( new User("john", "role"), - new RealmRef(null, null, null), - new RealmRef(null, null, null) + new RealmRef("auth_name", "auth_type", "node"), + new RealmRef("look_name", "look_type", "node") ); final String action = IndicesAliasesAction.NAME; IndicesAccessControl accessControl = new IndicesAccessControl(true, Collections.emptyMap()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptorTests.java index cfb10b19382bc..27fa49ba3f984 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptorTests.java @@ -62,7 +62,11 @@ public void testResizeRequestInterceptorThrowsWhenFLSDLSEnabled() { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext); AuditTrailService auditTrailService = new AuditTrailService(Collections.emptyList(), licenseState); - final Authentication authentication = new Authentication(new User("john", "role"), new RealmRef(null, null, null), null); + final Authentication authentication = new Authentication( + new User("john", "role"), + new RealmRef("realm", "type", "node", null), + null + ); final FieldPermissions fieldPermissions; final boolean useFls = randomBoolean(); if (useFls) { @@ -122,7 +126,11 @@ public void testResizeRequestInterceptorThrowsWhenTargetHasGreaterPermissions() ThreadContext threadContext = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext); AuditTrailService auditTrailService = new AuditTrailService(Collections.emptyList(), licenseState); - final Authentication authentication = new Authentication(new User("john", "role"), new RealmRef(null, null, null), null); + final Authentication authentication = new Authentication( + new User("john", "role"), + new RealmRef("realm", "type", "node", null), + null + ); Role role = Role.builder(Automatons.EMPTY).add(IndexPrivilege.ALL, "target").add(IndexPrivilege.READ, "source").build(); final String action = randomFrom(ShrinkAction.NAME, ResizeAction.NAME); IndicesAccessControl accessControl = new IndicesAccessControl(true, Collections.emptyMap()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java index 9a2ef77815056..883c2d4464c64 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java @@ -124,7 +124,7 @@ public void sendRequest( assertTrue(calledWrappedSender.get()); assertEquals(user, sendingUser.get()); assertEquals(user, securityContext.getUser()); - verify(securityContext, never()).executeAsUser(any(User.class), anyConsumer(), any(Version.class)); + verify(securityContext, never()).executeAsInternalUser(any(User.class), any(Version.class), anyConsumer()); } public void testSendAsyncSwitchToSystem() throws Exception { @@ -173,7 +173,7 @@ public void sendRequest( assertNotEquals(user, sendingUser.get()); assertEquals(SystemUser.INSTANCE, sendingUser.get()); assertEquals(user, securityContext.getUser()); - verify(securityContext).executeAsUser(any(User.class), anyConsumer(), eq(Version.CURRENT)); + verify(securityContext).executeAsInternalUser(any(User.class), eq(Version.CURRENT), anyConsumer()); } public void testSendWithoutUser() throws Exception { @@ -216,7 +216,7 @@ public void sendRequest( ); assertEquals("there should always be a user when sending a message for action [indices:foo]", e.getMessage()); assertNull(securityContext.getUser()); - verify(securityContext, never()).executeAsUser(any(User.class), anyConsumer(), any(Version.class)); + verify(securityContext, never()).executeAsInternalUser(any(User.class), any(Version.class), anyConsumer()); } public void testSendToNewerVersionSetsCorrectVersion() throws Exception {