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