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 5428072d1d5ec..2a5508eab9878 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 @@ -10,7 +10,6 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.Assertions; import org.elasticsearch.Version; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; @@ -40,7 +39,6 @@ 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.List; @@ -65,76 +63,73 @@ import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.FALLBACK_REALM_TYPE; import static org.elasticsearch.xpack.core.security.authc.RealmDomain.REALM_DOMAIN_PARSER; -// 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 final 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; - private final RealmRef lookedUpBy; - private final Version version; private final AuthenticationType type; - private final Map metadata; // authentication contains metadata, includes api_key details (including api_key metadata) - private final Subject authenticatingSubject; private final Subject effectiveSubject; - private Authentication( - User user, - RealmRef authenticatedBy, - RealmRef lookedUpBy, - Version version, - AuthenticationType type, - Map metadata - ) { - this.user = Objects.requireNonNull(user); - this.authenticatedBy = Objects.requireNonNull(authenticatedBy); - this.lookedUpBy = lookedUpBy; - this.version = version; + private Authentication(Subject subject, AuthenticationType type) { + this(subject, subject, type); + } + + private Authentication(Subject effectiveSubject, Subject authenticatingSubject, AuthenticationType type) { + this.effectiveSubject = effectiveSubject; + this.authenticatingSubject = authenticatingSubject; this.type = type; - this.metadata = metadata; - if (user instanceof RunAsUser runAsUser) { - authenticatingSubject = new Subject(runAsUser.authenticatingUser, authenticatedBy, version, metadata); - // The lookup user for run-as currently doesn't have authentication metadata associated with them because - // lookupUser only returns the User object. The lookup user for authorization delegation does have - // authentication metadata, but the realm does not expose this difference between authenticatingUser and - // delegateUser so effectively this is handled together with the authenticatingSubject not effectiveSubject. - effectiveSubject = new Subject(user, lookedUpBy, version, Map.of()); - } else { - authenticatingSubject = effectiveSubject = new Subject(user, authenticatedBy, version, metadata); - } - this.assertApiKeyMetadata(); - this.assertDomainAssignment(); + assertInternalConsistency(); } public Authentication(StreamInput in) throws IOException { - this.user = AuthenticationSerializationHelper.readUserFrom(in); - this.authenticatedBy = new RealmRef(in); + // Read the user(s) + final User outerUser = AuthenticationSerializationHelper.readUserWithoutTrailingBoolean(in); + final boolean hasInnerUser; + if (User.isInternal(outerUser)) { + hasInnerUser = false; + } else { + hasInnerUser = in.readBoolean(); + } + final User innerUser; + if (hasInnerUser) { + innerUser = AuthenticationSerializationHelper.readUserFrom(in); + assert false == User.isInternal(innerUser) : "internal users cannot participate in run-as"; + } else { + innerUser = null; + } + + final RealmRef authenticatedBy = new RealmRef(in); + final RealmRef lookedUpBy; if (in.readBoolean()) { - this.lookedUpBy = new RealmRef(in); + lookedUpBy = new RealmRef(in); } else { - this.lookedUpBy = null; + lookedUpBy = null; } - this.version = in.getVersion(); + + // The valid combinations for innerUser and lookedUpBy are: + // 1. InnerUser == null -> no run-as -> lookedUpBy must be null as well + // 2. innerUser != null -> lookedUp by can be either null (failed run-as lookup) or non-null (successful lookup) + // 3. lookedUpBy == null -> innerUser can be either null (no run-as) or non-null (failed run-as lookup) + // 4. lookedUpBy != null -> successful run-as -> innerUser must be NOT null + assert innerUser != null || lookedUpBy == null : "Authentication has no inner-user, but looked-up-by is [" + lookedUpBy + "]"; + + final Version version = in.getVersion(); type = AuthenticationType.values()[in.readVInt()]; - metadata = in.readMap(); - if (user instanceof RunAsUser runAsUser) { - authenticatingSubject = new Subject(runAsUser.authenticatingUser, authenticatedBy, version, metadata); + final Map metadata = in.readMap(); + if (innerUser != null) { + authenticatingSubject = new Subject(innerUser, authenticatedBy, version, metadata); // The lookup user for run-as currently doesn't have authentication metadata associated with them because // lookupUser only returns the User object. The lookup user for authorization delegation does have // authentication metadata, but the realm does not expose this difference between authenticatingUser and // delegateUser so effectively this is handled together with the authenticatingSubject not effectiveSubject. - effectiveSubject = new Subject(user, lookedUpBy, version, Map.of()); + effectiveSubject = new Subject(outerUser, lookedUpBy, version, Map.of()); } else { - authenticatingSubject = effectiveSubject = new Subject(user, authenticatedBy, version, metadata); + authenticatingSubject = effectiveSubject = new Subject(outerUser, authenticatedBy, version, metadata); } - this.assertApiKeyMetadata(); - this.assertDomainAssignment(); + assertInternalConsistency(); } /** @@ -152,6 +147,10 @@ public Subject getEffectiveSubject() { return effectiveSubject; } + public AuthenticationType getAuthenticationType() { + return type; + } + /** * Whether the authentication contains a subject run-as another subject. That is, the authentication subject * is different from the effective subject. @@ -165,7 +164,7 @@ public boolean isRunAs() { */ @Deprecated public User getUser() { - return user; + return effectiveSubject.getUser(); } /** @@ -173,7 +172,7 @@ public User getUser() { */ @Deprecated public RealmRef getAuthenticatedBy() { - return authenticatedBy; + return authenticatingSubject.getRealm(); } /** @@ -182,7 +181,12 @@ public RealmRef getAuthenticatedBy() { */ @Deprecated public RealmRef getLookedUpBy() { - return lookedUpBy; + if (isRunAs()) { + return effectiveSubject.getRealm(); + } else { + // retain the behaviour of returning null for lookup realm for if the authentication is not run-as + return null; + } } /** @@ -193,7 +197,14 @@ public RealmRef getLookedUpBy() { */ @Deprecated public RealmRef getSourceRealm() { - return lookedUpBy == null ? authenticatedBy : lookedUpBy; + // TODO: This code retains the existing behaviour which is slightly wrong because + // when run-as lookup fails, the effectiveSubject will have a null realm. In this + // case, the code returns the authenticatingSubject's realm. This is wrong in theory + // because it is not the intention of this method. In practice, it does not matter + // because failed lookup will be rejected at authZ time. But fixing it causes test + // failures. So leave it for now. + final RealmRef sourceRealm = effectiveSubject.getRealm(); + return sourceRealm == null ? authenticatingSubject.getRealm() : sourceRealm; } /** @@ -203,17 +214,20 @@ public RealmRef getSourceRealm() { * 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. + * + * Use {@code getEffectiveSubject().getVersion()} instead. */ + @Deprecated public Version getVersion() { - return version; - } - - public AuthenticationType getAuthenticationType() { - return type; + return effectiveSubject.getVersion(); } + /** + * Use {@code getAuthenticatingSubject().getMetadata()} instead. + */ + @Deprecated public Map getMetadata() { - return metadata; + return authenticatingSubject.getMetadata(); } /** @@ -223,14 +237,42 @@ public Map getMetadata() { 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) - ); + + final Map newMetadata = maybeRewriteMetadataForApiKeyRoleDescriptors(olderVersion, this); + + final Authentication newAuthentication; + if (isRunAs()) { + // The lookup user for run-as currently doesn't have authentication metadata associated with them because + // lookupUser only returns the User object. The lookup user for authorization delegation does have + // authentication metadata, but the realm does not expose this difference between authenticatingUser and + // delegateUser so effectively this is handled together with the authenticatingSubject not effectiveSubject. + newAuthentication = new Authentication( + new Subject( + effectiveSubject.getUser(), + maybeRewriteRealmRef(olderVersion, effectiveSubject.getRealm()), + olderVersion, + effectiveSubject.getMetadata() + ), + new Subject( + authenticatingSubject.getUser(), + maybeRewriteRealmRef(olderVersion, authenticatingSubject.getRealm()), + olderVersion, + newMetadata + ), + type + ); + } else { + newAuthentication = new Authentication( + new Subject( + authenticatingSubject.getUser(), + maybeRewriteRealmRef(olderVersion, authenticatingSubject.getRealm()), + olderVersion, + newMetadata + ), + type + ); + + } if (isAssignedToDomain() && false == newAuthentication.isAssignedToDomain()) { logger.info("Rewriting authentication [" + this + "] without domain"); } @@ -240,22 +282,19 @@ public Authentication maybeRewriteForOlderVersion(Version olderVersion) { /** * 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. + * + * @param runAs The user to be impersonated + * @param lookupRealmRef The realm where the impersonated user is looked up from. It can be null if the user does + * not exist. The null lookup realm is used to indicate the lookup failure which will be rejected + * at authorization time. */ public Authentication runAs(User runAs, @Nullable RealmRef lookupRealmRef) { assert supportsRunAs(null); - assert false == runAs instanceof RunAsUser; assert false == runAs instanceof AnonymousUser; assert false == hasSyntheticRealmNameOrType(lookupRealmRef) : "should not use synthetic realm name/type for lookup realms"; Objects.requireNonNull(runAs); - return new Authentication( - new RunAsUser(runAs, getUser()), - getAuthenticatedBy(), - lookupRealmRef, - getVersion(), - getAuthenticationType(), - getMetadata() - ); + return new Authentication(new Subject(runAs, lookupRealmRef, getVersion(), Map.of()), authenticatingSubject, type); } /** Returns a new {@code Authentication} for tokens created by the current {@code Authentication}, which is used when @@ -263,14 +302,7 @@ public Authentication runAs(User runAs, @Nullable RealmRef lookupRealmRef) { */ public Authentication token() { assert false == isServiceAccount(); - final Authentication newTokenAuthentication = new Authentication( - getUser(), - getAuthenticatedBy(), - getLookedUpBy(), - getVersion(), - AuthenticationType.TOKEN, - getMetadata() - ); + final Authentication newTokenAuthentication = new Authentication(effectiveSubject, authenticatingSubject, AuthenticationType.TOKEN); assert Objects.equals(getDomain(), newTokenAuthentication.getDomain()); return newTokenAuthentication; } @@ -307,31 +339,31 @@ public Authentication maybeAddAnonymousRoles(@Nullable AnonymousUser anonymousUs } final String[] allRoleNames = ArrayUtils.concat(getUser().roles(), anonymousUser.roles()); - final User user; - if (getUser()instanceof RunAsUser runAsUser) { - user = new RunAsUser( - new User( - runAsUser.principal(), - allRoleNames, - runAsUser.fullName(), - runAsUser.email(), - runAsUser.metadata(), - runAsUser.enabled() + if (isRunAs()) { + final User user = effectiveSubject.getUser(); + return new Authentication( + new Subject( + new User(user.principal(), allRoleNames, user.fullName(), user.email(), user.metadata(), user.enabled()), + effectiveSubject.getRealm(), + effectiveSubject.getVersion(), + effectiveSubject.getMetadata() ), - runAsUser.authenticatingUser + authenticatingSubject, + type ); + } else { - user = new User( - getUser().principal(), - allRoleNames, - getUser().fullName(), - getUser().email(), - getUser().metadata(), - getUser().enabled() + final User user = authenticatingSubject.getUser(); + return new Authentication( + new Subject( + new User(user.principal(), allRoleNames, user.fullName(), user.email(), user.metadata(), user.enabled()), + authenticatingSubject.getRealm(), + authenticatingSubject.getVersion(), + authenticatingSubject.getMetadata() + ), + type ); } - - return new Authentication(user, getAuthenticatedBy(), getLookedUpBy(), getVersion(), getAuthenticationType(), getMetadata()); } /** @@ -396,7 +428,6 @@ public boolean supportsRunAs(@Nullable AnonymousUser anonymousUser) { if (isRunAs()) { return false; } - assert false == getUser() instanceof RunAsUser; // We may allow service account to run-as in the future, but for now no service account requires it if (isServiceAccount()) { @@ -447,15 +478,31 @@ public void writeToContext(ThreadContext ctx) throws IOException, IllegalArgumen public String encode() throws IOException { BytesStreamOutput output = new BytesStreamOutput(); - output.setVersion(version); - Version.writeVersion(version, output); + output.setVersion(getVersion()); + Version.writeVersion(getVersion(), output); writeTo(output); return Base64.getEncoder().encodeToString(BytesReference.toBytes(output.bytes())); } public void writeTo(StreamOutput out) throws IOException { - AuthenticationSerializationHelper.writeUserTo(user, out); - authenticatedBy.writeTo(out); + if (isRunAs()) { + final User outerUser = effectiveSubject.getUser(); + final User innerUser = authenticatingSubject.getUser(); + assert false == User.isInternal(outerUser) && false == User.isInternal(innerUser) + : "internal users cannot participate in run-as"; + User.writeUser(outerUser, out); + out.writeBoolean(true); + User.writeUser(innerUser, out); + out.writeBoolean(false); + } else { + final User user = effectiveSubject.getUser(); + AuthenticationSerializationHelper.writeUserTo(user, out); + } + authenticatingSubject.getRealm().writeTo(out); + final RealmRef lookedUpBy = getLookedUpBy(); + // See detailed comment on the same assertion in the Constructor with StreamInput + assert isRunAs() || lookedUpBy == null : "Authentication has no inner-user, but looked-up-by is [" + lookedUpBy + "]"; + if (lookedUpBy != null) { out.writeBoolean(true); lookedUpBy.writeTo(out); @@ -463,7 +510,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(false); } out.writeVInt(type.ordinal()); - out.writeGenericMap(metadata); + out.writeGenericMap(getMetadata()); } /** @@ -501,17 +548,14 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Authentication that = (Authentication) o; - return user.equals(that.user) - && authenticatedBy.equals(that.authenticatedBy) - && Objects.equals(lookedUpBy, that.lookedUpBy) - && version.equals(that.version) - && type == that.type - && metadata.equals(that.metadata); + return type == that.type + && authenticatingSubject.equals(that.authenticatingSubject) + && effectiveSubject.equals(that.effectiveSubject); } @Override public int hashCode() { - return Objects.hash(user, authenticatedBy, lookedUpBy, version, type, metadata); + return Objects.hash(type, authenticatingSubject, effectiveSubject); } @Override @@ -525,6 +569,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws * Generates XContent without the start/end object. */ public void toXContentFragment(XContentBuilder builder) throws IOException { + final User user = effectiveSubject.getUser(); builder.field(User.Fields.USERNAME.getPreferredName(), user.principal()); builder.array(User.Fields.ROLES.getPreferredName(), user.roles()); builder.field(User.Fields.FULL_NAME.getPreferredName(), user.fullName()); @@ -567,9 +612,8 @@ public void toXContentFragment(XContentBuilder builder) throws IOException { builder.endObject(); builder.field(User.Fields.AUTHENTICATION_TYPE.getPreferredName(), getAuthenticationType().name().toLowerCase(Locale.ROOT)); if (isApiKey()) { - this.assertApiKeyMetadata(); - final String apiKeyId = (String) this.metadata.get(AuthenticationField.API_KEY_ID_KEY); - final String apiKeyName = (String) this.metadata.get(AuthenticationField.API_KEY_NAME_KEY); + final String apiKeyId = (String) getMetadata().get(AuthenticationField.API_KEY_ID_KEY); + final String apiKeyName = (String) getMetadata().get(AuthenticationField.API_KEY_NAME_KEY); if (apiKeyName == null) { builder.field("api_key", Map.of("id", apiKeyId)); } else { @@ -578,19 +622,34 @@ public void toXContentFragment(XContentBuilder builder) throws IOException { } } - private void assertApiKeyMetadata() { - assert (false == isAuthenticatedAsApiKey()) || (this.metadata.get(AuthenticationField.API_KEY_ID_KEY) != null) + private void assertInternalConsistency() { + if (false == Assertions.ENABLED) { + return; + } + + assert effectiveSubject != null; + assert authenticatingSubject != null; + assert type != null; + assert effectiveSubject.getVersion().equals(authenticatingSubject.getVersion()); + + if (isRunAs()) { + assert authenticatingSubject != effectiveSubject : "isRunAs logic does not hold"; + assert false == User.isInternal(effectiveSubject.getUser()) && false == User.isInternal(authenticatingSubject.getUser()) + : "internal users cannot participate in run-as"; + } else { + assert authenticatingSubject == effectiveSubject : "isRunAs logic does not hold"; + } + + // Assert API key metadata + assert (false == isAuthenticatedAsApiKey()) || (this.getMetadata().get(AuthenticationField.API_KEY_ID_KEY) != null) : "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(); - } + // Assert domain assignment + if (isAssignedToDomain()) { + assert false == isApiKey(); + assert false == isServiceAccount(); + assert false == isAuthenticatedAnonymously(); + assert false == isAuthenticatedInternally(); } } @@ -611,14 +670,11 @@ private boolean hasSyntheticRealmNameOrType(@Nullable RealmRef realmRef) { @Override public String toString() { - StringBuilder builder = new StringBuilder("Authentication[").append(user) - .append(",type=") - .append(type) - .append(",by=") - .append(authenticatedBy); - if (lookedUpBy != null) { - builder.append(",lookup=").append(lookedUpBy); + StringBuilder builder = new StringBuilder("Authentication[effectiveSubject=").append(effectiveSubject); + if (isRunAs()) { + builder.append(",authenticatingSubject=").append(authenticatingSubject); } + builder.append(",type=").append(type); builder.append("]"); return builder.toString(); } @@ -779,12 +835,8 @@ public static Authentication newInternalAuthentication(User internalUser, Versio assert User.isInternal(internalUser); final Authentication.RealmRef authenticatedBy = newInternalAttachRealmRef(nodeName); Authentication authentication = new Authentication( - internalUser, - authenticatedBy, - null, - version, - AuthenticationType.INTERNAL, - Collections.emptyMap() + new Subject(internalUser, authenticatedBy, version, Map.of()), + AuthenticationType.INTERNAL ); assert false == authentication.isAssignedToDomain(); return authentication; @@ -794,12 +846,8 @@ public static Authentication newInternalFallbackAuthentication(User fallbackUser // 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() + new Subject(fallbackUser, authenticatedBy, Version.CURRENT, Map.of()), + Authentication.AuthenticationType.INTERNAL ); assert false == authentication.isAssignedToDomain(); return authentication; @@ -808,12 +856,8 @@ public static Authentication newInternalFallbackAuthentication(User fallbackUser 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() + new Subject(anonymousUser, authenticatedBy, Version.CURRENT, Map.of()), + Authentication.AuthenticationType.ANONYMOUS ); assert false == authentication.isAssignedToDomain(); return authentication; @@ -821,15 +865,10 @@ public static Authentication newAnonymousAuthentication(AnonymousUser anonymousU public static Authentication newServiceAccountAuthentication(User serviceAccountUser, String nodeName, Map metadata) { // TODO make the service account user a separate class/interface - assert false == serviceAccountUser instanceof RunAsUser; final Authentication.RealmRef authenticatedBy = newServiceAccountRealmRef(nodeName); Authentication authentication = new Authentication( - serviceAccountUser, - authenticatedBy, - null, - Version.CURRENT, - AuthenticationType.TOKEN, - metadata + new Subject(serviceAccountUser, authenticatedBy, Version.CURRENT, metadata), + AuthenticationType.TOKEN ); assert false == authentication.isAssignedToDomain(); return authentication; @@ -837,8 +876,10 @@ public static Authentication newServiceAccountAuthentication(User serviceAccount 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 instanceof RunAsUser; - Authentication authentication = new Authentication(user, realmRef, null, Version.CURRENT, AuthenticationType.REALM, Map.of()); + Authentication authentication = new Authentication( + new Subject(user, realmRef, Version.CURRENT, Map.of()), + AuthenticationType.REALM + ); assert false == authentication.isServiceAccount(); assert false == authentication.isApiKey(); assert false == authentication.isAuthenticatedInternally(); @@ -849,16 +890,11 @@ public static Authentication newRealmAuthentication(User user, RealmRef realmRef 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 instanceof RunAsUser; assert apiKeyUser.roles().length == 0 : "The user associated to an API key authentication must have no role"; final Authentication.RealmRef authenticatedBy = newApiKeyRealmRef(nodeName); Authentication authentication = new Authentication( - apiKeyUser, - authenticatedBy, - null, - Version.CURRENT, - AuthenticationType.API_KEY, - authResult.getMetadata() + new Subject(apiKeyUser, authenticatedBy, Version.CURRENT, authResult.getMetadata()), + AuthenticationType.API_KEY ); assert false == authentication.isAssignedToDomain(); return authentication; @@ -950,59 +986,33 @@ public enum AuthenticationType { INTERNAL } - // Package private for testing - static class RunAsUser extends User { - final User authenticatingUser; - - RunAsUser(User effectiveUser, User authenticatingUser) { - super( - effectiveUser.principal(), - effectiveUser.roles(), - effectiveUser.fullName(), - effectiveUser.email(), - effectiveUser.metadata(), - effectiveUser.enabled() - ); - this.authenticatingUser = Objects.requireNonNull(authenticatingUser); - } + public static class AuthenticationSerializationHelper { - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (false == super.equals(o)) return false; - RunAsUser runAsUser = (RunAsUser) o; - return authenticatingUser.equals(runAsUser.authenticatingUser); - } + private AuthenticationSerializationHelper() {} - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), authenticatingUser); + /** + * Read the User object as well as the trailing boolean flag if the user is *not* an internal user. + * The trailing boolean, if exits, must be false (indicating no following inner-user). + */ + public static User readUserFrom(StreamInput input) throws IOException { + final User user = readUserWithoutTrailingBoolean(input); + if (false == User.isInternal(user)) { + boolean hasInnerUser = input.readBoolean(); + assert false == hasInnerUser : "no inner user is possible, otherwise use UserTuple.readFrom"; + } + return user; } - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("RunAsUser[username=").append(principal()); - sb.append(",roles=[").append(Strings.arrayToCommaDelimitedString(roles())).append("]"); - sb.append(",fullName=").append(fullName()); - sb.append(",email=").append(email()); - sb.append(",metadata="); - sb.append(metadata()); - if (enabled() == false) { - sb.append(",(disabled)"); + public static void writeUserTo(User user, StreamOutput output) throws IOException { + if (User.isInternal(user)) { + writeInternalUser(user, output); + } else { + User.writeUser(user, output); + output.writeBoolean(false); // simple user, no inner user possible } - sb.append(",authenticatingUser=[").append(authenticatingUser.toString()).append("]"); - sb.append("]"); - return sb.toString(); } - } - - public static class AuthenticationSerializationHelper { - - private AuthenticationSerializationHelper() {} - public static User readUserFrom(StreamInput input) throws IOException { + private static User readUserWithoutTrailingBoolean(StreamInput input) throws IOException { final boolean isInternalUser = input.readBoolean(); final String username = input.readString(); if (isInternalUser) { @@ -1019,57 +1029,31 @@ public static User readUserFrom(StreamInput input) throws IOException { } throw new IllegalStateException("username [" + username + "] does not match any internal user"); } - return partialReadUserFrom(username, input); + String[] roles = input.readStringArray(); + Map metadata = input.readMap(); + String fullName = input.readOptionalString(); + String email = input.readOptionalString(); + boolean enabled = input.readBoolean(); + return new User(username, roles, fullName, email, metadata, enabled); } - public static void writeUserTo(User user, StreamOutput output) throws IOException { + private static void writeInternalUser(User user, StreamOutput output) throws IOException { + assert User.isInternal(user); + output.writeBoolean(true); if (SystemUser.is(user)) { - output.writeBoolean(true); output.writeString(SystemUser.NAME); } else if (XPackUser.is(user)) { - output.writeBoolean(true); output.writeString(XPackUser.NAME); } else if (XPackSecurityUser.is(user)) { - output.writeBoolean(true); output.writeString(XPackSecurityUser.NAME); } else if (SecurityProfileUser.is(user)) { - output.writeBoolean(true); output.writeString(SecurityProfileUser.NAME); } else if (AsyncSearchUser.is(user)) { - output.writeBoolean(true); output.writeString(AsyncSearchUser.NAME); } else { - doWriteUserTo(user, output); - } - } - - private static User partialReadUserFrom(String username, StreamInput input) throws IOException { - String[] roles = input.readStringArray(); - Map metadata = input.readMap(); - String fullName = input.readOptionalString(); - String email = input.readOptionalString(); - boolean enabled = input.readBoolean(); - User outerUser = new User(username, roles, fullName, email, metadata, enabled); - boolean hasInnerUser = input.readBoolean(); - if (hasInnerUser) { - User innerUser = readUserFrom(input); - assert false == User.isInternal(innerUser) : "authenticating user cannot be internal"; - return new RunAsUser(outerUser, innerUser); - } else { - return outerUser; - } - } - - private static void doWriteUserTo(User user, StreamOutput output) throws IOException { - if (user instanceof RunAsUser runAsUser) { - User.writeUser(user, output); - output.writeBoolean(true); - User.writeUser(runAsUser.authenticatingUser, output); - } else { - // no backcompat necessary, since there is no inner user - User.writeUser(user, output); + assert false; + throw new IllegalStateException("user [" + user + "] is not internal"); } - output.writeBoolean(false); // last user written, regardless of bwc, does not have an inner user } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java index 509f33c93a0cb..6a7a1fedade69 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.core.security.user.User; import java.util.Map; +import java.util.Objects; import static org.elasticsearch.xpack.core.security.authc.Authentication.VERSION_API_KEY_ROLES_AS_BYTES; import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY; @@ -86,6 +87,10 @@ public Map getMetadata() { return metadata; } + Version getVersion() { + return version; + } + public RoleReferenceIntersection getRoleReferenceIntersection(@Nullable AnonymousUser anonymousUser) { switch (type) { case USER: @@ -141,6 +146,23 @@ public boolean canAccessResourcesOf(Subject resourceCreatorSubject) { } } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Subject subject = (Subject) o; + return version.equals(subject.version) + && user.equals(subject.user) + && Objects.equals(realm, subject.realm) + && type == subject.type + && metadata.equals(subject.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(version, user, realm, type, metadata); + } + @Override public String toString() { return "Subject{" diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationSerializationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationSerializationTests.java index 463643c21818c..783da11cb510b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationSerializationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationSerializationTests.java @@ -19,7 +19,7 @@ import java.util.Arrays; import static org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationSerializationHelper; -import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; @@ -37,31 +37,26 @@ public void testWriteToAndReadFrom() throws Exception { assertThat(readFrom, not(sameInstance(user))); assertThat(readFrom.principal(), is(user.principal())); assertThat(Arrays.equals(readFrom.roles(), user.roles()), is(true)); - assertThat(readFrom, not(instanceOf(Authentication.RunAsUser.class))); } public void testWriteToAndReadFromWithRunAs() throws Exception { - User authUser = new User(randomAlphaOfLengthBetween(4, 30), generateRandomStringArray(20, 30, false)); - User user = new Authentication.RunAsUser( - new User(randomAlphaOfLengthBetween(4, 30), randomBoolean() ? generateRandomStringArray(20, 30, false) : null), - authUser - ); + final Authentication authentication = AuthenticationTestHelper.builder().runAs().build(); + assertThat(authentication.isRunAs(), is(true)); BytesStreamOutput output = new BytesStreamOutput(); - - AuthenticationSerializationHelper.writeUserTo(user, output); - User readFrom = AuthenticationSerializationHelper.readUserFrom(output.bytes().streamInput()); - - assertThat(readFrom, not(sameInstance(user))); - assertThat(readFrom.principal(), is(user.principal())); - assertThat(Arrays.equals(readFrom.roles(), user.roles()), is(true)); - - assertThat(readFrom, instanceOf(Authentication.RunAsUser.class)); - User readFromAuthUser = ((Authentication.RunAsUser) readFrom).authenticatingUser; - assertThat(authUser, is(notNullValue())); - assertThat(readFromAuthUser.principal(), is(authUser.principal())); - assertThat(Arrays.equals(readFromAuthUser.roles(), authUser.roles()), is(true)); - assertThat(readFromAuthUser, not(instanceOf(Authentication.RunAsUser.class))); + authentication.writeTo(output); + final Authentication readFrom = new Authentication(output.bytes().streamInput()); + assertThat(readFrom.isRunAs(), is(true)); + + assertThat(readFrom, not(sameInstance(authentication))); + final User readFromEffectiveUser = readFrom.getEffectiveSubject().getUser(); + assertThat(readFromEffectiveUser, not(sameInstance(authentication.getEffectiveSubject().getUser()))); + assertThat(readFromEffectiveUser, equalTo(authentication.getEffectiveSubject().getUser())); + + User readFromAuthenticatingUser = readFrom.getAuthenticatingSubject().getUser(); + assertThat(readFromAuthenticatingUser, is(notNullValue())); + assertThat(readFromAuthenticatingUser, not(sameInstance(authentication.getAuthenticatingSubject().getUser()))); + assertThat(readFromAuthenticatingUser, equalTo(authentication.getAuthenticatingSubject().getUser())); } public void testSystemUserReadAndWrite() throws Exception { @@ -71,7 +66,6 @@ public void testSystemUserReadAndWrite() throws Exception { User readFrom = AuthenticationSerializationHelper.readUserFrom(output.bytes().streamInput()); assertThat(readFrom, is(sameInstance(SystemUser.INSTANCE))); - assertThat(readFrom, not(instanceOf(Authentication.RunAsUser.class))); } public void testXPackUserReadAndWrite() throws Exception { @@ -81,7 +75,6 @@ public void testXPackUserReadAndWrite() throws Exception { User readFrom = AuthenticationSerializationHelper.readUserFrom(output.bytes().streamInput()); assertThat(readFrom, is(sameInstance(XPackUser.INSTANCE))); - assertThat(readFrom, not(instanceOf(Authentication.RunAsUser.class))); } public void testAsyncSearchUserReadAndWrite() throws Exception { @@ -91,7 +84,6 @@ public void testAsyncSearchUserReadAndWrite() throws Exception { User readFrom = AuthenticationSerializationHelper.readUserFrom(output.bytes().streamInput()); assertThat(readFrom, is(sameInstance(AsyncSearchUser.INSTANCE))); - assertThat(readFrom, not(instanceOf(Authentication.RunAsUser.class))); } public void testFakeInternalUserSerialization() throws Exception {