Skip to content

Commit 663affe

Browse files
authored
[UIAM] Cloud API key authentication (elastic#128440)
This PR adds a new cloud API key subject type and authenticator, and an extension point for internal plugins to inject custom implementations for cloud API key authentication. Resolves: ES-11882
1 parent 37e2729 commit 663affe

File tree

17 files changed

+344
-31
lines changed

17 files changed

+344
-31
lines changed

server/src/main/java/org/elasticsearch/TransportVersions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ static TransportVersion def(int id) {
298298
public static final TransportVersion HEAP_USAGE_IN_CLUSTER_INFO = def(9_096_0_00);
299299
public static final TransportVersion NONE_CHUNKING_STRATEGY = def(9_097_0_00);
300300
public static final TransportVersion PROJECT_DELETION_GLOBAL_BLOCK = def(9_098_0_00);
301+
public static final TransportVersion SECURITY_CLOUD_API_KEY_REALM_AND_TYPE = def(9_099_0_00);
301302

302303
/*
303304
* STOP! READ THIS FIRST! No, really,

x-pack/plugin/core/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
exports org.elasticsearch.xpack.core.watcher.trigger;
231231
exports org.elasticsearch.xpack.core.watcher.watch;
232232
exports org.elasticsearch.xpack.core.watcher;
233+
exports org.elasticsearch.xpack.core.security.authc.apikey;
233234

234235
provides org.elasticsearch.action.admin.cluster.node.info.ComponentVersionNumber
235236
with

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.watcher.ResourceWatcherService;
1717
import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler;
1818
import org.elasticsearch.xpack.core.security.authc.Realm;
19+
import org.elasticsearch.xpack.core.security.authc.apikey.CustomApiKeyAuthenticator;
1920
import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore;
2021
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
2122
import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
@@ -128,6 +129,10 @@ default ServiceAccountTokenStore getServiceAccountTokenStore(SecurityComponents
128129
return null;
129130
}
130131

132+
default CustomApiKeyAuthenticator getCustomApiKeyAuthenticator(SecurityComponents components) {
133+
return null;
134+
}
135+
131136
/**
132137
* Returns a authorization engine for authorizing requests, or null to use the default authorization mechanism.
133138
*

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
6262
import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newAnonymousRealmRef;
6363
import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newApiKeyRealmRef;
64+
import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newCloudApiKeyRealmRef;
6465
import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newCrossClusterAccessRealmRef;
6566
import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newInternalAttachRealmRef;
6667
import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newInternalFallbackRealmRef;
@@ -71,6 +72,8 @@
7172
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_REALM_TYPE;
7273
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_NAME;
7374
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_TYPE;
75+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CLOUD_API_KEY_REALM_NAME;
76+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CLOUD_API_KEY_REALM_TYPE;
7477
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CROSS_CLUSTER_ACCESS_AUTHENTICATION_KEY;
7578
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CROSS_CLUSTER_ACCESS_REALM_NAME;
7679
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CROSS_CLUSTER_ACCESS_REALM_TYPE;
@@ -264,6 +267,16 @@ public Authentication maybeRewriteForOlderVersion(TransportVersion olderVersion)
264267
+ "]"
265268
);
266269
}
270+
if (isCloudApiKey() && olderVersion.before(TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE)) {
271+
throw new IllegalArgumentException(
272+
"versions of Elasticsearch before ["
273+
+ TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE.toReleaseVersion()
274+
+ "] can't handle cloud API key authentication and attempted to rewrite for ["
275+
+ olderVersion.toReleaseVersion()
276+
+ "]"
277+
);
278+
}
279+
267280
final Map<String, Object> newMetadata = maybeRewriteMetadata(olderVersion, this);
268281

269282
final Authentication newAuthentication;
@@ -528,6 +541,10 @@ public boolean isApiKey() {
528541
return effectiveSubject.getType() == Subject.Type.API_KEY;
529542
}
530543

544+
public boolean isCloudApiKey() {
545+
return effectiveSubject.getType() == Subject.Type.CLOUD_API_KEY;
546+
}
547+
531548
public boolean isCrossClusterAccess() {
532549
return effectiveSubject.getType() == Subject.Type.CROSS_CLUSTER_ACCESS;
533550
}
@@ -625,6 +642,16 @@ private static void doWriteTo(Subject effectiveSubject, Subject authenticatingSu
625642
+ "]"
626643
);
627644
}
645+
if (effectiveSubject.getType() == Subject.Type.CLOUD_API_KEY
646+
&& out.getTransportVersion().before(TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE)) {
647+
throw new IllegalArgumentException(
648+
"versions of Elasticsearch before ["
649+
+ TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE.toReleaseVersion()
650+
+ "] can't handle cloud API key authentication and attempted to send to ["
651+
+ out.getTransportVersion().toReleaseVersion()
652+
+ "]"
653+
);
654+
}
628655
final boolean isRunAs = authenticatingSubject != effectiveSubject;
629656
if (isRunAs) {
630657
final User outerUser = effectiveSubject.getUser();
@@ -772,6 +799,7 @@ public void toXContentFragment(XContentBuilder builder) throws IOException {
772799
builder.field("api_key", Map.of("id", apiKeyId, "name", apiKeyName));
773800
}
774801
}
802+
// TODO cloud API key fields such as managed_by
775803
}
776804

777805
public static Authentication getAuthenticationFromCrossClusterAccessMetadata(Authentication authentication) {
@@ -891,11 +919,15 @@ private void checkConsistencyForInternalAuthenticationType() {
891919

892920
private void checkConsistencyForApiKeyAuthenticationType() {
893921
final RealmRef authenticatingRealm = authenticatingSubject.getRealm();
894-
if (false == authenticatingRealm.isApiKeyRealm() && false == authenticatingRealm.isCrossClusterAccessRealm()) {
922+
if (false == authenticatingRealm.usesApiKeys()) {
895923
throw new IllegalArgumentException(
896924
Strings.format("API key authentication cannot have realm type [%s]", authenticatingRealm.type)
897925
);
898926
}
927+
if (authenticatingRealm.isCloudApiKeyRealm()) {
928+
// TODO consistency check for cloud API keys
929+
return;
930+
}
899931
checkConsistencyForApiKeyAuthenticatingSubject("API key");
900932
if (Subject.Type.CROSS_CLUSTER_ACCESS == authenticatingSubject.getType()) {
901933
if (authenticatingSubject.getMetadata().get(CROSS_CLUSTER_ACCESS_AUTHENTICATION_KEY) == null) {
@@ -1183,6 +1215,14 @@ private boolean isAnonymousRealm() {
11831215
return ANONYMOUS_REALM_NAME.equals(name) && ANONYMOUS_REALM_TYPE.equals(type);
11841216
}
11851217

1218+
private boolean usesApiKeys() {
1219+
return isCloudApiKeyRealm() || isApiKeyRealm() || isCrossClusterAccessRealm();
1220+
}
1221+
1222+
private boolean isCloudApiKeyRealm() {
1223+
return CLOUD_API_KEY_REALM_NAME.equals(name) && CLOUD_API_KEY_REALM_TYPE.equals(type);
1224+
}
1225+
11861226
private boolean isApiKeyRealm() {
11871227
return API_KEY_REALM_NAME.equals(name) && API_KEY_REALM_TYPE.equals(type);
11881228
}
@@ -1212,6 +1252,11 @@ static RealmRef newServiceAccountRealmRef(String nodeName) {
12121252
return new Authentication.RealmRef(ServiceAccountSettings.REALM_NAME, ServiceAccountSettings.REALM_TYPE, nodeName, null);
12131253
}
12141254

1255+
static RealmRef newCloudApiKeyRealmRef(String nodeName) {
1256+
// no domain for cloud API key tokens
1257+
return new RealmRef(CLOUD_API_KEY_REALM_NAME, CLOUD_API_KEY_REALM_TYPE, nodeName, null);
1258+
}
1259+
12151260
static RealmRef newApiKeyRealmRef(String nodeName) {
12161261
// no domain for API Key tokens
12171262
return new RealmRef(API_KEY_REALM_NAME, API_KEY_REALM_TYPE, nodeName, null);
@@ -1293,6 +1338,16 @@ public static Authentication newRealmAuthentication(User user, RealmRef realmRef
12931338
return authentication;
12941339
}
12951340

1341+
public static Authentication newCloudApiKeyAuthentication(AuthenticationResult<User> authResult, String nodeName) {
1342+
assert authResult.isAuthenticated() : "cloud API Key authn result must be successful";
1343+
final User apiKeyUser = authResult.getValue();
1344+
final Authentication.RealmRef authenticatedBy = newCloudApiKeyRealmRef(nodeName);
1345+
return new Authentication(
1346+
new Subject(apiKeyUser, authenticatedBy, TransportVersion.current(), authResult.getMetadata()),
1347+
AuthenticationType.API_KEY
1348+
);
1349+
}
1350+
12961351
public static Authentication newApiKeyAuthentication(AuthenticationResult<User> authResult, String nodeName) {
12971352
assert authResult.isAuthenticated() : "API Key authn result must be successful";
12981353
final User apiKeyUser = authResult.getValue();

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public final class AuthenticationField {
1313
public static final String PRIVILEGE_CATEGORY_VALUE_OPERATOR = "operator";
1414
public static final String PRIVILEGE_CATEGORY_VALUE_EMPTY = "__empty";
1515

16+
public static final String CLOUD_API_KEY_REALM_NAME = "_cloud_api_key";
17+
public static final String CLOUD_API_KEY_REALM_TYPE = "_cloud_api_key";
18+
1619
public static final String API_KEY_REALM_NAME = "_es_api_key";
1720
public static final String API_KEY_REALM_TYPE = "_es_api_key";
1821

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

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public class Subject {
4747
public enum Type {
4848
USER,
4949
API_KEY,
50+
CLOUD_API_KEY,
5051
SERVICE_ACCOUNT,
5152
CROSS_CLUSTER_ACCESS,
5253
}
@@ -72,6 +73,9 @@ public Subject(User user, Authentication.RealmRef realm, TransportVersion versio
7273
} else if (AuthenticationField.API_KEY_REALM_TYPE.equals(realm.getType())) {
7374
assert AuthenticationField.API_KEY_REALM_NAME.equals(realm.getName()) : "api key realm name mismatch";
7475
this.type = Type.API_KEY;
76+
} else if (AuthenticationField.CLOUD_API_KEY_REALM_TYPE.equals(realm.getType())) {
77+
assert AuthenticationField.CLOUD_API_KEY_REALM_NAME.equals(realm.getName()) : "cloud api key realm name mismatch";
78+
this.type = Type.CLOUD_API_KEY;
7579
} else if (ServiceAccountSettings.REALM_TYPE.equals(realm.getType())) {
7680
assert ServiceAccountSettings.REALM_NAME.equals(realm.getName()) : "service account realm name mismatch";
7781
this.type = Type.SERVICE_ACCOUNT;
@@ -105,19 +109,12 @@ public TransportVersion getTransportVersion() {
105109
}
106110

107111
public RoleReferenceIntersection getRoleReferenceIntersection(@Nullable AnonymousUser anonymousUser) {
108-
switch (type) {
109-
case USER:
110-
return buildRoleReferencesForUser(anonymousUser);
111-
case API_KEY:
112-
return buildRoleReferencesForApiKey();
113-
case SERVICE_ACCOUNT:
114-
return new RoleReferenceIntersection(new RoleReference.ServiceAccountRoleReference(user.principal()));
115-
case CROSS_CLUSTER_ACCESS:
116-
return buildRoleReferencesForCrossClusterAccess();
117-
default:
118-
assert false : "unknown subject type: [" + type + "]";
119-
throw new IllegalStateException("unknown subject type: [" + type + "]");
120-
}
112+
return switch (type) {
113+
case CLOUD_API_KEY, USER -> buildRoleReferencesForUser(anonymousUser);
114+
case API_KEY -> buildRoleReferencesForApiKey();
115+
case SERVICE_ACCOUNT -> new RoleReferenceIntersection(new RoleReference.ServiceAccountRoleReference(user.principal()));
116+
case CROSS_CLUSTER_ACCESS -> buildRoleReferencesForCrossClusterAccess();
117+
};
121118
}
122119

123120
public boolean canAccessResourcesOf(Subject resourceCreatorSubject) {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.authc.apikey;
9+
10+
import org.elasticsearch.action.ActionListener;
11+
import org.elasticsearch.common.settings.SecureString;
12+
import org.elasticsearch.core.Nullable;
13+
import org.elasticsearch.xpack.core.security.authc.Authentication;
14+
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
15+
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
16+
17+
/**
18+
* An extension point to provide a custom API key authenticator implementation.
19+
* The implementation is wrapped by a core `Authenticator` class and included in the authenticator chain _before_ the
20+
* default API key authenticator.
21+
*/
22+
public interface CustomApiKeyAuthenticator {
23+
String name();
24+
25+
AuthenticationToken extractCredentials(@Nullable SecureString apiKeyCredentials);
26+
27+
void authenticate(@Nullable AuthenticationToken authenticationToken, ActionListener<AuthenticationResult<Authentication>> listener);
28+
29+
/**
30+
* A no-op implementation of {@link CustomApiKeyAuthenticator} that is effectively skipped in the authenticator chain.
31+
*/
32+
class Noop implements CustomApiKeyAuthenticator {
33+
@Override
34+
public String name() {
35+
return "noop";
36+
}
37+
38+
@Override
39+
public AuthenticationToken extractCredentials(@Nullable SecureString apiKeyCredentials) {
40+
return null;
41+
}
42+
43+
@Override
44+
public void authenticate(
45+
@Nullable AuthenticationToken authenticationToken,
46+
ActionListener<AuthenticationResult<Authentication>> listener
47+
) {
48+
listener.onResponse(AuthenticationResult.notHandled());
49+
}
50+
}
51+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ private static void addSubjectInfo(XContentBuilder builder, Subject subject) thr
128128
}
129129
builder.endObject();
130130
}
131+
case CLOUD_API_KEY -> {
132+
// TODO Add cloud API key information here
133+
}
131134
}
132135
}
133136

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,52 @@ public void testWriteToWithCrossClusterAccessThrowsOnUnsupportedVersion() throws
115115
}
116116
}
117117

