| /* |
| * 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 |
| { |
| DatabaseDescriptor.daemonInitialization(); |
| 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; |
| } |
| } |
| } |