1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.hadoop.hbase.security;
20
21 import org.apache.commons.logging.Log;
22 import org.apache.commons.logging.LogFactory;
23 import org.apache.hadoop.hbase.classification.InterfaceAudience;
24 import org.apache.hadoop.io.WritableUtils;
25 import org.apache.hadoop.ipc.RemoteException;
26 import org.apache.hadoop.security.SaslInputStream;
27 import org.apache.hadoop.security.SaslOutputStream;
28 import org.apache.hadoop.security.token.Token;
29 import org.apache.hadoop.security.token.TokenIdentifier;
30
31 import javax.security.auth.callback.Callback;
32 import javax.security.auth.callback.CallbackHandler;
33 import javax.security.auth.callback.NameCallback;
34 import javax.security.auth.callback.PasswordCallback;
35 import javax.security.auth.callback.UnsupportedCallbackException;
36 import javax.security.sasl.RealmCallback;
37 import javax.security.sasl.RealmChoiceCallback;
38 import javax.security.sasl.Sasl;
39 import javax.security.sasl.SaslClient;
40 import javax.security.sasl.SaslException;
41
42 import java.io.BufferedInputStream;
43 import java.io.BufferedOutputStream;
44 import java.io.DataInputStream;
45 import java.io.DataOutputStream;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.OutputStream;
49
50 import com.google.common.annotations.VisibleForTesting;
51
52
53
54
55
56 @InterfaceAudience.Private
57 public class HBaseSaslRpcClient {
58 private static final Log LOG = LogFactory.getLog(HBaseSaslRpcClient.class);
59
60 private final SaslClient saslClient;
61 private final boolean fallbackAllowed;
62
63
64
65
66
67
68
69
70
71
72
73
74
75 public HBaseSaslRpcClient(AuthMethod method,
76 Token<? extends TokenIdentifier> token, String serverPrincipal, boolean fallbackAllowed)
77 throws IOException {
78 this(method, token, serverPrincipal, fallbackAllowed, "authentication");
79 }
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95 public HBaseSaslRpcClient(AuthMethod method,
96 Token<? extends TokenIdentifier> token, String serverPrincipal, boolean fallbackAllowed,
97 String rpcProtection) throws IOException {
98 this.fallbackAllowed = fallbackAllowed;
99 SaslUtil.initSaslProperties(rpcProtection);
100 switch (method) {
101 case DIGEST:
102 if (LOG.isDebugEnabled())
103 LOG.debug("Creating SASL " + AuthMethod.DIGEST.getMechanismName()
104 + " client to authenticate to service at " + token.getService());
105 saslClient = createDigestSaslClient(
106 new String[] { AuthMethod.DIGEST.getMechanismName() },
107 SaslUtil.SASL_DEFAULT_REALM, new SaslClientCallbackHandler(token));
108 break;
109 case KERBEROS:
110 if (LOG.isDebugEnabled()) {
111 LOG
112 .debug("Creating SASL " + AuthMethod.KERBEROS.getMechanismName()
113 + " client. Server's Kerberos principal name is "
114 + serverPrincipal);
115 }
116 if (serverPrincipal == null || serverPrincipal.length() == 0) {
117 throw new IOException(
118 "Failed to specify server's Kerberos principal name");
119 }
120 String[] names = SaslUtil.splitKerberosName(serverPrincipal);
121 if (names.length != 3) {
122 throw new IOException(
123 "Kerberos principal does not have the expected format: "
124 + serverPrincipal);
125 }
126 saslClient = createKerberosSaslClient(
127 new String[] { AuthMethod.KERBEROS.getMechanismName() },
128 names[0], names[1]);
129 break;
130 default:
131 throw new IOException("Unknown authentication method " + method);
132 }
133 if (saslClient == null)
134 throw new IOException("Unable to find SASL client implementation");
135 }
136
137 protected SaslClient createDigestSaslClient(String[] mechanismNames,
138 String saslDefaultRealm, CallbackHandler saslClientCallbackHandler)
139 throws IOException {
140 return Sasl.createSaslClient(mechanismNames, null, null, saslDefaultRealm,
141 SaslUtil.SASL_PROPS, saslClientCallbackHandler);
142 }
143
144 protected SaslClient createKerberosSaslClient(String[] mechanismNames,
145 String userFirstPart, String userSecondPart) throws IOException {
146 return Sasl.createSaslClient(mechanismNames, null, userFirstPart,
147 userSecondPart, SaslUtil.SASL_PROPS, null);
148 }
149
150 private static void readStatus(DataInputStream inStream) throws IOException {
151 int status = inStream.readInt();
152 if (status != SaslStatus.SUCCESS.state) {
153 throw new RemoteException(WritableUtils.readString(inStream),
154 WritableUtils.readString(inStream));
155 }
156 }
157
158
159
160
161
162
163
164
165
166
167
168
169
170 public boolean saslConnect(InputStream inS, OutputStream outS)
171 throws IOException {
172 DataInputStream inStream = new DataInputStream(new BufferedInputStream(inS));
173 DataOutputStream outStream = new DataOutputStream(new BufferedOutputStream(
174 outS));
175
176 try {
177 byte[] saslToken = new byte[0];
178 if (saslClient.hasInitialResponse())
179 saslToken = saslClient.evaluateChallenge(saslToken);
180 if (saslToken != null) {
181 outStream.writeInt(saslToken.length);
182 outStream.write(saslToken, 0, saslToken.length);
183 outStream.flush();
184 if (LOG.isDebugEnabled())
185 LOG.debug("Have sent token of size " + saslToken.length
186 + " from initSASLContext.");
187 }
188 if (!saslClient.isComplete()) {
189 readStatus(inStream);
190 int len = inStream.readInt();
191 if (len == SaslUtil.SWITCH_TO_SIMPLE_AUTH) {
192 if (!fallbackAllowed) {
193 throw new IOException("Server asks us to fall back to SIMPLE auth, " +
194 "but this client is configured to only allow secure connections.");
195 }
196 if (LOG.isDebugEnabled()) {
197 LOG.debug("Server asks us to fall back to simple auth.");
198 }
199 saslClient.dispose();
200 return false;
201 }
202 saslToken = new byte[len];
203 if (LOG.isDebugEnabled())
204 LOG.debug("Will read input token of size " + saslToken.length
205 + " for processing by initSASLContext");
206 inStream.readFully(saslToken);
207 }
208
209 while (!saslClient.isComplete()) {
210 saslToken = saslClient.evaluateChallenge(saslToken);
211 if (saslToken != null) {
212 if (LOG.isDebugEnabled())
213 LOG.debug("Will send token of size " + saslToken.length
214 + " from initSASLContext.");
215 outStream.writeInt(saslToken.length);
216 outStream.write(saslToken, 0, saslToken.length);
217 outStream.flush();
218 }
219 if (!saslClient.isComplete()) {
220 readStatus(inStream);
221 saslToken = new byte[inStream.readInt()];
222 if (LOG.isDebugEnabled())
223 LOG.debug("Will read input token of size " + saslToken.length
224 + " for processing by initSASLContext");
225 inStream.readFully(saslToken);
226 }
227 }
228 if (LOG.isDebugEnabled()) {
229 LOG.debug("SASL client context established. Negotiated QoP: "
230 + saslClient.getNegotiatedProperty(Sasl.QOP));
231 }
232 return true;
233 } catch (IOException e) {
234 try {
235 saslClient.dispose();
236 } catch (SaslException ignored) {
237
238 }
239 throw e;
240 }
241 }
242
243
244
245
246
247
248
249
250
251
252 public InputStream getInputStream(InputStream in) throws IOException {
253 if (!saslClient.isComplete()) {
254 throw new IOException("Sasl authentication exchange hasn't completed yet");
255 }
256 return new SaslInputStream(in, saslClient);
257 }
258
259
260
261
262
263
264
265
266
267
268 public OutputStream getOutputStream(OutputStream out) throws IOException {
269 if (!saslClient.isComplete()) {
270 throw new IOException("Sasl authentication exchange hasn't completed yet");
271 }
272 return new SaslOutputStream(out, saslClient);
273 }
274
275
276 public void dispose() throws SaslException {
277 saslClient.dispose();
278 }
279
280 @VisibleForTesting
281 static class SaslClientCallbackHandler implements CallbackHandler {
282 private final String userName;
283 private final char[] userPassword;
284
285 public SaslClientCallbackHandler(Token<? extends TokenIdentifier> token) {
286 this.userName = SaslUtil.encodeIdentifier(token.getIdentifier());
287 this.userPassword = SaslUtil.encodePassword(token.getPassword());
288 }
289
290 @Override
291 public void handle(Callback[] callbacks)
292 throws UnsupportedCallbackException {
293 NameCallback nc = null;
294 PasswordCallback pc = null;
295 RealmCallback rc = null;
296 for (Callback callback : callbacks) {
297 if (callback instanceof RealmChoiceCallback) {
298 continue;
299 } else if (callback instanceof NameCallback) {
300 nc = (NameCallback) callback;
301 } else if (callback instanceof PasswordCallback) {
302 pc = (PasswordCallback) callback;
303 } else if (callback instanceof RealmCallback) {
304 rc = (RealmCallback) callback;
305 } else {
306 throw new UnsupportedCallbackException(callback,
307 "Unrecognized SASL client callback");
308 }
309 }
310 if (nc != null) {
311 if (LOG.isDebugEnabled())
312 LOG.debug("SASL client callback: setting username: " + userName);
313 nc.setName(userName);
314 }
315 if (pc != null) {
316 if (LOG.isDebugEnabled())
317 LOG.debug("SASL client callback: setting userPassword");
318 pc.setPassword(userPassword);
319 }
320 if (rc != null) {
321 if (LOG.isDebugEnabled())
322 LOG.debug("SASL client callback: setting realm: "
323 + rc.getDefaultText());
324 rc.setText(rc.getDefaultText());
325 }
326 }
327 }
328 }