diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Server.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Server.java index e0e4517ad584..c1e12d4cc52b 100644 --- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Server.java +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ipc_/Server.java @@ -97,7 +97,9 @@ import org.apache.hadoop.ipc_.protobuf.RpcHeaderProtos.RpcSaslProto.SaslState; import org.apache.hadoop.net.NetUtils; import org.apache.hadoop.security.AccessControlException; +import org.apache.hadoop.security_.SaslMechanismFactory; import org.apache.hadoop.security.SaslPropertiesResolver; +import org.apache.hadoop.security_.SaslRpcClient; import org.apache.hadoop.security_.SaslRpcServer; import org.apache.hadoop.security.SaslRpcServer.AuthMethod; import org.apache.hadoop.security.SecurityUtil; @@ -1916,6 +1918,10 @@ public Server getServer() { return Server.this; } + public Configuration getConf() { + return Server.this.getConf(); + } + /* Return true if the connection has no outstanding rpc */ private boolean isIdle() { return rpcCount.get() == 0; @@ -2383,7 +2389,8 @@ private RpcSaslProto buildSaslNegotiateResponse() RpcSaslProto negotiateMessage = negotiateResponse; // accelerate token negotiation by sending initial challenge // in the negotiation response - if (enabledAuthMethods.contains(AuthMethod.TOKEN)) { + if (enabledAuthMethods.contains(AuthMethod.TOKEN) + && SaslMechanismFactory.isDigestMechanism(AuthMethod.TOKEN)) { saslServer = createSaslServer(AuthMethod.TOKEN); byte[] challenge = saslServer.evaluateResponse(new byte[0]); RpcSaslProto.Builder negotiateBuilder = diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/CustomizedCallbackHandler.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/CustomizedCallbackHandler.java new file mode 100644 index 000000000000..2b60a4a971ec --- /dev/null +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/CustomizedCallbackHandler.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.security_; + +import org.apache.hadoop.conf.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** For handling customized {@link Callback}. */ +public interface CustomizedCallbackHandler { + Logger LOG = LoggerFactory.getLogger(CustomizedCallbackHandler.class); + + class Cache { + private static final Map MAP = new HashMap<>(); + + private static synchronized CustomizedCallbackHandler getSynchronously( + String key, Configuration conf) { + //check again synchronously + final CustomizedCallbackHandler cached = MAP.get(key); + if (cached != null) { + return cached; //cache hit + } + + //cache miss + final Class clazz = conf.getClass(key, DefaultHandler.class); + LOG.debug("{} = {}", key, clazz); + if (clazz == DefaultHandler.class) { + return DefaultHandler.INSTANCE; + } + + final Object created; + try { + created = clazz.newInstance(); + } catch (Exception e) { + LOG.warn("Failed to create a new instance of {}, fallback to {}", + clazz, DefaultHandler.class, e); + return DefaultHandler.INSTANCE; + } + + final CustomizedCallbackHandler handler = created instanceof CustomizedCallbackHandler ? + (CustomizedCallbackHandler) created : CustomizedCallbackHandler.delegate(created); + MAP.put(key, handler); + return handler; + } + + private static CustomizedCallbackHandler get(String key, Configuration conf) { + final CustomizedCallbackHandler cached = MAP.get(key); + return cached != null ? cached : getSynchronously(key, conf); + } + + public static synchronized void clear() { + MAP.clear(); + } + + private Cache() { } + } + + class DefaultHandler implements CustomizedCallbackHandler { + private static final DefaultHandler INSTANCE = new DefaultHandler(); + + @Override + public void handleCallbacks(List callbacks, String username, char[] password) + throws UnsupportedCallbackException { + if (!callbacks.isEmpty()) { + final Callback cb = callbacks.get(0); + throw new UnsupportedCallbackException(callbacks.get(0), + "Unsupported callback: " + (cb == null ? null : cb.getClass())); + } + } + } + + static CustomizedCallbackHandler delegate(Object delegated) { + final String methodName = "handleCallbacks"; + final Class clazz = delegated.getClass(); + final Method method; + try { + method = clazz.getMethod(methodName, List.class, String.class, char[].class); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Failed to get method " + methodName + " from " + clazz, e); + } + + return (callbacks, name, password) -> { + try { + method.invoke(delegated, callbacks, name, password); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IOException("Failed to invoke " + method, e); + } + }; + } + + static CustomizedCallbackHandler get(String key, Configuration conf) { + return Cache.get(key, conf); + } + + void handleCallbacks(List callbacks, String name, char[] password) + throws UnsupportedCallbackException, IOException; +} diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslMechanismFactory.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslMechanismFactory.java new file mode 100644 index 000000000000..f3b9f92934e6 --- /dev/null +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslMechanismFactory.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.security_; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.SaslRpcServer.AuthMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SASL related constants. + */ +public final class SaslMechanismFactory { + static final Logger LOG = LoggerFactory.getLogger(SaslMechanismFactory.class); + + public static final String HADOOP_SECURITY_SASL_MECHANISM_KEY + = "hadoop.security.sasl.mechanism"; + public static final String HADOOP_SECURITY_SASL_MECHANISM_DEFAULT + = "DIGEST-MD5"; + public static final String HADOOP_SECURITY_SASL_CUSTOMIZEDCALLBACKHANDLER_CLASS_KEY + = "hadoop.security.sasl.CustomizedCallbackHandler.class"; + + private static final String SASL_MECHANISM_ENV = "HADOOP_SASL_MECHANISM"; + private static volatile String mechanism; + + private static synchronized String getSynchronously() { + // env + final String envValue = System.getenv(SASL_MECHANISM_ENV); + LOG.debug("{} = {} (env)", SASL_MECHANISM_ENV, envValue); + + // conf + final Configuration conf = new Configuration(); + final String confValue = conf.get(HADOOP_SECURITY_SASL_MECHANISM_KEY, + HADOOP_SECURITY_SASL_MECHANISM_DEFAULT); + LOG.debug("{} = {} (conf)", HADOOP_SECURITY_SASL_MECHANISM_KEY, confValue); + + mechanism = envValue != null ? envValue + : confValue != null ? confValue + : HADOOP_SECURITY_SASL_MECHANISM_DEFAULT; + LOG.debug("SASL_MECHANISM = {} (effective)", mechanism); + return mechanism; + } + + public static String getMechanism() { + final String value = mechanism; + return value != null ? value : getSynchronously(); + } + + public static boolean isDefaultMechanism(AuthMethod authMethod) { + return HADOOP_SECURITY_SASL_MECHANISM_DEFAULT.equals(getMechanismName(authMethod)); + } + + public static boolean isDigestMechanism(AuthMethod authMethod) { + return getMechanismName(authMethod).startsWith("DIGEST-"); + } + + private SaslMechanismFactory() {} + + public static void main(String[] args) { + System.out.println("SASL_MECHANISM = " + getMechanism()); + } + + /** Helper to get actual mechanism name from config. Required because {@code AuthMethod} is from Hadoop, + * not forked (because it is used in UGI, etc.). */ + public static String getMechanismName(AuthMethod authMethod) { + switch (authMethod) { + case DIGEST: + case TOKEN: + return getMechanism(); + default: + return authMethod.getMechanismName(); + + } + } +} diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcClient.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcClient.java index 8efeb0738101..d3c0cedebadf 100644 --- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcClient.java +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcClient.java @@ -39,6 +39,7 @@ import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.kerberos.KerberosPrincipal; +import javax.security.sasl.AuthorizeCallback; import javax.security.sasl.RealmCallback; import javax.security.sasl.RealmChoiceCallback; import javax.security.sasl.Sasl; @@ -185,7 +186,7 @@ private boolean isValidAuthType(SaslAuth authType) { } // do we know what it is? is it using our mechanism? return authMethod != null && - authMethod.getMechanismName().equals(authType.getMechanism()); + SaslMechanismFactory.getMechanismName(authMethod).equals(authType.getMechanism()); } /** @@ -242,7 +243,7 @@ private SaslClient createSaslClient(SaslAuth authType) throw new IOException("Unknown authentication method " + method); } - String mechanism = method.getMechanismName(); + String mechanism = SaslMechanismFactory.getMechanismName(method); if (LOG.isDebugEnabled()) { LOG.debug("Creating SASL " + mechanism + "(" + method + ") " + " client to authenticate to service at " + saslServerName); @@ -664,9 +665,17 @@ public void handle(Callback[] callbacks) pc = (PasswordCallback) callback; } else if (callback instanceof RealmCallback) { rc = (RealmCallback) callback; + } else if (callback instanceof AuthorizeCallback) { + final AuthorizeCallback ac = (AuthorizeCallback) callback; + final String authId = ac.getAuthenticationID(); + final String authzId = ac.getAuthorizationID(); + ac.setAuthorized(authId.equals(authzId)); + if (ac.isAuthorized()) { + ac.setAuthorizedID(authzId); + } } else { throw new UnsupportedCallbackException(callback, - "Unrecognized SASL client callback"); + "Unrecognized SASL client callback " + callback.getClass()); } } if (nc != null) { @@ -712,4 +721,5 @@ public static String getHostName(String name) { } } } + } diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcServer.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcServer.java index 0fef4f21f8db..8f82854ed176 100644 --- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcServer.java +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/security_/SaslRpcServer.java @@ -18,10 +18,10 @@ package org.apache.hadoop.security_; +import static org.apache.hadoop.security_.SaslMechanismFactory.HADOOP_SECURITY_SASL_CUSTOMIZEDCALLBACKHANDLER_CLASS_KEY; + import java.io.ByteArrayInputStream; -import java.io.DataInput; import java.io.DataInputStream; -import java.io.DataOutput; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.PrivilegedExceptionAction; @@ -46,10 +46,8 @@ import org.apache.commons.codec.binary.Base64; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.ipc_.RetriableException; import org.apache.hadoop.ipc_.Server; import org.apache.hadoop.ipc_.Server.Connection; -import org.apache.hadoop.ipc_.StandbyException; import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.SaslPlainServer; import org.apache.hadoop.security.SaslRpcServer.AuthMethod; @@ -91,7 +89,7 @@ public String getSaslQop() { public SaslRpcServer(AuthMethod authMethod) throws IOException { this.authMethod = authMethod; - mechanism = authMethod.getMechanismName(); + mechanism = SaslMechanismFactory.getMechanismName(authMethod); switch (authMethod) { case SIMPLE: { return; // no sasl for simple @@ -214,30 +212,46 @@ public static String[] splitKerberosName(String fullName) { return fullName.split("[/@]"); } - /** CallbackHandler for SASL DIGEST-MD5 mechanism */ + /** CallbackHandler for SASL mechanism */ public static class SaslDigestCallbackHandler implements CallbackHandler { + private final CustomizedCallbackHandler customizedCallbackHandler; private SecretManager secretManager; private Server.Connection connection; public SaslDigestCallbackHandler( SecretManager secretManager, Server.Connection connection) { + this(secretManager, connection, connection.getConf()); + } + + public SaslDigestCallbackHandler( + SecretManager secretManager, + Server.Connection connection, + Configuration conf) { this.secretManager = secretManager; this.connection = connection; + this.customizedCallbackHandler = CustomizedCallbackHandler.get( + HADOOP_SECURITY_SASL_CUSTOMIZEDCALLBACKHANDLER_CLASS_KEY, conf); } - private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken, - StandbyException, RetriableException, IOException { + private char[] getPassword(TokenIdentifier tokenid) throws IOException { return encodePassword(secretManager.retriableRetrievePassword(tokenid)); } + private char[] getPassword(String name) throws IOException { + final TokenIdentifier tokenIdentifier = getIdentifier(name, secretManager); + final UserGroupInformation user = tokenIdentifier.getUser(); + connection.attemptingUser = user; + LOG.debug("SASL server callback: setting password for client: {}", user); + return getPassword(tokenIdentifier); + } + @Override - public void handle(Callback[] callbacks) throws InvalidToken, - UnsupportedCallbackException, StandbyException, RetriableException, - IOException { + public void handle(Callback[] callbacks) throws UnsupportedCallbackException, IOException { NameCallback nc = null; PasswordCallback pc = null; AuthorizeCallback ac = null; + List unknownCallbacks = null; for (Callback callback : callbacks) { if (callback instanceof AuthorizeCallback) { ac = (AuthorizeCallback) callback; @@ -248,23 +262,14 @@ public void handle(Callback[] callbacks) throws InvalidToken, } else if (callback instanceof RealmCallback) { continue; // realm is ignored } else { - throw new UnsupportedCallbackException(callback, - "Unrecognized SASL DIGEST-MD5 Callback"); + if (unknownCallbacks == null) { + unknownCallbacks = new ArrayList<>(); + } + unknownCallbacks.add(callback); } } if (pc != null) { - TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(), - secretManager); - char[] password = getPassword(tokenIdentifier); - UserGroupInformation user = null; - user = tokenIdentifier.getUser(); // may throw exception - connection.attemptingUser = user; - - if (LOG.isDebugEnabled()) { - LOG.debug("SASL server DIGEST-MD5 callback: setting password " - + "for client: " + tokenIdentifier.getUser()); - } - pc.setPassword(password); + pc.setPassword(getPassword(nc.getDefaultName())); } if (ac != null) { String authid = ac.getAuthenticationID(); @@ -279,12 +284,16 @@ public void handle(Callback[] callbacks) throws InvalidToken, UserGroupInformation logUser = getIdentifier(authzid, secretManager).getUser(); String username = logUser == null ? null : logUser.getUserName(); - LOG.debug("SASL server DIGEST-MD5 callback: setting " - + "canonicalized client ID: " + username); + LOG.debug("SASL server callback: setting authorizedID: {}", username); } ac.setAuthorizedID(authzid); } } + if (unknownCallbacks != null) { + final String name = nc != null ? nc.getDefaultName() : null; + final char[] password = name != null ? getPassword(name) : null; + customizedCallbackHandler.handleCallbacks(unknownCallbacks, name, password); + } } }