1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package org.apache.zookeeper.server.quorum.auth; 20 21 import java.io.BufferedOutputStream; 22 import java.io.DataInputStream; 23 import java.io.DataOutputStream; 24 import java.io.IOException; 25 import java.net.Socket; 26 import java.security.PrivilegedActionException; 27 import java.security.PrivilegedExceptionAction; 28 import javax.security.auth.Subject; 29 import javax.security.auth.login.AppConfigurationEntry; 30 import javax.security.auth.login.Configuration; 31 import javax.security.auth.login.LoginException; 32 import javax.security.sasl.SaslClient; 33 import javax.security.sasl.SaslException; 34 import org.apache.jute.BinaryInputArchive; 35 import org.apache.jute.BinaryOutputArchive; 36 import org.apache.zookeeper.Login; 37 import org.apache.zookeeper.SaslClientCallbackHandler; 38 import org.apache.zookeeper.common.ZKConfig; 39 import org.apache.zookeeper.server.quorum.QuorumAuthPacket; 40 import org.apache.zookeeper.util.SecurityUtils; 41 import org.slf4j.Logger; 42 import org.slf4j.LoggerFactory; 43 44 public class SaslQuorumAuthLearner implements QuorumAuthLearner { 45 46 private static final Logger LOG = LoggerFactory.getLogger(SaslQuorumAuthLearner.class); 47 48 private final Login learnerLogin; 49 private final boolean quorumRequireSasl; 50 private final String quorumServicePrincipal; 51 SaslQuorumAuthLearner( boolean quorumRequireSasl, String quorumServicePrincipal, String loginContext)52 public SaslQuorumAuthLearner( 53 boolean quorumRequireSasl, 54 String quorumServicePrincipal, 55 String loginContext) throws SaslException { 56 this.quorumRequireSasl = quorumRequireSasl; 57 this.quorumServicePrincipal = quorumServicePrincipal; 58 try { 59 AppConfigurationEntry[] entries = Configuration.getConfiguration().getAppConfigurationEntry(loginContext); 60 if (entries == null || entries.length == 0) { 61 throw new LoginException(String.format( 62 "SASL-authentication failed because the specified JAAS configuration section '%s' could not be found.", 63 loginContext)); 64 } 65 this.learnerLogin = new Login( 66 loginContext, 67 new SaslClientCallbackHandler(null, "QuorumLearner"), 68 new ZKConfig()); 69 this.learnerLogin.startThreadIfNeeded(); 70 } catch (LoginException e) { 71 throw new SaslException("Failed to initialize authentication mechanism using SASL", e); 72 } 73 } 74 75 @Override authenticate(Socket sock, String hostName)76 public void authenticate(Socket sock, String hostName) throws IOException { 77 if (!quorumRequireSasl) { // let it through, we don't require auth 78 LOG.info( 79 "Skipping SASL authentication as {}={}", 80 QuorumAuth.QUORUM_LEARNER_SASL_AUTH_REQUIRED, 81 quorumRequireSasl); 82 return; 83 } 84 SaslClient sc = null; 85 String principalConfig = SecurityUtils.getServerPrincipal(quorumServicePrincipal, hostName); 86 try { 87 DataOutputStream dout = new DataOutputStream(sock.getOutputStream()); 88 DataInputStream din = new DataInputStream(sock.getInputStream()); 89 byte[] responseToken = new byte[0]; 90 sc = SecurityUtils.createSaslClient( 91 learnerLogin.getSubject(), 92 principalConfig, 93 QuorumAuth.QUORUM_SERVER_PROTOCOL_NAME, 94 QuorumAuth.QUORUM_SERVER_SASL_DIGEST, 95 LOG, 96 "QuorumLearner"); 97 98 if (sc.hasInitialResponse()) { 99 responseToken = createSaslToken(new byte[0], sc, learnerLogin); 100 } 101 send(dout, responseToken); 102 QuorumAuthPacket authPacket = receive(din); 103 QuorumAuth.Status qpStatus = QuorumAuth.Status.getStatus(authPacket.getStatus()); 104 while (!sc.isComplete()) { 105 switch (qpStatus) { 106 case SUCCESS: 107 responseToken = createSaslToken(authPacket.getToken(), sc, learnerLogin); 108 // we're done; don't expect to send another BIND 109 if (responseToken != null) { 110 throw new SaslException("Protocol error: attempting to send response after completion"); 111 } 112 break; 113 case IN_PROGRESS: 114 responseToken = createSaslToken(authPacket.getToken(), sc, learnerLogin); 115 send(dout, responseToken); 116 authPacket = receive(din); 117 qpStatus = QuorumAuth.Status.getStatus(authPacket.getStatus()); 118 break; 119 case ERROR: 120 throw new SaslException("Authentication failed against server addr: " + sock.getRemoteSocketAddress()); 121 default: 122 LOG.warn("Unknown status:{}!", qpStatus); 123 throw new SaslException("Authentication failed against server addr: " + sock.getRemoteSocketAddress()); 124 } 125 } 126 127 // Validate status code at the end of authentication exchange. 128 checkAuthStatus(sock, qpStatus); 129 } finally { 130 if (sc != null) { 131 try { 132 sc.dispose(); 133 } catch (SaslException e) { 134 LOG.error("SaslClient dispose() failed", e); 135 } 136 } 137 } 138 } 139 checkAuthStatus(Socket sock, QuorumAuth.Status qpStatus)140 private void checkAuthStatus(Socket sock, QuorumAuth.Status qpStatus) throws SaslException { 141 if (qpStatus == QuorumAuth.Status.SUCCESS) { 142 LOG.info( 143 "Successfully completed the authentication using SASL. server addr: {}, status: {}", 144 sock.getRemoteSocketAddress(), 145 qpStatus); 146 } else { 147 throw new SaslException("Authentication failed against server addr: " + sock.getRemoteSocketAddress() 148 + ", qpStatus: " + qpStatus); 149 } 150 } 151 receive(DataInputStream din)152 private QuorumAuthPacket receive(DataInputStream din) throws IOException { 153 QuorumAuthPacket authPacket = new QuorumAuthPacket(); 154 BinaryInputArchive bia = BinaryInputArchive.getArchive(din); 155 authPacket.deserialize(bia, QuorumAuth.QUORUM_AUTH_MESSAGE_TAG); 156 return authPacket; 157 } 158 send(DataOutputStream dout, byte[] response)159 private void send(DataOutputStream dout, byte[] response) throws IOException { 160 QuorumAuthPacket authPacket; 161 BufferedOutputStream bufferedOutput = new BufferedOutputStream(dout); 162 BinaryOutputArchive boa = BinaryOutputArchive.getArchive(bufferedOutput); 163 authPacket = QuorumAuth.createPacket(QuorumAuth.Status.IN_PROGRESS, response); 164 boa.writeRecord(authPacket, QuorumAuth.QUORUM_AUTH_MESSAGE_TAG); 165 bufferedOutput.flush(); 166 } 167 168 // TODO: need to consolidate the #createSaslToken() implementation between ZooKeeperSaslClient#createSaslToken(). createSaslToken( final byte[] saslToken, final SaslClient saslClient, final Login login)169 private byte[] createSaslToken( 170 final byte[] saslToken, 171 final SaslClient saslClient, 172 final Login login) throws SaslException { 173 if (saslToken == null) { 174 throw new SaslException("Error in authenticating with a Zookeeper Quorum member: the quorum member's saslToken is null."); 175 } 176 if (login.getSubject() != null) { 177 synchronized (login) { 178 try { 179 final byte[] retval = Subject.doAs(login.getSubject(), new PrivilegedExceptionAction<byte[]>() { 180 public byte[] run() throws SaslException { 181 LOG.debug("saslClient.evaluateChallenge(len={})", saslToken.length); 182 return saslClient.evaluateChallenge(saslToken); 183 } 184 }); 185 return retval; 186 } catch (PrivilegedActionException e) { 187 String error = "An error: (" + e + ") occurred when evaluating Zookeeper Quorum Member's received SASL token."; 188 // Try to provide hints to use about what went wrong so they 189 // can fix their configuration. 190 // TODO: introspect about e: look for GSS information. 191 final String UNKNOWN_SERVER_ERROR_TEXT = "(Mechanism level: Server not found in Kerberos database (7) - UNKNOWN_SERVER)"; 192 if (e.toString().indexOf(UNKNOWN_SERVER_ERROR_TEXT) > -1) { 193 error += " This may be caused by Java's being unable to resolve the Zookeeper Quorum Member's" 194 + " hostname correctly. You may want to try to adding" 195 + " '-Dsun.net.spi.nameservice.provider.1=dns,sun' to your server's JVMFLAGS environment."; 196 } 197 LOG.error(error); 198 throw new SaslException(error, e); 199 } 200 } 201 } else { 202 throw new SaslException("Cannot make SASL token without subject defined. " 203 + "For diagnosis, please look for WARNs and ERRORs in your log related to the Login class."); 204 } 205 } 206 207 } 208