All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.xpack.security.authc.kerberos.KerberosRealm Maven / Gradle / Ivy

There is a newer version: 8.16.1
Show newest version
/*
 * 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.kerberos;

import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.cache.Cache;
import org.elasticsearch.common.cache.CacheBuilder;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.core.security.authc.Realm;
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings;
import org.elasticsearch.xpack.core.security.authc.support.CachingRealm;
import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport;
import org.ietf.jgss.GSSException;

import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.security.auth.login.LoginException;

import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.AUTH_HEADER;
import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.NEGOTIATE_AUTH_HEADER_PREFIX;
import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.NEGOTIATE_SCHEME_NAME;
import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.WWW_AUTHENTICATE;
import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.unauthorized;
import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthenticationToken.unauthorizedWithOutputToken;

/**
 * This class provides support for Kerberos authentication using spnego
 * mechanism.
 * 

* It provides support to extract kerberos ticket using * {@link KerberosAuthenticationToken#extractToken(String)} to build * {@link KerberosAuthenticationToken} and then authenticating user when * {@link KerberosTicketValidator} validates the ticket. *

* On successful authentication, it will build {@link User} object populated * with roles and will return {@link AuthenticationResult} with user object. On * authentication failure, it will return {@link AuthenticationResult} with * status to terminate authentication process. */ public final class KerberosRealm extends Realm implements CachingRealm { public static final String KRB_METADATA_REALM_NAME_KEY = "kerberos_realm"; public static final String KRB_METADATA_UPN_KEY = "kerberos_user_principal_name"; private final Cache userPrincipalNameToUserCache; private final UserRoleMapper userRoleMapper; private final KerberosTicketValidator kerberosTicketValidator; private final ThreadPool threadPool; private final Path keytabPath; private final boolean enableKerberosDebug; private final boolean removeRealmName; private DelegatedAuthorizationSupport delegatedRealms; public KerberosRealm(final RealmConfig config, final UserRoleMapper userRoleMapper, final ThreadPool threadPool) { this(config, userRoleMapper, new KerberosTicketValidator(), threadPool, null); } // pkg scoped for testing KerberosRealm( final RealmConfig config, final UserRoleMapper userRoleMapper, final KerberosTicketValidator kerberosTicketValidator, final ThreadPool threadPool, final Cache userPrincipalNameToUserCache ) { super(config); this.userRoleMapper = userRoleMapper; this.userRoleMapper.clearRealmCacheOnChange(this); final TimeValue ttl = config.getSetting(KerberosRealmSettings.CACHE_TTL_SETTING); if (ttl.getNanos() > 0) { this.userPrincipalNameToUserCache = (userPrincipalNameToUserCache == null) ? CacheBuilder.builder() .setExpireAfterWrite(config.getSetting(KerberosRealmSettings.CACHE_TTL_SETTING)) .setMaximumWeight(config.getSetting(KerberosRealmSettings.CACHE_MAX_USERS_SETTING)) .build() : userPrincipalNameToUserCache; } else { this.userPrincipalNameToUserCache = null; } this.kerberosTicketValidator = kerberosTicketValidator; this.threadPool = threadPool; this.keytabPath = config.env().configFile().resolve(config.getSetting(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH)); validateKeytab(this.keytabPath); this.enableKerberosDebug = config.getSetting(KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE); this.removeRealmName = config.getSetting(KerberosRealmSettings.SETTING_REMOVE_REALM_NAME); this.delegatedRealms = null; } private static void validateKeytab(Path keytabPath) { boolean fileExists = AccessController.doPrivileged((PrivilegedAction) () -> Files.exists(keytabPath)); if (fileExists == false) { throw new IllegalArgumentException("configured service key tab file [" + keytabPath + "] does not exist"); } boolean pathIsDir = AccessController.doPrivileged((PrivilegedAction) () -> Files.isDirectory(keytabPath)); if (pathIsDir) { throw new IllegalArgumentException("configured service key tab file [" + keytabPath + "] is a directory"); } boolean isReadable = AccessController.doPrivileged((PrivilegedAction) () -> Files.isReadable(keytabPath)); if (isReadable == false) { throw new IllegalArgumentException("configured service key tab file [" + keytabPath + "] must have read permission"); } } @Override public void initialize(Iterable realms, XPackLicenseState licenseState) { if (delegatedRealms != null) { throw new IllegalStateException("Realm has already been initialized"); } delegatedRealms = new DelegatedAuthorizationSupport(realms, config, licenseState); } @Override public Map> getAuthenticationFailureHeaders() { return Collections.singletonMap(WWW_AUTHENTICATE, Collections.singletonList(NEGOTIATE_SCHEME_NAME)); } @Override public void expire(final String username) { if (userPrincipalNameToUserCache != null) { userPrincipalNameToUserCache.invalidate(username); } } @Override public void expireAll() { if (userPrincipalNameToUserCache != null) { userPrincipalNameToUserCache.invalidateAll(); } } @Override public boolean supports(final AuthenticationToken token) { return token instanceof KerberosAuthenticationToken; } @Override public AuthenticationToken token(final ThreadContext context) { return KerberosAuthenticationToken.extractToken(context.getHeader(AUTH_HEADER)); } @Override public void authenticate(final AuthenticationToken token, final ActionListener> listener) { assert delegatedRealms != null : "Realm has not been initialized correctly"; assert token instanceof KerberosAuthenticationToken; final KerberosAuthenticationToken kerbAuthnToken = (KerberosAuthenticationToken) token; kerberosTicketValidator.validateTicket( (byte[]) kerbAuthnToken.credentials(), keytabPath, enableKerberosDebug, ActionListener.wrap(userPrincipalNameOutToken -> { if (userPrincipalNameOutToken.v1() != null) { resolveUser(userPrincipalNameOutToken.v1(), userPrincipalNameOutToken.v2(), listener); } else { /** * This is when security context could not be established may be due to ongoing * negotiation and requires token to be sent back to peer for continuing * further. We are terminating the authentication process as this is spengo * negotiation and no other realm can handle this. We can have only one Kerberos * realm in the system so terminating with RestStatus Unauthorized (401) and * with 'WWW-Authenticate' header populated with value with token in the form * 'Negotiate oYH1MIHyoAMK...' */ String errorMessage = "failed to authenticate user, gss context negotiation not complete"; ElasticsearchSecurityException ese = unauthorized(errorMessage, null); ese = unauthorizedWithOutputToken(ese, userPrincipalNameOutToken.v2()); listener.onResponse(AuthenticationResult.terminate(errorMessage, ese)); } }, e -> handleException(e, listener)) ); } private static String[] splitUserPrincipalName(final String userPrincipalName) { return userPrincipalName.split("@"); } private void handleException(Exception e, final ActionListener> listener) { if (e instanceof LoginException) { logger.debug("failed to authenticate user, service login failure", e); listener.onResponse( AuthenticationResult.terminate( "failed to authenticate user, service login failure", unauthorized(e.getLocalizedMessage(), e) ) ); } else if (e instanceof GSSException) { logger.debug("failed to authenticate user, gss context negotiation failure", e); listener.onResponse( AuthenticationResult.terminate( "failed to authenticate user, gss context negotiation failure", unauthorized(e.getLocalizedMessage(), e) ) ); } else { logger.debug("failed to authenticate user", e); listener.onFailure(e); } } private void resolveUser( final String userPrincipalName, final String outToken, final ActionListener> listener ) { // if outToken is present then it needs to be communicated with peer, add it to // response header in thread context. if (Strings.hasText(outToken)) { threadPool.getThreadContext().addResponseHeader(WWW_AUTHENTICATE, NEGOTIATE_AUTH_HEADER_PREFIX + outToken); } final String[] userAndRealmName = splitUserPrincipalName(userPrincipalName); /* * Usually principal names are in the form 'user/instance@REALM'. If * KerberosRealmSettings#SETTING_REMOVE_REALM_NAME is true then remove * '@REALM' part from the user principal name to get username. */ final String username = (this.removeRealmName) ? userAndRealmName[0] : userPrincipalName; if (delegatedRealms.hasDelegation()) { delegatedRealms.resolve(username, listener); } else { final User user = (userPrincipalNameToUserCache != null) ? userPrincipalNameToUserCache.get(username) : null; if (user != null) { listener.onResponse(AuthenticationResult.success(user)); } else if (userAndRealmName.length > 1) { final String realmName = userAndRealmName[1]; buildUser(username, Map.of(KRB_METADATA_REALM_NAME_KEY, realmName, KRB_METADATA_UPN_KEY, userPrincipalName), listener); } else { buildUser(username, Map.of(KRB_METADATA_UPN_KEY, userPrincipalName), listener); } } } private void buildUser( final String username, final Map metadata, final ActionListener> listener ) { final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Set.of(), metadata, this.config); userRoleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, userData.getMetadata(), true); if (userPrincipalNameToUserCache != null) { userPrincipalNameToUserCache.put(username, computedUser); } listener.onResponse(AuthenticationResult.success(computedUser)); }, listener::onFailure)); } @Override public void lookupUser(final String username, final ActionListener listener) { listener.onResponse(null); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy