blob: 6fe46b69f399538aa8817627e2980af87b54c57d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.security.krb;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.RefreshFailedException;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.kerberos.KerberosTicket;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Base class for implementations of KerberosUser.
*
* Generally implementations must provide the specific Configuration instance for performing the login,
* along with an optional CallbackHandler.
*
* Some functionality in this class is adapted from Hadoop's UserGroupInformation.
*/
public abstract class AbstractKerberosUser implements KerberosUser {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractKerberosUser.class);
static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
/**
* Percentage of the ticket window to use before we renew the TGT.
*/
static final float TICKET_RENEW_WINDOW = 0.80f;
/**
* The name of the configuration entry to use from the Configuration instance.
*/
static final String KERBEROS_USER_CONFIG_ENTRY = "KerberosUser";
protected final String principal;
protected final AtomicBoolean loggedIn = new AtomicBoolean(false);
protected Subject subject;
protected LoginContext loginContext;
public AbstractKerberosUser(final String principal) {
this.principal = principal;
Validate.notBlank(this.principal);
}
/**
* Performs a login using the specified principal and keytab.
*
* @throws KerberosLoginException if the login fails
*/
@Override
public synchronized void login() {
if (isLoggedIn()) {
return;
}
try {
// If it's the first time ever calling login then we need to initialize a new context
if (loginContext == null) {
LOGGER.debug("Initializing new login context...");
if (this.subject == null) {
// only create a new subject if a current one does not exist
// other classes may be referencing an existing subject and replacing it may break functionality of those other classes after relogin
this.subject = new Subject();
}
// the Configuration implementations have only one config entry and always return it regardless of the passed in name
this.loginContext = new LoginContext(KERBEROS_USER_CONFIG_ENTRY, subject, createCallbackHandler(), createConfiguration());
}
loginContext.login();
loggedIn.set(true);
LOGGER.debug("Successful login for {}", new Object[]{principal});
} catch (final LoginException le) {
throw new KerberosLoginException("Unable to login with " + principal + " due to: " + le.getMessage(), le);
}
}
/**
* Allow sub-classes to provide the Configuration instance.
*
* @return the Configuration instance
*/
protected abstract Configuration createConfiguration();
/**
* Allow sub-classes to provide an optional CallbackHandler.
*
* @return the CallbackHandler instance, or null
*/
protected abstract CallbackHandler createCallbackHandler();
@Override
public AppConfigurationEntry getConfigurationEntry() {
final Configuration configuration = createConfiguration();
final AppConfigurationEntry[] configurationEntries = configuration.getAppConfigurationEntry("KerberosUser");
if (configurationEntries == null || configurationEntries.length != 1) {
throw new IllegalStateException("Configuration must return one entry");
}
return configurationEntries[0];
}
/**
* Performs a logout of the current user.
*
* @throws KerberosLoginException if the logout fails
*/
@Override
public synchronized void logout() {
if (!isLoggedIn()) {
return;
}
try {
loginContext.logout();
loggedIn.set(false);
LOGGER.debug("Successful logout for {}", new Object[]{principal});
loginContext = null;
} catch (final LoginException e) {
throw new KerberosLoginException("Logout failed due to: " + e.getMessage(), e);
}
}
/**
* Executes the PrivilegedAction as this user.
*
* @param action the action to execute
* @param <T> the type of result
* @return the result of the action
* @throws IllegalStateException if this method is called while not logged in
*/
@Override
public <T> T doAs(final PrivilegedAction<T> action) throws IllegalStateException {
if (!isLoggedIn()) {
throw new IllegalStateException("Must login before executing actions");
}
return Subject.doAs(subject, action);
}
/**
* Executes the PrivilegedAction as this user.
*
* @param action the action to execute
* @param <T> the type of result
* @return the result of the action
* @throws IllegalStateException if this method is called while not logged in
* @throws PrivilegedActionException if an exception is thrown from the action
*/
@Override
public <T> T doAs(final PrivilegedExceptionAction<T> action)
throws IllegalStateException, PrivilegedActionException {
if (!isLoggedIn()) {
throw new IllegalStateException("Must login before executing actions");
}
return Subject.doAs(subject, action);
}
/**
* Re-login a user from keytab if TGT is expired or is close to expiry.
*
* @throws LoginException if an error happens performing the re-login
*/
@Override
public synchronized boolean checkTGTAndRelogin() {
final KerberosTicket tgt = getTGT();
if (tgt == null) {
LOGGER.debug("TGT for {} was not found, performing logout/login", principal);
logout();
login();
return true;
}
if (tgt != null && System.currentTimeMillis() < getRefreshTime(tgt)) {
LOGGER.debug("TGT for {} was found, but has not reached expiration window", principal);
return false;
}
try {
tgt.refresh();
LOGGER.debug("TGT for {} was refreshed", principal);
return true;
} catch (final RefreshFailedException e) {
LOGGER.debug("TGT for {} could not be refreshed", principal);
LOGGER.trace("", e);
LOGGER.debug("Performing logout/login for {}", principal);
logout();
login();
return true;
}
}
/**
* Get the Kerberos TGT.
*
* @return the user's TGT or null if none was found
*/
private synchronized KerberosTicket getTGT() {
final Set<KerberosTicket> tickets = subject.getPrivateCredentials(KerberosTicket.class);
for (KerberosTicket ticket : tickets) {
if (isTGSPrincipal(ticket.getServer())) {
return ticket;
}
}
return null;
}
/**
* TGS must have the server principal of the form "krbtgt/FOO@FOO".
*
* @param principal the principal to check
* @return true if the principal is the TGS, false otherwise
*/
private boolean isTGSPrincipal(final KerberosPrincipal principal) {
if (principal == null) {
return false;
}
if (principal.getName().equals("krbtgt/" + principal.getRealm() + "@" + principal.getRealm())) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Found TGS principal: " + principal.getName());
}
return true;
}
return false;
}
private long getRefreshTime(final KerberosTicket tgt) {
final long start = tgt.getStartTime().getTime();
final long end = tgt.getEndTime().getTime();
final long renewUntil = tgt.getRenewTill().getTime();
if (LOGGER.isTraceEnabled()) {
final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
final String startDate = dateFormat.format(new Date(start));
final String endDate = dateFormat.format(new Date(end));
final String renewUntilDate = dateFormat.format(new Date(renewUntil));
LOGGER.trace("TGT for {} is valid starting at [{}]", principal, startDate);
LOGGER.trace("TGT for {} expires at [{}]", principal, endDate);
LOGGER.trace("TGT for {} renews until [{}]", principal, renewUntilDate);
}
return start + (long) ((end - start) * TICKET_RENEW_WINDOW);
}
/**
* @return true if this user is currently logged in, false otherwise
*/
@Override
public boolean isLoggedIn() {
return loggedIn.get();
}
/**
* @return the principal for this user
*/
@Override
public String getPrincipal() {
return principal;
}
// Visible for testing
Subject getSubject() {
return this.subject;
}
@Override
public String toString() {
return "KerberosUser{" +
"principal='" + principal + '\'' +
", loggedIn=" + loggedIn +
'}';
}
}