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