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