diff --git a/hadoop-common-project/hadoop-common/CHANGES.txt b/hadoop-common-project/hadoop-common/CHANGES.txt index 3c06b8f91b5..00490fee1f4 100644 --- a/hadoop-common-project/hadoop-common/CHANGES.txt +++ b/hadoop-common-project/hadoop-common/CHANGES.txt @@ -9,6 +9,8 @@ Release 2.0.3-alpha - Unreleased HADOOP-8597. Permit FsShell's text command to read Avro files. (Ivan Vladimirov Ivanov via cutting) + HADOOP-9020. Add a SASL PLAIN server (daryn via bobby) + IMPROVEMENTS HADOOP-8789. Tests setLevel(Level.OFF) should be Level.ERROR. diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/SaslPlainServer.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/SaslPlainServer.java new file mode 100644 index 00000000000..7d1b98062b0 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/SaslPlainServer.java @@ -0,0 +1,159 @@ +/** + * 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 java.security.Provider; +import java.util.Map; + +import javax.security.auth.callback.*; +import javax.security.sasl.AuthorizeCallback; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslException; +import javax.security.sasl.SaslServer; +import javax.security.sasl.SaslServerFactory; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +@InterfaceAudience.Private +@InterfaceStability.Evolving +public class SaslPlainServer implements SaslServer { + @SuppressWarnings("serial") + public static class SecurityProvider extends Provider { + public SecurityProvider() { + super("SaslPlainServer", 1.0, "SASL PLAIN Authentication Server"); + put("SaslServerFactory.PLAIN", + SaslPlainServerFactory.class.getName()); + } + } + + public static class SaslPlainServerFactory implements SaslServerFactory { + @Override + public SaslServer createSaslServer(String mechanism, String protocol, + String serverName, Map props, CallbackHandler cbh) + throws SaslException { + return "PLAIN".equals(mechanism) ? new SaslPlainServer(cbh) : null; + } + @Override + public String[] getMechanismNames(Map props){ + return (props == null) || "false".equals(props.get(Sasl.POLICY_NOPLAINTEXT)) + ? new String[]{"PLAIN"} + : new String[0]; + } + } + + private CallbackHandler cbh; + private boolean completed; + private String authz; + + SaslPlainServer(CallbackHandler callback) { + this.cbh = callback; + } + + @Override + public String getMechanismName() { + return "PLAIN"; + } + + @Override + public byte[] evaluateResponse(byte[] response) throws SaslException { + if (completed) { + throw new IllegalStateException("PLAIN authentication has completed"); + } + if (response == null) { + throw new IllegalArgumentException("Received null response"); + } + try { + String payload; + try { + payload = new String(response, "UTF-8"); + } catch (Exception e) { + throw new IllegalArgumentException("Received corrupt response", e); + } + // [ authz, authn, password ] + String[] parts = payload.split("\u0000", 3); + if (parts.length != 3) { + throw new IllegalArgumentException("Received corrupt response"); + } + if (parts[0].isEmpty()) { // authz = authn + parts[0] = parts[1]; + } + + NameCallback nc = new NameCallback("SASL PLAIN"); + nc.setName(parts[1]); + PasswordCallback pc = new PasswordCallback("SASL PLAIN", false); + pc.setPassword(parts[2].toCharArray()); + AuthorizeCallback ac = new AuthorizeCallback(parts[1], parts[0]); + cbh.handle(new Callback[]{nc, pc, ac}); + if (ac.isAuthorized()) { + authz = ac.getAuthorizedID(); + } + } catch (Exception e) { + throw new SaslException("PLAIN auth failed: " + e.getMessage()); + } finally { + completed = true; + } + return null; + } + + private void throwIfNotComplete() { + if (!completed) { + throw new IllegalStateException("PLAIN authentication not completed"); + } + } + + @Override + public boolean isComplete() { + return completed; + } + + @Override + public String getAuthorizationID() { + throwIfNotComplete(); + return authz; + } + + @Override + public Object getNegotiatedProperty(String propName) { + throwIfNotComplete(); + return Sasl.QOP.equals(propName) ? "auth" : null; + } + + @Override + public byte[] wrap(byte[] outgoing, int offset, int len) + throws SaslException { + throwIfNotComplete(); + throw new IllegalStateException( + "PLAIN supports neither integrity nor privacy"); + } + + @Override + public byte[] unwrap(byte[] incoming, int offset, int len) + throws SaslException { + throwIfNotComplete(); + throw new IllegalStateException( + "PLAIN supports neither integrity nor privacy"); + } + + @Override + public void dispose() throws SaslException { + cbh = null; + authz = null; + } +} \ No newline at end of file diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/SaslRpcServer.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/SaslRpcServer.java index 31b4c35dae2..33942dc0885 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/SaslRpcServer.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/SaslRpcServer.java @@ -23,6 +23,7 @@ import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutput; import java.io.IOException; +import java.security.Security; import java.util.Map; import java.util.TreeMap; @@ -89,6 +90,7 @@ public class SaslRpcServer { SASL_PROPS.put(Sasl.QOP, saslQOP.getSaslQop()); SASL_PROPS.put(Sasl.SERVER_AUTH, "true"); + Security.addProvider(new SaslPlainServer.SecurityProvider()); } static String encodeIdentifier(byte[] identifier) { @@ -138,7 +140,8 @@ public class SaslRpcServer { public static enum AuthMethod { SIMPLE((byte) 80, ""), KERBEROS((byte) 81, "GSSAPI"), - DIGEST((byte) 82, "DIGEST-MD5"); + DIGEST((byte) 82, "DIGEST-MD5"), + PLAIN((byte) 83, "PLAIN"); /** The code for this method. */ public final byte code; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/TestSaslRPC.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/TestSaslRPC.java index 8308f323274..d9c2d7ad800 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/TestSaslRPC.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/TestSaslRPC.java @@ -27,12 +27,13 @@ import java.io.IOException; import java.lang.annotation.Annotation; import java.net.InetSocketAddress; import java.security.PrivilegedExceptionAction; +import java.security.Security; import java.util.Collection; import java.util.Set; import java.util.regex.Pattern; -import javax.security.sasl.Sasl; - +import javax.security.auth.callback.*; +import javax.security.sasl.*; import junit.framework.Assert; import org.apache.commons.logging.Log; @@ -44,6 +45,7 @@ import org.apache.hadoop.io.Text; import org.apache.hadoop.ipc.Client.ConnectionId; import org.apache.hadoop.net.NetUtils; import org.apache.hadoop.security.*; +import org.apache.hadoop.security.SaslRpcServer.AuthMethod; import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod; import org.apache.hadoop.security.token.SecretManager; import org.apache.hadoop.security.token.Token; @@ -53,7 +55,6 @@ import org.apache.hadoop.security.token.TokenSelector; import org.apache.hadoop.security.token.SecretManager.InvalidToken; import org.apache.log4j.Level; -import org.apache.tools.ant.types.Assertions.EnabledAssertion; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -76,7 +77,8 @@ public class TestSaslRPC { @BeforeClass public static void setupKerb() { System.setProperty("java.security.krb5.kdc", ""); - System.setProperty("java.security.krb5.realm", "NONE"); + System.setProperty("java.security.krb5.realm", "NONE"); + Security.addProvider(new SaslPlainServer.SecurityProvider()); } @Before @@ -448,6 +450,120 @@ public class TestSaslRPC { System.out.println("Test is successful."); } + @Test + public void testSaslPlainServer() throws IOException { + runNegotiation( + new TestPlainCallbacks.Client("user", "pass"), + new TestPlainCallbacks.Server("user", "pass")); + } + + @Test + public void testSaslPlainServerBadPassword() throws IOException { + SaslException e = null; + try { + runNegotiation( + new TestPlainCallbacks.Client("user", "pass1"), + new TestPlainCallbacks.Server("user", "pass2")); + } catch (SaslException se) { + e = se; + } + assertNotNull(e); + assertEquals("PLAIN auth failed: wrong password", e.getMessage()); + } + + + private void runNegotiation(CallbackHandler clientCbh, + CallbackHandler serverCbh) + throws SaslException { + String mechanism = AuthMethod.PLAIN.getMechanismName(); + + SaslClient saslClient = Sasl.createSaslClient( + new String[]{ mechanism }, null, null, null, null, clientCbh); + assertNotNull(saslClient); + + SaslServer saslServer = Sasl.createSaslServer( + mechanism, null, "localhost", null, serverCbh); + assertNotNull("failed to find PLAIN server", saslServer); + + byte[] response = saslClient.evaluateChallenge(new byte[0]); + assertNotNull(response); + assertTrue(saslClient.isComplete()); + + response = saslServer.evaluateResponse(response); + assertNull(response); + assertTrue(saslServer.isComplete()); + assertNotNull(saslServer.getAuthorizationID()); + } + + static class TestPlainCallbacks { + public static class Client implements CallbackHandler { + String user = null; + String password = null; + + Client(String user, String password) { + this.user = user; + this.password = password; + } + + @Override + public void handle(Callback[] callbacks) + throws UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(user); + } else if (callback instanceof PasswordCallback) { + ((PasswordCallback) callback).setPassword(password.toCharArray()); + } else { + throw new UnsupportedCallbackException(callback, + "Unrecognized SASL PLAIN Callback"); + } + } + } + } + + public static class Server implements CallbackHandler { + String user = null; + String password = null; + + Server(String user, String password) { + this.user = user; + this.password = password; + } + + @Override + public void handle(Callback[] callbacks) + throws UnsupportedCallbackException, SaslException { + NameCallback nc = null; + PasswordCallback pc = null; + AuthorizeCallback ac = null; + + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) { + nc = (NameCallback)callback; + assertEquals(user, nc.getName()); + } else if (callback instanceof PasswordCallback) { + pc = (PasswordCallback)callback; + if (!password.equals(new String(pc.getPassword()))) { + throw new IllegalArgumentException("wrong password"); + } + } else if (callback instanceof AuthorizeCallback) { + ac = (AuthorizeCallback)callback; + assertEquals(user, ac.getAuthorizationID()); + assertEquals(user, ac.getAuthenticationID()); + ac.setAuthorized(true); + ac.setAuthorizedID(ac.getAuthenticationID()); + } else { + throw new UnsupportedCallbackException(callback, + "Unsupported SASL PLAIN Callback"); + } + } + assertNotNull(nc); + assertNotNull(pc); + assertNotNull(ac); + } + } + } + private static Pattern BadToken = Pattern.compile(".*DIGEST-MD5: digest response format violation.*"); private static Pattern KrbFailed =