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