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
088   * which is not whitelisted
089   * @result An IOException should be thrown and caught
090   *         to show coprocessor is working as desired
091   * @param whitelistedPaths A String array of paths to add in
092   *         for the whitelisting configuration
093   * @param coprocessorPath A String to use as the
094   *         path for a mock coprocessor
095   */
096  private static void positiveTestCase(String[] whitelistedPaths,
097      String coprocessorPath) throws Exception {
098    Configuration conf = UTIL.getConfiguration();
099    // load coprocessor under test
100    conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
101        CoprocessorWhitelistMasterObserver.class.getName());
102    conf.setStrings(
103        CoprocessorWhitelistMasterObserver.CP_COPROCESSOR_WHITELIST_PATHS_KEY,
104        whitelistedPaths);
105    // set retries low to raise exception quickly
106    conf.setInt("hbase.client.retries.number", 5);
107    UTIL.startMiniCluster();
108    UTIL.createTable(TEST_TABLE, new byte[][] { TEST_FAMILY });
109    UTIL.waitUntilAllRegionsAssigned(TEST_TABLE);
110    Connection connection = ConnectionFactory.createConnection(conf);
111    Table t = connection.getTable(TEST_TABLE);
112    HTableDescriptor htd = new HTableDescriptor(t.getTableDescriptor());
113    htd.addCoprocessor("net.clayb.hbase.coprocessor.NotWhitelisted",
114      new Path(coprocessorPath),
115      Coprocessor.PRIORITY_USER, null);
116    LOG.info("Modifying Table");
117    try {
118      connection.getAdmin().modifyTable(TEST_TABLE, htd);
119      fail("Expected coprocessor to raise IOException");
120    } catch (IOException e) {
121      // swallow exception from coprocessor
122    }
123    LOG.info("Done Modifying Table");
124    assertEquals(0, t.getTableDescriptor().getCoprocessors().size());
125  }
126
127  /**
128   * Test a table modification adding a coprocessor path
129   * which is whitelisted
130   * @result The coprocessor should be added to the table
131   *         descriptor successfully
132   * @param whitelistedPaths A String array of paths to add in
133   *         for the whitelisting configuration
134   * @param coprocessorPath A String to use as the
135   *         path for a mock coprocessor
136   */
137  private static void negativeTestCase(String[] whitelistedPaths,
138      String coprocessorPath) throws Exception {
139    Configuration conf = UTIL.getConfiguration();
140    conf.setInt("hbase.client.retries.number", 5);
141    // load coprocessor under test
142    conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
143        CoprocessorWhitelistMasterObserver.class.getName());
144    // set retries low to raise exception quickly
145    // set a coprocessor whitelist path for test
146    conf.setStrings(
147        CoprocessorWhitelistMasterObserver.CP_COPROCESSOR_WHITELIST_PATHS_KEY,
148        whitelistedPaths);
149    UTIL.startMiniCluster();
150    UTIL.createTable(TEST_TABLE, new byte[][] { TEST_FAMILY });
151    UTIL.waitUntilAllRegionsAssigned(TEST_TABLE);
152    Connection connection = ConnectionFactory.createConnection(conf);
153    Admin admin = connection.getAdmin();
154    // disable table so we do not actually try loading non-existant
155    // coprocessor file
156    admin.disableTable(TEST_TABLE);
157    Table t = connection.getTable(TEST_TABLE);
158    HTableDescriptor htd = new HTableDescriptor(t.getTableDescriptor());
159    htd.addCoprocessor("net.clayb.hbase.coprocessor.Whitelisted",
160      new Path(coprocessorPath),
161      Coprocessor.PRIORITY_USER, null);
162    LOG.info("Modifying Table");
163    admin.modifyTable(TEST_TABLE, htd);
164    assertEquals(1, t.getTableDescriptor().getCoprocessors().size());
165    LOG.info("Done Modifying Table");
166  }
167
168  /**
169   * Test a table modification adding a coprocessor path
170   * which is not whitelisted
171   * @result An IOException should be thrown and caught
172   *         to show coprocessor is working as desired
173   */
174  @Test
175  public void testSubstringNonWhitelisted() throws Exception {
176    positiveTestCase(new String[]{"/permitted/*"},
177        "file:///notpermitted/couldnotpossiblyexist.jar");
178  }
179
180  /**
181   * Test a table creation including a coprocessor path
182   * which is not whitelisted
183   * @result Coprocessor should be added to table descriptor
184   *         Table is disabled to avoid an IOException due to
185   *         the added coprocessor not actually existing on disk
186   */
187  @Test
188  public void testDifferentFileSystemNonWhitelisted() throws Exception {
189    positiveTestCase(new String[]{"hdfs://foo/bar"},
190        "file:///notpermitted/couldnotpossiblyexist.jar");
191  }
192
193  /**
194   * Test a table modification adding a coprocessor path
195   * which is whitelisted
196   * @result Coprocessor should be added to table descriptor
197   *         Table is disabled to avoid an IOException due to
198   *         the added coprocessor not actually existing on disk
199   */
200  @Test
201  public void testSchemeAndDirectorywhitelisted() throws Exception {
202    negativeTestCase(new String[]{"/tmp","file:///permitted/*"},
203        "file:///permitted/couldnotpossiblyexist.jar");
204  }
205
206  /**
207   * Test a table modification adding a coprocessor path
208   * which is whitelisted
209   * @result Coprocessor should be added to table descriptor
210   *         Table is disabled to avoid an IOException due to
211   *         the added coprocessor not actually existing on disk
212   */
213  @Test
214  public void testSchemeWhitelisted() throws Exception {
215    negativeTestCase(new String[]{"file:///"},
216        "file:///permitted/couldnotpossiblyexist.jar");
217  }
218
219  /**
220   * Test a table modification adding a coprocessor path
221   * which is whitelisted
222   * @result Coprocessor should be added to table descriptor
223   *         Table is disabled to avoid an IOException due to
224   *         the added coprocessor not actually existing on disk
225   */
226  @Test
227  public void testDFSNameWhitelistedWorks() throws Exception {
228    negativeTestCase(new String[]{"hdfs://Your-FileSystem"},
229        "hdfs://Your-FileSystem/permitted/couldnotpossiblyexist.jar");
230  }
231
232  /**
233   * Test a table modification adding a coprocessor path
234   * which is whitelisted
235   * @result Coprocessor should be added to table descriptor
236   *         Table is disabled to avoid an IOException due to
237   *         the added coprocessor not actually existing on disk
238   */
239  @Test
240  public void testDFSNameNotWhitelistedFails() throws Exception {
241    positiveTestCase(new String[]{"hdfs://Your-FileSystem"},
242        "hdfs://My-FileSystem/permitted/couldnotpossiblyexist.jar");
243  }
244
245  /**
246   * Test a table modification adding a coprocessor path
247   * which is whitelisted
248   * @result Coprocessor should be added to table descriptor
249   *         Table is disabled to avoid an IOException due to
250   *         the added coprocessor not actually existing on disk
251   */
252  @Test
253  public void testBlanketWhitelist() throws Exception {
254    negativeTestCase(new String[]{"*"},
255        "hdfs:///permitted/couldnotpossiblyexist.jar");
256  }
257
258  /**
259   * Test a table creation including a coprocessor path
260   * which is not whitelisted
261   * @result Table will not be created due to the offending coprocessor
262   */
263  @Test
264  public void testCreationNonWhitelistedCoprocessorPath() throws Exception {
265    Configuration conf = UTIL.getConfiguration();
266    // load coprocessor under test
267    conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
268        CoprocessorWhitelistMasterObserver.class.getName());
269    conf.setStrings(CoprocessorWhitelistMasterObserver.CP_COPROCESSOR_WHITELIST_PATHS_KEY,
270        new String[]{});
271    // set retries low to raise exception quickly
272    conf.setInt("hbase.client.retries.number", 5);
273    UTIL.startMiniCluster();
274    HTableDescriptor htd = new HTableDescriptor(TEST_TABLE);
275    HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAMILY);
276    htd.addFamily(hcd);
277    htd.addCoprocessor("net.clayb.hbase.coprocessor.NotWhitelisted",
278      new Path("file:///notpermitted/couldnotpossiblyexist.jar"),
279      Coprocessor.PRIORITY_USER, null);
280    Connection connection = ConnectionFactory.createConnection(conf);
281    Admin admin = connection.getAdmin();
282    LOG.info("Creating Table");
283    try {
284      admin.createTable(htd);
285      fail("Expected coprocessor to raise IOException");
286    } catch (IOException e) {
287      // swallow exception from coprocessor
288    }
289    LOG.info("Done Creating Table");
290    // ensure table was not created
291    assertEquals(new HTableDescriptor[0],
292      admin.listTables("^" + TEST_TABLE.getNameAsString() + "$"));
293  }
294
295  public static class TestRegionObserver implements RegionCoprocessor, RegionObserver {
296    @Override
297    public Optional<RegionObserver> getRegionObserver() {
298      return Optional.of(this);
299    }
300
301  }
302
303  /**
304   * Test a table creation including a coprocessor path
305   * which is on the classpath
306   * @result Table will be created with the coprocessor
307   */
308  @Test
309  public void testCreationClasspathCoprocessor() throws Exception {
310    Configuration conf = UTIL.getConfiguration();
311    // load coprocessor under test
312    conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
313        CoprocessorWhitelistMasterObserver.class.getName());
314    conf.setStrings(CoprocessorWhitelistMasterObserver.CP_COPROCESSOR_WHITELIST_PATHS_KEY,
315        new String[]{});
316    // set retries low to raise exception quickly
317    conf.setInt("hbase.client.retries.number", 5);
318    UTIL.startMiniCluster();
319    HTableDescriptor htd = new HTableDescriptor(TEST_TABLE);
320    HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAMILY);
321    htd.addFamily(hcd);
322    htd.addCoprocessor(TestRegionObserver.class.getName());
323    Connection connection = ConnectionFactory.createConnection(conf);
324    Admin admin = connection.getAdmin();
325    LOG.info("Creating Table");
326    admin.createTable(htd);
327    // ensure table was created and coprocessor is added to table
328    LOG.info("Done Creating Table");
329    Table t = connection.getTable(TEST_TABLE);
330    assertEquals(1, t.getTableDescriptor().getCoprocessors().size());
331  }
332}