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(final Connection connection, 132 final Map<String,?> saslProperties, 133 SecretManager<TokenIdentifier> secretManager 134 ) throws IOException, InterruptedException { 135 UserGroupInformation ugi = null; 136 final CallbackHandler callback; 137 switch (authMethod) { 138 case TOKEN: { 139 callback = new SaslDigestCallbackHandler(secretManager, connection); 140 break; 141 } 142 case KERBEROS: { 143 ugi = UserGroupInformation.getCurrentUser(); 144 if (serverId.isEmpty()) { 145 throw new AccessControlException( 146 "Kerberos principal name does NOT have the expected " 147 + "hostname part: " + ugi.getUserName()); 148 } 149 callback = new SaslGssCallbackHandler(); 150 break; 151 } 152 default: 153 // we should never be able to get here 154 throw new AccessControlException( 155 "Server does not support SASL " + authMethod); 156 } 157 158 final SaslServer saslServer; 159 if (ugi != null) { 160 saslServer = ugi.doAs( 161 new PrivilegedExceptionAction<SaslServer>() { 162 @Override 163 public SaslServer run() throws SaslException { 164 return saslFactory.createSaslServer(mechanism, protocol, serverId, 165 saslProperties, callback); 166 } 167 }); 168 } else { 169 saslServer = saslFactory.createSaslServer(mechanism, protocol, serverId, 170 saslProperties, callback); 171 } 172 if (saslServer == null) { 173 throw new AccessControlException( 174 "Unable to find SASL server implementation for " + mechanism); 175 } 176 if (LOG.isDebugEnabled()) { 177 LOG.debug("Created SASL server with mechanism = " + mechanism); 178 } 179 return saslServer; 180 } 181 182 public static void init(Configuration conf) { 183 Security.addProvider(new SaslPlainServer.SecurityProvider()); 184 // passing null so factory is populated with all possibilities. the 185 // properties passed when instantiating a server are what really matter 186 saslFactory = new FastSaslServerFactory(null); 187 } 188 189 static String encodeIdentifier(byte[] identifier) { 190 return new String(Base64.encodeBase64(identifier)); 191 } 192 193 static byte[] decodeIdentifier(String identifier) { 194 return Base64.decodeBase64(identifier.getBytes()); 195 } 196 197 public static <T extends TokenIdentifier> T getIdentifier(String id, 198 SecretManager<T> secretManager) throws InvalidToken { 199 byte[] tokenId = decodeIdentifier(id); 200 T tokenIdentifier = secretManager.createIdentifier(); 201 try { 202 tokenIdentifier.readFields(new DataInputStream(new ByteArrayInputStream( 203 tokenId))); 204 } catch (IOException e) { 205 throw (InvalidToken) new InvalidToken( 206 "Can't de-serialize tokenIdentifier").initCause(e); 207 } 208 return tokenIdentifier; 209 } 210 211 static char[] encodePassword(byte[] password) { 212 return new String(Base64.encodeBase64(password)).toCharArray(); 213 } 214 215 /** Splitting fully qualified Kerberos name into parts */ 216 public static String[] splitKerberosName(String fullName) { 217 return fullName.split("[/@]"); 218 } 219 220 /** Authentication method */ 221 @InterfaceStability.Evolving 222 public static enum AuthMethod { 223 SIMPLE((byte) 80, ""), 224 KERBEROS((byte) 81, "GSSAPI"), 225 @Deprecated 226 DIGEST((byte) 82, "DIGEST-MD5"), 227 TOKEN((byte) 82, "DIGEST-MD5"), 228 PLAIN((byte) 83, "PLAIN"); 229 230 /** The code for this method. */ 231 public final byte code; 232 public final String mechanismName; 233 234 private AuthMethod(byte code, String mechanismName) { 235 this.code = code; 236 this.mechanismName = mechanismName; 237 } 238 239 private static final int FIRST_CODE = values()[0].code; 240 241 /** Return the object represented by the code. */ 242 private static AuthMethod valueOf(byte code) { 243 final int i = (code & 0xff) - FIRST_CODE; 244 return i < 0 || i >= values().length ? null : values()[i]; 245 } 246 247 /** Return the SASL mechanism name */ 248 public String getMechanismName() { 249 return mechanismName; 250 } 251 252 /** Read from in */ 253 public static AuthMethod read(DataInput in) throws IOException { 254 return valueOf(in.readByte()); 255 } 256 257 /** Write to out */ 258 public void write(DataOutput out) throws IOException { 259 out.write(code); 260 } 261 }; 262 263 /** CallbackHandler for SASL DIGEST-MD5 mechanism */ 264 @InterfaceStability.Evolving 265 public static class SaslDigestCallbackHandler implements CallbackHandler { 266 private SecretManager<TokenIdentifier> secretManager; 267 private Server.Connection connection; 268 269 public SaslDigestCallbackHandler( 270 SecretManager<TokenIdentifier> secretManager, 271 Server.Connection connection) { 272 this.secretManager = secretManager; 273 this.connection = connection; 274 } 275 276 private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken, 277 StandbyException, RetriableException, IOException { 278 return encodePassword(secretManager.retriableRetrievePassword(tokenid)); 279 } 280 281 @Override 282 public void handle(Callback[] callbacks) throws InvalidToken, 283 UnsupportedCallbackException, StandbyException, RetriableException, 284 IOException { 285 NameCallback nc = null; 286 PasswordCallback pc = null; 287 AuthorizeCallback ac = null; 288 for (Callback callback : callbacks) { 289 if (callback instanceof AuthorizeCallback) { 290 ac = (AuthorizeCallback) callback; 291 } else if (callback instanceof NameCallback) { 292 nc = (NameCallback) callback; 293 } else if (callback instanceof PasswordCallback) { 294 pc = (PasswordCallback) callback; 295 } else if (callback instanceof RealmCallback) { 296 continue; // realm is ignored 297 } else { 298 throw new UnsupportedCallbackException(callback, 299 "Unrecognized SASL DIGEST-MD5 Callback"); 300 } 301 } 302 if (pc != null) { 303 TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(), 304 secretManager); 305 char[] password = getPassword(tokenIdentifier); 306 UserGroupInformation user = null; 307 user = tokenIdentifier.getUser(); // may throw exception 308 connection.attemptingUser = user; 309 310 if (LOG.isDebugEnabled()) { 311 LOG.debug("SASL server DIGEST-MD5 callback: setting password " 312 + "for client: " + tokenIdentifier.getUser()); 313 } 314 pc.setPassword(password); 315 } 316 if (ac != null) { 317 String authid = ac.getAuthenticationID(); 318 String authzid = ac.getAuthorizationID(); 319 if (authid.equals(authzid)) { 320 ac.setAuthorized(true); 321 } else { 322 ac.setAuthorized(false); 323 } 324 if (ac.isAuthorized()) { 325 if (LOG.isDebugEnabled()) { 326 String username = 327 getIdentifier(authzid, secretManager).getUser().getUserName(); 328 LOG.debug("SASL server DIGEST-MD5 callback: setting " 329 + "canonicalized client ID: " + username); 330 } 331 ac.setAuthorizedID(authzid); 332 } 333 } 334 } 335 } 336 337 /** CallbackHandler for SASL GSSAPI Kerberos mechanism */ 338 @InterfaceStability.Evolving 339 public static class SaslGssCallbackHandler implements CallbackHandler { 340 341 @Override 342 public void handle(Callback[] callbacks) throws 343 UnsupportedCallbackException { 344 AuthorizeCallback ac = null; 345 for (Callback callback : callbacks) { 346 if (callback instanceof AuthorizeCallback) { 347 ac = (AuthorizeCallback) callback; 348 } else { 349 throw new UnsupportedCallbackException(callback, 350 "Unrecognized SASL GSSAPI Callback"); 351 } 352 } 353 if (ac != null) { 354 String authid = ac.getAuthenticationID(); 355 String authzid = ac.getAuthorizationID(); 356 if (authid.equals(authzid)) { 357 ac.setAuthorized(true); 358 } else { 359 ac.setAuthorized(false); 360 } 361 if (ac.isAuthorized()) { 362 if (LOG.isDebugEnabled()) 363 LOG.debug("SASL server GSSAPI callback: setting " 364 + "canonicalized client ID: " + authzid); 365 ac.setAuthorizedID(authzid); 366 } 367 } 368 } 369 } 370 371 // Sasl.createSaslServer is 100-200X slower than caching the factories! 372 private static class FastSaslServerFactory implements SaslServerFactory { 373 private final Map<String,List<SaslServerFactory>> factoryCache = 374 new HashMap<String,List<SaslServerFactory>>(); 375 376 FastSaslServerFactory(Map<String,?> props) { 377 final Enumeration<SaslServerFactory> factories = 378 Sasl.getSaslServerFactories(); 379 while (factories.hasMoreElements()) { 380 SaslServerFactory factory = factories.nextElement(); 381 for (String mech : factory.getMechanismNames(props)) { 382 if (!factoryCache.containsKey(mech)) { 383 factoryCache.put(mech, new ArrayList<SaslServerFactory>()); 384 } 385 factoryCache.get(mech).add(factory); 386 } 387 } 388 } 389 390 @Override 391 public SaslServer createSaslServer(String mechanism, String protocol, 392 String serverName, Map<String,?> props, CallbackHandler cbh) 393 throws SaslException { 394 SaslServer saslServer = null; 395 List<SaslServerFactory> factories = factoryCache.get(mechanism); 396 if (factories != null) { 397 for (SaslServerFactory factory : factories) { 398 saslServer = factory.createSaslServer( 399 mechanism, protocol, serverName, props, cbh); 400 if (saslServer != null) { 401 break; 402 } 403 } 404 } 405 return saslServer; 406 } 407 408 @Override 409 public String[] getMechanismNames(Map<String, ?> props) { 410 return factoryCache.keySet().toArray(new String[0]); 411 } 412 } 413 }