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