HADOOP-6543. Allows secure clients to talk to unsecure clusters. Contributed by Kan Zhang.

git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/trunk@915097 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Devaraj Das 2010-02-22 22:28:29 +00:00
parent ffdde40b9f
commit c5622e5d4d
8 changed files with 109 additions and 49 deletions

View File

@ -158,6 +158,9 @@ Trunk (unreleased changes)
HADOOP-6583. Captures authentication and authorization metrics. (ddas) HADOOP-6583. Captures authentication and authorization metrics. (ddas)
HADOOP-6543. Allows secure clients to talk to unsecure clusters.
(Kan Zhang via ddas)
OPTIMIZATIONS OPTIMIZATIONS
BUG FIXES BUG FIXES

View File

@ -209,8 +209,8 @@ private class Connection extends Thread {
private String serverPrincipal; // server's krb5 principal name private String serverPrincipal; // server's krb5 principal name
private ConnectionHeader header; // connection header private ConnectionHeader header; // connection header
private final ConnectionId remoteId; // connection id private final ConnectionId remoteId; // connection id
private final AuthMethod authMethod; // authentication method private AuthMethod authMethod; // authentication method
private final boolean useSasl; private boolean useSasl;
private Token<? extends TokenIdentifier> token; private Token<? extends TokenIdentifier> token;
private SaslRpcClient saslRpcClient; private SaslRpcClient saslRpcClient;
@ -364,13 +364,13 @@ private synchronized void disposeSasl() {
} }
} }
private synchronized void setupSaslConnection(final InputStream in2, private synchronized boolean setupSaslConnection(final InputStream in2,
final OutputStream out2) final OutputStream out2)
throws javax.security.sasl.SaslException,IOException,InterruptedException { throws IOException {
try { try {
saslRpcClient = new SaslRpcClient(authMethod, token, saslRpcClient = new SaslRpcClient(authMethod, token,
serverPrincipal); serverPrincipal);
saslRpcClient.saslConnect(in2, out2); return saslRpcClient.saslConnect(in2, out2);
} catch (javax.security.sasl.SaslException je) { } catch (javax.security.sasl.SaslException je) {
if (authMethod == AuthMethod.KERBEROS && if (authMethod == AuthMethod.KERBEROS &&
UserGroupInformation.isLoginKeytabBased()) { UserGroupInformation.isLoginKeytabBased()) {
@ -378,9 +378,10 @@ private synchronized void setupSaslConnection(final InputStream in2,
UserGroupInformation.getCurrentUser().reloginFromKeytab(); UserGroupInformation.getCurrentUser().reloginFromKeytab();
//try setting up the connection again //try setting up the connection again
try { try {
disposeSasl();
saslRpcClient = new SaslRpcClient(authMethod, token, saslRpcClient = new SaslRpcClient(authMethod, token,
serverPrincipal); serverPrincipal);
saslRpcClient.saslConnect(in2, out2); return saslRpcClient.saslConnect(in2, out2);
} catch (javax.security.sasl.SaslException jee) { } catch (javax.security.sasl.SaslException jee) {
UserGroupInformation. UserGroupInformation.
setLastUnsuccessfulAuthenticationAttemptTime setLastUnsuccessfulAuthenticationAttemptTime
@ -437,15 +438,22 @@ private synchronized void setupIOstreams() throws InterruptedException {
ticket = ticket.getRealUser(); ticket = ticket.getRealUser();
} }
} }
ticket.doAs(new PrivilegedExceptionAction<Object>() { if (ticket.doAs(new PrivilegedExceptionAction<Boolean>() {
@Override @Override
public Object run() throws IOException, InterruptedException { public Boolean run() throws IOException {
setupSaslConnection(in2, out2); return setupSaslConnection(in2, out2);
return null;
} }
}); })) {
// Sasl connect is successful. Let's set up Sasl i/o streams.
inStream = saslRpcClient.getInputStream(inStream); inStream = saslRpcClient.getInputStream(inStream);
outStream = saslRpcClient.getOutputStream(outStream); outStream = saslRpcClient.getOutputStream(outStream);
} else {
// fall back to simple auth because server told us so.
authMethod = AuthMethod.SIMPLE;
header = new ConnectionHeader(header.getProtocol(),
header.getUgi(), authMethod);
useSasl = false;
}
} }
if (doPing) { if (doPing) {
this.in = new DataInputStream(new BufferedInputStream this.in = new DataInputStream(new BufferedInputStream

View File

@ -20,7 +20,6 @@
import java.io.DataInput; import java.io.DataInput;
import java.io.DataOutput; import java.io.DataOutput;
import java.io.IOException; import java.io.IOException;
import java.util.Collection;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@ -28,8 +27,6 @@
import org.apache.hadoop.io.Writable; import org.apache.hadoop.io.Writable;
import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.SaslRpcServer.AuthMethod; import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.security.token.TokenIdentifier;
/** /**
* The IPC connection header sent by the client to the server * The IPC connection header sent by the client to the server
@ -86,16 +83,14 @@ public void readFields(DataInput in) throws IOException {
public void write(DataOutput out) throws IOException { public void write(DataOutput out) throws IOException {
Text.writeString(out, (protocol == null) ? "" : protocol); Text.writeString(out, (protocol == null) ? "" : protocol);
if (ugi != null) { if (ugi != null) {
if (UserGroupInformation.isSecurityEnabled()) {
if (authMethod == AuthMethod.KERBEROS) { if (authMethod == AuthMethod.KERBEROS) {
// Send effective user for Kerberos auth // Send effective user for Kerberos auth
out.writeBoolean(true); out.writeBoolean(true);
out.writeUTF(ugi.getUserName()); out.writeUTF(ugi.getUserName());
out.writeBoolean(false); out.writeBoolean(false);
} else { } else if (authMethod == AuthMethod.DIGEST) {
// Don't send user for token auth // Don't send user for token auth
out.writeBoolean(false); out.writeBoolean(false);
}
} else { } else {
//Send both effective user and real user for simple auth //Send both effective user and real user for simple auth
out.writeBoolean(true); out.writeBoolean(true);

View File

@ -85,6 +85,7 @@
*/ */
public abstract class Server { public abstract class Server {
private final boolean authorize; private final boolean authorize;
private boolean isSecurityEnabled;
/** /**
* The first four bytes of Hadoop RPC connections * The first four bytes of Hadoop RPC connections
@ -746,6 +747,7 @@ private class Connection {
SaslServer saslServer; SaslServer saslServer;
private AuthMethod authMethod; private AuthMethod authMethod;
private boolean saslContextEstablished; private boolean saslContextEstablished;
private boolean skipInitialSaslHandshake;
private ByteBuffer rpcHeaderBuffer; private ByteBuffer rpcHeaderBuffer;
private ByteBuffer unwrappedData; private ByteBuffer unwrappedData;
private ByteBuffer unwrappedDataLengthBuffer; private ByteBuffer unwrappedDataLengthBuffer;
@ -929,6 +931,15 @@ private void disposeSasl() {
} }
} }
private void askClientToUseSimpleAuth() throws IOException {
saslCall.connection = this;
saslResponse.reset();
DataOutputStream out = new DataOutputStream(saslResponse);
out.writeInt(SaslRpcServer.SWITCH_TO_SIMPLE_AUTH);
saslCall.setResponse(ByteBuffer.wrap(saslResponse.toByteArray()));
responder.doRespond(saslCall);
}
public int readAndProcess() throws IOException, InterruptedException { public int readAndProcess() throws IOException, InterruptedException {
while (true) { while (true) {
/* Read at most one RPC. If the header is not read completely yet /* Read at most one RPC. If the header is not read completely yet
@ -957,13 +968,16 @@ public int readAndProcess() throws IOException, InterruptedException {
if (authMethod == null) { if (authMethod == null) {
throw new IOException("Unable to read authentication method"); throw new IOException("Unable to read authentication method");
} }
if (UserGroupInformation.isSecurityEnabled() if (isSecurityEnabled && authMethod == AuthMethod.SIMPLE) {
&& authMethod == AuthMethod.SIMPLE) {
throw new IOException("Authentication is required"); throw new IOException("Authentication is required");
} }
if (!UserGroupInformation.isSecurityEnabled() if (!isSecurityEnabled && authMethod != AuthMethod.SIMPLE) {
&& authMethod != AuthMethod.SIMPLE) { askClientToUseSimpleAuth();
throw new IOException("Authentication is not supported"); authMethod = AuthMethod.SIMPLE;
// client has already sent the initial Sasl message and we
// should ignore it. Both client and server should fall back
// to simple auth from now on.
skipInitialSaslHandshake = true;
} }
if (authMethod != AuthMethod.SIMPLE) { if (authMethod != AuthMethod.SIMPLE) {
useSasl = true; useSasl = true;
@ -1000,6 +1014,11 @@ public int readAndProcess() throws IOException, InterruptedException {
if (data.remaining() == 0) { if (data.remaining() == 0) {
dataLengthBuffer.clear(); dataLengthBuffer.clear();
data.flip(); data.flip();
if (skipInitialSaslHandshake) {
data = null;
skipInitialSaslHandshake = false;
continue;
}
boolean isHeaderRead = headerRead; boolean isHeaderRead = headerRead;
if (useSasl) { if (useSasl) {
saslReadAndProcess(data.array()); saslReadAndProcess(data.array());
@ -1278,6 +1297,7 @@ protected Server(String bindAddress, int port,
this.authorize = this.authorize =
conf.getBoolean(ServiceAuthorizationManager.SERVICE_AUTHORIZATION_CONFIG, conf.getBoolean(ServiceAuthorizationManager.SERVICE_AUTHORIZATION_CONFIG,
false); false);
this.isSecurityEnabled = UserGroupInformation.isSecurityEnabled();
// Start the listener here and let it bind to the port // Start the listener here and let it bind to the port
listener = new Listener(); listener = new Listener();
@ -1355,6 +1375,11 @@ Configuration getConf() {
return conf; return conf;
} }
/** for unit testing only, should be called before server is started */
void disableSecurity() {
this.isSecurityEnabled = false;
}
/** Sets the socket buffer size used for responding to RPCs */ /** Sets the socket buffer size used for responding to RPCs */
public void setSocketSendBufSize(int size) { this.socketSendBufferSize = size; } public void setSocketSendBufSize(int size) { this.socketSendBufferSize = size; }

View File

@ -107,9 +107,11 @@ public SaslRpcClient(AuthMethod method,
* InputStream to use * InputStream to use
* @param outS * @param outS
* OutputStream to use * OutputStream to use
* @return true if connection is set up, or false if needs to switch
* to simple Auth.
* @throws IOException * @throws IOException
*/ */
public void saslConnect(InputStream inS, OutputStream outS) public boolean saslConnect(InputStream inS, OutputStream outS)
throws IOException { throws IOException {
DataInputStream inStream = new DataInputStream(new BufferedInputStream(inS)); DataInputStream inStream = new DataInputStream(new BufferedInputStream(inS));
DataOutputStream outStream = new DataOutputStream(new BufferedOutputStream( DataOutputStream outStream = new DataOutputStream(new BufferedOutputStream(
@ -128,7 +130,14 @@ public void saslConnect(InputStream inS, OutputStream outS)
+ " from initSASLContext."); + " from initSASLContext.");
} }
if (!saslClient.isComplete()) { if (!saslClient.isComplete()) {
saslToken = new byte[inStream.readInt()]; int len = inStream.readInt();
if (len == SaslRpcServer.SWITCH_TO_SIMPLE_AUTH) {
if (LOG.isDebugEnabled())
LOG.debug("Server asks us to fall back to simple auth.");
saslClient.dispose();
return false;
}
saslToken = new byte[len];
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Will read input token of size " + saslToken.length LOG.debug("Will read input token of size " + saslToken.length
+ " for processing by initSASLContext"); + " for processing by initSASLContext");
@ -157,8 +166,13 @@ public void saslConnect(InputStream inS, OutputStream outS)
LOG.debug("SASL client context established. Negotiated QoP: " LOG.debug("SASL client context established. Negotiated QoP: "
+ saslClient.getNegotiatedProperty(Sasl.QOP)); + saslClient.getNegotiatedProperty(Sasl.QOP));
} }
return true;
} catch (IOException e) { } catch (IOException e) {
try {
saslClient.dispose(); saslClient.dispose();
} catch (SaslException ignored) {
// ignore further exceptions during cleanup
}
throw e; throw e;
} }
} }

View File

@ -55,6 +55,7 @@ public class SaslRpcServer {
// Request mutual authentication // Request mutual authentication
SASL_PROPS.put(Sasl.SERVER_AUTH, "true"); SASL_PROPS.put(Sasl.SERVER_AUTH, "true");
} }
public static final int SWITCH_TO_SIMPLE_AUTH = -88;
static String encodeIdentifier(byte[] identifier) { static String encodeIdentifier(byte[] identifier) {
return new String(Base64.encodeBase64(identifier)); return new String(Base64.encodeBase64(identifier));

View File

@ -75,20 +75,14 @@ public static class TestTokenIdentifier extends TokenIdentifier {
final static Text KIND_NAME = new Text("test.token"); final static Text KIND_NAME = new Text("test.token");
public TestTokenIdentifier() { public TestTokenIdentifier() {
this.tokenid = new Text(); this(new Text(), new Text());
this.realUser = new Text();
} }
public TestTokenIdentifier(Text tokenid) { public TestTokenIdentifier(Text tokenid) {
this.tokenid = tokenid; this(tokenid, new Text());
this.realUser = new Text();
} }
public TestTokenIdentifier(Text tokenid, Text realUser) { public TestTokenIdentifier(Text tokenid, Text realUser) {
this.tokenid = tokenid; this.tokenid = tokenid == null ? new Text() : tokenid;
if (realUser == null) { this.realUser = realUser == null ? new Text() : realUser;
this.realUser = new Text();
} else {
this.realUser = realUser;
}
} }
@Override @Override
public Text getKind() { public Text getKind() {
@ -96,7 +90,7 @@ public Text getKind() {
} }
@Override @Override
public UserGroupInformation getUser() { public UserGroupInformation getUser() {
if ((realUser == null) || ("".equals(realUser.toString()))) { if ("".equals(realUser.toString())) {
return UserGroupInformation.createRemoteUser(tokenid.toString()); return UserGroupInformation.createRemoteUser(tokenid.toString());
} else { } else {
UserGroupInformation realUgi = UserGroupInformation UserGroupInformation realUgi = UserGroupInformation
@ -114,11 +108,9 @@ public void readFields(DataInput in) throws IOException {
@Override @Override
public void write(DataOutput out) throws IOException { public void write(DataOutput out) throws IOException {
tokenid.write(out); tokenid.write(out);
if (realUser != null) {
realUser.write(out); realUser.write(out);
} }
} }
}
public static class TestTokenSecretManager extends public static class TestTokenSecretManager extends
SecretManager<TestTokenIdentifier> { SecretManager<TestTokenIdentifier> {
@ -170,6 +162,20 @@ public void testDigestRpc() throws Exception {
final Server server = RPC.getServer(TestSaslProtocol.class, final Server server = RPC.getServer(TestSaslProtocol.class,
new TestSaslImpl(), ADDRESS, 0, 5, true, conf, sm); new TestSaslImpl(), ADDRESS, 0, 5, true, conf, sm);
doDigestRpc(server, sm);
}
@Test
public void testSecureToInsecureRpc() throws Exception {
Server server = RPC.getServer(TestSaslProtocol.class,
new TestSaslImpl(), ADDRESS, 0, 5, true, conf, null);
server.disableSecurity();
TestTokenSecretManager sm = new TestTokenSecretManager();
doDigestRpc(server, sm);
}
private void doDigestRpc(Server server, TestTokenSecretManager sm)
throws Exception {
server.start(); server.start();
final UserGroupInformation current = UserGroupInformation.getCurrentUser(); final UserGroupInformation current = UserGroupInformation.getCurrentUser();

View File

@ -44,6 +44,14 @@
<Field name="out" /> <Field name="out" />
<Bug pattern="IS2_INCONSISTENT_SYNC" /> <Bug pattern="IS2_INCONSISTENT_SYNC" />
</Match> </Match>
<!--
Further SaslException should be ignored during cleanup and
original exception should be re-thrown.
-->
<Match>
<Class name="org.apache.hadoop.security.SaslRpcClient" />
<Bug pattern="DE_MIGHT_IGNORE" />
</Match>
<!-- <!--
Ignore Cross Scripting Vulnerabilities Ignore Cross Scripting Vulnerabilities
--> -->