Skip to content

Commit bb9d1d6

Browse files
authored
Add Support for Providing a custom ServiceAccountTokenStore through SecurityExtensions (#126612)
* Add Project Service Account Auth
1 parent e9fe219 commit bb9d1d6

33 files changed

+388
-132
lines changed

docs/changelog/126612.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 126612
2+
summary: Add Support for Providing a custom `ServiceAccountTokenStore` through `SecurityExtensions`
3+
area: Authentication
4+
type: enhancement
5+
issues: []

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
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.service.NodeLocalServiceAccountTokenStore;
20+
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
1921
import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
2022
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
2123
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
@@ -114,6 +116,18 @@ default List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>> getRo
114116
return Collections.emptyList();
115117
}
116118

119+
/**
120+
* Returns a {@link NodeLocalServiceAccountTokenStore} used to authenticate service account tokens.
121+
* If {@code null} is returned, the default service account token stores will be used.
122+
*
123+
* Providing a custom {@link NodeLocalServiceAccountTokenStore} here overrides the default implementation.
124+
*
125+
* @param components Access to components that can be used to authenticate service account tokens
126+
*/
127+
default ServiceAccountTokenStore getServiceAccountTokenStore(SecurityComponents components) {
128+
return null;
129+
}
130+
117131
/**
118132
* Returns a authorization engine for authorizing requests, or null to use the default authorization mechanism.
119133
*
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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.service;
9+
10+
import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
11+
12+
import java.util.List;
13+
14+
public interface NodeLocalServiceAccountTokenStore extends ServiceAccountTokenStore {
15+
default List<TokenInfo> findNodeLocalTokensFor(ServiceAccount.ServiceAccountId accountId) {
16+
throw new IllegalStateException("Find node local tokens not supported by [" + this.getClass() + "]");
17+
}
18+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 2.0.
66
*/
77

8-
package org.elasticsearch.xpack.security.authc.service;
8+
package org.elasticsearch.xpack.core.security.authc.service;
99

1010
import org.apache.logging.log4j.util.Strings;
1111
import org.elasticsearch.common.io.stream.StreamInput;
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 2.0.
66
*/
77

8-
package org.elasticsearch.xpack.security.authc.service;
8+
package org.elasticsearch.xpack.core.security.authc.service;
99

1010
import org.apache.logging.log4j.LogManager;
1111
import org.apache.logging.log4j.Logger;
@@ -14,9 +14,9 @@
1414
import org.elasticsearch.common.settings.SecureString;
1515
import org.elasticsearch.core.CharArrays;
1616
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
17+
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
1718
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
1819
import org.elasticsearch.xpack.core.security.support.Validation;
19-
import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
2020

