You are viewing a plain text version of this content. The canonical link for it is here.
Posted to common-commits@hadoop.apache.org by om...@apache.org on 2010/02/27 07:17:01 UTC

svn commit: r916915 - in /hadoop/common/trunk: ./ src/java/org/apache/hadoop/ipc/ src/java/org/apache/hadoop/security/ src/test/core/org/apache/hadoop/ipc/

Author: omalley
Date: Sat Feb 27 06:17:00 2010
New Revision: 916915

URL: http://svn.apache.org/viewvc?rev=916915&view=rev
Log:
HADOOP-6589. Provide better error messages when RPC authentication fails.
(Kan Zhang via omalley)

Modified:
    hadoop/common/trunk/CHANGES.txt
    hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Server.java
    hadoop/common/trunk/src/java/org/apache/hadoop/security/SaslRpcClient.java
    hadoop/common/trunk/src/java/org/apache/hadoop/security/SaslRpcServer.java
    hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestRPC.java
    hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestSaslRPC.java

Modified: hadoop/common/trunk/CHANGES.txt
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/CHANGES.txt?rev=916915&r1=916914&r2=916915&view=diff
==============================================================================
--- hadoop/common/trunk/CHANGES.txt (original)
+++ hadoop/common/trunk/CHANGES.txt Sat Feb 27 06:17:00 2010
@@ -67,6 +67,7 @@
 
     HADOOP-6586. Log authentication and authorization failures and successes
     for RPC (boryas)
+
   IMPROVEMENTS
 
     HADOOP-6283. Improve the exception messages thrown by
@@ -177,6 +178,9 @@
 
     HADOOP-6594. Provide a fetchdt tool via bin/hdfs. (jhoman via acmurthy) 
 
+    HADOOP-6589. Provide better error messages when RPC authentication fails.
+    (Kan Zhang via omalley)
+
   OPTIMIZATIONS
 
     HADOOP-6467. Improve the performance on HarFileSystem.listStatus(..).

Modified: hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Server.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Server.java?rev=916915&r1=916914&r2=916915&view=diff
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Server.java (original)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/ipc/Server.java Sat Feb 27 06:17:00 2010
@@ -60,12 +60,15 @@
 import org.apache.commons.logging.LogFactory;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.CommonConfigurationKeys;
+import org.apache.hadoop.io.BytesWritable;
+import org.apache.hadoop.io.IntWritable;
 import org.apache.hadoop.io.Writable;
 import org.apache.hadoop.io.WritableUtils;
 import org.apache.hadoop.ipc.metrics.RpcMetrics;
 import org.apache.hadoop.security.AccessControlException;
 import org.apache.hadoop.security.SaslRpcServer;
 import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
+import org.apache.hadoop.security.SaslRpcServer.SaslStatus;
 import org.apache.hadoop.security.SaslRpcServer.SaslDigestCallbackHandler;
 import org.apache.hadoop.security.SaslRpcServer.SaslGssCallbackHandler;
 import org.apache.hadoop.security.UserGroupInformation;
@@ -74,6 +77,7 @@
 import org.apache.hadoop.security.authorize.ServiceAuthorizationManager;
 import org.apache.hadoop.security.token.TokenIdentifier;
 import org.apache.hadoop.security.token.SecretManager;
+import org.apache.hadoop.security.token.SecretManager.InvalidToken;
 import org.apache.hadoop.util.ReflectionUtils;
 import org.apache.hadoop.util.StringUtils;
 
@@ -757,11 +761,11 @@
     // Fake 'call' for failed authorization response
     private static final int AUTHROIZATION_FAILED_CALLID = -1;
     private final Call authFailedCall = 
-      new Call(AUTHROIZATION_FAILED_CALLID, null, null);
+      new Call(AUTHROIZATION_FAILED_CALLID, null, this);
     private ByteArrayOutputStream authFailedResponse = new ByteArrayOutputStream();
     // Fake 'call' for SASL context setup
     private static final int SASL_CALLID = -33;
