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