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(final Connection connection,
132 final Map<String,?> saslProperties,
133 SecretManager<TokenIdentifier> secretManager
134 ) throws IOException, InterruptedException {
135 UserGroupInformation ugi = null;
136 final CallbackHandler callback;
137 switch (authMethod) {
138 case TOKEN: {
139 callback = new SaslDigestCallbackHandler(secretManager, connection);
140 break;
141 }
142 case KERBEROS: {
143 ugi = UserGroupInformation.getCurrentUser();
144 if (serverId.isEmpty()) {
145 throw new AccessControlException(
146 "Kerberos principal name does NOT have the expected "
147 + "hostname part: " + ugi.getUserName());
148 }
149 callback = new SaslGssCallbackHandler();
150 break;
151 }
152 default:
153 // we should never be able to get here
154 throw new AccessControlException(
155 "Server does not support SASL " + authMethod);
156 }
157
158 final SaslServer saslServer;
159 if (ugi != null) {
160 saslServer = ugi.doAs(
161 new PrivilegedExceptionAction<SaslServer>() {
162 @Override
163 public SaslServer run() throws SaslException {
164 return saslFactory.createSaslServer(mechanism, protocol, serverId,
165 saslProperties, callback);
166 }
167 });
168 } else {
169 saslServer = saslFactory.createSaslServer(mechanism, protocol, serverId,
170 saslProperties, callback);
171 }
172 if (saslServer == null) {
173 throw new AccessControlException(
174 "Unable to find SASL server implementation for " + mechanism);
175 }
176 if (LOG.isDebugEnabled()) {
177 LOG.debug("Created SASL server with mechanism = " + mechanism);
178 }
179 return saslServer;
180 }
181
182 public static void init(Configuration conf) {
183 Security.addProvider(new SaslPlainServer.SecurityProvider());
184 // passing null so factory is populated with all possibilities. the
185 // properties passed when instantiating a server are what really matter
186 saslFactory = new FastSaslServerFactory(null);
187 }
188
189 static String encodeIdentifier(byte[] identifier) {
190 return new String(Base64.encodeBase64(identifier));
191 }
192
193 static byte[] decodeIdentifier(String identifier) {
194 return Base64.decodeBase64(identifier.getBytes());
195 }
196
197 public static <T extends TokenIdentifier> T getIdentifier(String id,
198 SecretManager<T> secretManager) throws InvalidToken {
199 byte[] tokenId = decodeIdentifier(id);
200 T tokenIdentifier = secretManager.createIdentifier();
201 try {
202 tokenIdentifier.readFields(new DataInputStream(new ByteArrayInputStream(
203 tokenId)));
204 } catch (IOException e) {
205 throw (InvalidToken) new InvalidToken(
206 "Can't de-serialize tokenIdentifier").initCause(e);
207 }
208 return tokenIdentifier;
209 }
210
211 static char[] encodePassword(byte[] password) {
212 return new String(Base64.encodeBase64(password)).toCharArray();
213 }
214
215 /** Splitting fully qualified Kerberos name into parts */
216 public static String[] splitKerberosName(String fullName) {
217 return fullName.split("[/@]");
218 }
219
220 /** Authentication method */
221 @InterfaceStability.Evolving
222 public static enum AuthMethod {
223 SIMPLE((byte) 80, ""),
224 KERBEROS((byte) 81, "GSSAPI"),
225 @Deprecated
226 DIGEST((byte) 82, "DIGEST-MD5"),
227 TOKEN((byte) 82, "DIGEST-MD5"),
228 PLAIN((byte) 83, "PLAIN");
229
230 /** The code for this method. */
231 public final byte code;
232 public final String mechanismName;
233
234 private AuthMethod(byte code, String mechanismName) {
235 this.code = code;
236 this.mechanismName = mechanismName;
237 }
238
239 private static final int FIRST_CODE = values()[0].code;
240
241 /** Return the object represented by the code. */
242 private static AuthMethod valueOf(byte code) {
243 final int i = (code & 0xff) - FIRST_CODE;
244 return i < 0 || i >= values().length ? null : values()[i];
245 }
246
247 /** Return the SASL mechanism name */
248 public String getMechanismName() {
249 return mechanismName;
250 }
251
252 /** Read from in */
253 public static AuthMethod read(DataInput in) throws IOException {
254 return valueOf(in.readByte());
255 }
256
257 /** Write to out */
258 public void write(DataOutput out) throws IOException {
259 out.write(code);
260 }
261 };
262
263 /** CallbackHandler for SASL DIGEST-MD5 mechanism */
264 @InterfaceStability.Evolving
265 public static class SaslDigestCallbackHandler implements CallbackHandler {
266 private SecretManager<TokenIdentifier> secretManager;
267 private Server.Connection connection;
268
269 public SaslDigestCallbackHandler(
270 SecretManager<TokenIdentifier> secretManager,
271 Server.Connection connection) {
272 this.secretManager = secretManager;
273 this.connection = connection;
274 }
275
276 private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken,
277 StandbyException, RetriableException, IOException {
278 return encodePassword(secretManager.retriableRetrievePassword(tokenid));
279 }
280
281 @Override
282 public void handle(Callback[] callbacks) throws InvalidToken,
283 UnsupportedCallbackException, StandbyException, RetriableException,
284 IOException {
285 NameCallback nc = null;
286 PasswordCallback pc = null;
287 AuthorizeCallback ac = null;
288 for (Callback callback : callbacks) {
289 if (callback instanceof AuthorizeCallback) {
290 ac = (AuthorizeCallback) callback;
291 } else if (callback instanceof NameCallback) {
292 nc = (NameCallback) callback;
293 } else if (callback instanceof PasswordCallback) {
294 pc = (PasswordCallback) callback;
295 } else if (callback instanceof RealmCallback) {
296 continue; // realm is ignored
297 } else {
298 throw new UnsupportedCallbackException(callback,
299 "Unrecognized SASL DIGEST-MD5 Callback");
300 }
301 }
302 if (pc != null) {
303 TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(),
304 secretManager);
305 char[] password = getPassword(tokenIdentifier);
306 UserGroupInformation user = null;
307 user = tokenIdentifier.getUser(); // may throw exception
308 connection.attemptingUser = user;
309
310 if (LOG.isDebugEnabled()) {
311 LOG.debug("SASL server DIGEST-MD5 callback: setting password "
312 + "for client: " + tokenIdentifier.getUser());
313 }
314 pc.setPassword(password);
315 }
316 if (ac != null) {
317 String authid = ac.getAuthenticationID();
318 String authzid = ac.getAuthorizationID();
319 if (authid.equals(authzid)) {
320 ac.setAuthorized(true);
321 } else {
322 ac.setAuthorized(false);
323 }
324 if (ac.isAuthorized()) {
325 if (LOG.isDebugEnabled()) {
326 String username =
327 getIdentifier(authzid, secretManager).getUser().getUserName();
328 LOG.debug("SASL server DIGEST-MD5 callback: setting "
329 + "canonicalized client ID: " + username);
330 }
331 ac.setAuthorizedID(authzid);
332 }
333 }
334 }
335 }
336
337 /** CallbackHandler for SASL GSSAPI Kerberos mechanism */
338 @InterfaceStability.Evolving
339 public static class SaslGssCallbackHandler implements CallbackHandler {
340
341 @Override
342 public void handle(Callback[] callbacks) throws
343 UnsupportedCallbackException {
344 AuthorizeCallback ac = null;
345 for (Callback callback : callbacks) {
346 if (callback instanceof AuthorizeCallback) {
347 ac = (AuthorizeCallback) callback;
348 } else {
349 throw new UnsupportedCallbackException(callback,
350 "Unrecognized SASL GSSAPI Callback");
351 }
352 }
353 if (ac != null) {
354 String authid = ac.getAuthenticationID();
355 String authzid = ac.getAuthorizationID();
356 if (authid.equals(authzid)) {
357 ac.setAuthorized(true);
358 } else {
359 ac.setAuthorized(false);
360 }
361 if (ac.isAuthorized()) {
362 if (LOG.isDebugEnabled())
363 LOG.debug("SASL server GSSAPI callback: setting "
364 + "canonicalized client ID: " + authzid);
365 ac.setAuthorizedID(authzid);
366 }
367 }
368 }
369 }
370
371 // Sasl.createSaslServer is 100-200X slower than caching the factories!
372 private static class FastSaslServerFactory implements SaslServerFactory {
373 private final Map<String,List<SaslServerFactory>> factoryCache =
374 new HashMap<String,List<SaslServerFactory>>();
375
376 FastSaslServerFactory(Map<String,?> props) {
377 final Enumeration<SaslServerFactory> factories =
378 Sasl.getSaslServerFactories();
379 while (factories.hasMoreElements()) {
380 SaslServerFactory factory = factories.nextElement();
381 for (String mech : factory.getMechanismNames(props)) {
382 if (!factoryCache.containsKey(mech)) {
383 factoryCache.put(mech, new ArrayList<SaslServerFactory>());
384 }
385 factoryCache.get(mech).add(factory);
386 }
387 }
388 }
389
390 @Override
391 public SaslServer createSaslServer(String mechanism, String protocol,
392 String serverName, Map<String,?> props, CallbackHandler cbh)
393 throws SaslException {
394 SaslServer saslServer = null;
395 List<SaslServerFactory> factories = factoryCache.get(mechanism);
396 if (factories != null) {
397 for (SaslServerFactory factory : factories) {
398 saslServer = factory.createSaslServer(
399 mechanism, protocol, serverName, props, cbh);
400 if (saslServer != null) {
401 break;
402 }
403 }
404 }
405 return saslServer;
406 }
407
408 @Override
409 public String[] getMechanismNames(Map<String, ?> props) {
410 return factoryCache.keySet().toArray(new String[0]);
411 }
412 }
413 }