2121
import java.io.ByteArrayInputStream;
2222
import java.io.ByteArrayOutputStream;
@@ -51,7 +51,6 @@ public class ServiceAccountToken implements AuthenticationToken, Closeable {
5151
private final ServiceAccountTokenId tokenId;
5252
private final SecureString secret;
5353

54-
// pkg private for testing
5554
ServiceAccountToken(ServiceAccountId accountId, String tokenName, SecureString secret) {
5655
tokenId = new ServiceAccountTokenId(accountId, tokenName);
5756
this.secret = Objects.requireNonNull(secret, "service account token secret cannot be null");
Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 2.0.
66
*/
77

8-
package org.elasticsearch.xpack.security.authc.service;
8+
package org.elasticsearch.xpack.core.security.authc.service;
99

1010
import org.elasticsearch.action.ActionListener;
1111
import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
@@ -24,11 +24,23 @@ class StoreAuthenticationResult {
2424
private final boolean success;
2525
private final TokenSource tokenSource;
2626

27-
public StoreAuthenticationResult(boolean success, TokenSource tokenSource) {
27+
private StoreAuthenticationResult(TokenSource tokenSource, boolean success) {
2828
this.success = success;
2929
this.tokenSource = tokenSource;
3030
}
3131

32+
public static StoreAuthenticationResult successful(TokenSource tokenSource) {
33+
return new StoreAuthenticationResult(tokenSource, true);
34+
}
35+
36+
public static StoreAuthenticationResult failed(TokenSource tokenSource) {
37+
return new StoreAuthenticationResult(tokenSource, false);
38+
}
39+
40+
public static StoreAuthenticationResult fromBooleanResult(TokenSource tokenSource, boolean result) {
41+
return result ? successful(tokenSource) : failed(tokenSource);
42+
}
43+
3244
public boolean isSuccess() {
3345
return success;
3446
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
* 2.0.
66
*/
77

8-
package org.elasticsearch.xpack.security.authc.service;
8+
package org.elasticsearch.xpack.core.security.authc.service;
99

1010
import org.elasticsearch.common.settings.SecureString;
1111
import org.elasticsearch.test.ESTestCase;
12+
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
1213
import org.elasticsearch.xpack.core.security.support.Validation;
1314
import org.elasticsearch.xpack.core.security.support.ValidationTests;
14-
import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
1515

1616
import java.io.IOException;
1717

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
exports org.elasticsearch.xpack.security.rest.action.apikey to org.elasticsearch.internal.security;
7575
exports org.elasticsearch.xpack.security.support to org.elasticsearch.internal.security;
7676
exports org.elasticsearch.xpack.security.authz.store to org.elasticsearch.internal.security;
77+
exports org.elasticsearch.xpack.security.authc.service;
7778

7879
provides org.elasticsearch.index.SlowLogFieldProvider with org.elasticsearch.xpack.security.slowlog.SecuritySlowLogFieldProvider;
7980

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

Lines changed: 96 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@
208208
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
209209
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
210210
import org.elasticsearch.xpack.core.security.authc.Subject;
211+
import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore;
212+
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
211213
import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
212214
import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
213215
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
@@ -310,6 +312,7 @@
310312
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
311313
import org.elasticsearch.xpack.security.authc.jwt.JwtRealm;
312314
import org.elasticsearch.xpack.security.authc.service.CachingServiceAccountTokenStore;
315+
import org.elasticsearch.xpack.security.authc.service.CompositeServiceAccountTokenStore;
313316
import org.elasticsearch.xpack.security.authc.service.FileServiceAccountTokenStore;
314317
import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountTokenStore;
315318
import org.elasticsearch.xpack.security.authc.service.ServiceAccountService;
@@ -915,12 +918,34 @@ Collection<Object> createComponents(
915918
this.realms.set(realms);
916919

917920
systemIndices.getMainIndexManager().addStateListener(nativeRoleMappingStore::onSecurityIndexStateChange);
918-
919921
final CacheInvalidatorRegistry cacheInvalidatorRegistry = new CacheInvalidatorRegistry();
920-
cacheInvalidatorRegistry.registerAlias("service", Set.of("file_service_account_token", "index_service_account_token"));
921922
components.add(cacheInvalidatorRegistry);
922-
systemIndices.getMainIndexManager().addStateListener(cacheInvalidatorRegistry::onSecurityIndexStateChange);
923923

924+
ServiceAccountService serviceAccountService = createServiceAccountService(
925+
components,
926+
cacheInvalidatorRegistry,
927+
extensionComponents,
928+
() -> new IndexServiceAccountTokenStore(
929+
settings,
930+
threadPool,
931+
getClock(),
932+
client,
933+
systemIndices.getMainIndexManager(),
934+
clusterService,
935+
cacheInvalidatorRegistry
936+
),
937+
() -> new FileServiceAccountTokenStore(
938+
environment,
939+
resourceWatcherService,
940+
threadPool,
941+
clusterService,
942+
cacheInvalidatorRegistry
943+
)
944+
);
945+
946+
components.add(serviceAccountService);
947+
948+
systemIndices.getMainIndexManager().addStateListener(cacheInvalidatorRegistry::onSecurityIndexStateChange);
924949
final NativePrivilegeStore privilegeStore = new NativePrivilegeStore(
925950
settings,
926951
client,
@@ -1004,33 +1029,6 @@ Collection<Object> createComponents(
10041029
);
10051030
components.add(apiKeyService);
10061031

1007-
final IndexServiceAccountTokenStore indexServiceAccountTokenStore = new IndexServiceAccountTokenStore(
1008-
settings,
1009-
threadPool,
1010-
getClock(),
1011-
client,
1012-
systemIndices.getMainIndexManager(),
1013-
clusterService,
1014-
cacheInvalidatorRegistry
1015-
);
1016-
components.add(indexServiceAccountTokenStore);
1017-
1018-
final FileServiceAccountTokenStore fileServiceAccountTokenStore = new FileServiceAccountTokenStore(
1019-
environment,
1020-
resourceWatcherService,
1021-
threadPool,
1022-
clusterService,
1023-
cacheInvalidatorRegistry
1024-
);
1025-
components.add(fileServiceAccountTokenStore);
1026-
1027-
final ServiceAccountService serviceAccountService = new ServiceAccountService(
1028-
client,
1029-
fileServiceAccountTokenStore,
1030-
indexServiceAccountTokenStore
1031-
);
1032-
components.add(serviceAccountService);
1033-
10341032
final RoleProviders roleProviders = new RoleProviders(
10351033
reservedRolesStore,
10361034
fileRolesStore.get(),
@@ -1250,6 +1248,74 @@ Collection<Object> createComponents(
12501248
return components;
12511249
}
12521250

1251+
private ServiceAccountService createServiceAccountService(
1252+
List<Object> components,
1253+
CacheInvalidatorRegistry cacheInvalidatorRegistry,
1254+
SecurityExtension.SecurityComponents extensionComponents,
1255+
Supplier<IndexServiceAccountTokenStore> indexServiceAccountTokenStoreSupplier,
1256+
Supplier<FileServiceAccountTokenStore> fileServiceAccountTokenStoreSupplier
1257+
) {
1258+
Map<String, ServiceAccountTokenStore> accountTokenStoreByExtension = new HashMap<>();
1259+
1260+
for (var extension : securityExtensions) {
1261+
var serviceAccountTokenStore = extension.getServiceAccountTokenStore(extensionComponents);
1262+
if (serviceAccountTokenStore != null) {
1263+
if (isInternalExtension(extension) == false) {
1264+
throw new IllegalStateException(
1265+
"The ["
1266+
+ extension.getClass().getName()
1267+
+ "] extension tried to install a custom ServiceAccountTokenStore. This functionality is not available to "
1268+
+ "external extensions."
1269+
);
1270+
}
1271+
accountTokenStoreByExtension.put(extension.extensionName(), serviceAccountTokenStore);
1272+
}
1273+
}
1274+
1275+
if (accountTokenStoreByExtension.size() > 1) {
1276+
throw new IllegalStateException(
1277+
"More than one extension provided a ServiceAccountTokenStore override: " + accountTokenStoreByExtension.keySet()
1278+
);
1279+
}
1280+
1281+
if (accountTokenStoreByExtension.isEmpty()) {
1282+
var fileServiceAccountTokenStore = fileServiceAccountTokenStoreSupplier.get();
1283+
var indexServiceAccountTokenStore = indexServiceAccountTokenStoreSupplier.get();
1284+
1285+
components.add(new PluginComponentBinding<>(NodeLocalServiceAccountTokenStore.class, fileServiceAccountTokenStore));
1286+
components.add(fileServiceAccountTokenStore);
1287+
components.add(indexServiceAccountTokenStore);
1288+
cacheInvalidatorRegistry.registerAlias("service", Set.of("file_service_account_token", "index_service_account_token"));
1289+
1290+
return new ServiceAccountService(
1291+
client.get(),
1292+
new CompositeServiceAccountTokenStore(
1293+
List.of(fileServiceAccountTokenStore, indexServiceAccountTokenStore),
1294+
client.get().threadPool().getThreadContext()
1295+
),
1296+
indexServiceAccountTokenStore
1297+
);
1298+
}
1299+
// Completely handover service account token management to the extension if provided,
1300+
// this will disable the index managed
1301+
// service account tokens managed through the service account token API
1302+
var extensionStore = accountTokenStoreByExtension.values().stream().findFirst();
1303+
components.add(new PluginComponentBinding<>(NodeLocalServiceAccountTokenStore.class, (token, listener) -> {
1304+
throw new IllegalStateException("Node local config not supported by [" + extensionStore.get().getClass() + "]");
1305+
}));
1306+
components.add(extensionStore);
1307+
logger.debug("Service account authentication handled by extension, disabling file and index token stores");
1308+
return new ServiceAccountService(client.get(), extensionStore.get());
1309+
}
1310+
1311+
private static boolean isInternalExtension(SecurityExtension extension) {
1312+
final String canonicalName = extension.getClass().getCanonicalName();
1313+
if (canonicalName == null) {
1314+
return false;
1315+
}
1316+
return canonicalName.startsWith("org.elasticsearch.xpack.") || canonicalName.startsWith("co.elastic.elasticsearch.");
1317+
}
1318+
12531319
@FixForMultiProject
12541320
// TODO : The migration task needs to be project aware
12551321
private void applyPendingSecurityMigrations(ProjectId projectId, SecurityIndexManager.IndexState newState) {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountRequest;
2020
import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountResponse;
2121
import org.elasticsearch.xpack.core.security.action.service.ServiceAccountInfo;
22-
import org.elasticsearch.xpack.security.authc.service.ServiceAccount;
22+
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount;
2323
import org.elasticsearch.xpack.security.authc.service.ServiceAccountService;
2424

2525
import java.util.function.Predicate;

0 commit comments

Comments
 (0)