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()); }
