Repository: cassandra
Updated Branches:
  refs/heads/trunk ad7e36b8a -> 7b0c7164a


http://git-wip-us.apache.org/repos/asf/cassandra/blob/7b0c7164/src/java/org/apache/cassandra/tools/NodeTool.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java 
b/src/java/org/apache/cassandra/tools/NodeTool.java
index d7cda95..8640b58 100644
--- a/src/java/org/apache/cassandra/tools/NodeTool.java
+++ b/src/java/org/apache/cassandra/tools/NodeTool.java
@@ -309,7 +309,7 @@ public class NodeTool
                     nodeClient = new NodeProbe(host, parseInt(port));
                 else
                     nodeClient = new NodeProbe(host, parseInt(port), username, 
password);
-            } catch (IOException e)
+            } catch (IOException | SecurityException e)
             {
                 Throwable rootCause = Throwables.getRootCause(e);
                 System.err.println(format("nodetool: Failed to connect to 
'%s:%s' - %s: '%s'.", host, port, rootCause.getClass().getSimpleName(), 
rootCause.getMessage()));

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7b0c7164/src/java/org/apache/cassandra/utils/JMXServerUtils.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/utils/JMXServerUtils.java 
b/src/java/org/apache/cassandra/utils/JMXServerUtils.java
new file mode 100644
index 0000000..b0e44a2
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/JMXServerUtils.java
@@ -0,0 +1,299 @@
+/*
+ * 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.cassandra.utils;
+
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.net.InetAddress;
+import java.rmi.NoSuchObjectException;
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.rmi.registry.LocateRegistry;
+import java.rmi.server.RMIClientSocketFactory;
+import java.rmi.server.RMIServerSocketFactory;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.management.remote.*;
+import javax.management.remote.rmi.RMIConnectorServer;
+import javax.management.remote.rmi.RMIJRMPServerImpl;
+import javax.rmi.ssl.SslRMIClientSocketFactory;
+import javax.rmi.ssl.SslRMIServerSocketFactory;
+import javax.security.auth.Subject;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sun.jmx.remote.internal.RMIExporter;
+import com.sun.jmx.remote.security.JMXPluggableAuthenticator;
+import org.apache.cassandra.auth.jmx.AuthenticationProxy;
+import sun.rmi.server.UnicastServerRef2;
+
+public class JMXServerUtils
+{
+    private static final Logger logger = 
LoggerFactory.getLogger(JMXServerUtils.class);
+
+
+    /**
+     * Creates a server programmatically. This allows us to set parameters 
which normally are
+     * inaccessable.
+     */
+    public static JMXConnectorServer createJMXServer(int port, boolean local)
+    throws IOException
+    {
+        Map<String, Object> env = new HashMap<>();
+
+        String urlTemplate = 
"service:jmx:rmi://%1$s/jndi/rmi://%1$s:%2$d/jmxrmi";
+        String url;
+        String host;
+        InetAddress serverAddress;
+        if (local)
+        {
+            serverAddress = InetAddress.getLoopbackAddress();
+            host = serverAddress.getHostAddress();
+            System.setProperty("java.rmi.server.hostname", host);
+        }
+        else
+        {
+            // if the java.rmi.server.hostname property is set, we'll take its 
value
+            // and use that when creating the RMIServerSocket to which we bind 
the RMI
+            // registry. This allows us to effectively restrict to a single 
interface
+            // if required. See 
http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4880793
+            // for more detail. If the hostname property is not set, the 
registry will
+            // be bound to the wildcard address
+            host = System.getProperty("java.rmi.server.hostname");
+            serverAddress = host == null ? null : InetAddress.getByName(host);
+        }
+
+        // Configure the RMI client & server socket factories, including SSL 
config.
+        env.putAll(configureJmxSocketFactories(serverAddress));
+
+        url = String.format(urlTemplate, (host == null ? "0.0.0.0" : 
serverAddress.getHostAddress()), port);
+        LocateRegistry.createRegistry(port,
+                                     (RMIClientSocketFactory) 
env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE),
+                                     (RMIServerSocketFactory) 
env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE));
+
+        // Configure authn, using a JMXAuthenticator which either wraps a set 
log LoginModules configured
+        // via a JAAS configuration entry, or one which delegates to the 
standard file based authenticator.
+        // Authn is disabled if com.sun.management.jmxremote.authenticate=false
+        env.putAll(configureJmxAuthentication());
+
+        // Configure authz - if a custom proxy class is specified an instance 
will be returned.
+        // If not, but a location for the standard access file is set in 
system properties, the
+        // return value is null, and an entry is added to the env map 
detailing that location
+        // If neither method is specified, no access control is applied
+        MBeanServerForwarder authzProxy = configureJmxAuthorization(env);
+
+        // Make sure we use our custom exporter so a full GC doesn't get 
scheduled every
+        // sun.rmi.dgc.server.gcInterval millis (default is 3600000ms/1 hour)
+        env.put(RMIExporter.EXPORTER_ATTRIBUTE, new Exporter());
+
+        JMXConnectorServer jmxServer =
+            JMXConnectorServerFactory.newJMXConnectorServer(new 
JMXServiceURL(url),
+                                                            env,
+                                                            
ManagementFactory.getPlatformMBeanServer());
+
+        // If a custom authz proxy was created, attach it to the server now.
+        if (authzProxy != null)
+            jmxServer.setMBeanServerForwarder(authzProxy);
+
+        logger.info("Configured JMX server at: {}", url);
+        return jmxServer;
+    }
+
+    private static Map<String, Object> configureJmxAuthentication()
+    {
+        Map<String, Object> env = new HashMap<>();
+        if (!Boolean.getBoolean("com.sun.management.jmxremote.authenticate"))
+            return env;
+
+        // If authentication is enabled, initialize the appropriate 
JMXAuthenticator
+        // and stash it in the environment settings.
+        // A JAAS configuration entry takes precedence. If one is supplied, use
+        // Cassandra's own custom JMXAuthenticator implementation which 
delegates
+        // auth to the LoginModules specified by the JAAS configuration entry.
+        // If no JAAS entry is found, an instance of the JDK's own
+        // JMXPluggableAuthenticator is created. In that case, the admin may 
have
+        // set a location for the JMX password file which must be added to env
+        // before creating the authenticator. If no password file has been
+        // explicitly set, it's read from the default location
+        // $JAVA_HOME/lib/management/jmxremote.password
+        String configEntry = 
System.getProperty("cassandra.jmx.remote.login.config");
+        if (configEntry != null)
+        {
+            env.put(JMXConnectorServer.AUTHENTICATOR, new 
AuthenticationProxy(configEntry));
+        }
+        else
+        {
+            String passwordFile = 
System.getProperty("com.sun.management.jmxremote.password.file");
+            if (passwordFile != null)
+            {
+                // stash the password file location where 
JMXPluggableAuthenticator expects it
+                env.put("jmx.remote.x.password.file", passwordFile);
+            }
+
+            env.put(JMXConnectorServer.AUTHENTICATOR, new 
JMXPluggableAuthenticatorWrapper(env));
+        }
+
+        return env;
+    }
+
+    private static MBeanServerForwarder configureJmxAuthorization(Map<String, 
Object> env)
+    {
+        // If a custom authz proxy is supplied (Cassandra ships with 
AuthorizationProxy, which
+        // delegates to its own role based IAuthorizer), then instantiate and 
return one which
+        // can be set as the JMXConnectorServer's MBeanServerForwarder.
+        // If no custom proxy is supplied, check system properties for the 
location of the
+        // standard access file & stash it in env
+        String authzProxyClass = 
System.getProperty("cassandra.jmx.authorizer");
+        if (authzProxyClass != null)
+        {
+            final InvocationHandler handler = 
FBUtilities.construct(authzProxyClass, "JMX authz proxy");
+            final Class[] interfaces = { MBeanServerForwarder.class };
+
+            Object proxy = 
Proxy.newProxyInstance(MBeanServerForwarder.class.getClassLoader(), interfaces, 
handler);
+            return MBeanServerForwarder.class.cast(proxy);
+        }
+        else
+        {
+            String accessFile = 
System.getProperty("com.sun.management.jmxremote.access.file");
+            if (accessFile != null)
+            {
+                env.put("jmx.remote.x.access.file", accessFile);
+            }
+            return null;
+        }
+    }
+
+    private static Map<String, Object> configureJmxSocketFactories(InetAddress 
serverAddress)
+    {
+        Map<String, Object> env = new HashMap<>();
+        if (Boolean.getBoolean("com.sun.management.jmxremote.ssl"))
+        {
+            boolean requireClientAuth = 
Boolean.getBoolean("com.sun.management.jmxremote.ssl.need.client.auth");
+            String[] protocols = null;
+            String protocolList = 
System.getProperty("com.sun.management.jmxremote.ssl.enabled.protocols");
+            if (protocolList != null)
+            {
+                System.setProperty("javax.rmi.ssl.client.enabledProtocols", 
protocolList);
+                protocols = StringUtils.split(protocolList, ',');
+            }
+
+            String[] ciphers = null;
+            String cipherList = 
System.getProperty("com.sun.management.jmxremote.ssl.enabled.cipher.suites");
+            if (cipherList != null)
+            {
+                System.setProperty("javax.rmi.ssl.client.enabledCipherSuites", 
cipherList);
+                ciphers = StringUtils.split(cipherList, ',');
+            }
+
+            SslRMIClientSocketFactory clientFactory = new 
SslRMIClientSocketFactory();
+            SslRMIServerSocketFactory serverFactory = new 
SslRMIServerSocketFactory(ciphers, protocols, requireClientAuth);
+            env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, 
serverFactory);
+            env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, 
clientFactory);
+            env.put("com.sun.jndi.rmi.factory.socket", clientFactory);
+            logJmxSslConfig(serverFactory);
+        }
+        else
+        {
+            env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE,
+                    new RMIServerSocketFactoryImpl(serverAddress));
+        }
+
+        return env;
+    }
+
+    private static void logJmxSslConfig(SslRMIServerSocketFactory 
serverFactory)
+    {
+        logger.debug("JMX SSL configuration. { protocols: [{}], cipher_suites: 
[{}], require_client_auth: {} }",
+                     serverFactory.getEnabledProtocols() == null
+                     ? "'JVM defaults'"
+                     : 
Arrays.stream(serverFactory.getEnabledProtocols()).collect(Collectors.joining("','",
 "'", "'")),
+                     serverFactory.getEnabledCipherSuites() == null
+                     ? "'JVM defaults'"
+                     : 
Arrays.stream(serverFactory.getEnabledCipherSuites()).collect(Collectors.joining("','",
 "'", "'")),
+                     serverFactory.getNeedClientAuth());
+    }
+
+    private static class JMXPluggableAuthenticatorWrapper implements 
JMXAuthenticator
+    {
+        final Map<?, ?> env;
+        private JMXPluggableAuthenticatorWrapper(Map<?, ?> env)
+        {
+            this.env = ImmutableMap.copyOf(env);
+        }
+
+        public Subject authenticate(Object credentials)
+        {
+            JMXPluggableAuthenticator authenticator = new 
JMXPluggableAuthenticator(env);
+            return authenticator.authenticate(credentials);
+        }
+    }
+
+    /**
+     * In the RMI subsystem, the ObjectTable instance holds references to 
remote
+     * objects for distributed garbage collection purposes. When objects are
+     * added to the ObjectTable (exported), a flag is passed to * indicate the
+     * "permanence" of that object. Exporting as permanent has two effects; the
+     * object is not eligible for distributed garbage collection, and its
+     * existence will not prevent the JVM from exiting after termination of all
+     * non-daemon threads terminate. Neither of these is bad for our case, as 
we
+     * attach the server exactly once (i.e. at startup, not subsequently using
+     * the Attach API) and don't disconnect it before shutdown. The primary
+     * benefit we gain is that it doesn't trigger the scheduled full GC that
+     * is otherwise incurred by programatically configuring the management 
server.
+     *
+     * To that end, we use this private implementation of RMIExporter to 
register
+     * our JMXConnectorServer as a permanent object by adding it to the map of
+     * environment variables under the key RMIExporter.EXPORTER_ATTRIBUTE
+     * (com.sun.jmx.remote.rmi.exporter) prior to calling server.start()
+     *
+     * See also:
+     *  * CASSANDRA-2967 for background
+     *  * https://www.jclarity.com/2015/01/27/rmi-system-gc-unplugged/ for 
more detail
+     *  * https://bugs.openjdk.java.net/browse/JDK-6760712 for info on setting 
the exporter
+     *  * sun.management.remote.ConnectorBootstrap to trace how the inbuilt 
management agent
+     *    sets up the JMXConnectorServer
+     */
+    private static class Exporter implements RMIExporter
+    {
+        public Remote exportObject(Remote obj, int port, 
RMIClientSocketFactory csf, RMIServerSocketFactory ssf)
+        throws RemoteException
+        {
+            // We should only ever get here by configuring our own JMX 
Connector server,
+            // so assert some invariants we expect to be true in that case
+            assert ssf != null; // we always configure a custom server socket 
factory
+
+            // as we always configure a custom server socket factory, either 
for SSL or to ensure
+            // only loopback addresses, we use a UnicastServerRef2 for 
exporting
+            return new UnicastServerRef2(port, csf, ssf).exportObject(obj, 
null, true);
+        }
+
+        public boolean unexportObject(Remote obj, boolean force) throws 
NoSuchObjectException
+        {
+            return UnicastRemoteObject.unexportObject(obj, force);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7b0c7164/src/java/org/apache/cassandra/utils/RMIServerSocketFactoryImpl.java
----------------------------------------------------------------------
diff --git 
a/src/java/org/apache/cassandra/utils/RMIServerSocketFactoryImpl.java 
b/src/java/org/apache/cassandra/utils/RMIServerSocketFactoryImpl.java
index ec81aa3..e3e901d 100644
--- a/src/java/org/apache/cassandra/utils/RMIServerSocketFactoryImpl.java
+++ b/src/java/org/apache/cassandra/utils/RMIServerSocketFactoryImpl.java
@@ -6,14 +6,19 @@ import java.net.ServerSocket;
 import java.rmi.server.RMIServerSocketFactory;
 import javax.net.ServerSocketFactory;
 
-
 public class RMIServerSocketFactoryImpl implements RMIServerSocketFactory
 {
+    // Address to bind server sockets too, may be null indicating all local 
interfaces are to be bound
+    private final InetAddress bindAddress;
+
+    public RMIServerSocketFactoryImpl(InetAddress bindAddress)
+    {
+        this.bindAddress = bindAddress;
+    }
 
     public ServerSocket createServerSocket(final int pPort) throws IOException
     {
-        ServerSocket socket = ServerSocketFactory.getDefault()
-                                                 .createServerSocket(pPort, 0, 
InetAddress.getLoopbackAddress());
+        ServerSocket socket = 
ServerSocketFactory.getDefault().createServerSocket(pPort, 0, bindAddress);
         socket.setReuseAddress(true);
         return socket;
     }
@@ -37,3 +42,4 @@ public class RMIServerSocketFactoryImpl implements 
RMIServerSocketFactory
         return RMIServerSocketFactoryImpl.class.hashCode();
     }
 }
+

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7b0c7164/test/resources/auth/cassandra-test-jaas.conf
----------------------------------------------------------------------
diff --git a/test/resources/auth/cassandra-test-jaas.conf 
b/test/resources/auth/cassandra-test-jaas.conf
new file mode 100644
index 0000000..ccb8b6a
--- /dev/null
+++ b/test/resources/auth/cassandra-test-jaas.conf
@@ -0,0 +1,4 @@
+// Delegates authentication to a stub login module, hardcoded to authenticate 
as a particular user - see JMXAuthTest
+TestLogin {
+  org.apache.cassandra.auth.jmx.JMXAuthTest$StubLoginModule REQUIRED 
role_name=test_role;
+};

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7b0c7164/test/unit/org/apache/cassandra/auth/StubAuthorizer.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/auth/StubAuthorizer.java 
b/test/unit/org/apache/cassandra/auth/StubAuthorizer.java
new file mode 100644
index 0000000..8e0d141
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/StubAuthorizer.java
@@ -0,0 +1,120 @@
+/*
+ * 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.cassandra.auth;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.exceptions.RequestValidationException;
+import org.apache.cassandra.utils.Pair;
+
+public class StubAuthorizer implements IAuthorizer
+{
+    Map<Pair<String, IResource>, Set<Permission>> userPermissions = new 
HashMap<>();
+
+    public void clear()
+    {
+        userPermissions.clear();
+    }
+
+    public Set<Permission> authorize(AuthenticatedUser user, IResource 
resource)
+    {
+        Pair<String, IResource> key = Pair.create(user.getName(), resource);
+        Set<Permission> perms = userPermissions.get(key);
+        return perms != null ? perms : Collections.emptySet();
+    }
+
+    public void grant(AuthenticatedUser performer,
+                      Set<Permission> permissions,
+                      IResource resource,
+                      RoleResource grantee) throws RequestValidationException, 
RequestExecutionException
+    {
+        Pair<String, IResource> key = Pair.create(grantee.getRoleName(), 
resource);
+        Set<Permission> perms = userPermissions.get(key);
+        if (null == perms)
+        {
+            perms = new HashSet<>();
+            userPermissions.put(key, perms);
+        }
+        perms.addAll(permissions);
+    }
+
+    public void revoke(AuthenticatedUser performer,
+                       Set<Permission> permissions,
+                       IResource resource,
+                       RoleResource revokee) throws 
RequestValidationException, RequestExecutionException
+    {
+        Pair<String, IResource> key = Pair.create(revokee.getRoleName(), 
resource);
+        Set<Permission> perms = userPermissions.get(key);
+        if (null != perms)
+        {
+            perms.removeAll(permissions);
+            if (perms.isEmpty())
+                userPermissions.remove(key);
+        }
+    }
+
+    public Set<PermissionDetails> list(AuthenticatedUser performer,
+                                       Set<Permission> permissions,
+                                       IResource resource,
+                                       RoleResource grantee) throws 
RequestValidationException, RequestExecutionException
+    {
+        return userPermissions.entrySet()
+                              .stream()
+                              .filter(entry -> 
entry.getKey().left.equals(grantee.getRoleName())
+                                               && (resource == null || 
entry.getKey().right.equals(resource)))
+                              .flatMap(entry -> entry.getValue()
+                                                     .stream()
+                                                     
.filter(permissions::contains)
+                                                     .map(p -> new 
PermissionDetails(entry.getKey().left,
+                                                                               
      entry.getKey().right,
+                                                                               
      p)))
+                              .collect(Collectors.toSet());
+
+    }
+
+    public void revokeAllFrom(RoleResource revokee)
+    {
+        for (Pair<String, IResource> key : userPermissions.keySet())
+            if (key.left.equals(revokee.getRoleName()))
+                userPermissions.remove(key);
+    }
+
+    public void revokeAllOn(IResource droppedResource)
+    {
+        for (Pair<String, IResource> key : userPermissions.keySet())
+            if (key.right.equals(droppedResource))
+                userPermissions.remove(key);
+    }
+
+    public Set<? extends IResource> protectedResources()
+    {
+        return Collections.emptySet();
+    }
+
+    public void validateConfiguration() throws ConfigurationException
+    {
+    }
+
+    public void setup()
+    {
+    }
+}

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7b0c7164/test/unit/org/apache/cassandra/auth/jmx/AuthorizationProxyTest.java
----------------------------------------------------------------------
diff --git 
a/test/unit/org/apache/cassandra/auth/jmx/AuthorizationProxyTest.java 
b/test/unit/org/apache/cassandra/auth/jmx/AuthorizationProxyTest.java
new file mode 100644
index 0000000..9943acb
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/jmx/AuthorizationProxyTest.java
@@ -0,0 +1,574 @@
+/*
+ * 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.cassandra.auth.jmx;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+import javax.management.remote.JMXPrincipal;
+import javax.security.auth.Subject;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import org.junit.Test;
+
+import org.apache.cassandra.auth.*;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class AuthorizationProxyTest
+{
+    JMXResource osBean = JMXResource.mbean("java.lang:type=OperatingSystem");
+    JMXResource runtimeBean = JMXResource.mbean("java.lang:type=Runtime");
+    JMXResource threadingBean = JMXResource.mbean("java.lang:type=Threading");
+    JMXResource javaLangWildcard = JMXResource.mbean("java.lang:type=*");
+
+    JMXResource hintsBean = 
JMXResource.mbean("org.apache.cassandra.hints:type=HintsService");
+    JMXResource batchlogBean = 
JMXResource.mbean("org.apache.cassandra.db:type=BatchlogManager");
+    JMXResource customBean = 
JMXResource.mbean("org.apache.cassandra:type=CustomBean,property=foo");
+    Set<ObjectName> allBeans = objectNames(osBean, runtimeBean, threadingBean, 
hintsBean, batchlogBean, customBean);
+
+    RoleResource role1 = RoleResource.role("r1");
+
+    @Test
+    public void roleHasRequiredPermission() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, 
osBean, Permission.SELECT)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     .isAuthzRequired(() -> 
true)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" 
}));
+    }
+
+    @Test
+    public void roleDoesNotHaveRequiredPermission() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, 
osBean, Permission.AUTHORIZE)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     .isAuthzRequired(() -> 
true).build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "setAttribute",
+                                    new Object[]{ objectName(osBean), "arch" 
}));
+    }
+
+    @Test
+    public void roleHasRequiredPermissionOnRootResource() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, 
JMXResource.root(), Permission.SELECT)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     .isAuthzRequired(() -> 
true)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" 
}));
+    }
+
+    @Test
+    public void roleHasOtherPermissionOnRootResource() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, 
JMXResource.root(), Permission.AUTHORIZE)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     .isAuthzRequired(() -> 
true)
+                                                     .build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "invoke",
+                                    new Object[]{ objectName(osBean), 
"bogusMethod" }));
+    }
+
+    @Test
+    public void roleHasNoPermissions() throws Throwable
+    {
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> 
false)
+                                                     .getPermissions((role) -> 
Collections.emptySet())
+                                                     .isAuthzRequired(() -> 
true)
+                                                     .build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "getAttribute",
+                                    new Object[]{ objectName(osBean), "arch" 
}));
+    }
+
+    @Test
+    public void roleHasNoPermissionsButIsSuperuser() throws Throwable
+    {
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> 
true)
+                                                     .getPermissions((role) -> 
Collections.emptySet())
+                                                     .isAuthzRequired(() -> 
true)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" 
}));
+    }
+
+    @Test
+    public void roleHasNoPermissionsButAuthzNotRequired() throws Throwable
+    {
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> 
false)
+                                                     .getPermissions((role) -> 
Collections.emptySet())
+                                                     .isAuthzRequired(() -> 
false)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" 
}));
+    }
+
+    @Test
+    public void authorizeWhenSubjectIsNull() throws Throwable
+    {
+        // a null subject indicates that the action is being performed by the
+        // connector itself, so we always authorize it
+        // Verify that the superuser status is never tested as the request 
returns early
+        // due to the null Subject
+        // Also, hardcode the permissions provider to return an empty set, so 
we know that
+        // can be doubly sure that it's the null Subject which causes the 
authz to succeed
+        final AtomicBoolean suStatusChecked = new AtomicBoolean(false);
+        AuthorizationProxy proxy = new ProxyBuilder().getPermissions((role) -> 
Collections.emptySet())
+                                                     .isAuthzRequired(() -> 
true)
+                                                     .isSuperuser((role) ->
+                                                                  {
+                                                                      
suStatusChecked.set(true);
+                                                                      return 
false;
+                                                                  })
+                                                     .build();
+
+        assertTrue(proxy.authorize(null,
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" 
}));
+        assertFalse(suStatusChecked.get());
+    }
+
+    @Test
+    public void rejectWhenSubjectNotAuthenticated() throws Throwable
+    {
+        // Access is denied to a Subject without any associated Principals
+        // Verify that the superuser status is never tested as the request is 
rejected early
+        // due to the Subject
+        final AtomicBoolean suStatusChecked = new AtomicBoolean(false);
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> 
true)
+                                                     .isSuperuser((role) ->
+                                                                  {
+                                                                      
suStatusChecked.set(true);
+                                                                      return 
true;
+                                                                  })
+                                                     .build();
+        assertFalse(proxy.authorize(new Subject(),
+                                    "getAttribute",
+                                    new Object[]{ objectName(osBean), "arch" 
}));
+        assertFalse(suStatusChecked.get());
+    }
+
+    @Test
+    public void authorizeWhenWildcardGrantCoversExactTarget() throws Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, 
javaLangWildcard, Permission.SELECT)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> 
true)
+                                                     .isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" 
}));
+    }
+
+    @Test
+    public void rejectWhenWildcardGrantDoesNotCoverExactTarget() throws 
Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, 
javaLangWildcard, Permission.SELECT)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> 
true)
+                                                     .isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     .build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "getAttribute",
+                                    new Object[]{ objectName(customBean), 
"arch" }));
+    }
+
+    @Test
+    public void authorizeWhenWildcardGrantCoversWildcardTarget() throws 
Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, 
javaLangWildcard, Permission.DESCRIBE)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> 
true)
+                                                     .isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     
.queryNames(matcher(allBeans))
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "queryNames",
+                                   new Object[]{ objectName(javaLangWildcard), 
null }));
+    }
+
+    @Test
+    public void rejectWhenWildcardGrantIsDisjointWithWildcardTarget() throws 
Throwable
+    {
+        JMXResource customWildcard = 
JMXResource.mbean("org.apache.cassandra:*");
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, 
customWildcard, Permission.DESCRIBE)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> 
true)
+                                                     .isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     
.queryNames(matcher(allBeans))
+                                                     .build();
+
+        // the grant on org.apache.cassandra:* shouldn't permit us to invoke 
queryNames with java.lang:*
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "queryNames",
+                                    new Object[]{ 
objectName(javaLangWildcard), null }));
+    }
+
+    @Test
+    public void rejectWhenWildcardGrantIntersectsWithWildcardTarget() throws 
Throwable
+    {
+        // in this test, permissions are granted on 
org.apache.cassandra:type=CustomBean,property=*
+        // and all beans in the org.apache.cassandra.hints domain, but
+        // but the target of the invocation is org.apache.cassandra*:*
+        // i.e. the subject has permissions on all CustomBeans and on the 
HintsService bean, but is
+        // attempting to query all names in the org.apache.cassandra* domain. 
The operation should
+        // be rejected as the permissions don't cover all known beans matching 
that domain, due to
+        // the BatchLogManager bean.
+
+        JMXResource allCustomBeans = 
JMXResource.mbean("org.apache.cassandra:type=CustomBean,property=*");
+        JMXResource allHintsBeans = 
JMXResource.mbean("org.apache.cassandra.hints:*");
+        ObjectName allCassandraBeans = 
ObjectName.getInstance("org.apache.cassandra*:*");
+
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, ImmutableSet.of(permission(role1, 
allCustomBeans, Permission.DESCRIBE),
+                                                   permission(role1, 
allHintsBeans, Permission.DESCRIBE)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> 
true)
+                                                     .isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     
.queryNames(matcher(allBeans))
+                                                     .build();
+
+        // the grant on org.apache.cassandra:* shouldn't permit us to invoke 
queryNames with java.lang:*
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "queryNames",
+                                    new Object[]{ allCassandraBeans, null }));
+    }
+
+    @Test
+    public void authorizeOnTargetWildcardWithPermissionOnRoot() throws 
Throwable
+    {
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+            ImmutableMap.of(role1, Collections.singleton(permission(role1, 
JMXResource.root(), Permission.SELECT)));
+
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> 
true)
+                                                     .isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(javaLangWildcard), 
"arch" }));
+    }
+
+    @Test
+    public void rejectInvocationOfUnknownMethod() throws Throwable
+    {
+        // Grant ALL permissions on the root resource, so we know that it's
+        // the unknown method that causes the authz rejection. Of course, this
+        // isn't foolproof but it's something.
+        Set<PermissionDetails> allPerms = Permission.ALL.stream()
+                                                        .map(perm -> 
permission(role1, JMXResource.root(), perm))
+                                                        
.collect(Collectors.toSet());
+        Map<RoleResource, Set<PermissionDetails>> permissions = 
ImmutableMap.of(role1, allPerms);
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> 
true)
+                                                     .isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     .build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                    "unKnownMethod",
+                                    new Object[] { 
ObjectName.getInstance(osBean.getObjectName()) }));
+    }
+
+    @Test
+    public void rejectInvocationOfBlacklistedMethods() throws Throwable
+    {
+        String[] methods = { "createMBean",
+                             "deserialize",
+                             "getClassLoader",
+                             "getClassLoaderFor",
+                             "instantiate",
+                             "registerMBean",
+                             "unregisterMBean" };
+
+        // Hardcode the superuser status check to return true, so any allowed 
method can be invoked.
+        AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> 
true)
+                                                     .isSuperuser((role) -> 
true)
+                                                     .build();
+
+        for (String method : methods)
+            // the arguments array isn't significant, so it can just be empty
+            assertFalse(proxy.authorize(subject(role1.getRoleName()), method, 
new Object[0]));
+    }
+
+    @Test
+    public void authorizeMethodsWithoutMBeanArgumentIfPermissionsGranted() 
throws Throwable
+    {
+        // Certain methods on MBeanServer don't take an ObjectName as their 
first argument.
+        // These methods are characterised by AuthorizationProxy as being 
concerned with
+        // the MBeanServer itself, as opposed to a specific managed bean. Of 
these methods,
+        // only those considered "descriptive" are allowed to be invoked by 
remote users.
+        // These require the DESCRIBE permission on the root JMXResource.
+        testNonMbeanMethods(true);
+    }
+
+    @Test
+    public void rejectMethodsWithoutMBeanArgumentIfPermissionsNotGranted() 
throws Throwable
+    {
+        testNonMbeanMethods(false);
+    }
+
+    @Test
+    public void rejectWhenAuthSetupIsNotComplete() throws Throwable
+    {
+        // IAuthorizer & IRoleManager should not be considered ready to use 
until
+        // we know that auth setup has completed. So, even though the 
IAuthorizer
+        // would theoretically grant access, the auth proxy should deny it if 
setup
+        // hasn't finished.
+
+        Map<RoleResource, Set<PermissionDetails>> permissions =
+        ImmutableMap.of(role1, Collections.singleton(permission(role1, osBean, 
Permission.SELECT)));
+
+        // verify that access is granted when setup is complete
+        AuthorizationProxy proxy = new ProxyBuilder().isSuperuser((role) -> 
false)
+                                                     
.getPermissions(permissions::get)
+                                                     .isAuthzRequired(() -> 
true)
+                                                     .isAuthSetupComplete(() 
-> true)
+                                                     .build();
+
+        assertTrue(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" 
}));
+
+        // and denied when it isn't
+        proxy = new ProxyBuilder().isSuperuser((role) -> false)
+                                  .getPermissions(permissions::get)
+                                  .isAuthzRequired(() -> true)
+                                  .isAuthSetupComplete(() -> false)
+                                  .build();
+
+        assertFalse(proxy.authorize(subject(role1.getRoleName()),
+                                   "getAttribute",
+                                   new Object[]{ objectName(osBean), "arch" 
}));
+    }
+
+    private void testNonMbeanMethods(boolean withPermission)
+    {
+        String[] methods = { "getDefaultDomain",
+                             "getDomains",
+                             "getMBeanCount",
+                             "hashCode",
+                             "queryMBeans",
+                             "queryNames",
+                             "toString" };
+
+
+        ProxyBuilder builder = new ProxyBuilder().isAuthzRequired(() -> 
true).isSuperuser((role) -> false);
+        if (withPermission)
+        {
+            Map<RoleResource, Set<PermissionDetails>> permissions =
+                ImmutableMap.of(role1, ImmutableSet.of(permission(role1, 
JMXResource.root(), Permission.DESCRIBE)));
+            builder.getPermissions(permissions::get);
+        }
+        else
+        {
+            builder.getPermissions((role) -> Collections.emptySet());
+        }
+        AuthorizationProxy proxy = builder.build();
+
+        for (String method : methods)
+            assertEquals(withPermission, 
proxy.authorize(subject(role1.getRoleName()), method, new Object[]{ null }));
+
+        // non-whitelisted methods should be rejected regardless.
+        // This isn't exactly comprehensive, but it's better than nothing
+        String[] notAllowed = { "fooMethod", "barMethod", "bazMethod" };
+        for (String method : notAllowed)
+            assertFalse(proxy.authorize(subject(role1.getRoleName()), method, 
new Object[]{ null }));
+    }
+
+    // provides a simple matching function which can be substituted for the 
proxy's queryMBeans
+    // utility (which by default just delegates to the MBeanServer)
+    // This function just iterates over a supplied set of ObjectNames and 
filters out those
+    // to which the target name *doesn't* apply
+    private static Function<ObjectName, Set<ObjectName>> 
matcher(Set<ObjectName> allBeans)
+    {
+        return (target) -> allBeans.stream()
+                                   .filter(target::apply)
+                                   .collect(Collectors.toSet());
+    }
+
+    private static PermissionDetails permission(RoleResource grantee, 
IResource resource, Permission permission)
+    {
+        return new PermissionDetails(grantee.getRoleName(), resource, 
permission);
+    }
+
+    private static Subject subject(String roleName)
+    {
+        Subject subject = new Subject();
+        subject.getPrincipals().add(new CassandraPrincipal(roleName));
+        return subject;
+    }
+
+    private static ObjectName objectName(JMXResource resource) throws 
MalformedObjectNameException
+    {
+        return ObjectName.getInstance(resource.getObjectName());
+    }
+
+    private static Set<ObjectName> objectNames(JMXResource... resource)
+    {
+        Set<ObjectName> names = new HashSet<>();
+        try
+        {
+            for (JMXResource r : resource)
+                names.add(objectName(r));
+        }
+        catch (MalformedObjectNameException e)
+        {
+            fail("JMXResource returned invalid object name: " + 
e.getMessage());
+        }
+        return names;
+    }
+
+    public static class ProxyBuilder
+    {
+        Function<RoleResource, Set<PermissionDetails>> getPermissions;
+        Function<ObjectName, Set<ObjectName>> queryNames;
+        Function<RoleResource, Boolean> isSuperuser;
+        Supplier<Boolean> isAuthzRequired;
+        Supplier<Boolean> isAuthSetupComplete = () -> true;
+
+        AuthorizationProxy build()
+        {
+            InjectableAuthProxy proxy = new InjectableAuthProxy();
+
+            if (getPermissions != null)
+                proxy.setGetPermissions(getPermissions);
+
+            if (queryNames != null)
+                proxy.setQueryNames(queryNames);
+
+            if (isSuperuser != null)
+                proxy.setIsSuperuser(isSuperuser);
+
+            if (isAuthzRequired != null)
+                proxy.setIsAuthzRequired(isAuthzRequired);
+
+            proxy.setIsAuthSetupComplete(isAuthSetupComplete);
+
+            return proxy;
+        }
+
+        ProxyBuilder getPermissions(Function<RoleResource, 
Set<PermissionDetails>> f)
+        {
+            getPermissions = f;
+            return this;
+        }
+
+        ProxyBuilder queryNames(Function<ObjectName, Set<ObjectName>> f)
+        {
+            queryNames = f;
+            return this;
+        }
+
+        ProxyBuilder isSuperuser(Function<RoleResource, Boolean> f)
+        {
+            isSuperuser = f;
+            return this;
+        }
+
+        ProxyBuilder isAuthzRequired(Supplier<Boolean> s)
+        {
+            isAuthzRequired = s;
+            return this;
+        }
+
+        ProxyBuilder isAuthSetupComplete(Supplier<Boolean> s)
+        {
+            isAuthSetupComplete = s;
+            return this;
+        }
+
+        private static class InjectableAuthProxy extends AuthorizationProxy
+        {
+            void setGetPermissions(Function<RoleResource, 
Set<PermissionDetails>> f)
+            {
+                this.getPermissions = f;
+            }
+
+            void setQueryNames(Function<ObjectName, Set<ObjectName>> f)
+            {
+                this.queryNames = f;
+            }
+
+            void setIsSuperuser(Function<RoleResource, Boolean> f)
+            {
+                this.isSuperuser = f;
+            }
+
+            void setIsAuthzRequired(Supplier<Boolean> s)
+            {
+                this.isAuthzRequired = s;
+            }
+
+            void setIsAuthSetupComplete(Supplier<Boolean> s)
+            {
+                this.isAuthSetupComplete = s;
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7b0c7164/test/unit/org/apache/cassandra/auth/jmx/JMXAuthTest.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/auth/jmx/JMXAuthTest.java 
b/test/unit/org/apache/cassandra/auth/jmx/JMXAuthTest.java
new file mode 100644
index 0000000..10c871b
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/jmx/JMXAuthTest.java
@@ -0,0 +1,279 @@
+/*
+ * 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.cassandra.auth.jmx;
+
+import java.lang.reflect.Field;
+import java.nio.file.Paths;
+import java.rmi.server.RMISocketFactory;
+import java.util.HashMap;
+import java.util.Map;
+import javax.management.JMX;
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+import javax.management.remote.*;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.auth.*;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.db.ColumnFamilyStoreMBean;
+import org.apache.cassandra.utils.JMXServerUtils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class JMXAuthTest extends CQLTester
+{
+    private static JMXConnectorServer jmxServer;
+    private static MBeanServerConnection connection;
+    private RoleResource role;
+    private String tableName;
+    private JMXResource tableMBean;
+
+    @FunctionalInterface
+    private interface MBeanAction
+    {
+        void execute();
+    }
+
+    @BeforeClass
+    public static void setupClass() throws Exception
+    {
+        setupAuthorizer();
+        setupJMXServer();
+    }
+
+    private static void setupAuthorizer()
+    {
+        try
+        {
+            IAuthorizer authorizer = new StubAuthorizer();
+            Field authorizerField = 
DatabaseDescriptor.class.getDeclaredField("authorizer");
+            authorizerField.setAccessible(true);
+            authorizerField.set(null, authorizer);
+            DatabaseDescriptor.setPermissionsValidity(0);
+        }
+        catch (IllegalAccessException | NoSuchFieldException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static void setupJMXServer() throws Exception
+    {
+        String config = 
Paths.get(ClassLoader.getSystemResource("auth/cassandra-test-jaas.conf").toURI()).toString();
+        System.setProperty("com.sun.management.jmxremote.authenticate", 
"true");
+        System.setProperty("java.security.auth.login.config", config);
+        System.setProperty("cassandra.jmx.remote.login.config", "TestLogin");
+        System.setProperty("cassandra.jmx.authorizer", 
NoSuperUserAuthorizationProxy.class.getName());
+        jmxServer = JMXServerUtils.createJMXServer(9999, true);
+        jmxServer.start();
+
+        JMXServiceURL jmxUrl = new 
JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi");
+        Map<String, Object> env = new HashMap<>();
+        env.put("com.sun.jndi.rmi.factory.socket", 
RMISocketFactory.getDefaultSocketFactory());
+        JMXConnector jmxc = JMXConnectorFactory.connect(jmxUrl, env);
+        connection = jmxc.getMBeanServerConnection();
+    }
+
+    @Before
+    public void setup() throws Throwable
+    {
+        role = RoleResource.role("test_role");
+        clearAllPermissions();
+        tableName = createTable("CREATE TABLE %s (k int, v int, PRIMARY KEY 
(k))");
+        tableMBean = 
JMXResource.mbean(String.format("org.apache.cassandra.db:type=Tables,keyspace=%s,table=%s",
+                                                     KEYSPACE, tableName));
+    }
+
+    @Test
+    public void readAttribute() throws Throwable
+    {
+        ColumnFamilyStoreMBean proxy = JMX.newMBeanProxy(connection,
+                                                         
ObjectName.getInstance(tableMBean.getObjectName()),
+                                                         
ColumnFamilyStoreMBean.class);
+
+        // grant SELECT on a single specific Table mbean
+        assertPermissionOnResource(Permission.SELECT, tableMBean, 
proxy::getTableName);
+
+        // grant SELECT on all Table mbeans in named keyspace
+        clearAllPermissions();
+        JMXResource allTablesInKeyspace = 
JMXResource.mbean(String.format("org.apache.cassandra.db:type=Tables,keyspace=%s,*",
+                                                                          
KEYSPACE));
+        assertPermissionOnResource(Permission.SELECT, allTablesInKeyspace, 
proxy::getTableName);
+
+        // grant SELECT on all Table mbeans
+        clearAllPermissions();
+        JMXResource allTables = 
JMXResource.mbean("org.apache.cassandra.db:type=Tables,*");
+        assertPermissionOnResource(Permission.SELECT, allTables, 
proxy::getTableName);
+
+        // grant SELECT ON ALL MBEANS
+        clearAllPermissions();
+        assertPermissionOnResource(Permission.SELECT, JMXResource.root(), 
proxy::getTableName);
+    }
+
+    @Test
+    public void writeAttribute() throws Throwable
+    {
+        ColumnFamilyStoreMBean proxy = JMX.newMBeanProxy(connection,
+                                                         
ObjectName.getInstance(tableMBean.getObjectName()),
+                                                         
ColumnFamilyStoreMBean.class);
+        MBeanAction action = () -> proxy.setMinimumCompactionThreshold(4);
+
+        // grant MODIFY on a single specific Table mbean
+        assertPermissionOnResource(Permission.MODIFY, tableMBean, action);
+
+        // grant MODIFY on all Table mbeans in named keyspace
+        clearAllPermissions();
+        JMXResource allTablesInKeyspace = 
JMXResource.mbean(String.format("org.apache.cassandra.db:type=Tables,keyspace=%s,*",
+                                                                          
KEYSPACE));
+        assertPermissionOnResource(Permission.MODIFY, allTablesInKeyspace, 
action);
+
+        // grant MODIFY on all Table mbeans
+        clearAllPermissions();
+        JMXResource allTables = 
JMXResource.mbean("org.apache.cassandra.db:type=Tables,*");
+        assertPermissionOnResource(Permission.MODIFY, allTables, action);
+
+        // grant MODIFY ON ALL MBEANS
+        clearAllPermissions();
+        assertPermissionOnResource(Permission.MODIFY, JMXResource.root(), 
action);
+    }
+
+    @Test
+    public void executeMethod() throws Throwable
+    {
+        ColumnFamilyStoreMBean proxy = JMX.newMBeanProxy(connection,
+                                                         
ObjectName.getInstance(tableMBean.getObjectName()),
+                                                         
ColumnFamilyStoreMBean.class);
+
+        // grant EXECUTE on a single specific Table mbean
+        assertPermissionOnResource(Permission.EXECUTE, tableMBean, 
proxy::estimateKeys);
+
+        // grant EXECUTE on all Table mbeans in named keyspace
+        clearAllPermissions();
+        JMXResource allTablesInKeyspace = 
JMXResource.mbean(String.format("org.apache.cassandra.db:type=Tables,keyspace=%s,*",
+                                                                          
KEYSPACE));
+        assertPermissionOnResource(Permission.EXECUTE, allTablesInKeyspace, 
proxy::estimateKeys);
+
+        // grant EXECUTE on all Table mbeans
+        clearAllPermissions();
+        JMXResource allTables = 
JMXResource.mbean("org.apache.cassandra.db:type=Tables,*");
+        assertPermissionOnResource(Permission.EXECUTE, allTables, 
proxy::estimateKeys);
+
+        // grant EXECUTE ON ALL MBEANS
+        clearAllPermissions();
+        assertPermissionOnResource(Permission.EXECUTE, JMXResource.root(), 
proxy::estimateKeys);
+    }
+
+    private void assertPermissionOnResource(Permission permission,
+                                            JMXResource resource,
+                                            MBeanAction action)
+    {
+        assertUnauthorized(action);
+        grantPermission(permission, resource, role);
+        assertAuthorized(action);
+    }
+
+    private void grantPermission(Permission permission, JMXResource resource, 
RoleResource role)
+    {
+        DatabaseDescriptor.getAuthorizer().grant(AuthenticatedUser.SYSTEM_USER,
+                                                 ImmutableSet.of(permission),
+                                                 resource,
+                                                 role);
+    }
+
+    private void assertAuthorized(MBeanAction action)
+    {
+        action.execute();
+    }
+
+    private void assertUnauthorized(MBeanAction action)
+    {
+        try
+        {
+            action.execute();
+            fail("Expected an UnauthorizedException, but none was thrown");
+        }
+        catch (SecurityException e)
+        {
+            assertEquals("Access Denied", e.getLocalizedMessage());
+        }
+    }
+
+    private void clearAllPermissions()
+    {
+        ((StubAuthorizer) DatabaseDescriptor.getAuthorizer()).clear();
+    }
+
+    public static class StubLoginModule implements LoginModule
+    {
+        private CassandraPrincipal principal;
+        private Subject subject;
+
+        public StubLoginModule(){}
+
+        public void initialize(Subject subject, CallbackHandler 
callbackHandler, Map<String, ?> sharedState, Map<String, ?> options)
+        {
+            this.subject = subject;
+            principal = new 
CassandraPrincipal((String)options.get("role_name"));
+        }
+
+        public boolean login() throws LoginException
+        {
+            return true;
+        }
+
+        public boolean commit() throws LoginException
+        {
+            if (!subject.getPrincipals().contains(principal))
+                subject.getPrincipals().add(principal);
+            return true;
+        }
+
+        public boolean abort() throws LoginException
+        {
+            return true;
+        }
+
+        public boolean logout() throws LoginException
+        {
+            return true;
+        }
+    }
+
+    // always answers false to isSuperUser and true to isAuthSetup complete - 
saves us having to initialize
+    // a real IRoleManager and StorageService for the test
+    public static class NoSuperUserAuthorizationProxy extends 
AuthorizationProxy
+    {
+        public NoSuperUserAuthorizationProxy()
+        {
+            super();
+            this.isSuperuser = (role) -> false;
+            this.isAuthSetupComplete = () -> true;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7b0c7164/test/unit/org/apache/cassandra/cql3/validation/entities/UFAuthTest.java
----------------------------------------------------------------------
diff --git 
a/test/unit/org/apache/cassandra/cql3/validation/entities/UFAuthTest.java 
b/test/unit/org/apache/cassandra/cql3/validation/entities/UFAuthTest.java
index 6993bec..d085a9d 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/UFAuthTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFAuthTest.java
@@ -29,14 +29,11 @@ import org.junit.Test;
 import org.apache.cassandra.auth.*;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.Schema;
-import org.apache.cassandra.cql3.Attributes;
-import org.apache.cassandra.cql3.CQLStatement;
-import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.cql3.functions.Function;
 import org.apache.cassandra.cql3.functions.FunctionName;
 import org.apache.cassandra.cql3.statements.BatchStatement;
 import org.apache.cassandra.cql3.statements.ModificationStatement;
-import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.exceptions.*;
 import org.apache.cassandra.service.ClientState;
 import org.apache.cassandra.utils.Pair;
@@ -626,99 +623,4 @@ public class UFAuthTest extends CQLTester
     {
         return String.format("%s(%s)", functionName, 
Joiner.on(",").join(args));
     }
-
-    static class StubAuthorizer implements IAuthorizer
-    {
-        Map<Pair<String, IResource>, Set<Permission>> userPermissions = new 
HashMap<>();
-
-        private void clear()
-        {
-            userPermissions.clear();
-        }
-
-        public Set<Permission> authorize(AuthenticatedUser user, IResource 
resource)
-        {
-            Pair<String, IResource> key = Pair.create(user.getName(), 
resource);
-            Set<Permission> perms = userPermissions.get(key);
-            return perms != null ? perms : Collections.<Permission>emptySet();
-        }
-
-        public void grant(AuthenticatedUser performer,
-                          Set<Permission> permissions,
-                          IResource resource,
-                          RoleResource grantee) throws 
RequestValidationException, RequestExecutionException
-        {
-            Pair<String, IResource> key = Pair.create(grantee.getRoleName(), 
resource);
-            Set<Permission> perms = userPermissions.get(key);
-            if (null == perms)
-            {
-                perms = new HashSet<>();
-                userPermissions.put(key, perms);
-            }
-            perms.addAll(permissions);
-        }
-
-        public void revoke(AuthenticatedUser performer,
-                           Set<Permission> permissions,
-                           IResource resource,
-                           RoleResource revokee) throws 
RequestValidationException, RequestExecutionException
-        {
-            Pair<String, IResource> key = Pair.create(revokee.getRoleName(), 
resource);
-            Set<Permission> perms = userPermissions.get(key);
-            if (null != perms)
-                perms.removeAll(permissions);
-            if (perms.isEmpty())
-                userPermissions.remove(key);
-        }
-
-        public Set<PermissionDetails> list(AuthenticatedUser performer,
-                                           Set<Permission> permissions,
-                                           IResource resource,
-                                           RoleResource grantee) throws 
RequestValidationException, RequestExecutionException
-        {
-            Pair<String, IResource> key = Pair.create(grantee.getRoleName(), 
resource);
-            Set<Permission> perms = userPermissions.get(key);
-            if (perms == null)
-                return Collections.emptySet();
-
-
-            Set<PermissionDetails> details = new HashSet<>();
-            for (Permission permission : perms)
-            {
-                if (permissions.contains(permission))
-                    details.add(new PermissionDetails(grantee.getRoleName(), 
resource, permission));
-            }
-            return details;
-        }
-
-        public void revokeAllFrom(RoleResource revokee)
-        {
-            for (Pair<String, IResource> key : userPermissions.keySet())
-                if (key.left.equals(revokee.getRoleName()))
-                    userPermissions.remove(key);
-        }
-
-        public void revokeAllOn(IResource droppedResource)
-        {
-            for (Pair<String, IResource> key : userPermissions.keySet())
-                if (key.right.equals(droppedResource))
-                    userPermissions.remove(key);
-
-        }
-
-        public Set<? extends IResource> protectedResources()
-        {
-            return Collections.emptySet();
-        }
-
-        public void validateConfiguration() throws ConfigurationException
-        {
-
-        }
-
-        public void setup()
-        {
-
-        }
-    }
 }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7b0c7164/test/unit/org/apache/cassandra/service/RMIServerSocketFactoryImplTest.java
----------------------------------------------------------------------
diff --git 
a/test/unit/org/apache/cassandra/service/RMIServerSocketFactoryImplTest.java 
b/test/unit/org/apache/cassandra/service/RMIServerSocketFactoryImplTest.java
index 3459ec3..76e2f12 100644
--- a/test/unit/org/apache/cassandra/service/RMIServerSocketFactoryImplTest.java
+++ b/test/unit/org/apache/cassandra/service/RMIServerSocketFactoryImplTest.java
@@ -16,7 +16,7 @@ public class RMIServerSocketFactoryImplTest
     @Test
     public void testReusableAddrSocket() throws IOException
     {
-        RMIServerSocketFactory serverFactory = new 
RMIServerSocketFactoryImpl();
+        RMIServerSocketFactory serverFactory = new 
RMIServerSocketFactoryImpl(null);
         ServerSocket socket = serverFactory.createServerSocket(7199);
         assertTrue(socket.getReuseAddress());
     }

Reply via email to