118+
public void testWriteToAndReadFromWithCloudApiKeyAuthentication() throws Exception {
119+
final Authentication authentication = Authentication.newCloudApiKeyAuthentication(
120+
AuthenticationResult.success(new User(randomAlphanumericOfLength(5), "superuser"), Map.of()),
121+
randomAlphanumericOfLength(10)
122+
);
123+
124+
assertThat(authentication.isCloudApiKey(), is(true));
125+
126+
BytesStreamOutput output = new BytesStreamOutput();
127+
authentication.writeTo(output);
128+
final Authentication readFrom = new Authentication(output.bytes().streamInput());
129+
assertThat(readFrom.isCloudApiKey(), is(true));
130+
131+
assertThat(readFrom, not(sameInstance(authentication)));
132+
assertThat(readFrom, equalTo(authentication));
133+
}
134+
135+
public void testWriteToWithCloudApiKeyThrowsOnUnsupportedVersion() {
136+
final Authentication authentication = Authentication.newCloudApiKeyAuthentication(
137+
AuthenticationResult.success(new User(randomAlphanumericOfLength(5), "superuser"), Map.of()),
138+
randomAlphanumericOfLength(10)
139+
);
140+
141+
try (BytesStreamOutput out = new BytesStreamOutput()) {
142+
final TransportVersion version = TransportVersionUtils.randomVersionBetween(
143+
random(),
144+
TransportVersions.V_8_0_0,
145+
TransportVersionUtils.getPreviousVersion(TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE)
146+
);
147+
out.setTransportVersion(version);
148+
149+
final var ex = expectThrows(IllegalArgumentException.class, () -> authentication.writeTo(out));
150+
assertThat(
151+
ex.getMessage(),
152+
containsString(
153+
"versions of Elasticsearch before ["
154+
+ TransportVersions.SECURITY_CLOUD_API_KEY_REALM_AND_TYPE.toReleaseVersion()
155+
+ "] can't handle cloud API key authentication and attempted to send to ["
156+
+ out.getTransportVersion().toReleaseVersion()
157+
+ "]"
158+
)
159+
);
160+
}
161+
162+
}
163+
118164
public void testSystemUserReadAndWrite() throws Exception {
119165
BytesStreamOutput output = new BytesStreamOutput();
120166

x-pack/plugin/security/src/main/java/module-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
exports org.elasticsearch.xpack.security.action.settings to org.elasticsearch.server;
7070
exports org.elasticsearch.xpack.security.operator to org.elasticsearch.internal.operator, org.elasticsearch.internal.security;
7171
exports org.elasticsearch.xpack.security.authz to org.elasticsearch.internal.security;
72-
exports org.elasticsearch.xpack.security.authc to org.elasticsearch.xcontent;
72+
exports org.elasticsearch.xpack.security.authc to org.elasticsearch.xcontent, org.elasticsearch.internal.security;
7373
exports org.elasticsearch.xpack.security.authc.saml to org.elasticsearch.internal.security;
7474
exports org.elasticsearch.xpack.security.slowlog to org.elasticsearch.server;
7575
exports org.elasticsearch.xpack.security.authc.support to org.elasticsearch.internal.security;

0 commit comments

Comments
 (0)