/*
* 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;
import java.lang.reflect.Constructor;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslClient;
import javax.security.sasl.SaslException;
import org.hbase.async.auth.ClientAuthProvider;
import org.hbase.async.auth.KerberosClientAuthProvider;
import org.hbase.async.auth.SimpleClientAuthProvider;
import org.hbase.async.SecureRpcHelper;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This base class provides the logic needed to interface with SASL supported
* RPC versions. It is used by RegionClient to perform RPC handshaking as well
* as wrapping/unwrapping the rpc payload depending on the selected QOP level.
* <br>
*
* For Kerberos the following configurations need to be set as system properties.
* <ul>
* <li>hbase.security.authentication=<MECHANISM></li>
* <li>hbase.kerberos.regionserver.principal=<REGIONSERVER PRINCIPAL></li>
* <li>hbase.rpc.protection=[authentication|integrity|privacy]</li>
* <li>hbase.sasl.clientconfig=<JAAS Profile Name></li>
* <li>java.security.auth.login.config=<Path to JAAS conf></li>
* </ul>
* i.e.
* <br>
* <ul>
* <li>hbase.security.authentication=kerberos</li>
* <li>hbase.kerberos.regionserver.principal=hbase/_HOST@MYREALM.COM</li>
* <li>hbase.rpc.protection=authentication</li>
* <li>hbase.sasl.clientconfig=Client</li>
* <li>java.security.auth.login.config=/path/to/jaas.conf</li>
* </ul>
* @since 1.7
*/
public abstract class SecureRpcHelper {
public static final String SECURITY_AUTHENTICATION_KEY =
"hbase.security.authentication";
public static final String RPC_QOP_KEY =
"hbase.rpc.protection";
private static final Logger LOG = LoggerFactory.getLogger(SecureRpcHelper.class);
/** The HBaseClient config to pull values from */
protected final Config config;
/** The region client this helper will be working with */
protected final RegionClient region_client;
/** The IP of the region client */
protected final String host_ip;
/** The authentication provider, e.g. Simple or Kerberos */
protected ClientAuthProvider client_auth_provider;
/** The Sasl client if used */
protected SaslClient sasl_client;
/** Whether or not to encrypt/decrypt the payload on the socket */
protected boolean use_wrap;
/**
* Ctor that instantiates the authentication provider and attempts to
* authenticate at the same time.
* @param hbase_client The Hbase client we belong to
* @param region_client The region client we're dealing with
* @param remote_endpoint The remote endpoint of the HBase Region server.
*/
public SecureRpcHelper(final HBaseClient hbase_client,
final RegionClient region_client, final SocketAddress remote_endpoint) {
config = hbase_client.getConfig();
this.host_ip = ((InetSocketAddress)remote_endpoint).getAddress()
.getHostAddress();
this.region_client = region_client;
initSecureClientProvider(hbase_client);
}
/**
* Instantiates the proper security provider based on the configuration. The
* provider can be either "simple", "kerberos" or a canonical class name
* to load from the class path. Note that this will attempt to authenticate
* so it will throw an exception if the login failed.
* @param hbase_client The hbase client we belong to.
* @throws IllegalStateException if the config contained a class name and we
* couldn't instantiate the class.
*/
private void initSecureClientProvider(final HBaseClient hbase_client) {
final String mechanism = config.hasProperty(SECURITY_AUTHENTICATION_KEY) ?
config.getString(SECURITY_AUTHENTICATION_KEY) : "simple";
if ("simple".equalsIgnoreCase(mechanism)) {
client_auth_provider = new SimpleClientAuthProvider(hbase_client);
use_wrap = false;
return;
}
if ("kerberos".equalsIgnoreCase(mechanism)) {
client_auth_provider = new KerberosClientAuthProvider(hbase_client);
} else {
try {
final Class<?> clazz = Class.forName(mechanism);
final Constructor<?> ctor = clazz.getConstructor(HBaseClient.class);
client_auth_provider = (ClientAuthProvider)ctor.newInstance(hbase_client);
LOG.info("Successfully instantiated a security provider of type: " +
clazz.getCanonicalName());
} catch (Exception e) {
throw new IllegalStateException(
"Failed to load specified SecureClientProvider: " + mechanism, e);
}
}
final String qop = parseQOP();
use_wrap = qop != null && !"auth".equalsIgnoreCase(qop);
final Map<String, String> props = new HashMap<String, String>(2);
props.put(Sasl.QOP, parseQOP());
props.put(Sasl.SERVER_AUTH, "true");
sasl_client = client_auth_provider.newSaslClient(host_ip, props);
}
/**
* Helper that checks to see if we should encrypt the packets between HBase
* based on the client config. If the client has set "integrity" or "privacy"
* then the packets will be encrypted. If set to "authentication" then no
* encryption is used. Any other value will throw an exception
* @return A string to compare against to see if we should wrap or not.
* @throws IllegalArgumentException if the config doesn't contain one of
* the three strings above.
*/
private String parseQOP() {
final String protection = config.hasProperty(RPC_QOP_KEY) ?
config.getString(RPC_QOP_KEY) : "authentication";
if ("integrity".equalsIgnoreCase(protection)) {
return "auth-int";
}
if ("privacy".equalsIgnoreCase(protection)) {
return "auth-conf";
}
if ("authentication".equalsIgnoreCase(protection)) {
return "auth";
}
throw new IllegalArgumentException("Unrecognized rpc protection level: "
+ protection);
}
/**
* When QOP of auth-int or auth-conf is selected
* This is used to unwrap the contents from the passed buffer payload. It
* should be called as soon as it comes off the Netty channel.
* Note that the ReplayingBufferDecoder may throw an error if we are unable
* to read the full buffer and replay the data.
* @param payload A wrapper around content.
* @return The content extracted from the payload.
* @throws IllegalStateException if the sasl client was unable to unwrap
* the payload
*/
public ChannelBuffer unwrap(final ChannelBuffer payload) {
if (!use_wrap) {
return payload;
}
final int len = payload.readInt();
try {
final ChannelBuffer unwrapped = ChannelBuffers.wrappedBuffer(
sasl_client.unwrap(payload.readBytes(len).array(), 0, len));
// If encryption was enabled, it's a good bet that you shouldn't log it
//if (LOG.isDebugEnabled()) {
// LOG.debug("Unwrapped payload: " + Bytes.pretty(unwrapped));
//}
return unwrapped;
} catch (SaslException e) {
throw new IllegalStateException("Failed to unwrap payload", e);
}
}
/**
* When QOP of auth-int or auth-conf is selected
* This is used to wrap the contents into the proper payload (ie encryption,
* signature, etc) and should be called just before sending the buffer to Netty.
* @param content The content to be wrapped.
* @return The wrapped payload.
* @throws IllegalStateException if the sasl client was unable to wrap
* the payload
*/
public ChannelBuffer wrap(final ChannelBuffer content) {
if (!use_wrap) {
return content;
}
try {
final byte[] payload = new byte[content.writerIndex()];
content.readBytes(payload);
final byte[] wrapped = sasl_client.wrap(payload, 0, payload.length);
final ChannelBuffer ret = ChannelBuffers.wrappedBuffer(
new byte[4 + wrapped.length]);
ret.clear();
ret.writeInt(wrapped.length);
ret.writeBytes(wrapped);
//if (LOG.isDebugEnabled()) {
// LOG.debug("Wrapped payload: " + Bytes.pretty(ret));
//}
return ret;
} catch (SaslException e) {
throw new IllegalStateException("Failed to wrap payload", e);
}
}
/**
* Sends the initial handshake and/or SASL challenge to the region server
* @param channel The channel to write to
*/
public abstract void sendHello(final Channel channel);
/**
* Handles a buffer received from the region server. It may be security
* related or it may be a wrapped packet if security has been negotiated.
* If wrapping is disabled, the original buffer is returned. If the data is
* security related, a null should be returned.
* NOTE: If the buffer IS security related, make sure to drain the entire
* buffer or Netty will replay it until you're done consuming.
* @param buf The buffer off the channel
* @param chan The channel that we received the buffer from
* @return The possibly unwrapped channel buffer for further decoding. If null
* then the response was security related.
*/
public abstract ChannelBuffer handleResponse(final ChannelBuffer buf,
final Channel chan);
/**
* Handles passing a buffer off to the sasl client for challenge evaluation
* @param b The buffer to process
* @return The results of the challenge if successful, or null if there was
* a SaslException.
* @throws IllegalStateException when processing of the action failed due to
* a PrivilegedActionException
*/
protected byte[] processChallenge(final byte[] b) {
try {
final class PrivilegedAction implements PrivilegedExceptionAction<byte[]> {
@Override
public byte[] run() {
try {
return sasl_client.evaluateChallenge(b);
} catch (SaslException e) {
LOG.error("Failed Sasl challenge", e);
return null;
}
}
@Override
public String toString() {
return "evaluate sasl challenge";
}
}
return Subject.doAs(client_auth_provider.getClientSubject(),
new PrivilegedAction());
} catch (PrivilegedActionException e) {
throw new IllegalStateException("Failed to process challenge", e);
}
}
}