001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019 package org.apache.hadoop.security;
020
021 import java.io.ByteArrayInputStream;
022 import java.io.DataInput;
023 import java.io.DataInputStream;
024 import java.io.DataOutput;
025 import java.io.IOException;
026 import java.security.PrivilegedExceptionAction;
027 import java.security.Security;
028 import java.util.ArrayList;
029 import java.util.Enumeration;
030 import java.util.HashMap;
031 import java.util.List;
032 import java.util.Map;
033 import java.util.TreeMap;
034
035 import javax.security.auth.callback.Callback;
036 import javax.security.auth.callback.CallbackHandler;
037 import javax.security.auth.callback.NameCallback;
038 import javax.security.auth.callback.PasswordCallback;
039 import javax.security.auth.callback.UnsupportedCallbackException;
040 import javax.security.sasl.AuthorizeCallback;
041 import javax.security.sasl.RealmCallback;
042 import javax.security.sasl.Sasl;
043 import javax.security.sasl.SaslException;
044 import javax.security.sasl.SaslServer;
045 import javax.security.sasl.SaslServerFactory;
046
047 import org.apache.commons.codec.binary.Base64;
048 import org.apache.commons.logging.Log;
049 import org.apache.commons.logging.LogFactory;
050 import org.apache.hadoop.classification.InterfaceAudience;
051 import org.apache.hadoop.classification.InterfaceStability;
052 import org.apache.hadoop.conf.Configuration;
053 import org.apache.hadoop.ipc.RetriableException;
054 import org.apache.hadoop.ipc.Server;
055 import org.apache.hadoop.ipc.Server.Connection;
056 import org.apache.hadoop.ipc.StandbyException;
057 import org.apache.hadoop.security.token.SecretManager;
058 import org.apache.hadoop.security.token.SecretManager.InvalidToken;
059 import org.apache.hadoop.security.token.TokenIdentifier;
060
061 /**
062 * A utility class for dealing with SASL on RPC server
063 */
064 @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
065 @InterfaceStability.Evolving
066 public class SaslRpcServer {
067 public static final Log LOG = LogFactory.getLog(SaslRpcServer.class);
068 public static final String SASL_DEFAULT_REALM = "default";
069 public static final Map<String, String> SASL_PROPS =
070 new TreeMap<String, String>();
071 private static SaslServerFactory saslFactory;
072
073 public static enum QualityOfProtection {
074 AUTHENTICATION("auth"),
075 INTEGRITY("auth-int"),
076 PRIVACY("auth-conf");
077
078 public final String saslQop;
079
080 private QualityOfProtection(String saslQop) {
081 this.saslQop = saslQop;
082 }
083
084 public String getSaslQop() {
085 return saslQop;
086 }
087 }
088
089 @InterfaceAudience.Private
090 @InterfaceStability.Unstable
091 public AuthMethod authMethod;
092 public String mechanism;
093 public String protocol;
094 public String serverId;
095
096 @InterfaceAudience.Private
097 @InterfaceStability.Unstable
098 public SaslRpcServer(AuthMethod authMethod) throws IOException {
099 this.authMethod = authMethod;
100 mechanism = authMethod.getMechanismName();
101 switch (authMethod) {
102 case SIMPLE: {
103 return; // no sasl for simple
104 }
105 case TOKEN: {
106 protocol = "";
107 serverId = SaslRpcServer.SASL_DEFAULT_REALM;
108 break;
109 }
110 case KERBEROS: {
111 String fullName = UserGroupInformation.getCurrentUser().getUserName();
112 if (LOG.isDebugEnabled())
113 LOG.debug("Kerberos principal name is " + fullName);
114 // don't use KerberosName because we don't want auth_to_local
115 String[] parts = fullName.split("[/@]", 3);
116 protocol = parts[0];
117 // should verify service host is present here rather than in create()
118 // but lazy tests are using a UGI that isn't a SPN...
119 serverId = (parts.length < 2) ? "" : parts[1];
120 break;
121 }
122 default:
123 // we should never be able to get here
124 throw new AccessControlException(
125 "Server does not support SASL " + authMethod);
126 }
127 }
128
129 @InterfaceAudience.Private
130 @InterfaceStability.Unstable
131 public SaslServer create(Connection connection,
132 SecretManager<TokenIdentifier> secretManager
133 ) throws IOException, InterruptedException {
134 UserGroupInformation ugi = null;
135 final CallbackHandler callback;
136 switch (authMethod) {
137 case TOKEN: {
138 callback = new SaslDigestCallbackHandler(secretManager, connection);
139 break;
140 }
141 case KERBEROS: {
142 ugi = UserGroupInformation.getCurrentUser();
143 if (serverId.isEmpty()) {
144 throw new AccessControlException(
145 "Kerberos principal name does NOT have the expected "
146 + "hostname part: " + ugi.getUserName());
147 }
148 callback = new SaslGssCallbackHandler();
149 break;
150 }
151 default:
152 // we should never be able to get here
153 throw new AccessControlException(
154 "Server does not support SASL " + authMethod);
155 }
156
157 final SaslServer saslServer;
158 if (ugi != null) {
159 saslServer = ugi.doAs(
160 new PrivilegedExceptionAction<SaslServer>() {
161 @Override
162 public SaslServer run() throws SaslException {
163 return saslFactory.createSaslServer(mechanism, protocol, serverId,
164 SaslRpcServer.SASL_PROPS, callback);
165 }
166 });
167 } else {
168 saslServer = saslFactory.createSaslServer(mechanism, protocol, serverId,
169 SaslRpcServer.SASL_PROPS, callback);
170 }
171 if (saslServer == null) {
172 throw new AccessControlException(
173 "Unable to find SASL server implementation for " + mechanism);
174 }
175 if (LOG.isDebugEnabled()) {
176 LOG.debug("Created SASL server with mechanism = " + mechanism);
177 }
178 return saslServer;
179 }
180
181 public static void init(Configuration conf) {
182 QualityOfProtection saslQOP = QualityOfProtection.AUTHENTICATION;
183 String rpcProtection = conf.get("hadoop.rpc.protection",
184 QualityOfProtection.AUTHENTICATION.name().toLowerCase());
185 if (QualityOfProtection.INTEGRITY.name().toLowerCase()
186 .equals(rpcProtection)) {
187 saslQOP = QualityOfProtection.INTEGRITY;
188 } else if (QualityOfProtection.PRIVACY.name().toLowerCase().equals(
189 rpcProtection)) {
190 saslQOP = QualityOfProtection.PRIVACY;
191 }
192
193 SASL_PROPS.put(Sasl.QOP, saslQOP.getSaslQop());
194 SASL_PROPS.put(Sasl.SERVER_AUTH, "true");
195 Security.addProvider(new SaslPlainServer.SecurityProvider());
196 saslFactory = new FastSaslServerFactory(SASL_PROPS);
197 }
198
199 static String encodeIdentifier(byte[] identifier) {
200 return new String(Base64.encodeBase64(identifier));
201 }
202
203 static byte[] decodeIdentifier(String identifier) {
204 return Base64.decodeBase64(identifier.getBytes());
205 }
206
207 public static <T extends TokenIdentifier> T getIdentifier(String id,
208 SecretManager<T> secretManager) throws InvalidToken {
209 byte[] tokenId = decodeIdentifier(id);
210 T tokenIdentifier = secretManager.createIdentifier();
211 try {
212 tokenIdentifier.readFields(new DataInputStream(new ByteArrayInputStream(
213 tokenId)));
214 } catch (IOException e) {
215 throw (InvalidToken) new InvalidToken(
216 "Can't de-serialize tokenIdentifier").initCause(e);
217 }
218 return tokenIdentifier;
219 }
220
221 static char[] encodePassword(byte[] password) {
222 return new String(Base64.encodeBase64(password)).toCharArray();
223 }
224
225 /** Splitting fully qualified Kerberos name into parts */
226 public static String[] splitKerberosName(String fullName) {
227 return fullName.split("[/@]");
228 }
229
230 /** Authentication method */
231 @InterfaceStability.Evolving
232 public static enum AuthMethod {
233 SIMPLE((byte) 80, ""),
234 KERBEROS((byte) 81, "GSSAPI"),
235 @Deprecated
236 DIGEST((byte) 82, "DIGEST-MD5"),
237 TOKEN((byte) 82, "DIGEST-MD5"),
238 PLAIN((byte) 83, "PLAIN");
239
240 /** The code for this method. */
241 public final byte code;
242 public final String mechanismName;
243
244 private AuthMethod(byte code, String mechanismName) {
245 this.code = code;
246 this.mechanismName = mechanismName;
247 }
248
249 private static final int FIRST_CODE = values()[0].code;
250
251 /** Return the object represented by the code. */
252 private static AuthMethod valueOf(byte code) {
253 final int i = (code & 0xff) - FIRST_CODE;
254 return i < 0 || i >= values().length ? null : values()[i];
255 }
256
257 /** Return the SASL mechanism name */
258 public String getMechanismName() {
259 return mechanismName;
260 }
261
262 /** Read from in */
263 public static AuthMethod read(DataInput in) throws IOException {
264 return valueOf(in.readByte());
265 }
266
267 /** Write to out */
268 public void write(DataOutput out) throws IOException {
269 out.write(code);
270 }
271 };
272
273 /** CallbackHandler for SASL DIGEST-MD5 mechanism */
274 @InterfaceStability.Evolving
275 public static class SaslDigestCallbackHandler implements CallbackHandler {
276 private SecretManager<TokenIdentifier> secretManager;
277 private Server.Connection connection;
278
279 public SaslDigestCallbackHandler(
280 SecretManager<TokenIdentifier> secretManager,
281 Server.Connection connection) {
282 this.secretManager = secretManager;
283 this.connection = connection;
284 }
285
286 private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken,
287 StandbyException, RetriableException, IOException {
288 return encodePassword(secretManager.retriableRetrievePassword(tokenid));
289 }
290
291 @Override
292 public void handle(Callback[] callbacks) throws InvalidToken,
293 UnsupportedCallbackException, StandbyException, RetriableException,
294 IOException {
295 NameCallback nc = null;
296 PasswordCallback pc = null;
297 AuthorizeCallback ac = null;
298 for (Callback callback : callbacks) {
299 if (callback instanceof AuthorizeCallback) {
300 ac = (AuthorizeCallback) callback;
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 continue; // realm is ignored
307 } else {
308 throw new UnsupportedCallbackException(callback,
309 "Unrecognized SASL DIGEST-MD5 Callback");
310 }
311 }
312 if (pc != null) {
313 TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(),
314 secretManager);
315 char[] password = getPassword(tokenIdentifier);
316 UserGroupInformation user = null;
317 user = tokenIdentifier.getUser(); // may throw exception
318 connection.attemptingUser = user;
319
320 if (LOG.isDebugEnabled()) {
321 LOG.debug("SASL server DIGEST-MD5 callback: setting password "
322 + "for client: " + tokenIdentifier.getUser());
323 }
324 pc.setPassword(password);
325 }
326 if (ac != null) {
327 String authid = ac.getAuthenticationID();
328 String authzid = ac.getAuthorizationID();
329 if (authid.equals(authzid)) {
330 ac.setAuthorized(true);
331 } else {
332 ac.setAuthorized(false);
333 }
334 if (ac.isAuthorized()) {
335 if (LOG.isDebugEnabled()) {
336 String username =
337 getIdentifier(authzid, secretManager).getUser().getUserName();
338 LOG.debug("SASL server DIGEST-MD5 callback: setting "
339 + "canonicalized client ID: " + username);
340 }
341 ac.setAuthorizedID(authzid);
342 }
343 }
344 }
345 }
346
347 /** CallbackHandler for SASL GSSAPI Kerberos mechanism */
348 @InterfaceStability.Evolving
349 public static class SaslGssCallbackHandler implements CallbackHandler {
350
351 @Override
352 public void handle(Callback[] callbacks) throws
353 UnsupportedCallbackException {
354 AuthorizeCallback ac = null;
355 for (Callback callback : callbacks) {
356 if (callback instanceof AuthorizeCallback) {
357 ac = (AuthorizeCallback) callback;
358 } else {
359 throw new UnsupportedCallbackException(callback,
360 "Unrecognized SASL GSSAPI Callback");
361 }
362 }
363 if (ac != null) {
364 String authid = ac.getAuthenticationID();
365 String authzid = ac.getAuthorizationID();
366 if (authid.equals(authzid)) {
367 ac.setAuthorized(true);
368 } else {
369 ac.setAuthorized(false);
370 }
371 if (ac.isAuthorized()) {
372 if (LOG.isDebugEnabled())
373 LOG.debug("SASL server GSSAPI callback: setting "
374 + "canonicalized client ID: " + authzid);
375 ac.setAuthorizedID(authzid);
376 }
377 }
378 }
379 }
380
381 // Sasl.createSaslServer is 100-200X slower than caching the factories!
382 private static class FastSaslServerFactory implements SaslServerFactory {
383 private final Map<String,List<SaslServerFactory>> factoryCache =
384 new HashMap<String,List<SaslServerFactory>>();
385
386 FastSaslServerFactory(Map<String,?> props) {
387 final Enumeration<SaslServerFactory> factories =
388 Sasl.getSaslServerFactories();
389 while (factories.hasMoreElements()) {
390 SaslServerFactory factory = factories.nextElement();
391 for (String mech : factory.getMechanismNames(props)) {
392 if (!factoryCache.containsKey(mech)) {
393 factoryCache.put(mech, new ArrayList<SaslServerFactory>());
394 }
395 factoryCache.get(mech).add(factory);
396 }
397 }
398 }
399
400 @Override
401 public SaslServer createSaslServer(String mechanism, String protocol,
402 String serverName, Map<String,?> props, CallbackHandler cbh)
403 throws SaslException {
404 SaslServer saslServer = null;
405 List<SaslServerFactory> factories = factoryCache.get(mechanism);
406 if (factories != null) {
407 for (SaslServerFactory factory : factories) {
408 saslServer = factory.createSaslServer(
409 mechanism, protocol, serverName, props, cbh);
410 if (saslServer != null) {
411 break;
412 }
413 }
414 }
415 return saslServer;
416 }
417
418 @Override
419 public String[] getMechanismNames(Map<String, ?> props) {
420 return factoryCache.keySet().toArray(new String[0]);
421 }
422 }
423 }