001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.zookeeper;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertFalse;
022import static org.junit.Assert.assertTrue;
023
024import java.io.File;
025import java.io.FileOutputStream;
026import java.io.IOException;
027import java.io.OutputStreamWriter;
028import java.nio.charset.StandardCharsets;
029import java.util.ArrayList;
030import java.util.List;
031import javax.security.auth.login.AppConfigurationEntry;
032import org.apache.hadoop.conf.Configuration;
033import org.apache.hadoop.hbase.HBaseClassTestRule;
034import org.apache.hadoop.hbase.HBaseConfiguration;
035import org.apache.hadoop.hbase.HBaseTestingUtil;
036import org.apache.hadoop.hbase.HConstants;
037import org.apache.hadoop.hbase.ServerName;
038import org.apache.hadoop.hbase.TestZooKeeper;
039import org.apache.hadoop.hbase.testclassification.MediumTests;
040import org.apache.hadoop.hbase.testclassification.ZKTests;
041import org.apache.zookeeper.ZooDefs;
042import org.apache.zookeeper.data.ACL;
043import org.apache.zookeeper.data.Stat;
044import org.junit.AfterClass;
045import org.junit.Before;
046import org.junit.BeforeClass;
047import org.junit.ClassRule;
048import org.junit.Test;
049import org.junit.experimental.categories.Category;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053@Category({ ZKTests.class, MediumTests.class })
054public class TestZooKeeperACL {
055
056  @ClassRule
057  public static final HBaseClassTestRule CLASS_RULE =
058      HBaseClassTestRule.forClass(TestZooKeeperACL.class);
059
060  private final static Logger LOG = LoggerFactory.getLogger(TestZooKeeperACL.class);
061  private final static HBaseTestingUtil TEST_UTIL =
062      new HBaseTestingUtil();
063
064  private static ZKWatcher zkw;
065  private static boolean secureZKAvailable;
066
067  @BeforeClass
068  public static void setUpBeforeClass() throws Exception {
069    File saslConfFile = File.createTempFile("tmp", "jaas.conf");
070    try (OutputStreamWriter fwriter = new OutputStreamWriter(
071          new FileOutputStream(saslConfFile), StandardCharsets.UTF_8)) {
072      fwriter.write(
073        "Server {\n" +
074          "org.apache.zookeeper.server.auth.DigestLoginModule required\n" +
075          "user_hbase=\"secret\";\n" +
076        "};\n" +
077        "Client {\n" +
078          "org.apache.zookeeper.server.auth.DigestLoginModule required\n" +
079          "username=\"hbase\"\n" +
080          "password=\"secret\";\n" +
081        "};" + "\n");
082    }
083    System.setProperty("java.security.auth.login.config",
084        saslConfFile.getAbsolutePath());
085    System.setProperty("zookeeper.authProvider.1",
086        "org.apache.zookeeper.server.auth.SASLAuthenticationProvider");
087
088    TEST_UTIL.getConfiguration().setInt("hbase.zookeeper.property.maxClientCnxns", 1000);
089
090    // If Hadoop is missing HADOOP-7070 the cluster will fail to start due to
091    // the JAAS configuration required by ZK being clobbered by Hadoop
092    try {
093      TEST_UTIL.startMiniCluster();
094    } catch (IOException e) {
095      LOG.warn("Hadoop is missing HADOOP-7070", e);
096      secureZKAvailable = false;
097      return;
098    }
099    zkw = new ZKWatcher(
100      new Configuration(TEST_UTIL.getConfiguration()),
101        TestZooKeeper.class.getName(), null);
102  }
103
104  @AfterClass
105  public static void tearDownAfterClass() throws Exception {
106    if (!secureZKAvailable) {
107      return;
108    }
109    TEST_UTIL.shutdownMiniCluster();
110  }
111
112  @Before
113  public void setUp() throws Exception {
114    if (!secureZKAvailable) {
115      return;
116    }
117    TEST_UTIL.ensureSomeRegionServersAvailable(2);
118  }
119
120  /**
121   * Create a node and check its ACL. When authentication is enabled on
122   * ZooKeeper, all nodes (except /hbase/root-region-server, /hbase/master
123   * and /hbase/hbaseid) should be created so that only the hbase server user
124   * (master or region server user) that created them can access them, and
125   * this user should have all permissions on this node. For
126   * /hbase/root-region-server, /hbase/master, and /hbase/hbaseid the
127   * permissions should be as above, but should also be world-readable. First
128   * we check the general case of /hbase nodes in the following test, and
129   * then check the subset of world-readable nodes in the three tests after
130   * that.
131   */
132  @Test
133  public void testHBaseRootZNodeACL() throws Exception {
134    if (!secureZKAvailable) {
135      return;
136    }
137
138    List<ACL> acls = zkw.getRecoverableZooKeeper().getZooKeeper()
139        .getACL("/hbase", new Stat());
140    assertEquals(1, acls.size());
141    assertEquals("sasl", acls.get(0).getId().getScheme());
142    assertEquals("hbase", acls.get(0).getId().getId());
143    assertEquals(ZooDefs.Perms.ALL, acls.get(0).getPerms());
144  }
145
146  /**
147   * When authentication is enabled on ZooKeeper, /hbase/root-region-server
148   * should be created with 2 ACLs: one specifies that the hbase user has
149   * full access to the node; the other, that it is world-readable.
150   */
151  @Test
152  public void testHBaseRootRegionServerZNodeACL() throws Exception {
153    if (!secureZKAvailable) {
154      return;
155    }
156
157    List<ACL> acls = zkw.getRecoverableZooKeeper().getZooKeeper()
158        .getACL("/hbase/root-region-server", new Stat());
159    assertEquals(2, acls.size());
160
161    boolean foundWorldReadableAcl = false;
162    boolean foundHBaseOwnerAcl = false;
163    for(int i = 0; i < 2; i++) {
164      if (acls.get(i).getId().getScheme().equals("world") == true) {
165        assertEquals("anyone", acls.get(0).getId().getId());
166        assertEquals(ZooDefs.Perms.READ, acls.get(0).getPerms());
167        foundWorldReadableAcl = true;
168      }
169      else {
170        if (acls.get(i).getId().getScheme().equals("sasl") == true) {
171          assertEquals("hbase", acls.get(1).getId().getId());
172          assertEquals("sasl", acls.get(1).getId().getScheme());
173          foundHBaseOwnerAcl = true;
174        } else { // error: should not get here: test fails.
175          assertTrue(false);
176        }
177      }
178    }
179    assertTrue(foundWorldReadableAcl);
180    assertTrue(foundHBaseOwnerAcl);
181  }
182
183  /**
184   * When authentication is enabled on ZooKeeper, /hbase/master should be
185   * created with 2 ACLs: one specifies that the hbase user has full access
186   * to the node; the other, that it is world-readable.
187   */
188  @Test
189  public void testHBaseMasterServerZNodeACL() throws Exception {
190    if (!secureZKAvailable) {
191      return;
192    }
193
194    List<ACL> acls = zkw.getRecoverableZooKeeper().getZooKeeper()
195        .getACL("/hbase/master", new Stat());
196    assertEquals(2, acls.size());
197
198    boolean foundWorldReadableAcl = false;
199    boolean foundHBaseOwnerAcl = false;
200    for(int i = 0; i < 2; i++) {
201      if (acls.get(i).getId().getScheme().equals("world") == true) {
202        assertEquals("anyone", acls.get(0).getId().getId());
203        assertEquals(ZooDefs.Perms.READ, acls.get(0).getPerms());
204        foundWorldReadableAcl = true;
205      } else {
206        if (acls.get(i).getId().getScheme().equals("sasl") == true) {
207          assertEquals("hbase", acls.get(1).getId().getId());
208          assertEquals("sasl", acls.get(1).getId().getScheme());
209          foundHBaseOwnerAcl = true;
210        } else { // error: should not get here: test fails.
211          assertTrue(false);
212        }
213      }
214    }
215    assertTrue(foundWorldReadableAcl);
216    assertTrue(foundHBaseOwnerAcl);
217  }
218
219  /**
220   * When authentication is enabled on ZooKeeper, /hbase/hbaseid should be
221   * created with 2 ACLs: one specifies that the hbase user has full access
222   * to the node; the other, that it is world-readable.
223   */
224  @Test
225  public void testHBaseIDZNodeACL() throws Exception {
226    if (!secureZKAvailable) {
227      return;
228    }
229
230    List<ACL> acls = zkw.getRecoverableZooKeeper().getZooKeeper()
231        .getACL("/hbase/hbaseid", new Stat());
232    assertEquals(2, acls.size());
233
234    boolean foundWorldReadableAcl = false;
235    boolean foundHBaseOwnerAcl = false;
236    for(int i = 0; i < 2; i++) {
237      if (acls.get(i).getId().getScheme().equals("world") == true) {
238        assertEquals("anyone", acls.get(0).getId().getId());
239        assertEquals(ZooDefs.Perms.READ, acls.get(0).getPerms());
240        foundWorldReadableAcl = true;
241      } else {
242        if (acls.get(i).getId().getScheme().equals("sasl") == true) {
243          assertEquals("hbase", acls.get(1).getId().getId());
244          assertEquals("sasl", acls.get(1).getId().getScheme());
245          foundHBaseOwnerAcl = true;
246        } else { // error: should not get here: test fails.
247          assertTrue(false);
248        }
249      }
250    }
251    assertTrue(foundWorldReadableAcl);
252    assertTrue(foundHBaseOwnerAcl);
253  }
254
255  /**
256   * Finally, we check the ACLs of a node outside of the /hbase hierarchy and
257   * verify that its ACL is simply 'hbase:Perms.ALL'.
258   */
259  @Test
260  public void testOutsideHBaseNodeACL() throws Exception {
261    if (!secureZKAvailable) {
262      return;
263    }
264
265    ZKUtil.createWithParents(zkw, "/testACLNode");
266    List<ACL> acls = zkw.getRecoverableZooKeeper().getZooKeeper()
267        .getACL("/testACLNode", new Stat());
268    assertEquals(1, acls.size());
269    assertEquals("sasl", acls.get(0).getId().getScheme());
270    assertEquals("hbase", acls.get(0).getId().getId());
271    assertEquals(ZooDefs.Perms.ALL, acls.get(0).getPerms());
272  }
273
274  /**
275   * Check if ZooKeeper JaasConfiguration is valid.
276   */
277  @Test
278  public void testIsZooKeeperSecure() throws Exception {
279    boolean testJaasConfig =
280        ZKAuthentication.isSecureZooKeeper(new Configuration(TEST_UTIL.getConfiguration()));
281    assertEquals(testJaasConfig, secureZKAvailable);
282    // Define Jaas configuration without ZooKeeper Jaas config
283    File saslConfFile = File.createTempFile("tmp", "fakeJaas.conf");
284    try (OutputStreamWriter fwriter = new OutputStreamWriter(
285          new FileOutputStream(saslConfFile), StandardCharsets.UTF_8)) {
286      fwriter.write("");
287    }
288
289    System.setProperty("java.security.auth.login.config",
290        saslConfFile.getAbsolutePath());
291
292    testJaasConfig = ZKAuthentication.isSecureZooKeeper(
293      new Configuration(TEST_UTIL.getConfiguration()));
294    assertFalse(testJaasConfig);
295    saslConfFile.delete();
296  }
297
298  /**
299   * Check if Programmatic way of setting zookeeper security settings is valid.
300   */
301  @Test
302  public void testIsZooKeeperSecureWithProgrammaticConfig() throws Exception {
303
304    javax.security.auth.login.Configuration.setConfiguration(new DummySecurityConfiguration());
305
306    Configuration config = new Configuration(HBaseConfiguration.create());
307    boolean testJaasConfig = ZKAuthentication.isSecureZooKeeper(config);
308    assertFalse(testJaasConfig);
309
310    // Now set authentication scheme to Kerberos still it should return false
311    // because no configuration set
312    config.set("hbase.security.authentication", "kerberos");
313    testJaasConfig = ZKAuthentication.isSecureZooKeeper(config);
314    assertFalse(testJaasConfig);
315
316    // Now set programmatic options related to security
317    config.set(HConstants.ZK_CLIENT_KEYTAB_FILE, "/dummy/file");
318    config.set(HConstants.ZK_CLIENT_KERBEROS_PRINCIPAL, "dummy");
319    config.set(HConstants.ZK_SERVER_KEYTAB_FILE, "/dummy/file");
320    config.set(HConstants.ZK_SERVER_KERBEROS_PRINCIPAL, "dummy");
321    testJaasConfig = ZKAuthentication.isSecureZooKeeper(config);
322    assertTrue(testJaasConfig);
323  }
324
325  private static class DummySecurityConfiguration extends javax.security.auth.login.Configuration {
326    @Override
327    public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
328      return null;
329    }
330  }
331
332  @Test
333  public void testAdminDrainAllowedOnSecureZK() throws Exception {
334    if (!secureZKAvailable) {
335      return;
336    }
337    List<ServerName> decommissionedServers = new ArrayList<>(1);
338    decommissionedServers.add(ServerName.parseServerName("ZZZ,123,123"));
339
340    // If unable to connect to secure ZK cluster then this operation would fail.
341    TEST_UTIL.getAdmin().decommissionRegionServers(decommissionedServers, false);
342
343    decommissionedServers = TEST_UTIL.getAdmin().listDecommissionedRegionServers();
344    assertEquals(1, decommissionedServers.size());
345    assertEquals(ServerName.parseServerName("ZZZ,123,123"), decommissionedServers.get(0));
346
347    TEST_UTIL.getAdmin().recommissionRegionServer(decommissionedServers.get(0), null);
348    decommissionedServers = TEST_UTIL.getAdmin().listDecommissionedRegionServers();
349    assertEquals(0, decommissionedServers.size());
350  }
351
352}
353