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.util.Map; 027 import java.util.TreeMap; 028 029 import javax.security.auth.callback.Callback; 030 import javax.security.auth.callback.CallbackHandler; 031 import javax.security.auth.callback.NameCallback; 032 import javax.security.auth.callback.PasswordCallback; 033 import javax.security.auth.callback.UnsupportedCallbackException; 034 import javax.security.sasl.AuthorizeCallback; 035 import javax.security.sasl.RealmCallback; 036 import javax.security.sasl.Sasl; 037 038 import org.apache.commons.codec.binary.Base64; 039 import org.apache.commons.logging.Log; 040 import org.apache.commons.logging.LogFactory; 041 import org.apache.hadoop.classification.InterfaceAudience; 042 import org.apache.hadoop.classification.InterfaceStability; 043 import org.apache.hadoop.conf.Configuration; 044 import org.apache.hadoop.ipc.Server; 045 import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod; 046 import org.apache.hadoop.security.token.SecretManager; 047 import org.apache.hadoop.security.token.TokenIdentifier; 048 import org.apache.hadoop.security.token.SecretManager.InvalidToken; 049 050 /** 051 * A utility class for dealing with SASL on RPC server 052 */ 053 @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"}) 054 @InterfaceStability.Evolving 055 public class SaslRpcServer { 056 public static final Log LOG = LogFactory.getLog(SaslRpcServer.class); 057 public static final String SASL_DEFAULT_REALM = "default"; 058 public static final Map<String, String> SASL_PROPS = 059 new TreeMap<String, String>(); 060 061 public static final int SWITCH_TO_SIMPLE_AUTH = -88; 062 063 public static enum QualityOfProtection { 064 AUTHENTICATION("auth"), 065 INTEGRITY("auth-int"), 066 PRIVACY("auth-conf"); 067 068 public final String saslQop; 069 070 private QualityOfProtection(String saslQop) { 071 this.saslQop = saslQop; 072 } 073 074 public String getSaslQop() { 075 return saslQop; 076 } 077 } 078 079 public static void init(Configuration conf) { 080 QualityOfProtection saslQOP = QualityOfProtection.AUTHENTICATION; 081 String rpcProtection = conf.get("hadoop.rpc.protection", 082 QualityOfProtection.AUTHENTICATION.name().toLowerCase()); 083 if (QualityOfProtection.INTEGRITY.name().toLowerCase() 084 .equals(rpcProtection)) { 085 saslQOP = QualityOfProtection.INTEGRITY; 086 } else if (QualityOfProtection.PRIVACY.name().toLowerCase().equals( 087 rpcProtection)) { 088 saslQOP = QualityOfProtection.PRIVACY; 089 } 090 091 SASL_PROPS.put(Sasl.QOP, saslQOP.getSaslQop()); 092 SASL_PROPS.put(Sasl.SERVER_AUTH, "true"); 093 } 094 095 static String encodeIdentifier(byte[] identifier) { 096 return new String(Base64.encodeBase64(identifier)); 097 } 098 099 static byte[] decodeIdentifier(String identifier) { 100 return Base64.decodeBase64(identifier.getBytes()); 101 } 102 103 public static <T extends TokenIdentifier> T getIdentifier(String id, 104 SecretManager<T> secretManager) throws InvalidToken { 105 byte[] tokenId = decodeIdentifier(id); 106 T tokenIdentifier = secretManager.createIdentifier(); 107 try { 108 tokenIdentifier.readFields(new DataInputStream(new ByteArrayInputStream( 109 tokenId))); 110 } catch (IOException e) { 111 throw (InvalidToken) new InvalidToken( 112 "Can't de-serialize tokenIdentifier").initCause(e); 113 } 114 return tokenIdentifier; 115 } 116 117 static char[] encodePassword(byte[] password) { 118 return new String(Base64.encodeBase64(password)).toCharArray(); 119 } 120 121 /** Splitting fully qualified Kerberos name into parts */ 122 public static String[] splitKerberosName(String fullName) { 123 return fullName.split("[/@]"); 124 } 125 126 @InterfaceStability.Evolving 127 public enum SaslStatus { 128 SUCCESS (0), 129 ERROR (1); 130 131 public final int state; 132 private SaslStatus(int state) { 133 this.state = state; 134 } 135 } 136 137 /** Authentication method */ 138 @InterfaceStability.Evolving 139 public static enum AuthMethod { 140 SIMPLE((byte) 80, "", AuthenticationMethod.SIMPLE), 141 KERBEROS((byte) 81, "GSSAPI", AuthenticationMethod.KERBEROS), 142 DIGEST((byte) 82, "DIGEST-MD5", AuthenticationMethod.TOKEN); 143 144 /** The code for this method. */ 145 public final byte code; 146 public final String mechanismName; 147 public final AuthenticationMethod authenticationMethod; 148 149 private AuthMethod(byte code, String mechanismName, 150 AuthenticationMethod authMethod) { 151 this.code = code; 152 this.mechanismName = mechanismName; 153 this.authenticationMethod = authMethod; 154 } 155 156 private static final int FIRST_CODE = values()[0].code; 157 158 /** Return the object represented by the code. */ 159 private static AuthMethod valueOf(byte code) { 160 final int i = (code & 0xff) - FIRST_CODE; 161 return i < 0 || i >= values().length ? null : values()[i]; 162 } 163 164 /** Return the SASL mechanism name */ 165 public String getMechanismName() { 166 return mechanismName; 167 } 168 169 /** Read from in */ 170 public static AuthMethod read(DataInput in) throws IOException { 171 return valueOf(in.readByte()); 172 } 173 174 /** Write to out */ 175 public void write(DataOutput out) throws IOException { 176 out.write(code); 177 } 178 }; 179 180 /** CallbackHandler for SASL DIGEST-MD5 mechanism */ 181 @InterfaceStability.Evolving 182 public static class SaslDigestCallbackHandler implements CallbackHandler { 183 private SecretManager<TokenIdentifier> secretManager; 184 private Server.Connection connection; 185 186 public SaslDigestCallbackHandler( 187 SecretManager<TokenIdentifier> secretManager, 188 Server.Connection connection) { 189 this.secretManager = secretManager; 190 this.connection = connection; 191 } 192 193 private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken { 194 return encodePassword(secretManager.retrievePassword(tokenid)); 195 } 196 197 /** {@inheritDoc} */ 198 @Override 199 public void handle(Callback[] callbacks) throws InvalidToken, 200 UnsupportedCallbackException { 201 NameCallback nc = null; 202 PasswordCallback pc = null; 203 AuthorizeCallback ac = null; 204 for (Callback callback : callbacks) { 205 if (callback instanceof AuthorizeCallback) { 206 ac = (AuthorizeCallback) callback; 207 } else if (callback instanceof NameCallback) { 208 nc = (NameCallback) callback; 209 } else if (callback instanceof PasswordCallback) { 210 pc = (PasswordCallback) callback; 211 } else if (callback instanceof RealmCallback) { 212 continue; // realm is ignored 213 } else { 214 throw new UnsupportedCallbackException(callback, 215 "Unrecognized SASL DIGEST-MD5 Callback"); 216 } 217 } 218 if (pc != null) { 219 TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(), secretManager); 220 char[] password = getPassword(tokenIdentifier); 221 UserGroupInformation user = null; 222 user = tokenIdentifier.getUser(); // may throw exception 223 connection.attemptingUser = user; 224 225 if (LOG.isDebugEnabled()) { 226 LOG.debug("SASL server DIGEST-MD5 callback: setting password " 227 + "for client: " + tokenIdentifier.getUser()); 228 } 229 pc.setPassword(password); 230 } 231 if (ac != null) { 232 String authid = ac.getAuthenticationID(); 233 String authzid = ac.getAuthorizationID(); 234 if (authid.equals(authzid)) { 235 ac.setAuthorized(true); 236 } else { 237 ac.setAuthorized(false); 238 } 239 if (ac.isAuthorized()) { 240 if (LOG.isDebugEnabled()) { 241 String username = 242 getIdentifier(authzid, secretManager).getUser().getUserName(); 243 LOG.debug("SASL server DIGEST-MD5 callback: setting " 244 + "canonicalized client ID: " + username); 245 } 246 ac.setAuthorizedID(authzid); 247 } 248 } 249 } 250 } 251 252 /** CallbackHandler for SASL GSSAPI Kerberos mechanism */ 253 @InterfaceStability.Evolving 254 public static class SaslGssCallbackHandler implements CallbackHandler { 255 256 /** {@inheritDoc} */ 257 @Override 258 public void handle(Callback[] callbacks) throws 259 UnsupportedCallbackException { 260 AuthorizeCallback ac = null; 261 for (Callback callback : callbacks) { 262 if (callback instanceof AuthorizeCallback) { 263 ac = (AuthorizeCallback) callback; 264 } else { 265 throw new UnsupportedCallbackException(callback, 266 "Unrecognized SASL GSSAPI Callback"); 267 } 268 } 269 if (ac != null) { 270 String authid = ac.getAuthenticationID(); 271 String authzid = ac.getAuthorizationID(); 272 if (authid.equals(authzid)) { 273 ac.setAuthorized(true); 274 } else { 275 ac.setAuthorized(false); 276 } 277 if (ac.isAuthorized()) { 278 if (LOG.isDebugEnabled()) 279 LOG.debug("SASL server GSSAPI callback: setting " 280 + "canonicalized client ID: " + authzid); 281 ac.setAuthorizedID(authzid); 282 } 283 } 284 } 285 } 286 }