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 = new HBaseTestingUtil();
062
063  private static ZKWatcher zkw;
064  private static boolean secureZKAvailable;
065
066  @BeforeClass
067  public static void setUpBeforeClass() throws Exception {
068    File saslConfFile = File.createTempFile("tmp", "jaas.conf");
069    try (OutputStreamWriter fwriter =
070      new OutputStreamWriter(new FileOutputStream(saslConfFile), StandardCharsets.UTF_8)) {
071      fwriter.write("Server {\n" + "org.apache.zookeeper.server.auth.DigestLoginModule required\n"
072        + "user_hbase=\"secret\";\n" + "};\n" + "Client {\n"
073        + "org.apache.zookeeper.server.auth.DigestLoginModule required\n" + "username=\"hbase\"\n"
074        + "password=\"secret\";\n" + "};" + "\n");
075    }
076    System.setProperty("java.security.auth.login.config", saslConfFile.getAbsolutePath());
077    System.setProperty("zookeeper.authProvider.1",
078      "org.apache.zookeeper.server.auth.SASLAuthenticationProvider");
079
080    TEST_UTIL.getConfiguration().setInt("hbase.zookeeper.property.maxClientCnxns", 1000);
081
082    // If Hadoop is missing HADOOP-7070 the cluster will fail to start due to
083    // the JAAS configuration required by ZK being clobbered by Hadoop
084    try {
085      TEST_UTIL.startMiniCluster();
086    } catch (IOException e) {
087      LOG.warn("Hadoop is missing HADOOP-7070", e);
088      secureZKAvailable = false;
089      return;
090    }
091    zkw = new ZKWatcher(new Configuration(TEST_UTIL.getConfiguration()),
092      TestZooKeeper.class.getName(), null);
093  }
094
095  @AfterClass
096  public static void tearDownAfterClass() throws Exception {
097    if (!secureZKAvailable) {
098      return;
099    }
100    TEST_UTIL.shutdownMiniCluster();
101  }
102
103  @Before
104  public void setUp() throws Exception {
105    if (!secureZKAvailable) {
106      return;
107    }
108    TEST_UTIL.ensureSomeRegionServersAvailable(2);
109  }
110
111  /**
112   * Create a node and check its ACL. When authentication is enabled on ZooKeeper, all nodes (except
113   * /hbase/root-region-server, /hbase/master and /hbase/hbaseid) should be created so that only the
114   * hbase server user (master or region server user) that created them can access them, and this
115   * user should have all permissions on this node. For /hbase/root-region-server, /hbase/master,
116   * and /hbase/hbaseid the permissions should be as above, but should also be world-readable. First
117   * we check the general case of /hbase nodes in the following test, and then check the subset of
118   * world-readable nodes in the three tests after that.
119   */
120  @Test
121  public void testHBaseRootZNodeACL() throws Exception {
122    if (!secureZKAvailable) {
123      return;
124    }
125
126    List<ACL> acls = zkw.getRecoverableZooKeeper().getZooKeeper().getACL("/hbase", new Stat());
127    assertEquals(1, acls.size());
128    assertEquals("sasl", acls.get(0).getId().getScheme());
129    assertEquals("hbase", acls.get(0).getId().getId());
130    assertEquals(ZooDefs.Perms.ALL, acls.get(0).getPerms());
131  }
132
133  /**
134   * When authentication is enabled on ZooKeeper, /hbase/root-region-server should be created with 2
135   * ACLs: one specifies that the hbase user has full access to the node; the other, that it is
136   * world-readable.
137   */
138  @Test
139  public void testHBaseRootRegionServerZNodeACL() throws Exception {
140    if (!secureZKAvailable) {
141      return;
142    }
143
144    List<ACL> acls =
145      zkw.getRecoverableZooKeeper().getZooKeeper().getACL("/hbase/root-region-server", new Stat());
146    assertEquals(2, acls.size());
147
148    boolean foundWorldReadableAcl = false;
149    boolean foundHBaseOwnerAcl = false;
150    for (int i = 0; i < 2; i++) {
151      if (acls.get(i).getId().getScheme().equals("world") == true) {
152        assertEquals("anyone", acls.get(0).getId().getId());
153        assertEquals(ZooDefs.Perms.READ, acls.get(0).getPerms());
154        foundWorldReadableAcl = true;
155      } else {
156        if (acls.get(i).getId().getScheme().equals("sasl") == true) {
157          assertEquals("hbase", acls.get(1).getId().getId());
158          assertEquals("sasl", acls.get(1).getId().getScheme());
159          foundHBaseOwnerAcl = true;
160        } else { // error: should not get here: test fails.
161          assertTrue(false);
162        }
163      }
164    }
165    assertTrue(foundWorldReadableAcl);
166    assertTrue(foundHBaseOwnerAcl);
167  }
168
169  /**
170   * When authentication is enabled on ZooKeeper, /hbase/master should be created with 2 ACLs: one
171   * specifies that the hbase user has full access to the node; the other, that it is
172   * world-readable.
173   */
174  @Test
175  public void testHBaseMasterServerZNodeACL() throws Exception {
176    if (!secureZKAvailable) {
177      return;
178    }
179
180    List<ACL> acls =
181      zkw.getRecoverableZooKeeper().getZooKeeper().getACL("/hbase/master", new Stat());
182    assertEquals(2, acls.size());
183
184    boolean foundWorldReadableAcl = false;
185    boolean foundHBaseOwnerAcl = false;
186    for (int i = 0; i < 2; i++) {
187      if (acls.get(i).getId().getScheme().equals("world") == true) {
188        assertEquals("anyone", acls.get(0).getId().getId());
189        assertEquals(ZooDefs.Perms.READ, acls.get(0).getPerms());
190        foundWorldReadableAcl = true;
191      } else {
192        if (acls.get(i).getId().getScheme().equals("sasl") == true) {
193          assertEquals("hbase", acls.get(1).getId().getId());
194          assertEquals("sasl", acls.get(1).getId().getScheme());
195          foundHBaseOwnerAcl = true;
196        } else { // error: should not get here: test fails.
197          assertTrue(false);
198        }
199      }
200    }
201    assertTrue(foundWorldReadableAcl);
202    assertTrue(foundHBaseOwnerAcl);
203  }
204
205  /**
206   * When authentication is enabled on ZooKeeper, /hbase/hbaseid should be created with 2 ACLs: one
207   * specifies that the hbase user has full access to the node; the other, that it is
208   * world-readable.
209   */
210  @Test
211  public void testHBaseIDZNodeACL() throws Exception {
212    if (!secureZKAvailable) {
213      return;
214    }
215
216    List<ACL> acls =
217      zkw.getRecoverableZooKeeper().getZooKeeper().getACL("/hbase/hbaseid", new Stat());
218    assertEquals(2, acls.size());
219
220    boolean foundWorldReadableAcl = false;
221    boolean foundHBaseOwnerAcl = false;
222    for (int i = 0; i < 2; i++) {
223      if (acls.get(i).getId().getScheme().equals("world") == true) {
224        assertEquals("anyone", acls.get(0).getId().getId());
225        assertEquals(ZooDefs.Perms.READ, acls.get(0).getPerms());
226        foundWorldReadableAcl = true;
227      } else {
228        if (acls.get(i).getId().getScheme().equals("sasl") == true) {
229          assertEquals("hbase", acls.get(1).getId().getId());
230          assertEquals("sasl", acls.get(1).getId().getScheme());
231          foundHBaseOwnerAcl = true;
232        } else { // error: should not get here: test fails.
233          assertTrue(false);
234        }
235      }
236    }
237    assertTrue(foundWorldReadableAcl);
238    assertTrue(foundHBaseOwnerAcl);
239  }
240
241  /**
242   * Finally, we check the ACLs of a node outside of the /hbase hierarchy and verify that its ACL is
243   * simply 'hbase:Perms.ALL'.
244   */
245  @Test
246  public void testOutsideHBaseNodeACL() throws Exception {
247    if (!secureZKAvailable) {
248      return;
249    }
250
251    ZKUtil.createWithParents(zkw, "/testACLNode");
252    List<ACL> acls =
253      zkw.getRecoverableZooKeeper().getZooKeeper().getACL("/testACLNode", new Stat());
254    assertEquals(1, acls.size());
255    assertEquals("sasl", acls.get(0).getId().getScheme());
256    assertEquals("hbase", acls.get(0).getId().getId());
257    assertEquals(ZooDefs.Perms.ALL, acls.get(0).getPerms());
258  }
259
260  /**
261   * Check if ZooKeeper JaasConfiguration is valid.
262   */
263  @Test
264  public void testIsZooKeeperSecure() throws Exception {
265    boolean testJaasConfig =
266      ZKAuthentication.isSecureZooKeeper(new Configuration(TEST_UTIL.getConfiguration()));
267    assertEquals(testJaasConfig, secureZKAvailable);
268    // Define Jaas configuration without ZooKeeper Jaas config
269    File saslConfFile = File.createTempFile("tmp", "fakeJaas.conf");
270    try (OutputStreamWriter fwriter =
271      new OutputStreamWriter(new FileOutputStream(saslConfFile), StandardCharsets.UTF_8)) {
272      fwriter.write("");
273    }
274
275    System.setProperty("java.security.auth.login.config", saslConfFile.getAbsolutePath());
276
277    testJaasConfig =
278      ZKAuthentication.isSecureZooKeeper(new Configuration(TEST_UTIL.getConfiguration()));
279    assertFalse(testJaasConfig);
280    saslConfFile.delete();
281  }
282
283  /**
284   * Check if Programmatic way of setting zookeeper security settings is valid.
285   */
286  @Test
287  public void testIsZooKeeperSecureWithProgrammaticConfig() throws Exception {
288
289    javax.security.auth.login.Configuration.setConfiguration(new DummySecurityConfiguration());
290
291    Configuration config = new Configuration(HBaseConfiguration.create());
292    boolean testJaasConfig = ZKAuthentication.isSecureZooKeeper(config);
293    assertFalse(testJaasConfig);
294
295    // Now set authentication scheme to Kerberos still it should return false
296    // because no configuration set
297    config.set("hbase.security.authentication", "kerberos");
298    testJaasConfig = ZKAuthentication.isSecureZooKeeper(config);
299    assertFalse(testJaasConfig);
300
301    // Now set programmatic options related to security
302    config.set(HConstants.ZK_CLIENT_KEYTAB_FILE, "/dummy/file");
303    config.set(HConstants.ZK_CLIENT_KERBEROS_PRINCIPAL, "dummy");
304    config.set(HConstants.ZK_SERVER_KEYTAB_FILE, "/dummy/file");
305    config.set(HConstants.ZK_SERVER_KERBEROS_PRINCIPAL, "dummy");
306    testJaasConfig = ZKAuthentication.isSecureZooKeeper(config);
307    assertTrue(testJaasConfig);
308  }
309
310  private static class DummySecurityConfiguration extends javax.security.auth.login.Configuration {
311    @Override
312    public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
313      return null;
314    }
315  }
316
317  @Test
318  public void testAdminDrainAllowedOnSecureZK() throws Exception {
319    if (!secureZKAvailable) {
320      return;
321    }
322    List<ServerName> decommissionedServers = new ArrayList<>(1);
323    decommissionedServers.add(ServerName.parseServerName("ZZZ,123,123"));
324
325    // If unable to connect to secure ZK cluster then this operation would fail.
326    TEST_UTIL.getAdmin().decommissionRegionServers(decommissionedServers, false);
327
328    decommissionedServers = TEST_UTIL.getAdmin().listDecommissionedRegionServers();
329    assertEquals(1, decommissionedServers.size());
330    assertEquals(ServerName.parseServerName("ZZZ,123,123"), decommissionedServers.get(0));
331
332    TEST_UTIL.getAdmin().recommissionRegionServer(decommissionedServers.get(0), null);
333    decommissionedServers = TEST_UTIL.getAdmin().listDecommissionedRegionServers();
334    assertEquals(0, decommissionedServers.size());
335  }
336
337}