| /* |
| * 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.hadoop.registry.secure; |
| |
| |
| import org.apache.hadoop.conf.Configuration; |
| import org.apache.hadoop.fs.PathPermissionException; |
| import org.apache.hadoop.security.UserGroupInformation; |
| import org.apache.hadoop.service.ServiceStateException; |
| import org.apache.hadoop.registry.client.api.RegistryConstants; |
| import org.apache.hadoop.registry.client.api.RegistryOperations; |
| import org.apache.hadoop.registry.client.api.RegistryOperationsFactory; |
| import org.apache.hadoop.registry.client.exceptions.NoPathPermissionsException; |
| import org.apache.hadoop.registry.client.impl.zk.ZKPathDumper; |
| import org.apache.hadoop.registry.client.impl.RegistryOperationsClient; |
| import org.apache.hadoop.registry.client.impl.zk.RegistrySecurity; |
| import org.apache.hadoop.registry.client.impl.zk.ZookeeperConfigOptions; |
| import org.apache.hadoop.registry.server.integration.RMRegistryOperationsService; |
| import org.apache.hadoop.registry.server.services.RegistryAdminService; |
| import org.apache.zookeeper.client.ZooKeeperSaslClient; |
| import org.apache.zookeeper.data.ACL; |
| import org.apache.zookeeper.data.Id; |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import javax.security.auth.login.LoginException; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.security.PrivilegedExceptionAction; |
| import java.util.List; |
| |
| import static org.apache.hadoop.registry.client.api.RegistryConstants.*; |
| |
| /** |
| * Verify that the {@link RMRegistryOperationsService} works securely |
| */ |
| public class TestSecureRMRegistryOperations extends AbstractSecureRegistryTest { |
| private static final Logger LOG = |
| LoggerFactory.getLogger(TestSecureRMRegistryOperations.class); |
| private Configuration secureConf; |
| private Configuration zkClientConf; |
| private UserGroupInformation zookeeperUGI; |
| |
| @Before |
| public void setupTestSecureRMRegistryOperations() throws Exception { |
| startSecureZK(); |
| secureConf = new Configuration(); |
| secureConf.setBoolean(KEY_REGISTRY_SECURE, true); |
| |
| // create client conf containing the ZK quorum |
| zkClientConf = new Configuration(secureZK.getConfig()); |
| zkClientConf.setBoolean(KEY_REGISTRY_SECURE, true); |
| assertNotEmpty(zkClientConf.get(RegistryConstants.KEY_REGISTRY_ZK_QUORUM)); |
| |
| // ZK is in charge |
| secureConf.set(KEY_REGISTRY_SYSTEM_ACCOUNTS, "sasl:zookeeper@"); |
| zookeeperUGI = loginUGI(ZOOKEEPER, keytab_zk); |
| } |
| |
| @After |
| public void teardownTestSecureRMRegistryOperations() { |
| } |
| |
| /** |
| * Create the RM registry operations as the current user |
| * @return the service |
| * @throws LoginException |
| * @throws FileNotFoundException |
| */ |
| public RMRegistryOperationsService startRMRegistryOperations() throws |
| LoginException, IOException, InterruptedException { |
| // kerberos |
| secureConf.set(KEY_REGISTRY_CLIENT_AUTH, |
| REGISTRY_CLIENT_AUTH_KERBEROS); |
| secureConf.set(KEY_REGISTRY_CLIENT_JAAS_CONTEXT, ZOOKEEPER_CLIENT_CONTEXT); |
| |
| RMRegistryOperationsService registryOperations = zookeeperUGI.doAs( |
| new PrivilegedExceptionAction<RMRegistryOperationsService>() { |
| @Override |
| public RMRegistryOperationsService run() throws Exception { |
| RMRegistryOperationsService operations |
| = new RMRegistryOperationsService("rmregistry", secureZK); |
| addToTeardown(operations); |
| operations.init(secureConf); |
| LOG.info(operations.bindingDiagnosticDetails()); |
| operations.start(); |
| return operations; |
| } |
| }); |
| |
| return registryOperations; |
| } |
| |
| /** |
| * test that ZK can write as itself |
| * @throws Throwable |
| */ |
| @Test |
| public void testZookeeperCanWriteUnderSystem() throws Throwable { |
| |
| RMRegistryOperationsService rmRegistryOperations = |
| startRMRegistryOperations(); |
| RegistryOperations operations = rmRegistryOperations; |
| operations.mknode(PATH_SYSTEM_SERVICES + "hdfs", |
| false); |
| ZKPathDumper pathDumper = rmRegistryOperations.dumpPath(true); |
| LOG.info(pathDumper.toString()); |
| } |
| |
| @Test |
| public void testAnonReadAccess() throws Throwable { |
| RMRegistryOperationsService rmRegistryOperations = |
| startRMRegistryOperations(); |
| describe(LOG, "testAnonReadAccess"); |
| RegistryOperations operations = |
| RegistryOperationsFactory.createAnonymousInstance(zkClientConf); |
| addToTeardown(operations); |
| operations.start(); |
| |
| assertFalse("RegistrySecurity.isClientSASLEnabled()==true", |
| RegistrySecurity.isClientSASLEnabled()); |
| operations.list(PATH_SYSTEM_SERVICES); |
| } |
| |
| @Test |
| public void testAnonNoWriteAccess() throws Throwable { |
| RMRegistryOperationsService rmRegistryOperations = |
| startRMRegistryOperations(); |
| describe(LOG, "testAnonNoWriteAccess"); |
| RegistryOperations operations = |
| RegistryOperationsFactory.createAnonymousInstance(zkClientConf); |
| addToTeardown(operations); |
| operations.start(); |
| |
| String servicePath = PATH_SYSTEM_SERVICES + "hdfs"; |
| expectMkNodeFailure(operations, servicePath); |
| } |
| |
| @Test |
| public void testAnonNoWriteAccessOffRoot() throws Throwable { |
| RMRegistryOperationsService rmRegistryOperations = |
| startRMRegistryOperations(); |
| describe(LOG, "testAnonNoWriteAccessOffRoot"); |
| RegistryOperations operations = |
| RegistryOperationsFactory.createAnonymousInstance(zkClientConf); |
| addToTeardown(operations); |
| operations.start(); |
| assertFalse("mknode(/)", operations.mknode("/", false)); |
| expectMkNodeFailure(operations, "/sub"); |
| expectDeleteFailure(operations, PATH_SYSTEM_SERVICES, true); |
| } |
| |
| /** |
| * Expect a mknode operation to fail |
| * @param operations operations instance |
| * @param path path |
| * @throws IOException An IO failure other than those permitted |
| */ |
| public void expectMkNodeFailure(RegistryOperations operations, |
| String path) throws IOException { |
| try { |
| operations.mknode(path, false); |
| fail("should have failed to create a node under " + path); |
| } catch (PathPermissionException expected) { |
| // expected |
| } catch (NoPathPermissionsException expected) { |
| // expected |
| } |
| } |
| |
| /** |
| * Expect a delete operation to fail |
| * @param operations operations instance |
| * @param path path |
| * @param recursive |
| * @throws IOException An IO failure other than those permitted |
| */ |
| public void expectDeleteFailure(RegistryOperations operations, |
| String path, boolean recursive) throws IOException { |
| try { |
| operations.delete(path, recursive); |
| fail("should have failed to delete the node " + path); |
| } catch (PathPermissionException expected) { |
| // expected |
| } catch (NoPathPermissionsException expected) { |
| // expected |
| } |
| } |
| |
| @Test |
| public void testAlicePathRestrictedAnonAccess() throws Throwable { |
| RMRegistryOperationsService rmRegistryOperations = |
| startRMRegistryOperations(); |
| String aliceHome = rmRegistryOperations.initUserRegistry(ALICE); |
| describe(LOG, "Creating anonymous accessor"); |
| RegistryOperations anonOperations = |
| RegistryOperationsFactory.createAnonymousInstance(zkClientConf); |
| addToTeardown(anonOperations); |
| anonOperations.start(); |
| anonOperations.list(aliceHome); |
| expectMkNodeFailure(anonOperations, aliceHome + "/anon"); |
| expectDeleteFailure(anonOperations, aliceHome, true); |
| } |
| |
| @Test |
| public void testUserZookeeperHomePathAccess() throws Throwable { |
| RMRegistryOperationsService rmRegistryOperations = |
| startRMRegistryOperations(); |
| final String home = rmRegistryOperations.initUserRegistry(ZOOKEEPER); |
| describe(LOG, "Creating ZK client"); |
| |
| RegistryOperations operations = zookeeperUGI.doAs( |
| new PrivilegedExceptionAction<RegistryOperations>() { |
| @Override |
| public RegistryOperations run() throws Exception { |
| RegistryOperations operations = |
| RegistryOperationsFactory.createKerberosInstance(zkClientConf, |
| ZOOKEEPER_CLIENT_CONTEXT); |
| addToTeardown(operations); |
| operations.start(); |
| |
| return operations; |
| } |
| }); |
| operations.list(home); |
| String path = home + "/subpath"; |
| operations.mknode(path, false); |
| operations.delete(path, true); |
| } |
| |
| @Test |
| public void testUserHomedirsPermissionsRestricted() throws Throwable { |
| // test that the /users/$user permissions are restricted |
| RMRegistryOperationsService rmRegistryOperations = |
| startRMRegistryOperations(); |
| // create Alice's dir, so it should have an ACL for Alice |
| final String home = rmRegistryOperations.initUserRegistry(ALICE); |
| List<ACL> acls = rmRegistryOperations.zkGetACLS(home); |
| ACL aliceACL = null; |
| for (ACL acl : acls) { |
| LOG.info(RegistrySecurity.aclToString(acl)); |
| Id id = acl.getId(); |
| if (id.getScheme().equals(ZookeeperConfigOptions.SCHEME_SASL) |
| && id.getId().startsWith(ALICE)) { |
| |
| aliceACL = acl; |
| break; |
| } |
| } |
| assertNotNull(aliceACL); |
| assertEquals(RegistryAdminService.USER_HOMEDIR_ACL_PERMISSIONS, |
| aliceACL.getPerms()); |
| } |
| |
| @Test |
| public void testDigestAccess() throws Throwable { |
| RMRegistryOperationsService registryAdmin = |
| startRMRegistryOperations(); |
| String id = "username"; |
| String pass = "password"; |
| registryAdmin.addWriteAccessor(id, pass); |
| List<ACL> clientAcls = registryAdmin.getClientAcls(); |
| LOG.info("Client ACLS=\n{}", RegistrySecurity.aclsToString(clientAcls)); |
| |
| String base = "/digested"; |
| registryAdmin.mknode(base, false); |
| List<ACL> baseACLs = registryAdmin.zkGetACLS(base); |
| String aclset = RegistrySecurity.aclsToString(baseACLs); |
| LOG.info("Base ACLs=\n{}", aclset); |
| ACL found = null; |
| for (ACL acl : baseACLs) { |
| if (ZookeeperConfigOptions.SCHEME_DIGEST.equals(acl.getId().getScheme())) { |
| found = acl; |
| break; |
| } |
| } |
| assertNotNull("Did not find digest entry in ACLs " + aclset, found); |
| zkClientConf.set(KEY_REGISTRY_USER_ACCOUNTS, |
| "sasl:somebody@EXAMPLE.COM, sasl:other"); |
| RegistryOperations operations = |
| RegistryOperationsFactory.createAuthenticatedInstance(zkClientConf, |
| id, |
| pass); |
| addToTeardown(operations); |
| operations.start(); |
| RegistryOperationsClient operationsClient = |
| (RegistryOperationsClient) operations; |
| List<ACL> digestClientACLs = operationsClient.getClientAcls(); |
| LOG.info("digest client ACLs=\n{}", |
| RegistrySecurity.aclsToString(digestClientACLs)); |
| operations.stat(base); |
| operations.mknode(base + "/subdir", false); |
| ZKPathDumper pathDumper = registryAdmin.dumpPath(true); |
| LOG.info(pathDumper.toString()); |
| } |
| |
| @Test(expected = IllegalArgumentException.class) |
| public void testNoDigestAuthMissingId() throws Throwable { |
| RegistryOperationsFactory.createAuthenticatedInstance(zkClientConf, |
| "", |
| "pass"); |
| } |
| |
| @Test(expected = ServiceStateException.class) |
| public void testNoDigestAuthMissingId2() throws Throwable { |
| zkClientConf.set(KEY_REGISTRY_CLIENT_AUTH, REGISTRY_CLIENT_AUTH_DIGEST); |
| zkClientConf.set(KEY_REGISTRY_CLIENT_AUTHENTICATION_ID, ""); |
| zkClientConf.set(KEY_REGISTRY_CLIENT_AUTHENTICATION_PASSWORD, "pass"); |
| RegistryOperationsFactory.createInstance("DigestRegistryOperations", |
| zkClientConf); |
| } |
| |
| @Test(expected = IllegalArgumentException.class) |
| public void testNoDigestAuthMissingPass() throws Throwable { |
| RegistryOperationsFactory.createAuthenticatedInstance(zkClientConf, |
| "id", |
| ""); |
| } |
| |
| @Test(expected = ServiceStateException.class) |
| public void testNoDigestAuthMissingPass2() throws Throwable { |
| zkClientConf.set(KEY_REGISTRY_CLIENT_AUTH, REGISTRY_CLIENT_AUTH_DIGEST); |
| zkClientConf.set(KEY_REGISTRY_CLIENT_AUTHENTICATION_ID, "id"); |
| zkClientConf.set(KEY_REGISTRY_CLIENT_AUTHENTICATION_PASSWORD, ""); |
| RegistryOperationsFactory.createInstance("DigestRegistryOperations", |
| zkClientConf); |
| } |
| |
| } |