Browse Source

ZOOKEEPER-1497. Allow server-side SASL login with JAAS configuration to be programmatically set (rather than only by reading JAAS configuration file) (Matteo Bertozzi via phunt)

git-svn-id: https://svn.apache.org/repos/asf/zookeeper/trunk@1378285 13f79535-47bb-0310-9956-ffa450edef68
Patrick D. Hunt 13 năm trước cách đây
mục cha
commit
fe34bbf011

+ 5 - 0
CHANGES.txt

@@ -350,6 +350,11 @@ IMPROVEMENTS:
   ZOOKEEPER-1510. Should not log SASL errors for non-secure usage
     (Todd Lipcon via phunt)
 
+  ZOOKEEPER-1497. Allow server-side SASL login with JAAS configuration
+  to be programmatically set (rather than only by reading JAAS
+  configuration file) (Matteo Bertozzi via phunt)
+
+
 Release 3.4.0 - 
 
 Non-backward compatible changes:

+ 2 - 0
src/java/main/org/apache/zookeeper/Environment.java

@@ -30,6 +30,8 @@ import org.slf4j.Logger;
  *
  */
 public class Environment {
+    public static String JAAS_CONF_KEY = "java.security.auth.login.config";
+
     public static class Entry {
         private String k;
         private String v;

+ 5 - 6
src/java/main/org/apache/zookeeper/client/ZooKeeperSaslClient.java

@@ -22,6 +22,7 @@ import org.apache.zookeeper.AsyncCallback;
 import org.apache.zookeeper.ClientCnxn;
 import org.apache.zookeeper.Login;
 import org.apache.zookeeper.ZooDefs;
+import org.apache.zookeeper.Environment;
 import org.apache.zookeeper.data.Stat;
 import org.apache.zookeeper.proto.GetSASLRequest;
 import org.apache.zookeeper.proto.ReplyHeader;
@@ -111,10 +112,8 @@ public class ZooKeeperSaslClient {
                 if (securityException != null) {
                     throw new LoginException("Zookeeper client cannot authenticate using the " + explicitClientSection +
                             " section of the supplied JAAS configuration: '" +
-                            System.getProperty("java.security.auth.login.config") + "' because of a " +
+                            System.getProperty(Environment.JAAS_CONF_KEY) + "' because of a " +
                             "SecurityException: " + securityException);
-                    
-                    
                 } else {
                     throw new LoginException("Client cannot SASL-authenticate because the specified JAAS configuration " +
                             "section '" + explicitClientSection + "' could not be found.");
@@ -130,19 +129,19 @@ public class ZooKeeperSaslClient {
                 }
                 this.configStatus = msg;
             }
-            if (System.getProperty("java.security.auth.login.config")  != null) {
+            if (System.getProperty(Environment.JAAS_CONF_KEY)  != null) {
                 // Again, the user explicitly set something SASL-related, so they probably expected SASL to succeed.
                 if (securityException != null) {
                     throw new LoginException("Zookeeper client cannot authenticate using the '" +
                             System.getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY, "Client") +
                             "' section of the supplied JAAS configuration: '" +
-                            System.getProperty("java.security.auth.login.config") + "' because of a " +
+                            System.getProperty(Environment.JAAS_CONF_KEY) + "' because of a " +
                             "SecurityException: " + securityException);
                 } else {
                     throw new LoginException("No JAAS configuration section named '" +
                             System.getProperty(ZooKeeperSaslClient.LOGIN_CONTEXT_NAME_KEY, "Client") +
                             "' was found in specified JAAS configuration file: '" +
-                            System.getProperty("java.security.auth.login.config") + "'.");
+                            System.getProperty(Environment.JAAS_CONF_KEY) + "'.");
                 }
             }
         }

+ 2 - 16
src/java/main/org/apache/zookeeper/server/NIOServerCnxnFactory.java

@@ -32,11 +32,6 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Set;
 
-import javax.security.auth.login.Configuration;
-import javax.security.auth.login.LoginException;
-
-import org.apache.zookeeper.Login;
-import org.apache.zookeeper.server.auth.SaslServerCallbackHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -89,17 +84,8 @@ public class NIOServerCnxnFactory extends ServerCnxnFactory implements Runnable
     Thread thread;
     @Override
     public void configure(InetSocketAddress addr, int maxcc) throws IOException {
-        if (System.getProperty("java.security.auth.login.config") != null) {
-            try {
-                saslServerCallbackHandler = new SaslServerCallbackHandler(Configuration.getConfiguration());
-                login = new Login("Server",saslServerCallbackHandler);
-                login.startThreadIfNeeded();
-            }
-            catch (LoginException e) {
-                throw new IOException("Could not configure server because SASL configuration did not allow the "
-                  + " Zookeeper server to authenticate itself properly: " + e);
-            }
-        }
+        configureSaslLogin();
+
         thread = new Thread(this, "NIOServerCxn.Factory:" + addr);
         thread.setDaemon(true);
         maxClientCnxns = maxcc;

+ 1 - 16
src/java/main/org/apache/zookeeper/server/NettyServerCnxnFactory.java

@@ -28,8 +28,6 @@ import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.Executors;
 
-import org.apache.zookeeper.Login;
-import org.apache.zookeeper.server.auth.SaslServerCallbackHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.jboss.netty.bootstrap.ServerBootstrap;
@@ -47,9 +45,6 @@ import org.jboss.netty.channel.group.ChannelGroup;
 import org.jboss.netty.channel.group.DefaultChannelGroup;
 import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
 
-import javax.security.auth.login.Configuration;
-import javax.security.auth.login.LoginException;
-
 public class NettyServerCnxnFactory extends ServerCnxnFactory {
     Logger LOG = LoggerFactory.getLogger(NettyServerCnxnFactory.class);
 
@@ -306,17 +301,7 @@ public class NettyServerCnxnFactory extends ServerCnxnFactory {
     public void configure(InetSocketAddress addr, int maxClientCnxns)
             throws IOException
     {
-        if (System.getProperty("java.security.auth.login.config") != null) {
-            try {
-                saslServerCallbackHandler = new SaslServerCallbackHandler(Configuration.getConfiguration());
-                login = new Login("Server",saslServerCallbackHandler);
-                login.startThreadIfNeeded();
-            }
-            catch (LoginException e) {
-                throw new IOException("Could not configure server because SASL configuration did not allow the "
-                  + " Zookeeper server to authenticate itself properly: " + e);
-            }
-        }
+        configureSaslLogin();
         localAddress = addr;
         this.maxClientCnxns = maxClientCnxns;
     }

+ 60 - 0
src/java/main/org/apache/zookeeper/server/ServerCnxnFactory.java

@@ -24,9 +24,14 @@ import java.nio.ByteBuffer;
 import java.util.HashMap;
 import java.util.HashSet;
 
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.login.AppConfigurationEntry;
+
 import javax.management.JMException;
 
 import org.apache.zookeeper.Login;
+import org.apache.zookeeper.Environment;
 import org.apache.zookeeper.jmx.MBeanRegistry;
 import org.apache.zookeeper.server.auth.SaslServerCallbackHandler;
 import org.slf4j.Logger;
@@ -146,4 +151,59 @@ public abstract class ServerCnxnFactory {
 
     }
 
+    /**
+     * Initialize the server SASL if specified.
+     *
+     * If the user has specified a "ZooKeeperServer.LOGIN_CONTEXT_NAME_KEY"
+     * or a jaas.conf using "java.security.auth.login.config"
+     * the authentication is required and an exception is raised.
+     * Otherwise no authentication is configured and no exception is raised.
+     *
+     * @throws IOException if jaas.conf is missing or there's an error in it.
+     */
+    protected void configureSaslLogin() throws IOException {
+        String serverSection = System.getProperty(ZooKeeperSaslServer.LOGIN_CONTEXT_NAME_KEY,
+                                                  ZooKeeperSaslServer.DEFAULT_LOGIN_CONTEXT_NAME);
+
+        // Note that 'Configuration' here refers to javax.security.auth.login.Configuration.
+        AppConfigurationEntry entries[] = null;
+        SecurityException securityException = null;
+        try {
+            entries = Configuration.getConfiguration().getAppConfigurationEntry(serverSection);
+        } catch (SecurityException e) {
+            // handle below: might be harmless if the user doesn't intend to use JAAS authentication.
+            securityException = e;
+        }
+
+        // No entries in jaas.conf
+        // If there's a configuration exception fetching the jaas section and
+        // the user has required sasl by specifying a LOGIN_CONTEXT_NAME_KEY or a jaas file
+        // we throw an exception otherwise we continue without authentication.
+        if (entries == null) {
+            String jaasFile = System.getProperty(Environment.JAAS_CONF_KEY);
+            String loginContextName = System.getProperty(ZooKeeperSaslServer.LOGIN_CONTEXT_NAME_KEY);
+            if (securityException != null && (loginContextName != null || jaasFile != null)) {
+                String errorMessage = "No JAAS configuration section named '" + serverSection +  "' was found";
+                if (jaasFile != null) {
+                    errorMessage += "in '" + jaasFile + "'.";
+                }
+                if (loginContextName != null) {
+                    errorMessage += " But " + ZooKeeperSaslServer.LOGIN_CONTEXT_NAME_KEY + " was set.";
+                }
+                LOG.error(errorMessage);
+                throw new IOException(errorMessage);
+            }
+            return;
+        }
+
+        // jaas.conf entry available
+        try {
+            saslServerCallbackHandler = new SaslServerCallbackHandler(Configuration.getConfiguration());
+            login = new Login(serverSection, saslServerCallbackHandler);
+            login.startThreadIfNeeded();
+        } catch (LoginException e) {
+            throw new IOException("Could not configure server because SASL configuration did not allow the "
+              + " ZooKeeper server to authenticate itself properly: " + e);
+        }
+    }
 }

+ 3 - 0
src/java/main/org/apache/zookeeper/server/ZooKeeperSaslServer.java

@@ -30,6 +30,9 @@ import javax.security.sasl.SaslServer;
 import org.apache.zookeeper.Login;
 
 public class ZooKeeperSaslServer {
+    public static final String LOGIN_CONTEXT_NAME_KEY = "zookeeper.sasl.serverconfig";
+    public static final String DEFAULT_LOGIN_CONTEXT_NAME = "Server";
+
     Logger LOG = LoggerFactory.getLogger(ZooKeeperSaslServer.class);
     private SaslServer saslServer;
 

+ 5 - 1
src/java/main/org/apache/zookeeper/server/auth/SaslServerCallbackHandler.java

@@ -34,6 +34,8 @@ import javax.security.auth.login.Configuration;
 import javax.security.sasl.AuthorizeCallback;
 import javax.security.sasl.RealmCallback;
 
+import org.apache.zookeeper.server.ZooKeeperSaslServer;
+
 public class SaslServerCallbackHandler implements CallbackHandler {
     private static final String USER_PREFIX = "user_";
     private static final Logger LOG = LoggerFactory.getLogger(SaslServerCallbackHandler.class);
@@ -45,7 +47,9 @@ public class SaslServerCallbackHandler implements CallbackHandler {
     private final Map<String,String> credentials = new HashMap<String,String>();
 
     public SaslServerCallbackHandler(Configuration configuration) throws IOException {
-        AppConfigurationEntry configurationEntries[] = configuration.getAppConfigurationEntry("Server");
+        String serverSection = System.getProperty(ZooKeeperSaslServer.LOGIN_CONTEXT_NAME_KEY,
+                                                  ZooKeeperSaslServer.DEFAULT_LOGIN_CONTEXT_NAME);
+        AppConfigurationEntry configurationEntries[] = configuration.getAppConfigurationEntry(serverSection);
 
         if (configurationEntries == null) {
             String errorMessage = "Could not find a 'Server' entry in this configuration: Server cannot start.";

+ 75 - 0
src/java/test/org/apache/zookeeper/JaasConfiguration.java

@@ -0,0 +1,75 @@
+/**
+ * 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.zookeeper;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
+
+/**
+ * This helper class allows to programmatically create a JAAS configuration.
+ * Each section must have a name and a login module, and a set of key/values
+ * to describe login options.
+ *
+ * Example:
+ *   jaas = new JaasConfiguration();
+ *   jaas.addSection("Server", "org.apache.zookeeper.server.auth.DigestLoginModule",
+ *                   "username", "passowrd");
+ */
+public class JaasConfiguration extends javax.security.auth.login.Configuration {
+    private final Map<String, AppConfigurationEntry[]> sections =
+      new HashMap<String, AppConfigurationEntry[]>();
+
+    public JaasConfiguration() {
+    }
+
+    /**
+     * Add a section to the jaas.conf
+     * @param name Section name
+     * @param loginModuleName Login module name
+     * @param args login key/value args
+     */
+    public void addSection(String name, String loginModuleName, String... args) {
+        Map<String, String> conf = new HashMap<String, String>();
+        // loop through the args (must be key/value sequence)
+        for (int i = 0; i < args.length - 1; i += 2) {
+            conf.put(args[i], args[i + 1]);
+        }
+        addSection(name, loginModuleName, conf);
+    }
+
+    /**
+     * Add a section to the jaas.conf
+     * @param name Section name
+     * @param loginModuleName Login module name
+     * @param conf login key/value args
+     */
+    public void addSection(String name, String loginModuleName, final Map<String,String> conf) {
+        AppConfigurationEntry[] entries = new AppConfigurationEntry[1];
+        entries[0] = new AppConfigurationEntry(loginModuleName, LoginModuleControlFlag.REQUIRED, conf);
+        this.sections.put(name, entries);
+    }
+
+    @Override
+    public AppConfigurationEntry[] getAppConfigurationEntry(String appName) {
+        return sections.get(appName);
+    }
+}

+ 6 - 0
src/java/test/org/apache/zookeeper/test/ClientBase.java

@@ -159,6 +159,12 @@ public abstract class ClientBase extends ZKTestCase {
         return createClient(watcher, hp);
     }
 
+    protected TestableZooKeeper createClient(CountdownWatcher watcher)
+        throws IOException, InterruptedException
+    {
+        return createClient(watcher, hostPort);
+    }
+
     private LinkedList<ZooKeeper> allClients;
     private boolean allClientsSetup = false;
 

+ 104 - 0
src/java/test/org/apache/zookeeper/test/SaslAuthDesignatedServerTest.java

@@ -0,0 +1,104 @@
+/**
+ * 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.zookeeper.test;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.WatchedEvent;
+import org.apache.zookeeper.ZooKeeper;
+import org.apache.zookeeper.Watcher.Event.KeeperState;
+import org.apache.zookeeper.ZooDefs.Ids;
+import org.apache.zookeeper.server.ZooKeeperSaslServer;
+import org.apache.zookeeper.JaasConfiguration;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SaslAuthDesignatedServerTest extends ClientBase {
+    public static int AUTHENTICATION_TIMEOUT = 30000;
+
+    static {
+        System.setProperty("zookeeper.authProvider.1","org.apache.zookeeper.server.auth.SASLAuthenticationProvider");
+        System.setProperty(ZooKeeperSaslServer.LOGIN_CONTEXT_NAME_KEY, "MyZookeeperServer");
+
+        JaasConfiguration conf = new JaasConfiguration();
+
+        /* this 'Server' section has an incorrect password, but we're not configured
+         * to  use it (we're configured by the above System.setProperty(...LOGIN_CONTEXT_NAME_KEY...)
+         * to use the 'MyZookeeperServer' section below, which has the correct password).
+         */
+        conf.addSection("Server", "org.apache.zookeeper.server.auth.DigestLoginModule",
+                        "user_myuser", "wrongpassword");
+
+        conf.addSection("MyZookeeperServer", "org.apache.zookeeper.server.auth.DigestLoginModule",
+                        "user_myuser", "mypassword");
+
+        conf.addSection("Client", "org.apache.zookeeper.server.auth.DigestLoginModule",
+                        "username", "myuser", "password", "mypassword");
+
+        javax.security.auth.login.Configuration.setConfiguration(conf);
+    }
+
+    private AtomicInteger authFailed = new AtomicInteger(0);
+
+    private class MyWatcher extends CountdownWatcher {
+        volatile CountDownLatch authCompleted;
+
+        @Override
+        synchronized public void reset() {
+            authCompleted = new CountDownLatch(1);
+            super.reset();
+        }
+
+        @Override
+        public synchronized void process(WatchedEvent event) {
+            if (event.getState() == KeeperState.AuthFailed) {
+                authFailed.incrementAndGet();
+                authCompleted.countDown();
+            } else if (event.getState() == KeeperState.SaslAuthenticated) {
+                authCompleted.countDown();
+            } else {
+                super.process(event);
+            }
+        }
+    }
+
+    @Test
+    public void testAuth() throws Exception {
+        MyWatcher watcher = new MyWatcher();
+        ZooKeeper zk = createClient(watcher);
+        watcher.authCompleted.await(AUTHENTICATION_TIMEOUT, TimeUnit.MILLISECONDS);
+        Assert.assertEquals(authFailed.get(), 0);
+
+        try {
+            zk.create("/path1", null, Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);
+        } catch (KeeperException e) {
+          Assert.fail("test failed :" + e);
+        }
+        finally {
+            zk.close();
+        }
+    }
+}