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 }