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