org.elasticsearch.xpack.security.authc.service.ServiceAccountService Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of x-pack-security Show documentation
Show all versions of x-pack-security Show documentation
Elasticsearch Expanded Pack Plugin - Security
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
package org.elasticsearch.xpack.security.authc.service;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.client.internal.Client;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest;
import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse;
import org.elasticsearch.xpack.core.security.action.service.DeleteServiceAccountTokenRequest;
import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsNodesRequest;
import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsRequest;
import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsResponse;
import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountNodesCredentialsAction;
import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN;
import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin;
import static org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings.TOKEN_NAME_FIELD;
import static org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings.TOKEN_SOURCE_FIELD;
import static org.elasticsearch.xpack.security.authc.service.ElasticServiceAccounts.ACCOUNTS;
public class ServiceAccountService {
private static final Logger logger = LogManager.getLogger(ServiceAccountService.class);
private static final int MIN_TOKEN_SECRET_LENGTH = 10;
private final Client client;
private final IndexServiceAccountTokenStore indexServiceAccountTokenStore;
private final CompositeServiceAccountTokenStore compositeServiceAccountTokenStore;
public ServiceAccountService(
Client client,
FileServiceAccountTokenStore fileServiceAccountTokenStore,
IndexServiceAccountTokenStore indexServiceAccountTokenStore
) {
this.client = client;
this.indexServiceAccountTokenStore = indexServiceAccountTokenStore;
this.compositeServiceAccountTokenStore = new CompositeServiceAccountTokenStore(
List.of(fileServiceAccountTokenStore, indexServiceAccountTokenStore),
client.threadPool().getThreadContext()
);
}
public static boolean isServiceAccountPrincipal(String principal) {
return ACCOUNTS.containsKey(principal);
}
public static Collection getServiceAccountPrincipals() {
return ACCOUNTS.keySet();
}
public static Map getServiceAccounts() {
return Map.copyOf(ACCOUNTS);
}
/**
* Parses a token object from the content of a {@link ServiceAccountToken#asBearerString()} bearer string}.
* This bearer string would typically be extracted from an HTTP authorization header.
*
*
* This method does not validate the credential, it simply parses it.
* There is no guarantee that the {@link ServiceAccountToken#getSecret() secret} is valid,
* or even that the {@link ServiceAccountToken#getAccountId() account} exists.
*
* @param bearerString A raw token string (if this is from an HTTP header, then the "Bearer "
prefix must be removed before
* calling this method.
* @return An unvalidated token object.
*/
public static ServiceAccountToken tryParseToken(SecureString bearerString) {
try {
if (bearerString == null) {
return null;
}
return ServiceAccountToken.fromBearerString(bearerString);
} catch (Exception e) {
logger.trace("Cannot parse possible service account token", e);
return null;
}
}
public void authenticateToken(ServiceAccountToken serviceAccountToken, String nodeName, ActionListener listener) {
logger.trace("attempt to authenticate service account token [{}]", serviceAccountToken.getQualifiedName());
if (ElasticServiceAccounts.NAMESPACE.equals(serviceAccountToken.getAccountId().namespace()) == false) {
logger.debug(
"only [{}] service accounts are supported, but received [{}]",
ElasticServiceAccounts.NAMESPACE,
serviceAccountToken.getAccountId().asPrincipal()
);
listener.onFailure(createAuthenticationException(serviceAccountToken));
return;
}
final ServiceAccount account = ACCOUNTS.get(serviceAccountToken.getAccountId().asPrincipal());
if (account == null) {
logger.debug("the [{}] service account does not exist", serviceAccountToken.getAccountId().asPrincipal());
listener.onFailure(createAuthenticationException(serviceAccountToken));
return;
}
if (serviceAccountToken.getSecret().length() < MIN_TOKEN_SECRET_LENGTH) {
logger.debug(
"failing authentication for service account token [{}],"
+ " the provided credential has length [{}]"
+ " but a token's secret value must be at least [{}] characters",
serviceAccountToken.getQualifiedName(),
serviceAccountToken.getSecret().length(),
MIN_TOKEN_SECRET_LENGTH
);
listener.onFailure(createAuthenticationException(serviceAccountToken));
return;
}
compositeServiceAccountTokenStore.authenticate(serviceAccountToken, ActionListener.wrap(storeAuthenticationResult -> {
if (storeAuthenticationResult.isSuccess()) {
listener.onResponse(
createAuthentication(account, serviceAccountToken, storeAuthenticationResult.getTokenSource(), nodeName)
);
} else {
final ElasticsearchSecurityException e = createAuthenticationException(serviceAccountToken);
logger.debug(e.getMessage());
listener.onFailure(e);
}
}, listener::onFailure));
}
public void createIndexToken(
Authentication authentication,
CreateServiceAccountTokenRequest request,
ActionListener listener
) {
indexServiceAccountTokenStore.createToken(authentication, request, listener);
}
public void deleteIndexToken(DeleteServiceAccountTokenRequest request, ActionListener listener) {
indexServiceAccountTokenStore.deleteToken(request, listener);
}
public void findTokensFor(GetServiceAccountCredentialsRequest request, ActionListener listener) {
final ServiceAccountId accountId = new ServiceAccountId(request.getNamespace(), request.getServiceName());
findIndexTokens(accountId, listener);
}
// TODO: No production code usage
public static void getRoleDescriptor(Authentication authentication, ActionListener listener) {
assert authentication.isServiceAccount() : "authentication is not for service account: " + authentication;
final String principal = authentication.getEffectiveSubject().getUser().principal();
getRoleDescriptorForPrincipal(principal, listener);
}
public static void getRoleDescriptorForPrincipal(String principal, ActionListener listener) {
final ServiceAccount account = ACCOUNTS.get(principal);
if (account == null) {
listener.onFailure(
new ElasticsearchSecurityException("cannot load role for service account [" + principal + "] - no such service account")
);
return;
}
listener.onResponse(account.roleDescriptor());
}
private static Authentication createAuthentication(
ServiceAccount account,
ServiceAccountToken token,
TokenSource tokenSource,
String nodeName
) {
final User user = account.asUser();
return Authentication.newServiceAccountAuthentication(
user,
nodeName,
Map.of(TOKEN_NAME_FIELD, token.getTokenName(), TOKEN_SOURCE_FIELD, tokenSource.name().toLowerCase(Locale.ROOT))
);
}
private static ElasticsearchSecurityException createAuthenticationException(ServiceAccountToken serviceAccountToken) {
return new ElasticsearchSecurityException(
"failed to authenticate service account [{}] with token name [{}]",
RestStatus.UNAUTHORIZED,
serviceAccountToken.getAccountId().asPrincipal(),
serviceAccountToken.getTokenName()
);
}
private void findIndexTokens(ServiceAccountId accountId, ActionListener listener) {
indexServiceAccountTokenStore.findTokensFor(accountId, ActionListener.wrap(indexTokenInfos -> {
findFileTokens(indexTokenInfos, accountId, listener);
}, listener::onFailure));
}
private void findFileTokens(
Collection indexTokenInfos,
ServiceAccountId accountId,
ActionListener listener
) {
executeAsyncWithOrigin(
client,
SECURITY_ORIGIN,
GetServiceAccountNodesCredentialsAction.INSTANCE,
new GetServiceAccountCredentialsNodesRequest(accountId.namespace(), accountId.serviceName()),
ActionListener.wrap(
fileTokensResponse -> listener.onResponse(
new GetServiceAccountCredentialsResponse(accountId.asPrincipal(), indexTokenInfos, fileTokensResponse)
),
listener::onFailure
)
);
}
}