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