/**
* 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.zookeeper.client;
import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.ClientCnxn;
import org.apache.zookeeper.Login;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.Environment;
import org.apache.zookeeper.data.Stat;
import org.apache.zookeeper.proto.GetSASLRequest;
import org.apache.zookeeper.proto.SetSASLResponse;
import org.apache.zookeeper.server.auth.KerberosName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
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.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
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 javax.security.sasl.SaslException;
/**
* This class manages SASL authentication for the client. It
* allows ClientCnxn to authenticate using SASL with a Zookeeper server.
*/
public class ZooKeeperSaslClient {
public static final String LOGIN_CONTEXT_NAME_KEY = "zookeeper.sasl.clientconfig";
private static final Logger LOG = LoggerFactory.getLogger(ZooKeeperSaslClient.class);
private static Login login = null;
private SaslClient saslClient;
private byte[] saslToken = new byte[0];
public enum SaslState {
INITIAL,INTERMEDIATE,COMPLETE,FAILED
}
private SaslState saslState = SaslState.INITIAL;
private boolean gotLastPacket = false;
/** informational message indicating the current configuration status */
private final String configStatus;
public SaslState getSaslState() {
return saslState;
}
public String getLoginContext() {
if (login != null)
return login.getLoginContextName();
return null;
}
public ZooKeeperSaslClient(final String serverPrincipal)
throws LoginException {
/**
* ZOOKEEPER-1373: allow system property to specify the JAAS
* configuration section that the zookeeper client should use.
* Default to "Client".
*/
String clientSection = System.getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY, "Client");
// Note that 'Configuration' here refers to javax.security.auth.login.Configuration.
AppConfigurationEntry entries[] = null;
SecurityException securityException = null;
try {
entries = Configuration.getConfiguration().getAppConfigurationEntry(clientSection);
} catch (SecurityException e) {
// handle below: might be harmless if the user doesn't intend to use JAAS authentication.
securityException = e;
}
if (entries != null) {
this.configStatus = "Will attempt to SASL-authenticate using Login Context section '" + clientSection + "'";
this.saslClient = createSaslClient(serverPrincipal, clientSection);
} else {
// Handle situation of clientSection's being null: it might simply because the client does not intend to
// use SASL, so not necessarily an error.
saslState = SaslState.FAILED;
String explicitClientSection = System.getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY);
if (explicitClientSection != null) {
// If the user explicitly overrides the default Login Context, they probably expected SASL to
// succeed. But if we got here, SASL failed.
if (securityException != null) {
throw new LoginException("Zookeeper client cannot authenticate using the " + explicitClientSection +
" section of the supplied JAAS configuration: '" +
System.getProperty(Environment.JAAS_CONF_KEY) + "' because of a " +
"SecurityException: " + securityException);
} else {
throw new LoginException("Client cannot SASL-authenticate because the specified JAAS configuration " +
"section '" + explicitClientSection + "' could not be found.");
}
} else {
// The user did not override the default context. It might be that they just don't intend to use SASL,
// so log at INFO, not WARN, since they don't expect any SASL-related information.
String msg = "Will not attempt to authenticate using SASL ";
if (securityException != null) {
msg += "(" + securityException.getLocalizedMessage() + ")";
} else {
msg += "(unknown error)";
}
this.configStatus = msg;
}
if (System.getProperty(Environment.JAAS_CONF_KEY) != null) {
// Again, the user explicitly set something SASL-related, so they probably expected SASL to succeed.
if (securityException != null) {
throw new LoginException("Zookeeper client cannot authenticate using the '" +
System.getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY, "Client") +
"' section of the supplied JAAS configuration: '" +
System.getProperty(Environment.JAAS_CONF_KEY) + "' because of a " +
"SecurityException: " + securityException);
} else {
throw new LoginException("No JAAS configuration section named '" +
System.getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY, "Client") +
"' was found in specified JAAS configuration file: '" +
System.getProperty(Environment.JAAS_CONF_KEY) + "'.");
}
}
}
}
/**
* @return informational message indicating the current configuration status.
*/
public String getConfigStatus() {
return configStatus;
}
public boolean isComplete() {
return (saslState == SaslState.COMPLETE);
}
public boolean isFailed() {
return (saslState == SaslState.FAILED);
}
public static class ServerSaslResponseCallback implements AsyncCallback.DataCallback {
public void processResult(int rc, String path, Object ctx, byte data[], Stat stat) {
// processResult() is used by ClientCnxn's sendThread to respond to
// data[] contains the Zookeeper Server's SASL token.
// ctx is the ZooKeeperSaslClient object. We use this object's respondToServer() method
// to reply to the Zookeeper Server's SASL token
ZooKeeperSaslClient client = ((ClientCnxn)ctx).zooKeeperSaslClient;
if (client == null) {
LOG.warn("sasl client was unexpectedly null: cannot respond to Zookeeper server.");
return;
}
byte[] usedata = data;
if (data != null) {
LOG.debug("ServerSaslResponseCallback(): saslToken server response: (length="+usedata.length+")");
}
else {
usedata = new byte[0];
LOG.debug("ServerSaslResponseCallback(): using empty data[] as server response (length="+usedata.length+")");
}
client.respondToServer(usedata, (ClientCnxn)ctx);
}
}
synchronized private SaslClient createSaslClient(final String servicePrincipal,
final String loginContext) throws LoginException {
try {
if (login == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("JAAS loginContext is: " + loginContext);
}
// note that the login object is static: it's shared amongst all zookeeper-related connections.
// createSaslClient() must be declared synchronized so that login is initialized only once.
login = new Login(loginContext, new ClientCallbackHandler(null));
login.startThreadIfNeeded();
}
Subject subject = login.getSubject();
SaslClient saslClient;
// Use subject.getPrincipals().isEmpty() as an indication of which SASL mechanism to use:
// if empty, use DIGEST-MD5; otherwise, use GSSAPI.
if (subject.getPrincipals().isEmpty()) {
// no principals: must not be GSSAPI: use DIGEST-MD5 mechanism instead.
LOG.info("Client will use DIGEST-MD5 as SASL mechanism.");
String[] mechs = {"DIGEST-MD5"};
String username = (String)(subject.getPublicCredentials().toArray()[0]);
String password = (String)(subject.getPrivateCredentials().toArray()[0]);
// "zk-sasl-md5" is a hard-wired 'domain' parameter shared with zookeeper server code (see ServerCnxnFactory.java)
saslClient = Sasl.createSaslClient(mechs, username, "zookeeper", "zk-sasl-md5", null, new ClientCallbackHandler(password));
return saslClient;
}
else { // GSSAPI.
final Object[] principals = subject.getPrincipals().toArray();
// determine client principal from subject.
final Principal clientPrincipal = (Principal)principals[0];
final KerberosName clientKerberosName = new KerberosName(clientPrincipal.getName());
// assume that server and client are in the same realm (by default; unless the system property
// "zookeeper.server.realm" is set).
String serverRealm = System.getProperty("zookeeper.server.realm",clientKerberosName.getRealm());
KerberosName serviceKerberosName = new KerberosName(servicePrincipal+"@"+serverRealm);
final String serviceName = serviceKerberosName.getServiceName();
final String serviceHostname = serviceKerberosName.getHostName();
final String clientPrincipalName = clientKerberosName.toString();
try {
saslClient = Subject.doAs(subject,new PrivilegedExceptionAction<SaslClient>() {
public SaslClient run() throws SaslException {
LOG.info("Client will use GSSAPI as SASL mechanism.");
String[] mechs = {"GSSAPI"};
LOG.debug("creating sasl client: client="+clientPrincipalName+";service="+serviceName+";serviceHostname="+serviceHostname);
SaslClient saslClient = Sasl.createSaslClient(mechs,clientPrincipalName,serviceName,serviceHostname,null,new ClientCallbackHandler(null));
return saslClient;
}
});
return saslClient;
}
catch (Exception e) {
LOG.error("Error creating SASL client:" + e);
e.printStackTrace();
return null;
}
}
} catch (LoginException e) {
// We throw LoginExceptions...
throw e;
} catch (Exception e) {
// ..but consume (with a log message) all other types of exceptions.
LOG.error("Exception while trying to create SASL client: " + e);
return null;
}
}
public void respondToServer(byte[] serverToken, ClientCnxn cnxn) {
if (saslClient == null) {
LOG.error("saslClient is unexpectedly null. Cannot respond to server's SASL message; ignoring.");
return;
}
if (!(saslClient.isComplete())) {
try {
saslToken = createSaslToken(serverToken);
if (saslToken != null) {
sendSaslPacket(saslToken, cnxn);
}
} catch (SaslException e) {
LOG.error("SASL authentication failed using login context '" +
this.getLoginContext() + "'.");
saslState = SaslState.FAILED;
gotLastPacket = true;
}
}
if (saslClient.isComplete()) {
// GSSAPI: server sends a final packet after authentication succeeds
// or fails.
if ((serverToken == null) && (saslClient.getMechanismName() == "GSSAPI"))
gotLastPacket = true;
// non-GSSAPI: no final packet from server.
if (saslClient.getMechanismName() != "GSSAPI") {
gotLastPacket = true;
}
// SASL authentication is completed, successfully or not:
// enable the socket's writable flag so that any packets waiting for authentication to complete in
// the outgoing queue will be sent to the Zookeeper server.
cnxn.enableWrite();
}
}
private byte[] createSaslToken() throws SaslException {
saslState = SaslState.INTERMEDIATE;
return createSaslToken(saslToken);
}
private byte[] createSaslToken(final byte[] saslToken) throws SaslException {
if (saslToken == null) {
// TODO: introspect about runtime environment (such as jaas.conf)
saslState = SaslState.FAILED;
throw new SaslException("Error in authenticating with a Zookeeper Quorum member: the quorum member's saslToken is null.");
}
Subject subject = login.getSubject();
if (subject != null) {
synchronized(login) {
try {
final byte[] retval =
Subject.doAs(subject, new PrivilegedExceptionAction<byte[]>() {
public byte[] run() throws SaslException {
LOG.debug("saslClient.evaluateChallenge(len="+saslToken.length+")");
return saslClient.evaluateChallenge(saslToken);
}
});
return retval;
}
catch (PrivilegedActionException e) {
String error = "An error: (" + e + ") occurred when evaluating Zookeeper Quorum Member's " +
" received SASL token.";
// Try to provide hints to use about what went wrong so they can fix their configuration.
// TODO: introspect about e: look for GSS information.
final String UNKNOWN_SERVER_ERROR_TEXT =
"(Mechanism level: Server not found in Kerberos database (7) - UNKNOWN_SERVER)";
if (e.toString().indexOf(UNKNOWN_SERVER_ERROR_TEXT) > -1) {
error += " This may be caused by Java's being unable to resolve the Zookeeper Quorum Member's" +
" hostname correctly. You may want to try to adding" +
" '-Dsun.net.spi.nameservice.provider.1=dns,sun' to your client's JVMFLAGS environment.";
}
error += " Zookeeper Client will go to AUTH_FAILED state.";
LOG.error(error);
saslState = SaslState.FAILED;
throw new SaslException(error);
}
}
}
else {
throw new SaslException("Cannot make SASL token without subject defined. " +
"For diagnosis, please look for WARNs and ERRORs in your log related to the Login class.");
}
}
private void sendSaslPacket(byte[] saslToken, ClientCnxn cnxn)
throws SaslException{
if (LOG.isDebugEnabled()) {
LOG.debug("ClientCnxn:sendSaslPacket:length="+saslToken.length);
}
GetSASLRequest request = new GetSASLRequest();
request.setToken(saslToken);
SetSASLResponse response = new SetSASLResponse();
ServerSaslResponseCallback cb = new ServerSaslResponseCallback();
try {
cnxn.sendPacket(request,response,cb, ZooDefs.OpCode.sasl);
} catch (IOException e) {
throw new SaslException("Failed to send SASL packet to server.",
e);
}
}
private void sendSaslPacket(ClientCnxn cnxn) throws SaslException {
if (LOG.isDebugEnabled()) {
LOG.debug("ClientCnxn:sendSaslPacket:length="+saslToken.length);
}
GetSASLRequest request = new GetSASLRequest();
request.setToken(createSaslToken());
SetSASLResponse response = new SetSASLResponse();
ServerSaslResponseCallback cb = new ServerSaslResponseCallback();
try {
cnxn.sendPacket(request,response,cb, ZooDefs.OpCode.sasl);
} catch (IOException e) {
throw new SaslException("Failed to send SASL packet to server due " +
"to IOException:", e);
}
}
// used by ClientCnxn to know whether to emit a SASL-related event: either AuthFailed or SaslAuthenticated,
// or none, if not ready yet. Sets saslState to COMPLETE as a side-effect.
public KeeperState getKeeperState() {
if (saslClient != null) {
if (saslState == SaslState.FAILED) {
return KeeperState.AuthFailed;
}
if (saslClient.isComplete()) {
if (saslState == SaslState.INTERMEDIATE) {
saslState = SaslState.COMPLETE;
return KeeperState.SaslAuthenticated;
}
}
}
// No event ready to emit yet.
return null;
}
// Initialize the client's communications with the Zookeeper server by sending the server the first
// authentication packet.
public void initialize(ClientCnxn cnxn) throws SaslException {
if (saslClient == null) {
saslState = SaslState.FAILED;
throw new SaslException("saslClient failed to initialize properly: it's null.");
}
if (saslState == SaslState.INITIAL) {
if (saslClient.hasInitialResponse()) {
sendSaslPacket(cnxn);
}
else {
byte[] emptyToken = new byte[0];
sendSaslPacket(emptyToken, cnxn);
}
saslState = SaslState.INTERMEDIATE;
}
}
// The CallbackHandler interface here refers to
// javax.security.auth.callback.CallbackHandler.
// It should not be confused with Zookeeper packet callbacks like
// org.apache.zookeeper.server.auth.SaslServerCallbackHandler.
public static class ClientCallbackHandler implements CallbackHandler {
private String password = null;
public ClientCallbackHandler(String password) {
this.password = password;
}
public void handle(Callback[] callbacks) throws
UnsupportedCallbackException {
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
NameCallback nc = (NameCallback) callback;
nc.setName(nc.getDefaultName());
}
else {
if (callback instanceof PasswordCallback) {
PasswordCallback pc = (PasswordCallback)callback;
if (password != null) {
pc.setPassword(this.password.toCharArray());
} else {
LOG.warn("Could not login: the client is being asked for a password, but the Zookeeper" +
" 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 Zookeeper 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) {
RealmCallback rc = (RealmCallback) callback;
rc.setText(rc.getDefaultText());
}
else {
if (callback instanceof AuthorizeCallback) {
AuthorizeCallback ac = (AuthorizeCallback) callback;
String authid = ac.getAuthenticationID();
String authzid = ac.getAuthorizationID();
if (authid.equals(authzid)) {
ac.setAuthorized(true);
} else {
ac.setAuthorized(false);
}
if (ac.isAuthorized()) {
ac.setAuthorizedID(authzid);
}
}
else {
throw new UnsupportedCallbackException(callback,"Unrecognized SASL ClientCallback");
}
}
}
}
}
}
}
public boolean clientTunneledAuthenticationInProgress() {
// TODO: Rather than checking a disjunction here, should be a single member
// variable or method in this class to determine whether the client is
// configured to use SASL. (see also ZOOKEEPER-1455).
try {
if ((System.getProperty(Environment.JAAS_CONF_KEY) != null) ||
((javax.security.auth.login.Configuration.getConfiguration() != null) &&
(javax.security.auth.login.Configuration.getConfiguration().
getAppConfigurationEntry(System.
getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY,"Client"))
!= null))) {
// Client is configured to use a valid login Configuration, so
// authentication is either in progress, successful, or failed.
// 1. Authentication hasn't finished yet: we must wait for it to do so.
if ((isComplete() == false) &&
(isFailed() == false)) {
return true;
}
// 2. SASL authentication has succeeded or failed..
if (isComplete() || isFailed()) {
if (gotLastPacket == false) {
// ..but still in progress, because there is a final SASL
// message from server which must be received.
return true;
}
}
}
// Either client is not configured to use a tunnelled authentication
// scheme, or tunnelled authentication has completed (successfully or
// not), and all server SASL messages have been received.
return false;
} catch (SecurityException e) {
// Thrown if the caller does not have permission to retrieve the Configuration.
// In this case, simply returning false is correct.
if (LOG.isDebugEnabled() == true) {
LOG.debug("Could not retrieve login configuration: " + e);
}
return false;
}
}
}