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