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