Skip to content

Commit 63ca708

Browse files
authored
Authentication model changes for remote access (#93151)
This PR implements the necessary changes to the Authentication class, to support remote access authentication under the new remote cluster security model. Upon successful authentication, a new authentication instance will be constructed by the fulfilling cluster which combines information from the remote access API key used and the user authentication and role info sent by the querying cluster with a cross cluster request. Remote access authentication is modeled in way that exposes (and assumes) that the underlying authentication method is an API key; for example, it includes the metadata associated with API keys in its metadata directly, re-using existing metadata field keys. I chose this approach instead of trying to generalize away from API keys because there are no medium-term plans to support any other authentication forms for remote access; generalizing would have made the change more complex. This change is stand-alone and not wired up to active code flows yet. A proof of concept in #92089 highlights how the model change in this PR fits into the broader context of the fulfilling cluster processing cross cluster requests.
1 parent 37c510c commit 63ca708

File tree

10 files changed

+504
-12
lines changed

10 files changed

+504
-12
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java

Lines changed: 155 additions & 7 deletions
Large diffs are not rendered by default.

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,10 @@ public final class AuthenticationField {
3333
public static final String ATTACH_REALM_NAME = "__attach";
3434
public static final String ATTACH_REALM_TYPE = "__attach";
3535

36+
public static final String REMOTE_ACCESS_REALM_NAME = "_es_remote_access";
37+
public static final String REMOTE_ACCESS_REALM_TYPE = "_es_remote_access";
38+
public static final String REMOTE_ACCESS_AUTHENTICATION_KEY = "_security_remote_access_authentication";
39+
public static final String REMOTE_ACCESS_ROLE_DESCRIPTORS_KEY = "_security_remote_access_role_descriptors";
40+
3641
private AuthenticationField() {}
3742
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthentication.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
import java.io.UncheckedIOException;
2929
import java.util.ArrayList;
3030
import java.util.Base64;
31+
import java.util.Collections;
32+
import java.util.HashMap;
3133
import java.util.List;
34+
import java.util.Map;
3235
import java.util.Objects;
3336
import java.util.Set;
3437

@@ -124,6 +127,26 @@ public static RemoteAccessAuthentication decode(final String header) throws IOEx
124127
return new RemoteAccessAuthentication(authentication, roleDescriptorsBytesList);
125128
}
126129

130+
/**
131+
* Returns a copy of the passed-in metadata map, with the relevant remote access fields included. Does not modify the original map.
132+
*/
133+
public Map<String, Object> copyWithRemoteAccessEntries(final Map<String, Object> authenticationMetadata) {
134+
assert false == authenticationMetadata.containsKey(AuthenticationField.REMOTE_ACCESS_AUTHENTICATION_KEY)
135+
: "metadata already contains [" + AuthenticationField.REMOTE_ACCESS_AUTHENTICATION_KEY + "] entry";
136+
assert false == authenticationMetadata.containsKey(AuthenticationField.REMOTE_ACCESS_ROLE_DESCRIPTORS_KEY)
137+
: "metadata already contains [" + AuthenticationField.REMOTE_ACCESS_ROLE_DESCRIPTORS_KEY + "] entry";
138+
assert false == getAuthentication().isRemoteAccess()
139+
: "authentication included in remote access header cannot itself be remote access";
140+
final Map<String, Object> copy = new HashMap<>(authenticationMetadata);
141+
try {
142+
copy.put(AuthenticationField.REMOTE_ACCESS_AUTHENTICATION_KEY, getAuthentication().encode());
143+
} catch (IOException e) {
144+
throw new UncheckedIOException(e);
145+
}
146+
copy.put(AuthenticationField.REMOTE_ACCESS_ROLE_DESCRIPTORS_KEY, getRoleDescriptorsBytesList());
147+
return Collections.unmodifiableMap(copy);
148+
}
149+
127150
public static final class RoleDescriptorsBytes extends AbstractBytesReference {
128151
private final BytesReference rawBytes;
129152

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@
2626
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY;
2727
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY;
2828
import static org.elasticsearch.xpack.core.security.authc.Subject.Type.API_KEY;
29+
import static org.elasticsearch.xpack.core.security.authc.Subject.Type.REMOTE_ACCESS;
2930

3031
/**
3132
* A subject is a more generic concept similar to user and associated to the current authentication.
32-
* It is more generic than user because it can also represent API keys and service accounts.
33+
* It is more generic than user because it can also represent API keys, service accounts, or remote access users.
3334
* It also contains authentication level information, e.g. realm and metadata so that it can answer
3435
* queries in a better encapsulated way.
3536
*/
@@ -39,6 +40,7 @@ public enum Type {
3940
USER,
4041
API_KEY,
4142
SERVICE_ACCOUNT,
43+
REMOTE_ACCESS,
4244
}
4345

4446
private final TransportVersion version;
@@ -65,6 +67,9 @@ public Subject(User user, Authentication.RealmRef realm, TransportVersion versio
6567
} else if (ServiceAccountSettings.REALM_TYPE.equals(realm.getType())) {
6668
assert ServiceAccountSettings.REALM_NAME.equals(realm.getName()) : "service account realm name mismatch";
6769
this.type = Type.SERVICE_ACCOUNT;
70+
} else if (AuthenticationField.REMOTE_ACCESS_REALM_TYPE.equals(realm.getType())) {
71+
assert AuthenticationField.REMOTE_ACCESS_REALM_NAME.equals(realm.getName()) : "remote access realm name mismatch";
72+
this.type = Type.REMOTE_ACCESS;
6873
} else {
6974
this.type = Type.USER;
7075
}
@@ -99,6 +104,9 @@ public RoleReferenceIntersection getRoleReferenceIntersection(@Nullable Anonymou
99104
return buildRoleReferencesForApiKey();
100105
case SERVICE_ACCOUNT:
101106
return new RoleReferenceIntersection(new RoleReference.ServiceAccountRoleReference(user.principal()));
107+
case REMOTE_ACCESS:
108+
assert false : "unsupported subject type: [" + type + "]";
109+
throw new UnsupportedOperationException("unsupported subject type: [" + type + "]");
102110
default:
103111
assert false : "unknown subject type: [" + type + "]";
104112
throw new IllegalStateException("unknown subject type: [" + type + "]");
@@ -116,6 +124,9 @@ public boolean canAccessResourcesOf(Subject resourceCreatorSubject) {
116124
|| (false == API_KEY.equals(getType()) && API_KEY.equals(resourceCreatorSubject.getType()))) {
117125
// an API Key cannot access resources created by non-API Keys or vice-versa
118126
return false;
127+
} else if (REMOTE_ACCESS.equals(getType()) || REMOTE_ACCESS.equals(resourceCreatorSubject.getType())) {
128+
// TODO implement this once remote authentication is fully supported
129+
return false;
119130
} else {
120131
if (false == getUser().principal().equals(resourceCreatorSubject.getUser().principal())) {
121132
return false;

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtils.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ public static void addAuthorizationInfo(final XContentBuilder builder, final Map
115115
builder.endObject();
116116
}
117117
case SERVICE_ACCOUNT -> builder.field("service_account", authenticationSubject.getUser().principal());
118+
case REMOTE_ACCESS -> {
119+
// TODO handle remote access authentication
120+
final String message = "remote access authentication is not yet supported";
121+
assert false : message;
122+
throw new UnsupportedOperationException(message);
123+
}
118124
}
119125
builder.endObject();
120126
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationSerializationTests.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
*/
77
package org.elasticsearch.xpack.core.security.authc;
88

9+
import org.elasticsearch.TransportVersion;
910
import org.elasticsearch.common.io.stream.BytesStreamOutput;
11+
import org.elasticsearch.common.io.stream.StreamInput;
1012
import org.elasticsearch.test.ESTestCase;
13+
import org.elasticsearch.test.TransportVersionUtils;
1114
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
1215
import org.elasticsearch.xpack.core.security.user.ElasticUser;
1316
import org.elasticsearch.xpack.core.security.user.KibanaSystemUser;
@@ -19,6 +22,7 @@
1922
import java.util.Arrays;
2023

2124
import static org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationSerializationHelper;
25+
import static org.hamcrest.Matchers.containsString;
2226
import static org.hamcrest.Matchers.equalTo;
2327
import static org.hamcrest.Matchers.is;
2428
import static org.hamcrest.Matchers.not;
@@ -59,6 +63,52 @@ public void testWriteToAndReadFromWithRunAs() throws Exception {
5963
assertThat(readFromAuthenticatingUser, equalTo(authentication.getAuthenticatingSubject().getUser()));
6064
}
6165

66+
public void testWriteToAndReadFromWithRemoteAccess() throws Exception {
67+
final Authentication authentication = AuthenticationTestHelper.builder().remoteAccess().build();
68+
assertThat(authentication.isRemoteAccess(), is(true));
69+
70+
BytesStreamOutput output = new BytesStreamOutput();
71+
authentication.writeTo(output);
72+
final Authentication readFrom = new Authentication(output.bytes().streamInput());
73+
assertThat(readFrom.isRemoteAccess(), is(true));
74+
75+
assertThat(readFrom, not(sameInstance(authentication)));
76+
assertThat(readFrom, equalTo(authentication));
77+
}
78+
79+
public void testWriteToWithRemoteAccessThrowsOnUnsupportedVersion() throws Exception {
80+
final Authentication authentication = randomBoolean()
81+
? AuthenticationTestHelper.builder().remoteAccess().build()
82+
: AuthenticationTestHelper.builder().build();
83+
84+
final BytesStreamOutput out = new BytesStreamOutput();
85+
final TransportVersion version = TransportVersionUtils.randomPreviousCompatibleVersion(
86+
random(),
87+
Authentication.VERSION_REMOTE_ACCESS_REALM
88+
);
89+
out.setTransportVersion(version);
90+
91+
if (authentication.isRemoteAccess()) {
92+
final var ex = expectThrows(IllegalArgumentException.class, () -> authentication.writeTo(out));
93+
assertThat(
94+
ex.getMessage(),
95+
containsString(
96+
"versions of Elasticsearch before ["
97+
+ Authentication.VERSION_REMOTE_ACCESS_REALM
98+
+ "] can't handle remote access authentication and attempted to send to ["
99+
+ out.getTransportVersion()
100+
+ "]"
101+
)
102+
);
103+
} else {
104+
authentication.writeTo(out);
105+
final StreamInput in = out.bytes().streamInput();
106+
in.setTransportVersion(out.getTransportVersion());
107+
final Authentication readFrom = new Authentication(in);
108+
assertThat(readFrom, equalTo(authentication.maybeRewriteForOlderVersion(out.getTransportVersion())));
109+
}
110+
}
111+
62112
public void testSystemUserReadAndWrite() throws Exception {
63113
BytesStreamOutput output = new BytesStreamOutput();
64114

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings;
2727
import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings;
2828
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
29+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
30+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection;
2931
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
3032
import org.elasticsearch.xpack.core.security.user.AsyncSearchUser;
3133
import org.elasticsearch.xpack.core.security.user.SecurityProfileUser;
@@ -36,6 +38,7 @@
3638
import org.elasticsearch.xpack.core.security.user.XPackUser;
3739

3840
import java.io.IOException;
41+
import java.io.UncheckedIOException;
3942
import java.util.Arrays;
4043
import java.util.EnumSet;
4144
import java.util.HashMap;
@@ -68,7 +71,8 @@ public class AuthenticationTestHelper {
6871
AuthenticationField.ANONYMOUS_REALM_TYPE,
6972
AuthenticationField.ATTACH_REALM_TYPE,
7073
AuthenticationField.FALLBACK_REALM_TYPE,
71-
ServiceAccountSettings.REALM_TYPE
74+
ServiceAccountSettings.REALM_TYPE,
75+
AuthenticationField.REMOTE_ACCESS_REALM_TYPE
7276
);
7377

7478
private static final Set<User> INTERNAL_USERS = Set.of(
@@ -204,7 +208,7 @@ private static AnonymousUser randomAnonymousUser() {
204208
);
205209
}
206210

207-
private static User stripRoles(User user) {
211+
static User stripRoles(User user) {
208212
if (user.roles() != null || user.roles().length == 0) {
209213
return new User(user.principal(), Strings.EMPTY_ARRAY, user.fullName(), user.email(), user.metadata(), user.enabled());
210214
} else {
@@ -233,6 +237,43 @@ public static String randomInternalRoleName() {
233237
);
234238
}
235239

240+
public static RemoteAccessAuthentication randomRemoteAccessAuthentication() {
241+
try {
242+
// TODO add apikey() once we have querying-cluster-side API key support
243+
final Authentication authentication = ESTestCase.randomFrom(
244+
AuthenticationTestHelper.builder().realm(),
245+
AuthenticationTestHelper.builder().internal(SystemUser.INSTANCE)
246+
).build();
247+
return new RemoteAccessAuthentication(
248+
authentication,
249+
new RoleDescriptorsIntersection(
250+
List.of(
251+
// TODO randomize to add a second set once we have querying-cluster-side API key support
252+
Set.of(
253+
new RoleDescriptor(
254+
"a",
255+
null,
256+
new RoleDescriptor.IndicesPrivileges[] {
257+
RoleDescriptor.IndicesPrivileges.builder()
258+
.indices("index1")
259+
.privileges("read", "read_cross_cluster")
260+
.build() },
261+
null,
262+
null,
263+
null,
264+
null,
265+
null,
266+
null
267+
)
268+
)
269+
)
270+
)
271+
);
272+
} catch (IOException e) {
273+
throw new UncheckedIOException(e);
274+
}
275+
}
276+
236277
public static class AuthenticationTestBuilder {
237278
private TransportVersion transportVersion;
238279
private Authentication authenticatingAuthentication;
@@ -242,6 +283,7 @@ public static class AuthenticationTestBuilder {
242283
private final Map<String, Object> metadata = new HashMap<>();
243284
private Boolean isServiceAccount;
244285
private Boolean isRealmUnderDomain;
286+
private RemoteAccessAuthentication remoteAccessAuthentication;
245287

246288
private AuthenticationTestBuilder() {}
247289

@@ -335,6 +377,22 @@ public AuthenticationTestBuilder user(User user) {
335377
}
336378
}
337379

380+
public AuthenticationTestBuilder remoteAccess() {
381+
return remoteAccess(ESTestCase.randomAlphaOfLength(20), randomRemoteAccessAuthentication());
382+
}
383+
384+
public AuthenticationTestBuilder remoteAccess(
385+
final String remoteAccessApiKeyId,
386+
final RemoteAccessAuthentication remoteAccessAuthentication
387+
) {
388+
if (authenticatingAuthentication != null) {
389+
throw new IllegalArgumentException("cannot use remote access authentication as run-as target");
390+
}
391+
apiKey(remoteAccessApiKeyId);
392+
this.remoteAccessAuthentication = Objects.requireNonNull(remoteAccessAuthentication);
393+
return this;
394+
}
395+
338396
public AuthenticationTestBuilder realmRef(Authentication.RealmRef realmRef) {
339397
assert false == SYNTHETIC_REALM_TYPES.contains(realmRef.getType()) : "use dedicate methods for synthetic realms";
340398
resetShortcutRelatedVariables();
@@ -363,6 +421,9 @@ public AuthenticationTestBuilder metadata(Map<String, Object> metadata) {
363421
}
364422

365423
public AuthenticationTestBuilder runAs() {
424+
if (remoteAccessAuthentication != null) {
425+
throw new IllegalArgumentException("cannot convert to run-as for remote access authentication");
426+
}
366427
if (authenticatingAuthentication != null) {
367428
throw new IllegalArgumentException("cannot convert to run-as again for run-as authentication");
368429
}
@@ -421,10 +482,16 @@ public Authentication build(boolean maybeRunAsIfNotAlready) {
421482
// User associated to API key authentication has empty roles
422483
user = stripRoles(user);
423484
prepareApiKeyMetadata();
424-
authentication = Authentication.newApiKeyAuthentication(
485+
final Authentication apiKeyAuthentication = Authentication.newApiKeyAuthentication(
425486
AuthenticationResult.success(user, metadata),
426487
ESTestCase.randomAlphaOfLengthBetween(3, 8)
427488
);
489+
// Remote access is authenticated via API key, but the underlying authentication instance has a different structure,
490+
// and a different subject type. If remoteAccessAuthentication is set, we transform the API key authentication
491+
// instance into a remote access authentication instance.
492+
authentication = remoteAccessAuthentication != null
493+
? apiKeyAuthentication.toRemoteAccess(remoteAccessAuthentication)
494+
: apiKeyAuthentication;
428495
}
429496
case TOKEN -> {
430497
if (isServiceAccount != null && isServiceAccount) {

0 commit comments

Comments
 (0)