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.security.access;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.fail;
022
023import java.io.IOException;
024import java.util.Collections;
025import java.util.Optional;
026import java.util.regex.Pattern;
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.hbase.Coprocessor;
029import org.apache.hadoop.hbase.HBaseTestingUtil;
030import org.apache.hadoop.hbase.TableName;
031import org.apache.hadoop.hbase.TableNotEnabledException;
032import org.apache.hadoop.hbase.TableNotFoundException;
033import org.apache.hadoop.hbase.client.Admin;
034import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
035import org.apache.hadoop.hbase.client.Connection;
036import org.apache.hadoop.hbase.client.ConnectionFactory;
037import org.apache.hadoop.hbase.client.CoprocessorDescriptorBuilder;
038import org.apache.hadoop.hbase.client.Table;
039import org.apache.hadoop.hbase.client.TableDescriptor;
040import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
041import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
042import org.apache.hadoop.hbase.coprocessor.RegionCoprocessor;
043import org.apache.hadoop.hbase.coprocessor.RegionObserver;
044import org.apache.hadoop.hbase.testclassification.LargeTests;
045import org.apache.hadoop.hbase.testclassification.SecurityTests;
046import org.apache.hadoop.hbase.util.Bytes;
047import org.junit.jupiter.api.AfterEach;
048import org.junit.jupiter.api.Tag;
049import org.junit.jupiter.api.Test;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053/**
054 * Performs coprocessor loads for various paths and malformed strings
055 */
056@Tag(SecurityTests.TAG)
057@Tag(LargeTests.TAG)
058public class TestCoprocessorWhitelistMasterObserver extends SecureTestUtil {
059
060  private static final Logger LOG =
061    LoggerFactory.getLogger(TestCoprocessorWhitelistMasterObserver.class);
062  private static final HBaseTestingUtil UTIL = new HBaseTestingUtil();
063  private static final TableName TEST_TABLE = TableName.valueOf("testTable");
064  private static final byte[] TEST_FAMILY = Bytes.toBytes("fam1");
065
066  @AfterEach
067  public void tearDownTestCoprocessorWhitelistMasterObserver() throws Exception {
068    Admin admin = UTIL.getAdmin();
069    try {
070      try {
071        admin.disableTable(TEST_TABLE);
072      } catch (TableNotEnabledException ex) {
073        // Table was left disabled by test
074        LOG.info("Table was left disabled by test");
075      }
076      admin.deleteTable(TEST_TABLE);
077    } catch (TableNotFoundException ex) {
078      // Table was not created for some reason?
079      LOG.info("Table was not created for some reason");
080    }
081    UTIL.shutdownMiniCluster();
082  }
083
084  /**
085   * Test a table modification adding a coprocessor path which is not whitelisted.
086   * @exception Exception should be thrown and caught to show coprocessor is working as desired
087   * @param whitelistedPaths A String array of paths to add in for the whitelisting configuration
088   * @param coprocessorPath  A String to use as the path for a mock coprocessor
089   */
090  private static void positiveTestCase(String[] whitelistedPaths, String coprocessorPath)
091    throws Exception {
092    Configuration conf = UTIL.getConfiguration();
093    // load coprocessor under test
094    conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
095      CoprocessorWhitelistMasterObserver.class.getName());
096    conf.setStrings(CoprocessorWhitelistMasterObserver.CP_COPROCESSOR_WHITELIST_PATHS_KEY,
097      whitelistedPaths);
098    // set retries low to raise exception quickly
099    conf.setInt("hbase.client.retries.number", 5);
100    UTIL.startMiniCluster();
101    UTIL.createTable(TEST_TABLE, new byte[][] { TEST_FAMILY });
102    UTIL.waitUntilAllRegionsAssigned(TEST_TABLE);
103    Connection connection = ConnectionFactory.createConnection(conf);
104    Table t = connection.getTable(TEST_TABLE);
105    TableDescriptor htd = TableDescriptorBuilder.newBuilder(t.getDescriptor())
106      .setCoprocessor(
107        CoprocessorDescriptorBuilder.newBuilder("net.clayb.hbase.coprocessor.NotWhitelisted")
108          .setJarPath(coprocessorPath).setPriority(Coprocessor.PRIORITY_USER).build())
109      .build();
110    LOG.info("Modifying Table");
111    try {
112      connection.getAdmin().modifyTable(htd);
113      fail("Expected coprocessor to raise IOException");
114    } catch (IOException e) {
115      // swallow exception from coprocessor
116    }
117    LOG.info("Done Modifying Table");
118    assertEquals(0, t.getDescriptor().getCoprocessorDescriptors().size());
119  }
120
121  /**
122   * Test a table modification adding a coprocessor path which is whitelisted. The coprocessor
123   * should be added to the table descriptor successfully.
124   * @param whitelistedPaths A String array of paths to add in for the whitelisting configuration
125   * @param coprocessorPath  A String to use as the path for a mock coprocessor
126   */
127  private static void negativeTestCase(String[] whitelistedPaths, String coprocessorPath)
128    throws Exception {
129    Configuration conf = UTIL.getConfiguration();
130    conf.setInt("hbase.client.retries.number", 5);
131    // load coprocessor under test
132    conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
133      CoprocessorWhitelistMasterObserver.class.getName());
134    // set retries low to raise exception quickly
135    // set a coprocessor whitelist path for test
136    conf.setStrings(CoprocessorWhitelistMasterObserver.CP_COPROCESSOR_WHITELIST_PATHS_KEY,
137      whitelistedPaths);
138    UTIL.startMiniCluster();
139    UTIL.createTable(TEST_TABLE, new byte[][] { TEST_FAMILY });
140    UTIL.waitUntilAllRegionsAssigned(TEST_TABLE);
141    Connection connection = ConnectionFactory.createConnection(conf);
142    Admin admin = connection.getAdmin();
143    // disable table so we do not actually try loading non-existant
144    // coprocessor file
145    admin.disableTable(TEST_TABLE);
146    Table t = connection.getTable(TEST_TABLE);
147    TableDescriptor htd = TableDescriptorBuilder.newBuilder(t.getDescriptor())
148      .setCoprocessor(
149        CoprocessorDescriptorBuilder.newBuilder("net.clayb.hbase.coprocessor.Whitelisted")
150          .setJarPath(coprocessorPath).setPriority(Coprocessor.PRIORITY_USER).build())
151      .build();
152    LOG.info("Modifying Table");
153    admin.modifyTable(htd);
154    assertEquals(1, t.getDescriptor().getCoprocessorDescriptors().size());
155    LOG.info("Done Modifying Table");
156  }
157
158  /**
159   * Test a table modification adding a coprocessor path which is not whitelisted.
160   * @exception Exception should be thrown and caught to show coprocessor is working as desired
161   */
162  @Test
163  public void testSubstringNonWhitelisted() throws Exception {
164    positiveTestCase(new String[] { "/permitted/*" },
165      "file:///notpermitted/couldnotpossiblyexist.jar");
166  }
167
168  /**
169   * Test a table creation including a coprocessor path which is not whitelisted. Coprocessor should
170   * be added to table descriptor. Table is disabled to avoid an IOException due to the added
171   * coprocessor not actually existing on disk.
172   */
173  @Test
174  public void testDifferentFileSystemNonWhitelisted() throws Exception {
175    positiveTestCase(new String[] { "hdfs://foo/bar" },
176      "file:///notpermitted/couldnotpossiblyexist.jar");
177  }
178
179  /**
180   * Test a table modification adding a coprocessor path which is whitelisted. Coprocessor should be
181   * added to table descriptor. Table is disabled to avoid an IOException due to the added
182   * coprocessor not actually existing on disk.
183   */
184  @Test
185  public void testSchemeAndDirectorywhitelisted() throws Exception {
186    negativeTestCase(new String[] { "/tmp", "file:///permitted/*" },
187      "file:///permitted/couldnotpossiblyexist.jar");
188  }
189
190  /**
191   * Test a table modification adding a coprocessor path which is whitelisted. Coprocessor should be
192   * added to table descriptor. Table is disabled to avoid an IOException due to the added
193   * coprocessor not actually existing on disk.
194   */
195  @Test
196  public void testSchemeWhitelisted() throws Exception {
197    negativeTestCase(new String[] { "file:///" }, "file:///permitted/couldnotpossiblyexist.jar");
198  }
199
200  /**
201   * Test a table modification adding a coprocessor path which is whitelisted. Coprocessor should be
202   * added to table descriptor. Table is disabled to avoid an IOException due to the added
203   * coprocessor not actually existing on disk.
204   */
205  @Test
206  public void testDFSNameWhitelistedWorks() throws Exception {
207    negativeTestCase(new String[] { "hdfs://Your-FileSystem" },
208      "hdfs://Your-FileSystem/permitted/couldnotpossiblyexist.jar");
209  }
210
211  /**
212   * Test a table modification adding a coprocessor path which is whitelisted. Coprocessor should be
213   * added to table descriptor. Table is disabled to avoid an IOException due to the added
214   * coprocessor not actually existing on disk.
215   */
216  @Test
217  public void testDFSNameNotWhitelistedFails() throws Exception {
218    positiveTestCase(new String[] { "hdfs://Your-FileSystem" },
219      "hdfs://My-FileSystem/permitted/couldnotpossiblyexist.jar");
220  }
221
222  /**
223   * Test a table modification adding a coprocessor path which is whitelisted. Coprocessor should be
224   * added to table descriptor. Table is disabled to avoid an IOException due to the added
225   * coprocessor not actually existing on disk.
226   */
227  @Test
228  public void testBlanketWhitelist() throws Exception {
229    negativeTestCase(new String[] { "*" }, "hdfs:///permitted/couldnotpossiblyexist.jar");
230  }
231
232  /**
233   * Test a table creation including a coprocessor path which is not whitelisted. Table will not be
234   * created due to the offending coprocessor.
235   */
236  @Test
237  public void testCreationNonWhitelistedCoprocessorPath() throws Exception {
238    Configuration conf = UTIL.getConfiguration();
239    // load coprocessor under test
240    conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
241      CoprocessorWhitelistMasterObserver.class.getName());
242    conf.setStrings(CoprocessorWhitelistMasterObserver.CP_COPROCESSOR_WHITELIST_PATHS_KEY,
243      new String[] {});
244    // set retries low to raise exception quickly
245    conf.setInt("hbase.client.retries.number", 5);
246    UTIL.startMiniCluster();
247    TableDescriptor tableDescriptor = TableDescriptorBuilder.newBuilder(TEST_TABLE)
248      .setColumnFamily(ColumnFamilyDescriptorBuilder.of(TEST_FAMILY))
249      .setCoprocessor(
250        CoprocessorDescriptorBuilder.newBuilder("net.clayb.hbase.coprocessor.NotWhitelisted")
251          .setJarPath("file:///notpermitted/couldnotpossiblyexist.jar")
252          .setPriority(Coprocessor.PRIORITY_USER).setProperties(Collections.emptyMap()).build())
253      .build();
254    Connection connection = ConnectionFactory.createConnection(conf);
255    Admin admin = connection.getAdmin();
256    LOG.info("Creating Table");
257    try {
258      admin.createTable(tableDescriptor);
259      fail("Expected coprocessor to raise IOException");
260    } catch (IOException e) {
261      // swallow exception from coprocessor
262    }
263    LOG.info("Done Creating Table");
264    // ensure table was not created
265    assertEquals(0,
266      admin.listTableDescriptors(Pattern.compile("^" + TEST_TABLE.getNameAsString() + "$")).size());
267  }
268
269  public static class TestRegionObserver implements RegionCoprocessor, RegionObserver {
270    @Override
271    public Optional<RegionObserver> getRegionObserver() {
272      return Optional.of(this);
273    }
274
275  }
276
277  /**
278   * Test a table creation including a coprocessor path which is on the classpath. Table will be
279   * created with the coprocessor.
280   */
281  @Test
282  public void testCreationClasspathCoprocessor() throws Exception {
283    Configuration conf = UTIL.getConfiguration();
284    // load coprocessor under test
285    conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
286      CoprocessorWhitelistMasterObserver.class.getName());
287    conf.setStrings(CoprocessorWhitelistMasterObserver.CP_COPROCESSOR_WHITELIST_PATHS_KEY,
288      new String[] {});
289    // set retries low to raise exception quickly
290    conf.setInt("hbase.client.retries.number", 5);
291    UTIL.startMiniCluster();
292    TableDescriptor tableDescriptor = TableDescriptorBuilder.newBuilder(TEST_TABLE)
293      .setColumnFamily(ColumnFamilyDescriptorBuilder.of(TEST_FAMILY))
294      .setCoprocessor(TestRegionObserver.class.getName()).build();
295    Connection connection = ConnectionFactory.createConnection(conf);
296    Admin admin = connection.getAdmin();
297    LOG.info("Creating Table");
298    admin.createTable(tableDescriptor);
299    // ensure table was created and coprocessor is added to table
300    LOG.info("Done Creating Table");
301    Table t = connection.getTable(TEST_TABLE);
302    assertEquals(1, t.getDescriptor().getCoprocessorDescriptors().size());
303  }
304}