-    private final Call saslCall = new Call(SASL_CALLID, null, null);
+    private final Call saslCall = new Call(SASL_CALLID, null, this);
     private final ByteArrayOutputStream saslResponse = new ByteArrayOutputStream();
     
     public Connection(SelectionKey key, SocketChannel channel, 
@@ -843,68 +847,78 @@
     private void saslReadAndProcess(byte[] saslToken) throws IOException,
         InterruptedException {
       if (!saslContextEstablished) {
-        if (saslServer == null) {
-          switch (authMethod) {
-          case DIGEST:
-            saslServer = Sasl.createSaslServer(AuthMethod.DIGEST
-                .getMechanismName(), null, SaslRpcServer.SASL_DEFAULT_REALM,
-                SaslRpcServer.SASL_PROPS, new SaslDigestCallbackHandler(
-                    secretManager, this));
-            break;
-          default:
-            UserGroupInformation current = UserGroupInformation
-                .getCurrentUser();
-            String fullName = current.getUserName();
-            if (LOG.isDebugEnabled())
-              LOG.debug("Kerberos principal name is " + fullName);
-            final String names[] = SaslRpcServer.splitKerberosName(fullName);
-            if (names.length != 3) {
-              throw new IOException(
-                  "Kerberos principal name does NOT have the expected "
-                      + "hostname part: " + fullName);
-            }
-            current.doAs(new PrivilegedExceptionAction<Object>() {
-              @Override
-              public Object run() throws IOException {
-                saslServer = Sasl.createSaslServer(AuthMethod.KERBEROS
-                    .getMechanismName(), names[0], names[1],
-                    SaslRpcServer.SASL_PROPS, new SaslGssCallbackHandler());
-                return null;
+        byte[] replyToken = null;
+        try {
+          if (saslServer == null) {
+            switch (authMethod) {
+            case DIGEST:
+              if (secretManager == null) {
+                throw new AccessControlException(
+                    "Server is not configured to do DIGEST authentication.");
               }
-            });
+              saslServer = Sasl.createSaslServer(AuthMethod.DIGEST
+                  .getMechanismName(), null, SaslRpcServer.SASL_DEFAULT_REALM,
+                  SaslRpcServer.SASL_PROPS, new SaslDigestCallbackHandler(
+                      secretManager, this));
+              break;
+            default:
+              UserGroupInformation current = UserGroupInformation
+                  .getCurrentUser();
+              String fullName = current.getUserName();
+              if (LOG.isDebugEnabled())
+                LOG.debug("Kerberos principal name is " + fullName);
+              final String names[] = SaslRpcServer.splitKerberosName(fullName);
+              if (names.length != 3) {
+                throw new AccessControlException(
+                    "Kerberos principal name does NOT have the expected "
+                        + "hostname part: " + fullName);
+              }
+              current.doAs(new PrivilegedExceptionAction<Object>() {
+                @Override
+                public Object run() throws SaslException {
+                  saslServer = Sasl.createSaslServer(AuthMethod.KERBEROS
+                      .getMechanismName(), names[0], names[1],
+                      SaslRpcServer.SASL_PROPS, new SaslGssCallbackHandler());
+                  return null;
+                }
+              });
+            }
+            if (saslServer == null)
+              throw new AccessControlException(
+                  "Unable to find SASL server implementation for "
+                      + authMethod.getMechanismName());
+            if (LOG.isDebugEnabled())
+              LOG.debug("Created SASL server with mechanism = "
+                  + authMethod.getMechanismName());
           }
-          if (saslServer == null)
-            throw new IOException(
-                "Unable to find SASL server implementation for "
-                    + authMethod.getMechanismName());
           if (LOG.isDebugEnabled())
-            LOG.debug("Created SASL server with mechanism = "
-                + authMethod.getMechanismName());
-        }
-        if (LOG.isDebugEnabled())
-          LOG.debug("Have read input token of size " + saslToken.length
-              + " for processing by saslServer.evaluateResponse()");
-        byte[] replyToken;
-        try {
+            LOG.debug("Have read input token of size " + saslToken.length
+                + " for processing by saslServer.evaluateResponse()");
           replyToken = saslServer.evaluateResponse(saslToken);
-        } catch (SaslException se) {
+        } catch (IOException e) {
+          IOException sendToClient = e;
+          Throwable cause = e;
+          while (cause != null) {
+            if (cause instanceof InvalidToken) {
+              sendToClient = (InvalidToken) cause;
+              break;
+            }
+            cause = cause.getCause();
+          }
+          doSaslReply(SaslStatus.ERROR, null, sendToClient.getClass().getName(), 
+              sendToClient.getLocalizedMessage());
           rpcMetrics.authenticationFailures.inc();
           String clientIP = this.toString();
           // attempting user could be null
-          auditLOG.warn(AUTH_FAILED_FOR + clientIP + ":" + attemptingUser, se);
-          throw se;
+          auditLOG.warn(AUTH_FAILED_FOR + clientIP + ":" + attemptingUser, e);
+          throw e;
         }
         if (replyToken != null) {
           if (LOG.isDebugEnabled())
             LOG.debug("Will send token of size " + replyToken.length
                 + " from saslServer.");
-          saslCall.connection = this;
-          saslResponse.reset();
-          DataOutputStream out = new DataOutputStream(saslResponse);
-          out.writeInt(replyToken.length);
-          out.write(replyToken, 0, replyToken.length);
-          saslCall.setResponse(ByteBuffer.wrap(saslResponse.toByteArray()));
-          responder.doRespond(saslCall);
+          doSaslReply(SaslStatus.SUCCESS, new BytesWritable(replyToken), null,
+              null);
         }
         if (saslServer.isComplete()) {
           if (LOG.isDebugEnabled()) {
@@ -927,6 +941,21 @@
       }
     }
     
+    private void doSaslReply(SaslStatus status, Writable rv,
+        String errorClass, String error) throws IOException {
+      saslResponse.reset();
+      DataOutputStream out = new DataOutputStream(saslResponse);
+      out.writeInt(status.state); // write status
+      if (status == SaslStatus.SUCCESS) {
+        rv.write(out);
+      } else {
+        WritableUtils.writeString(out, errorClass);
+        WritableUtils.writeString(out, error);
+      }
+      saslCall.setResponse(ByteBuffer.wrap(saslResponse.toByteArray()));
+      responder.doRespond(saslCall);
+    }
+    
     private void disposeSasl() {
       if (saslServer != null) {
         try {
@@ -936,15 +965,6 @@
       }
     }
     
-    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 {
       while (true) {
         /* Read at most one RPC. If the header is not read completely yet
@@ -974,10 +994,16 @@
             throw new IOException("Unable to read authentication method");
           }
           if (isSecurityEnabled && authMethod == AuthMethod.SIMPLE) {
-            throw new IOException("Authentication is required");
+            AccessControlException ae = new AccessControlException(
+                "Authentication is required");
+            setupResponse(authFailedResponse, authFailedCall, Status.FATAL,
+                null, ae.getClass().getName(), ae.getMessage());
+            responder.doRespond(authFailedCall);
+            throw ae;
           }
           if (!isSecurityEnabled && authMethod != AuthMethod.SIMPLE) {
-            askClientToUseSimpleAuth();
+            doSaslReply(SaslStatus.SUCCESS, new IntWritable(
+                SaslRpcServer.SWITCH_TO_SIMPLE_AUTH), null, null);
             authMethod = AuthMethod.SIMPLE;
             // client has already sent the initial Sasl message and we
             // should ignore it. Both client and server should fall back
@@ -1159,7 +1185,6 @@
         rpcMetrics.authorizationSuccesses.inc();
       } catch (AuthorizationException ae) {
         rpcMetrics.authorizationFailures.inc();
-        authFailedCall.connection = this;
         setupResponse(authFailedResponse, authFailedCall, Status.FATAL, null,
             ae.getClass().getName(), ae.getMessage());
         responder.doRespond(authFailedCall);
@@ -1387,6 +1412,11 @@
     this.isSecurityEnabled = false;
   }
   
+  /** for unit testing only, should be called before server is started */ 
+  void enableSecurity() {
+    this.isSecurityEnabled = true;
+  }
+  
   /** Sets the socket buffer size used for responding to RPCs */
   public void setSocketSendBufSize(int size) { this.socketSendBufferSize = size; }
 

Modified: hadoop/common/trunk/src/java/org/apache/hadoop/security/SaslRpcClient.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/security/SaslRpcClient.java?rev=916915&r1=916914&r2=916915&view=diff
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/security/SaslRpcClient.java (original)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/security/SaslRpcClient.java Sat Feb 27 06:17:00 2010
@@ -39,7 +39,10 @@
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.io.WritableUtils;
+import org.apache.hadoop.ipc.RemoteException;
 import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
+import org.apache.hadoop.security.SaslRpcServer.SaslStatus;
 import org.apache.hadoop.security.token.Token;
 import org.apache.hadoop.security.token.TokenIdentifier;
 
@@ -99,6 +102,14 @@
       throw new IOException("Unable to find SASL client implementation");
   }
 
+  private static void readStatus(DataInputStream inStream) throws IOException {
+    int status = inStream.readInt(); // read status
+    if (status != SaslStatus.SUCCESS.state) {
+      throw new RemoteException(WritableUtils.readString(inStream),
+          WritableUtils.readString(inStream));
+    }
+  }
+  
   /**
    * Do client side SASL authentication with server via the given InputStream
    * and OutputStream
@@ -130,6 +141,7 @@
               + " from initSASLContext.");
       }
       if (!saslClient.isComplete()) {
+        readStatus(inStream);
         int len = inStream.readInt();
         if (len == SaslRpcServer.SWITCH_TO_SIMPLE_AUTH) {
           if (LOG.isDebugEnabled())
@@ -155,6 +167,7 @@
           outStream.flush();
         }
         if (!saslClient.isComplete()) {
+          readStatus(inStream);
           saslToken = new byte[inStream.readInt()];
           if (LOG.isDebugEnabled())
             LOG.debug("Will read input token of size " + saslToken.length

Modified: hadoop/common/trunk/src/java/org/apache/hadoop/security/SaslRpcServer.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/java/org/apache/hadoop/security/SaslRpcServer.java?rev=916915&r1=916914&r2=916915&view=diff
==============================================================================
--- hadoop/common/trunk/src/java/org/apache/hadoop/security/SaslRpcServer.java (original)
+++ hadoop/common/trunk/src/java/org/apache/hadoop/security/SaslRpcServer.java Sat Feb 27 06:17:00 2010
@@ -41,6 +41,7 @@
 import org.apache.hadoop.ipc.Server;
 import org.apache.hadoop.security.token.SecretManager;
 import org.apache.hadoop.security.token.TokenIdentifier;
+import org.apache.hadoop.security.token.SecretManager.InvalidToken;
 
 /**
  * A utility class for dealing with SASL on RPC server
@@ -67,11 +68,16 @@
   }
 
   public static TokenIdentifier getIdentifier(String id,
-      SecretManager<TokenIdentifier> secretManager) throws IOException {
+      SecretManager<TokenIdentifier> secretManager) throws InvalidToken {
     byte[] tokenId = decodeIdentifier(id);
     TokenIdentifier tokenIdentifier = secretManager.createIdentifier();
-    tokenIdentifier.readFields(new DataInputStream(new ByteArrayInputStream(
-        tokenId)));
+    try {
+      tokenIdentifier.readFields(new DataInputStream(new ByteArrayInputStream(
+          tokenId)));
+    } catch (IOException e) {
+      throw (InvalidToken) new InvalidToken(
+          "Can't de-serialize tokenIdentifier").initCause(e);
+    }
     return tokenIdentifier;
   }
 
@@ -84,6 +90,16 @@
     return fullName.split("[/@]");
   }
 
+  public enum SaslStatus {
+    SUCCESS (0),
+    ERROR (1);
+    
+    public final int state;
+    private SaslStatus(int state) {
+      this.state = state;
+    }
+  }
+  
   /** Authentication method */
   public static enum AuthMethod {
     SIMPLE((byte) 80, ""), // no authentication
@@ -135,13 +151,13 @@
       this.connection = connection;
     }
 
-    private char[] getPassword(TokenIdentifier tokenid) throws IOException {
+    private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken {
       return encodePassword(secretManager.retrievePassword(tokenid));
     }
 
     /** {@inheritDoc} */
     @Override
-    public void handle(Callback[] callbacks) throws IOException,
+    public void handle(Callback[] callbacks) throws InvalidToken,
         UnsupportedCallbackException {
       NameCallback nc = null;
       PasswordCallback pc = null;
@@ -198,7 +214,7 @@
 
     /** {@inheritDoc} */
     @Override
-    public void handle(Callback[] callbacks) throws IOException,
+    public void handle(Callback[] callbacks) throws
         UnsupportedCallbackException {
       AuthorizeCallback ac = null;
       for (Callback callback : callbacks) {

Modified: hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestRPC.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestRPC.java?rev=916915&r1=916914&r2=916915&view=diff
==============================================================================
--- hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestRPC.java (original)
+++ hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestRPC.java Sat Feb 27 06:17:00 2010
@@ -39,6 +39,7 @@
 import org.apache.hadoop.security.authorize.PolicyProvider;
 import org.apache.hadoop.security.authorize.Service;
 import org.apache.hadoop.security.authorize.ServiceAuthorizationManager;
+import org.apache.hadoop.security.AccessControlException;
 
 import static org.mockito.Mockito.*;
 
@@ -421,6 +422,30 @@
     RPC.stopProxy(mock(TestProtocol.class));
   }
   
+  public void testErrorMsgForInsecureClient() throws Exception {
+    final Server server = RPC.getServer(TestProtocol.class,
+        new TestImpl(), ADDRESS, 0, 5, true, conf, null);
+    server.enableSecurity();
+    server.start();
+    boolean succeeded = false;
+    final InetSocketAddress addr = NetUtils.getConnectAddress(server);
+    TestProtocol proxy = null;
+    try {
+      proxy = (TestProtocol) RPC.getProxy(TestProtocol.class,
+          TestProtocol.versionID, addr, conf);
+    } catch (RemoteException e) {
+      LOG.info("LOGGING MESSAGE: " + e.getLocalizedMessage());
+      assertTrue(e.unwrapRemoteException() instanceof AccessControlException);
+      succeeded = true;
+    } finally {
+      server.stop();
+      if (proxy != null) {
+        RPC.stopProxy(proxy);
+      }
+    }
+    assertTrue(succeeded);
+  }
+  
   public static void main(String[] args) throws Exception {
 
     new TestRPC("test").testCalls(conf);

Modified: hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestSaslRPC.java
URL: http://svn.apache.org/viewvc/hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestSaslRPC.java?rev=916915&r1=916914&r2=916915&view=diff
==============================================================================
--- hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestSaslRPC.java (original)
+++ hadoop/common/trunk/src/test/core/org/apache/hadoop/ipc/TestSaslRPC.java Sat Feb 27 06:17:00 2010
@@ -19,6 +19,7 @@
 package org.apache.hadoop.ipc;
 
 import static org.apache.hadoop.fs.CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION;
+import static org.junit.Assert.*;
 
 import java.io.DataInput;
 import java.io.DataOutput;
@@ -38,6 +39,7 @@
 import org.apache.hadoop.security.token.TokenIdentifier;
 import org.apache.hadoop.security.token.TokenInfo;
 import org.apache.hadoop.security.token.TokenSelector;
+import org.apache.hadoop.security.token.SecretManager.InvalidToken;
 import org.apache.hadoop.security.SaslInputStream;
 import org.apache.hadoop.security.SaslRpcClient;
 import org.apache.hadoop.security.SaslRpcServer;
@@ -53,6 +55,7 @@
   public static final Log LOG =
     LogFactory.getLog(TestSaslRPC.class);
   
+  static final String ERROR_MESSAGE = "Token is invalid";
   static final String SERVER_PRINCIPAL_KEY = "test.ipc.server.principal";
   private static Configuration conf;
   static {
@@ -127,6 +130,14 @@
       return new TestTokenIdentifier();
     }
   }
+  
+  public static class BadTokenSecretManager extends TestTokenSecretManager {
+
+    public byte[] retrievePassword(TestTokenIdentifier id) 
+        throws InvalidToken {
+      throw new InvalidToken(ERROR_MESSAGE);
+    }
+  }
 
   public static class TestTokenSelector implements
       TokenSelector<TestTokenIdentifier> {
@@ -174,6 +185,24 @@
     doDigestRpc(server, sm);
   }
   
+  @Test
+  public void testErrorMessage() throws Exception {
+    BadTokenSecretManager sm = new BadTokenSecretManager();
+    final Server server = RPC.getServer(TestSaslProtocol.class,
+        new TestSaslImpl(), ADDRESS, 0, 5, true, conf, sm);
+
+    boolean succeeded = false;
+    try {
+      doDigestRpc(server, sm);
+    } catch (RemoteException e) {
+      LOG.info("LOGGING MESSAGE: " + e.getLocalizedMessage());
+      assertTrue(ERROR_MESSAGE.equals(e.getLocalizedMessage()));
+      assertTrue(e.unwrapRemoteException() instanceof InvalidToken);
+      succeeded = true;
+    }
+    assertTrue(succeeded);
+  }
+  
   private void doDigestRpc(Server server, TestTokenSecretManager sm)
       throws Exception {
     server.start();