/**
* 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.data.Stat;
import org.apache.zookeeper.proto.GetSASLRequest;
import org.apache.zookeeper.proto.ReplyHeader;
import org.apache.zookeeper.proto.RequestHeader;
import org.apache.zookeeper.proto.SetSASLResponse;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.server.auth.KerberosName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.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 {
private static final Logger LOG = LoggerFactory.getLogger(ZooKeeperSaslClient.class);
private static Login login = null;
private SaslClient saslClient;
private byte[] saslToken = new byte[0];
private ClientCnxn cnxn;
private enum SaslState {
INITIAL,INTERMEDIATE,COMPLETE
}
private SaslState saslState = SaslState.INITIAL;
public ZooKeeperSaslClient(ClientCnxn cnxn, String serverPrincipal) throws LoginException {
this.cnxn = cnxn;
this.saslClient = createSaslClient(serverPrincipal);
}
public boolean isComplete() {
return (saslState == SaslState.COMPLETE);
}
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 prepareSaslResponseToServer() 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.prepareSaslResponseToServer(usedata);
}
}
synchronized private SaslClient createSaslClient(final String servicePrincipal) throws LoginException {
try {
if (login == null) {
// 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("Client",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) {
throw e;
}
catch (Exception e) {
LOG.error("Exception while trying to create SASL client: " + e);
return null;
}
}
private void prepareSaslResponseToServer(byte[] serverToken) {
saslToken = serverToken;
if (saslClient == null) {
LOG.error("saslClient is unexpectedly null. Cannot respond to server's SASL message; ignoring.");
return;
}
LOG.debug("saslToken (server) length: " + saslToken.length);
if (!(saslClient.isComplete())) {
try {
saslToken = createSaslToken(saslToken);
if (saslToken != null) {
LOG.debug("saslToken (client) length: " + saslToken.length);
queueSaslPacket(saslToken);
}
} catch (SaslException e) {
// TODO sendThread should set state to AUTH_FAILED; but currently only sendThread modifies state.
LOG.error("SASL authentication failed.");
}
}
}
public 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)
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);
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.");
}
}
public void queueSaslPacket(byte[] saslToken) {
LOG.debug("ClientCnxn:sendSaslPacket:length="+saslToken.length);
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.sasl);
GetSASLRequest request = new GetSASLRequest();
request.setToken(saslToken);
SetSASLResponse response = new SetSASLResponse();
ServerSaslResponseCallback cb = new ServerSaslResponseCallback();
ReplyHeader r = new ReplyHeader();
cnxn.queuePacket(h,r,request,response,cb);
}
public void queueSaslPacket() throws SaslException {
queueSaslPacket(createSaslToken());
}
// used by ClientCnxn to know when to emit SaslAuthenticated event.
// transitions internally from INTERMEDIATE to COMPLETE as a side effect if
// it's ready to emit this event.
public boolean readyToSendSaslAuthEvent() {
if (saslClient != null) {
if (saslClient.isComplete()) {
if (saslState == SaslState.INTERMEDIATE) {
saslState = SaslState.COMPLETE;
return true;
}
}
}
else {
LOG.warn("saslClient is null: client could not authenticate properly.");
}
return false;
}
public void initialize() throws SaslException {
if (saslState == SaslState.INITIAL) {
if (saslClient.hasInitialResponse()) {
queueSaslPacket();
}
else {
byte[] emptyToken = new byte[0];
queueSaslPacket(emptyToken);
}
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");
}
}
}
}
}
}
}
}