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