/*
* Copyright (C) 2015 The Async HBase Authors. All rights reserved.
* This file is part of Async HBase.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the StumbleUpon nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.hbase.async.auth;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.Principal;
import java.security.PrivilegedExceptionAction;
import java.util.Map;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.RealmCallback;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslClient;
import org.apache.zookeeper.server.auth.KerberosName;
import org.hbase.async.HBaseClient;
import org.jboss.netty.util.HashedWheelTimer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles Kerberos based authentication using the Java Authentication and
* Authorization Service (JAAS).
* The class requires a few settings including:
* asynchbase.security.auth.kerberos.regionserver.principal - a principal name
* or template to use for authentication. The value can have the token "_HOST"
* which will be replaced with the cannonical name of the localhost.
* @since 1.7
*/
public class KerberosClientAuthProvider extends ClientAuthProvider {
public static final String PASSWORD_KEY = "hbase.regionserver.kerberos.password";
public static final String PRINCIPAL_KEY = "hbase.kerberos.regionserver.principal";
private static final Logger LOG = LoggerFactory.getLogger(
KerberosClientAuthProvider.class);
/** The principal name */
private String client_principal_name;
/**
* Default ctor that will attempt a login and setup the Login singleton
* @param hbase_client The HBaseClient to fetch configuration and timers from
* @throws IllegalArgumentException if the
* asynchbase.security.auth.simple.username is missing, null or empty.
* @throws IllegalStateException if the login was unsuccessful
*/
public KerberosClientAuthProvider(final HBaseClient hbase_client) {
super(hbase_client);
String password = null;
if (hbase_client.getConfig().hasProperty(PASSWORD_KEY)) {
password = hbase_client.getConfig().getString(PASSWORD_KEY);
}
try {
Login.initUserIfNeeded(hbase_client.getConfig(),
(HashedWheelTimer)hbase_client.getTimer(),
hbase_client.getConfig().getString(Login.LOGIN_CONTEXT_NAME_KEY),
new ClientCallbackHandler(password));
} catch (LoginException e) {
throw new IllegalStateException("Failed to get login context", e);
}
//-- Prepare principals needed for SaslClient --
final Login client_login = Login.getCurrentLogin();
client_principal_name = getClientPrincipalName(client_login);
}
@Override
public SaslClient newSaslClient(final String service_ip,
final Map<String, String> props) {
final Login client_login = Login.getCurrentLogin();
String server_principal = hbase_client.getConfig().getString(PRINCIPAL_KEY);
if (server_principal.contains("_HOST")) {
try {
final String host = InetAddress.getByName(service_ip)
.getCanonicalHostName();
server_principal = server_principal.replaceAll("_HOST", host);
} catch (UnknownHostException e) {
throw new IllegalStateException("Failed to resolve hostname for: " +
service_ip, e);
}
}
LOG.info("Connecting to " + server_principal);
final KerberosName service_kerberos_name = new KerberosName(server_principal);
final String service_name = service_kerberos_name.getServiceName();
final String service_hostname = service_kerberos_name.getHostName();
//-- create SaslClient --
try {
final class PriviledgedAction implements
PrivilegedExceptionAction<SaslClient> {
@Override
public SaslClient run() throws Exception {
LOG.info("Client will use GSSAPI as SASL mechanism.");
final String[] mechanism = { "GSSAPI" };
LOG.debug("Creating sasl client: client=" + client_principal_name +
", service=" + service_name + ", serviceHostname=" + service_hostname);
return Sasl.createSaslClient(
mechanism,
null, // authorization ID
service_name,
service_hostname,
props,
null); // callback
}
@Override
public String toString() {
return "create sasl client";
}
}
return Subject.doAs(client_login.getSubject(), new PriviledgedAction());
} catch (Exception e) {
LOG.error("Error creating SASL client", e);
throw new IllegalStateException("Error creating SASL client", e);
}
}
@Override
public String getClientUsername() {
return client_principal_name;
}
@Override
public byte getAuthMethodCode() {
return KEBEROS_CLIENT_AUTH_CODE;
}
@Override
public Subject getClientSubject() {
return Login.getCurrentLogin().getSubject();
}
/**
* Return the principal name if set
* @param login The login object to pull the name from
* @return The name if found, null if not
*/
private String getClientPrincipalName(final Login login) {
if (login.getSubject() == null) {
return null;
}
final Set<Principal> principals = login.getSubject().getPrincipals();
if (principals == null || principals.isEmpty()) {
return null;
}
final Principal principal = principals.iterator().next();
final KerberosName name = new KerberosName(principal.getName());
return name.toString();
}
/**
* A callback executed on authentication to validate the name or password.
* Right now the only way to set a password is via the config file, and that
* isn't particularly secure. So use a key cache instead.
*/
static class ClientCallbackHandler implements CallbackHandler {
private String password;
/** @param the password to use for auth */
public ClientCallbackHandler(final String password) {
this.password = password;
}
@Override
public void handle(final Callback[] callbacks)
throws UnsupportedCallbackException {
for (final Callback callback : callbacks) {
LOG.debug("Processing callback: " + callback.getClass());
if (callback instanceof NameCallback) {
final NameCallback name_callback = (NameCallback) callback;
name_callback.setName(name_callback.getDefaultName());
} else if (callback instanceof PasswordCallback) {
final PasswordCallback password_callback = (PasswordCallback)callback;
if (password != null) {
password_callback.setPassword(password.toCharArray());
} else {
LOG.warn("Could not login: the client is being asked for a password, but the " +
" client code does not currently support obtaining a password from the user." +
" Make sure that the client is configured to use a ticket cache (using" +
" the JAAS configuration setting 'useTicketCache=true)' and restart the client. If" +
" you still get this message after that, the TGT in the ticket cache has expired and must" +
" be manually refreshed. To do so, first determine if you are using a password or a" +
" keytab. If the former, run kinit in a Unix shell in the environment of the user who" +
" is running this asynchbase client using the command" +
" 'kinit <princ>' (where <princ> is the name of the client's Kerberos principal)." +
" If the latter, do" +
" 'kinit -k -t <keytab> <princ>' (where <princ> is the name of the Kerberos principal, and" +
" <keytab> is the location of the keytab file). After manually refreshing your cache," +
" restart this client. If you continue to see this message after manually refreshing" +
" your cache, ensure that your KDC host's clock is in sync with this host's clock.");
}
} else if (callback instanceof RealmCallback) {
final RealmCallback realm_callback = (RealmCallback) callback;
realm_callback.setText(realm_callback.getDefaultText());
} else if (callback instanceof AuthorizeCallback) {
final AuthorizeCallback authorize_callback =
(AuthorizeCallback) callback;
final String authid = authorize_callback.getAuthenticationID();
final String authzid = authorize_callback.getAuthorizationID();
if (authid.equals(authzid)) {
authorize_callback.setAuthorized(true);
authorize_callback.setAuthorizedID(authzid);
} else {
authorize_callback.setAuthorized(false);
}
} else {
throw new UnsupportedCallbackException(callback,
"Unrecognized SASL ClientCallback: " + callback.getClass());
}
}
}
}
}