HADOOP-9020. Add a SASL PLAIN server (daryn via bobby)
git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/trunk@1407622 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
9e3af0ccd2
commit
de1f33dc35
|
@ -291,6 +291,8 @@ Release 2.0.3-alpha - Unreleased
|
||||||
HADOOP-8597. Permit FsShell's text command to read Avro files.
|
HADOOP-8597. Permit FsShell's text command to read Avro files.
|
||||||
(Ivan Vladimirov Ivanov via cutting)
|
(Ivan Vladimirov Ivanov via cutting)
|
||||||
|
|
||||||
|
HADOOP-9020. Add a SASL PLAIN server (daryn via bobby)
|
||||||
|
|
||||||
IMPROVEMENTS
|
IMPROVEMENTS
|
||||||
|
|
||||||
HADOOP-8789. Tests setLevel(Level.OFF) should be Level.ERROR.
|
HADOOP-8789. Tests setLevel(Level.OFF) should be Level.ERROR.
|
||||||
|
|
|
@ -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<String,?> props, CallbackHandler cbh)
|
||||||
|
throws SaslException {
|
||||||
|
return "PLAIN".equals(mechanism) ? new SaslPlainServer(cbh) : null;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String[] getMechanismNames(Map<String,?> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import java.io.DataInput;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutput;
|
import java.io.DataOutput;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.security.Security;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
@ -89,6 +90,7 @@ public class SaslRpcServer {
|
||||||
|
|
||||||
SASL_PROPS.put(Sasl.QOP, saslQOP.getSaslQop());
|
SASL_PROPS.put(Sasl.QOP, saslQOP.getSaslQop());
|
||||||
SASL_PROPS.put(Sasl.SERVER_AUTH, "true");
|
SASL_PROPS.put(Sasl.SERVER_AUTH, "true");
|
||||||
|
Security.addProvider(new SaslPlainServer.SecurityProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
static String encodeIdentifier(byte[] identifier) {
|
static String encodeIdentifier(byte[] identifier) {
|
||||||
|
@ -138,7 +140,8 @@ public class SaslRpcServer {
|
||||||
public static enum AuthMethod {
|
public static enum AuthMethod {
|
||||||
SIMPLE((byte) 80, ""),
|
SIMPLE((byte) 80, ""),
|
||||||
KERBEROS((byte) 81, "GSSAPI"),
|
KERBEROS((byte) 81, "GSSAPI"),
|
||||||
DIGEST((byte) 82, "DIGEST-MD5");
|
DIGEST((byte) 82, "DIGEST-MD5"),
|
||||||
|
PLAIN((byte) 83, "PLAIN");
|
||||||
|
|
||||||
/** The code for this method. */
|
/** The code for this method. */
|
||||||
public final byte code;
|
public final byte code;
|
||||||
|
|
|
@ -27,12 +27,13 @@ import java.io.IOException;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.security.PrivilegedExceptionAction;
|
import java.security.PrivilegedExceptionAction;
|
||||||
|
import java.security.Security;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.security.sasl.Sasl;
|
import javax.security.auth.callback.*;
|
||||||
|
import javax.security.sasl.*;
|
||||||
import junit.framework.Assert;
|
import junit.framework.Assert;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
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.ipc.Client.ConnectionId;
|
||||||
import org.apache.hadoop.net.NetUtils;
|
import org.apache.hadoop.net.NetUtils;
|
||||||
import org.apache.hadoop.security.*;
|
import org.apache.hadoop.security.*;
|
||||||
|
import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
|
||||||
import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
|
import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
|
||||||
import org.apache.hadoop.security.token.SecretManager;
|
import org.apache.hadoop.security.token.SecretManager;
|
||||||
import org.apache.hadoop.security.token.Token;
|
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.hadoop.security.token.SecretManager.InvalidToken;
|
||||||
|
|
||||||
import org.apache.log4j.Level;
|
import org.apache.log4j.Level;
|
||||||
import org.apache.tools.ant.types.Assertions.EnabledAssertion;
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -77,6 +78,7 @@ public class TestSaslRPC {
|
||||||
public static void setupKerb() {
|
public static void setupKerb() {
|
||||||
System.setProperty("java.security.krb5.kdc", "");
|
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
|
@Before
|
||||||
|
@ -448,6 +450,120 @@ public class TestSaslRPC {
|
||||||
System.out.println("Test is successful.");
|
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 =
|
private static Pattern BadToken =
|
||||||
Pattern.compile(".*DIGEST-MD5: digest response format violation.*");
|
Pattern.compile(".*DIGEST-MD5: digest response format violation.*");
|
||||||
private static Pattern KrbFailed =
|
private static Pattern KrbFailed =
|
||||||
|
|
Loading…
Reference in New Issue