Skip to content

Commit 2502a36

Browse files
[UIAM] Cloud API key authentication metadata and validations (elastic#129227)
A followup to elastic#128440, which introduces a new `managed_by` field (`<1>`) that will be returned in the response of the Authenticate API. Besides `managed_by` field, it also captures additional `internal` field (`<2>`) for cloud API key authentication and exposes it as part of the `api_key` fields. ```json { "username": "omSAd5YBK3gZiBcD-GvX", "roles": [ "viewer" ], "metadata": { ... }, "enabled": true, "authentication_realm": { "name": "_cloud_api_key", "type": "_cloud_api_key" }, "lookup_realm": { "name": "_cloud_api_key", "type": "_cloud_api_key" }, "authentication_type": "api_key", "api_key": { "id": "omSAd5YBK3gZiBcD-GvX", "name": "my cloud API key", "managed_by": "cloud", <1> "internal": false <2> } } ``` - Additionally it implements the `Authentication#canAccessResourcesOf` for the cloud API keys. Ownership check allows access only to the same cloud API key. - And lastly, adds a consistency check for cloud API keys in `Authentication#checkConsistencyForApiKeyAuthenticationType`.
1 parent f1ea88e commit 2502a36

File tree

9 files changed

+221
-22
lines changed

9 files changed

+221
-22
lines changed

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

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,10 @@
6868
import static org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef.newServiceAccountRealmRef;
6969
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_NAME;
7070
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ANONYMOUS_REALM_TYPE;
71+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY;
7172
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_REALM_NAME;
7273
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_REALM_TYPE;
74+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY;
7375
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_NAME;
7476
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.ATTACH_REALM_TYPE;
7577
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CLOUD_API_KEY_REALM_NAME;
@@ -569,6 +571,11 @@ public boolean supportsRunAs(@Nullable AnonymousUser anonymousUser) {
569571
return false;
570572
}
571573

574+
// We may allow cloud API keys to run-as in the future, but for now there is no requirement
575+
if (isCloudApiKey()) {
576+
return false;
577+
}
578+
572579
// There is no reason for internal users to run-as. This check prevents either internal user itself
573580
// or a token created for it (though no such thing in current code) to run-as.
574581
if (getEffectiveSubject().getUser() instanceof InternalUser) {
@@ -748,14 +755,15 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
748755
*/
749756
public void toXContentFragment(XContentBuilder builder) throws IOException {
750757
final User user = effectiveSubject.getUser();
758+
final Map<String, Object> metadata = getAuthenticatingSubject().getMetadata();
751759
builder.field(User.Fields.USERNAME.getPreferredName(), user.principal());
752760
builder.array(User.Fields.ROLES.getPreferredName(), user.roles());
753761
builder.field(User.Fields.FULL_NAME.getPreferredName(), user.fullName());
754762
builder.field(User.Fields.EMAIL.getPreferredName(), user.email());
755763
if (isServiceAccount()) {
756-
final String tokenName = (String) getAuthenticatingSubject().getMetadata().get(ServiceAccountSettings.TOKEN_NAME_FIELD);
764+
final String tokenName = (String) metadata.get(ServiceAccountSettings.TOKEN_NAME_FIELD);
757765
assert tokenName != null : "token name cannot be null";
758-
final String tokenSource = (String) getAuthenticatingSubject().getMetadata().get(ServiceAccountSettings.TOKEN_SOURCE_FIELD);
766+
final String tokenSource = (String) metadata.get(ServiceAccountSettings.TOKEN_SOURCE_FIELD);
759767
assert tokenSource != null : "token source cannot be null";
760768
builder.field(
761769
User.Fields.TOKEN.getPreferredName(),
@@ -790,16 +798,31 @@ public void toXContentFragment(XContentBuilder builder) throws IOException {
790798
}
791799
builder.endObject();
792800
builder.field(User.Fields.AUTHENTICATION_TYPE.getPreferredName(), getAuthenticationType().name().toLowerCase(Locale.ROOT));
801+
793802
if (isApiKey() || isCrossClusterAccess()) {
794-
final String apiKeyId = (String) getAuthenticatingSubject().getMetadata().get(AuthenticationField.API_KEY_ID_KEY);
795-
final String apiKeyName = (String) getAuthenticatingSubject().getMetadata().get(AuthenticationField.API_KEY_NAME_KEY);
796-
if (apiKeyName == null) {
797-
builder.field("api_key", Map.of("id", apiKeyId));
798-
} else {
799-
builder.field("api_key", Map.of("id", apiKeyId, "name", apiKeyName));
803+
final String apiKeyId = (String) metadata.get(AuthenticationField.API_KEY_ID_KEY);
804+
final String apiKeyName = (String) metadata.get(AuthenticationField.API_KEY_NAME_KEY);
805+
final Map<String, Object> apiKeyField = new HashMap<>();
806+
apiKeyField.put("id", apiKeyId);
807+
if (apiKeyName != null) {
808+
apiKeyField.put("name", apiKeyName);
809+
}
810+
apiKeyField.put("managed_by", CredentialManagedBy.ELASTICSEARCH.getDisplayName());
811+
builder.field("api_key", Collections.unmodifiableMap(apiKeyField));
812+
813+
} else if (isCloudApiKey()) {
814+
final String apiKeyId = user.principal();
815+
final String apiKeyName = (String) user.metadata().get(AuthenticationField.API_KEY_NAME_KEY);
816+
final boolean internal = (boolean) user.metadata().get(AuthenticationField.API_KEY_INTERNAL_KEY);
817+
final Map<String, Object> apiKeyField = new HashMap<>();
818+
apiKeyField.put("id", apiKeyId);
819+
if (apiKeyName != null) {
820+
apiKeyField.put("name", apiKeyName);
800821
}
822+
apiKeyField.put("internal", internal);
823+
apiKeyField.put("managed_by", CredentialManagedBy.CLOUD.getDisplayName());
824+
builder.field("api_key", Collections.unmodifiableMap(apiKeyField));
801825
}
802-
// TODO cloud API key fields such as managed_by
803826
}
804827

805828
public static Authentication getAuthenticationFromCrossClusterAccessMetadata(Authentication authentication) {
@@ -924,10 +947,11 @@ private void checkConsistencyForApiKeyAuthenticationType() {
924947
Strings.format("API key authentication cannot have realm type [%s]", authenticatingRealm.type)
925948
);
926949
}
927-
if (authenticatingRealm.isCloudApiKeyRealm()) {
928-
// TODO consistency check for cloud API keys
950+
if (authenticatingSubject.getType() == Subject.Type.CLOUD_API_KEY) {
951+
checkConsistencyForCloudApiKeyAuthenticatingSubject("Cloud API key");
929952
return;
930953
}
954+
931955
checkConsistencyForApiKeyAuthenticatingSubject("API key");
932956
if (Subject.Type.CROSS_CLUSTER_ACCESS == authenticatingSubject.getType()) {
933957
if (authenticatingSubject.getMetadata().get(CROSS_CLUSTER_ACCESS_AUTHENTICATION_KEY) == null) {
@@ -1021,6 +1045,18 @@ private void checkConsistencyForApiKeyAuthenticatingSubject(String prefixMessage
10211045
}
10221046
}
10231047

1048+
private void checkConsistencyForCloudApiKeyAuthenticatingSubject(String prefixMessage) {
1049+
final RealmRef authenticatingRealm = authenticatingSubject.getRealm();
1050+
checkNoDomain(authenticatingRealm, prefixMessage);
1051+
checkNoInternalUser(authenticatingSubject, prefixMessage);
1052+
checkNoRunAs(this, prefixMessage);
1053+
if (authenticatingSubject.getMetadata().get(CROSS_CLUSTER_ACCESS_ROLE_DESCRIPTORS_KEY) != null
1054+
|| authenticatingSubject.getMetadata().get(API_KEY_ROLE_DESCRIPTORS_KEY) != null
1055+
|| authenticatingSubject.getMetadata().get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY) != null) {
1056+
throw new IllegalArgumentException(prefixMessage + " authentication cannot contain a role descriptors metadata field");
1057+
}
1058+
}
1059+
10241060
private static void checkNoInternalUser(Subject subject, String prefixMessage) {
10251061
if (subject.getUser() instanceof InternalUser) {
10261062
throw new IllegalArgumentException(
@@ -1057,7 +1093,8 @@ private static boolean hasSyntheticRealmNameOrType(@Nullable RealmRef realmRef)
10571093
ANONYMOUS_REALM_NAME,
10581094
FALLBACK_REALM_NAME,
10591095
ATTACH_REALM_NAME,
1060-
CROSS_CLUSTER_ACCESS_REALM_NAME
1096+
CROSS_CLUSTER_ACCESS_REALM_NAME,
1097+
CLOUD_API_KEY_REALM_NAME
10611098
).contains(realmRef.getName())) {
10621099
return true;
10631100
}
@@ -1067,7 +1104,8 @@ private static boolean hasSyntheticRealmNameOrType(@Nullable RealmRef realmRef)
10671104
ANONYMOUS_REALM_TYPE,
10681105
FALLBACK_REALM_TYPE,
10691106
ATTACH_REALM_TYPE,
1070-
CROSS_CLUSTER_ACCESS_REALM_TYPE
1107+
CROSS_CLUSTER_ACCESS_REALM_TYPE,
1108+
CLOUD_API_KEY_REALM_TYPE
10711109
).contains(realmRef.getType())) {
10721110
return true;
10731111
}
@@ -1649,6 +1687,20 @@ public enum AuthenticationType {
16491687
INTERNAL
16501688
}
16511689

1690+
/**
1691+
* Indicates if credentials are managed by Elasticsearch or by the Cloud.
1692+
*/
1693+
public enum CredentialManagedBy {
1694+
1695+
CLOUD,
1696+
1697+
ELASTICSEARCH;
1698+
1699+
public String getDisplayName() {
1700+
return name().toLowerCase(Locale.ROOT);
1701+
}
1702+
}
1703+
16521704
public static class AuthenticationSerializationHelper {
16531705

16541706
private AuthenticationSerializationHelper() {}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public final class AuthenticationField {
2323
public static final String API_KEY_CREATOR_REALM_TYPE = "_security_api_key_creator_realm_type";
2424
public static final String API_KEY_ID_KEY = "_security_api_key_id";
2525
public static final String API_KEY_NAME_KEY = "_security_api_key_name";
26+
public static final String API_KEY_INTERNAL_KEY = "_security_api_key_internal";
2627
public static final String API_KEY_TYPE_KEY = "_security_api_key_type";
2728
public static final String API_KEY_METADATA_KEY = "_security_api_key_metadata";
2829
public static final String API_KEY_ROLE_DESCRIPTORS_KEY = "_security_api_key_role_descriptors";

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY;
3535
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.CROSS_CLUSTER_ACCESS_AUTHENTICATION_KEY;
3636
import static org.elasticsearch.xpack.core.security.authc.Subject.Type.API_KEY;
37+
import static org.elasticsearch.xpack.core.security.authc.Subject.Type.CLOUD_API_KEY;
3738
import static org.elasticsearch.xpack.core.security.authc.Subject.Type.CROSS_CLUSTER_ACCESS;
3839

3940
/**
@@ -137,6 +138,13 @@ public boolean canAccessResourcesOf(Subject resourceCreatorSubject) {
137138
// A cross cluster access subject can never share resources with non-cross cluster access
138139
return false;
139140
}
141+
} else if (eitherIsACloudApiKey(resourceCreatorSubject)) {
142+
if (bothAreCloudApiKeys(resourceCreatorSubject)) {
143+
return getUser().principal().equals(resourceCreatorSubject.getUser().principal());
144+
} else {
145+
// a cloud API Key cannot access resources created by non-Cloud API Keys or vice versa
146+
return false;
147+
}
140148
} else {
141149
if (false == getUser().principal().equals(resourceCreatorSubject.getUser().principal())) {
142150
return false;
@@ -191,6 +199,14 @@ private boolean bothAreCrossClusterAccess(Subject resourceCreatorSubject) {
191199
return CROSS_CLUSTER_ACCESS.equals(getType()) && CROSS_CLUSTER_ACCESS.equals(resourceCreatorSubject.getType());
192200
}
193201

202+
private boolean eitherIsACloudApiKey(Subject resourceCreatorSubject) {
203+
return CLOUD_API_KEY.equals(getType()) || CLOUD_API_KEY.equals(resourceCreatorSubject.getType());
204+
}
205+
206+
private boolean bothAreCloudApiKeys(Subject resourceCreatorSubject) {
207+
return CLOUD_API_KEY.equals(getType()) && CLOUD_API_KEY.equals(resourceCreatorSubject.getType());
208+
}
209+
194210
@Override
195211
public boolean equals(Object o) {
196212
if (this == o) return true;

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,43 @@ private Map<String, String> getErrorMessageToEncodedAuthentication() throws IOEx
148148
Authentication.AuthenticationType.API_KEY
149149
)
150150
),
151+
entry(
152+
"Cloud API key authentication cannot have domain",
153+
encodeAuthentication(
154+
new Subject(
155+
userFoo,
156+
new Authentication.RealmRef(
157+
AuthenticationField.CLOUD_API_KEY_REALM_NAME,
158+
AuthenticationField.CLOUD_API_KEY_REALM_TYPE,
159+
"node",
160+
new RealmDomain(
161+
"domain1",
162+
Set.of(
163+
new RealmConfig.RealmIdentifier(
164+
AuthenticationField.CLOUD_API_KEY_REALM_NAME,
165+
AuthenticationField.CLOUD_API_KEY_REALM_TYPE
166+
)
167+
)
168+
)
169+
)
170+
),
171+
Authentication.AuthenticationType.API_KEY
172+
)
173+
),
151174
entry(
152175
"API key authentication cannot have internal user [_xpack]",
153176
encodeAuthentication(
154177
new Subject(InternalUsers.XPACK_USER, Authentication.RealmRef.newApiKeyRealmRef("node")),
155178
Authentication.AuthenticationType.API_KEY
156179
)
157180
),
181+
entry(
182+
"Cloud API key authentication cannot have internal user [_xpack]",
183+
encodeAuthentication(
184+
new Subject(InternalUsers.XPACK_USER, Authentication.RealmRef.newCloudApiKeyRealmRef("node")),
185+
Authentication.AuthenticationType.API_KEY
186+
)
187+
),
158188
entry(
159189
"API key authentication user must have no role",
160190
encodeAuthentication(
@@ -237,6 +267,33 @@ private Map<String, String> getErrorMessageToEncodedAuthentication() throws IOEx
237267
Authentication.AuthenticationType.API_KEY
238268
)
239269
),
270+
entry(
271+
"Cloud API key authentication cannot contain a role descriptors metadata field",
272+
encodeAuthentication(
273+
new Subject(
274+
new User("api_key_id", "role1"),
275+
Authentication.RealmRef.newCloudApiKeyRealmRef("node"),
276+
TransportVersion.current(),
277+
Map.of(
278+
randomFrom(
279+
AuthenticationField.CROSS_CLUSTER_ACCESS_ROLE_DESCRIPTORS_KEY,
280+
AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY,
281+
AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY
282+
),
283+
List.of()
284+
)
285+
),
286+
Authentication.AuthenticationType.API_KEY
287+
)
288+
),
289+
entry(
290+
"Cloud API key authentication cannot run-as other user",
291+
encodeAuthentication(
292+
new Subject(userBar, realm2),
293+
new Subject(userFoo, Authentication.RealmRef.newCloudApiKeyRealmRef("node")),
294+
Authentication.AuthenticationType.API_KEY
295+
)
296+
),
240297
// Authentication type: Realm
241298
entry(
242299
"Realm authentication must have subject type of user",
@@ -330,6 +387,14 @@ private Map<String, String> getErrorMessageToEncodedAuthentication() throws IOEx
330387
new Subject(userFoo, realm1),
331388
Authentication.AuthenticationType.REALM
332389
)
390+
),
391+
entry(
392+
"Run-as subject type cannot be [CLOUD_API_KEY]",
393+
encodeAuthentication(
394+
new Subject(userBar, Authentication.RealmRef.newCloudApiKeyRealmRef("node")),
395+
new Subject(userFoo, realm1),
396+
Authentication.AuthenticationType.REALM
397+
)
333398
)
334399
);
335400
}

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,48 @@ public static String randomInternalRoleName() {
244244
);
245245
}
246246

247+
public static Authentication randomCloudApiKeyAuthentication() {
248+
return randomCloudApiKeyAuthentication(null, null);
249+
}
250+
251+
public static Authentication randomCloudApiKeyAuthentication(String apiKeyId) {
252+
return randomCloudApiKeyAuthentication(null, apiKeyId);
253+
}
254+
255+
public static Authentication randomCloudApiKeyAuthentication(User user) {
256+
return randomCloudApiKeyAuthentication(user, null);
257+
}
258+
259+
public static Authentication randomCloudApiKeyAuthentication(User user, String apiKeyId) {
260+
if (apiKeyId == null) {
261+
apiKeyId = user != null ? user.principal() : ESTestCase.randomAlphanumericOfLength(64);
262+
}
263+
final Map<String, Object> metadata = ESTestCase.randomBoolean()
264+
? null
265+
: Map.ofEntries(
266+
Map.entry(AuthenticationField.API_KEY_NAME_KEY, ESTestCase.randomAlphanumericOfLength(64)),
267+
Map.entry(AuthenticationField.API_KEY_INTERNAL_KEY, ESTestCase.randomBoolean())
268+
);
269+
if (user == null) {
270+
user = new User(
271+
apiKeyId,
272+
ESTestCase.randomArray(1, 3, String[]::new, () -> "role_" + ESTestCase.randomAlphaOfLengthBetween(3, 8)),
273+
null,
274+
null,
275+
metadata,
276+
true
277+
);
278+
}
279+
280+
assert user.principal().equals(apiKeyId) : "user principal must match cloud API key ID";
281+
282+
return Authentication.newCloudApiKeyAuthentication(
283+
AuthenticationResult.success(user, metadata),
284+
"node_" + ESTestCase.randomAlphaOfLengthBetween(3, 8)
285+
);
286+
287+
}
288+
247289
public static CrossClusterAccessSubjectInfo randomCrossClusterAccessSubjectInfo(
248290
RoleDescriptorsIntersection roleDescriptorsIntersection
249291
) {

0 commit comments

Comments
 (0)