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