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;
021
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import org.apache.hadoop.conf.Configuration;
026import org.apache.hadoop.hbase.AuthUtil;
027import org.apache.hadoop.hbase.Cell;
028import org.apache.hadoop.hbase.Coprocessor;
029import org.apache.hadoop.hbase.HBaseClassTestRule;
030import org.apache.hadoop.hbase.HBaseTestingUtility;
031import org.apache.hadoop.hbase.HColumnDescriptor;
032import org.apache.hadoop.hbase.HConstants;
033import org.apache.hadoop.hbase.HTableDescriptor;
034import org.apache.hadoop.hbase.TableNotFoundException;
035import org.apache.hadoop.hbase.TestTableName;
036import org.apache.hadoop.hbase.client.Admin;
037import org.apache.hadoop.hbase.client.Connection;
038import org.apache.hadoop.hbase.client.ConnectionFactory;
039import org.apache.hadoop.hbase.client.Delete;
040import org.apache.hadoop.hbase.client.Get;
041import org.apache.hadoop.hbase.client.Increment;
042import org.apache.hadoop.hbase.client.Put;
043import org.apache.hadoop.hbase.client.Result;
044import org.apache.hadoop.hbase.client.ResultScanner;
045import org.apache.hadoop.hbase.client.Scan;
046import org.apache.hadoop.hbase.client.Table;
047import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
048import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost;
049import org.apache.hadoop.hbase.security.User;
050import org.apache.hadoop.hbase.security.access.Permission.Action;
051import org.apache.hadoop.hbase.testclassification.LargeTests;
052import org.apache.hadoop.hbase.testclassification.SecurityTests;
053import org.apache.hadoop.hbase.util.Bytes;
054import org.apache.hadoop.hbase.util.Threads;
055import org.junit.After;
056import org.junit.AfterClass;
057import org.junit.Before;
058import org.junit.BeforeClass;
059import org.junit.ClassRule;
060import org.junit.Rule;
061import org.junit.Test;
062import org.junit.experimental.categories.Category;
063import org.slf4j.Logger;
064import org.slf4j.LoggerFactory;
065
066import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
067
068@Category({SecurityTests.class, LargeTests.class})
069public class TestCellACLs extends SecureTestUtil {
070
071  @ClassRule
072  public static final HBaseClassTestRule CLASS_RULE =
073      HBaseClassTestRule.forClass(TestCellACLs.class);
074
075  private static final Logger LOG = LoggerFactory.getLogger(TestCellACLs.class);
076
077  @Rule
078  public TestTableName TEST_TABLE = new TestTableName();
079  private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
080  private static final byte[] TEST_FAMILY = Bytes.toBytes("f1");
081  private static final byte[] TEST_ROW = Bytes.toBytes("cellpermtest");
082  private static final byte[] TEST_Q1 = Bytes.toBytes("q1");
083  private static final byte[] TEST_Q2 = Bytes.toBytes("q2");
084  private static final byte[] TEST_Q3 = Bytes.toBytes("q3");
085  private static final byte[] TEST_Q4 = Bytes.toBytes("q4");
086  private static final byte[] ZERO = Bytes.toBytes(0L);
087  private static final byte[] ONE = Bytes.toBytes(1L);
088
089  private static Configuration conf;
090
091  private static final String GROUP = "group";
092  private static User GROUP_USER;
093  private static User USER_OWNER;
094  private static User USER_OTHER;
095  private static String[] usersAndGroups;
096
097  @BeforeClass
098  public static void setupBeforeClass() throws Exception {
099    // setup configuration
100    conf = TEST_UTIL.getConfiguration();
101    conf.setInt(HConstants.REGION_SERVER_HIGH_PRIORITY_HANDLER_COUNT, 10);
102    // Enable security
103    enableSecurity(conf);
104    // Verify enableSecurity sets up what we require
105    verifyConfiguration(conf);
106
107    // We expect 0.98 cell ACL semantics
108    conf.setBoolean(AccessControlConstants.CF_ATTRIBUTE_EARLY_OUT, false);
109
110    TEST_UTIL.startMiniCluster();
111    MasterCoprocessorHost cpHost = TEST_UTIL.getMiniHBaseCluster().getMaster()
112        .getMasterCoprocessorHost();
113    cpHost.load(AccessController.class, Coprocessor.PRIORITY_HIGHEST, conf);
114    AccessController ac = cpHost.findCoprocessor(AccessController.class);
115    cpHost.createEnvironment(ac, Coprocessor.PRIORITY_HIGHEST, 1, conf);
116    RegionServerCoprocessorHost rsHost = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0)
117        .getRegionServerCoprocessorHost();
118    rsHost.createEnvironment(ac, Coprocessor.PRIORITY_HIGHEST, 1, conf);
119
120    // Wait for the ACL table to become available
121    TEST_UTIL.waitTableEnabled(AccessControlLists.ACL_TABLE_NAME);
122
123    // create a set of test users
124    USER_OWNER = User.createUserForTesting(conf, "owner", new String[0]);
125    USER_OTHER = User.createUserForTesting(conf, "other", new String[0]);
126    GROUP_USER = User.createUserForTesting(conf, "group_user", new String[] { GROUP });
127
128    usersAndGroups = new String[] { USER_OTHER.getShortName(), AuthUtil.toGroupEntry(GROUP) };
129  }
130
131  @AfterClass
132  public static void tearDownAfterClass() throws Exception {
133    TEST_UTIL.shutdownMiniCluster();
134  }
135
136  @Before
137  public void setUp() throws Exception {
138    // Create the test table (owner added to the _acl_ table)
139    Admin admin = TEST_UTIL.getAdmin();
140    HTableDescriptor htd = new HTableDescriptor(TEST_TABLE.getTableName());
141    HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAMILY);
142    hcd.setMaxVersions(4);
143    htd.setOwner(USER_OWNER);
144    htd.addFamily(hcd);
145    admin.createTable(htd, new byte[][] { Bytes.toBytes("s") });
146    TEST_UTIL.waitTableEnabled(TEST_TABLE.getTableName());
147    LOG.info("Sleeping a second because of HBASE-12581");
148    Threads.sleep(1000);
149  }
150
151  @Test
152  public void testCellPermissions() throws Exception {
153    // store two sets of values, one store with a cell level ACL, and one without
154    verifyAllowed(new AccessTestAction() {
155      @Override
156      public Object run() throws Exception {
157        try(Connection connection = ConnectionFactory.createConnection(conf);
158            Table t = connection.getTable(TEST_TABLE.getTableName())) {
159          Put p;
160          // with ro ACL
161          p = new Put(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q1, ZERO);
162          p.setACL(prepareCellPermissions(usersAndGroups, Action.READ));
163          t.put(p);
164          // with rw ACL
165          p = new Put(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q2, ZERO);
166          p.setACL(prepareCellPermissions(usersAndGroups, Action.READ, Action.WRITE));
167          t.put(p);
168          // no ACL
169          p = new Put(TEST_ROW)
170              .addColumn(TEST_FAMILY, TEST_Q3, ZERO)
171              .addColumn(TEST_FAMILY, TEST_Q4, ZERO);
172          t.put(p);
173        }
174        return null;
175      }
176    }, USER_OWNER);
177
178    /* ---- Gets ---- */
179
180    AccessTestAction getQ1 = new AccessTestAction() {
181      @Override
182      public Object run() throws Exception {
183        Get get = new Get(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q1);
184        try(Connection connection = ConnectionFactory.createConnection(conf);
185            Table t = connection.getTable(TEST_TABLE.getTableName())) {
186          return t.get(get).listCells();
187        }
188      }
189    };
190
191    AccessTestAction getQ2 = new AccessTestAction() {
192      @Override
193      public Object run() throws Exception {
194        Get get = new Get(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q2);
195        try(Connection connection = ConnectionFactory.createConnection(conf);
196            Table t = connection.getTable(TEST_TABLE.getTableName())) {
197          return t.get(get).listCells();
198        }
199      }
200    };
201
202    AccessTestAction getQ3 = new AccessTestAction() {
203      @Override
204      public Object run() throws Exception {
205        Get get = new Get(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q3);
206        try(Connection connection = ConnectionFactory.createConnection(conf);
207            Table t = connection.getTable(TEST_TABLE.getTableName())) {
208          return t.get(get).listCells();
209        }
210      }
211    };
212
213    AccessTestAction getQ4 = new AccessTestAction() {
214      @Override
215      public Object run() throws Exception {
216        Get get = new Get(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q4);
217        try(Connection connection = ConnectionFactory.createConnection(conf);
218            Table t = connection.getTable(TEST_TABLE.getTableName())) {
219          return t.get(get).listCells();
220        }
221      }
222    };
223
224    // Confirm special read access set at cell level
225
226    verifyAllowed(getQ1, USER_OTHER, GROUP_USER);
227    verifyAllowed(getQ2, USER_OTHER, GROUP_USER);
228
229    // Confirm this access does not extend to other cells
230
231    verifyIfNull(getQ3, USER_OTHER, GROUP_USER);
232    verifyIfNull(getQ4, USER_OTHER, GROUP_USER);
233
234    /* ---- Scans ---- */
235
236    // check that a scan over the test data returns the expected number of KVs
237
238    final List<Cell> scanResults = Lists.newArrayList();
239
240    AccessTestAction scanAction = new AccessTestAction() {
241      @Override
242      public List<Cell> run() throws Exception {
243        Scan scan = new Scan();
244        scan.setStartRow(TEST_ROW);
245        scan.setStopRow(Bytes.add(TEST_ROW, new byte[]{ 0 } ));
246        scan.addFamily(TEST_FAMILY);
247        Connection connection = ConnectionFactory.createConnection(conf);
248        Table t = connection.getTable(TEST_TABLE.getTableName());
249        try {
250          ResultScanner scanner = t.getScanner(scan);
251          Result result = null;
252          do {
253            result = scanner.next();
254            if (result != null) {
255              scanResults.addAll(result.listCells());
256            }
257          } while (result != null);
258        } finally {
259          t.close();
260          connection.close();
261        }
262        return scanResults;
263      }
264    };
265
266    // owner will see all values
267    scanResults.clear();
268    verifyAllowed(scanAction, USER_OWNER);
269    assertEquals(4, scanResults.size());
270
271    // other user will see 2 values
272    scanResults.clear();
273    verifyAllowed(scanAction, USER_OTHER);
274    assertEquals(2, scanResults.size());
275
276    scanResults.clear();
277    verifyAllowed(scanAction, GROUP_USER);
278    assertEquals(2, scanResults.size());
279
280    /* ---- Increments ---- */
281
282    AccessTestAction incrementQ1 = new AccessTestAction() {
283      @Override
284      public Object run() throws Exception {
285        Increment i = new Increment(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q1, 1L);
286        try(Connection connection = ConnectionFactory.createConnection(conf);
287            Table t = connection.getTable(TEST_TABLE.getTableName())) {
288          t.increment(i);
289        }
290        return null;
291      }
292    };
293
294    AccessTestAction incrementQ2 = new AccessTestAction() {
295      @Override
296      public Object run() throws Exception {
297        Increment i = new Increment(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q2, 1L);
298        try(Connection connection = ConnectionFactory.createConnection(conf);
299            Table t = connection.getTable(TEST_TABLE.getTableName())) {
300          t.increment(i);
301        }
302        return null;
303      }
304    };
305
306    AccessTestAction incrementQ2newDenyACL = new AccessTestAction() {
307      @Override
308      public Object run() throws Exception {
309        Increment i = new Increment(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q2, 1L);
310        // Tag this increment with an ACL that denies write permissions to USER_OTHER and GROUP
311        i.setACL(prepareCellPermissions(usersAndGroups, Action.READ));
312        try(Connection connection = ConnectionFactory.createConnection(conf);
313            Table t = connection.getTable(TEST_TABLE.getTableName())) {
314          t.increment(i);
315        }
316        return null;
317      }
318    };
319
320    AccessTestAction incrementQ3 = new AccessTestAction() {
321      @Override
322      public Object run() throws Exception {
323        Increment i = new Increment(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q3, 1L);
324        try(Connection connection = ConnectionFactory.createConnection(conf);
325            Table t = connection.getTable(TEST_TABLE.getTableName())) {
326          t.increment(i);
327        }
328        return null;
329      }
330    };
331
332    verifyDenied(incrementQ1, USER_OTHER, GROUP_USER);
333    verifyDenied(incrementQ3, USER_OTHER, GROUP_USER);
334
335    // We should be able to increment until the permissions are revoked (including the action in
336    // which permissions are revoked, the previous ACL will be carried forward)
337    verifyAllowed(incrementQ2, USER_OTHER, GROUP_USER);
338    verifyAllowed(incrementQ2newDenyACL, USER_OTHER);
339    // But not again after we denied ourselves write permission with an ACL
340    // update
341    verifyDenied(incrementQ2, USER_OTHER, GROUP_USER);
342
343    /* ---- Deletes ---- */
344
345    AccessTestAction deleteFamily = new AccessTestAction() {
346      @Override
347      public Object run() throws Exception {
348        Delete delete = new Delete(TEST_ROW).addFamily(TEST_FAMILY);
349        try(Connection connection = ConnectionFactory.createConnection(conf);
350            Table t = connection.getTable(TEST_TABLE.getTableName())) {
351          t.delete(delete);
352        }
353        return null;
354      }
355    };
356
357    AccessTestAction deleteQ1 = new AccessTestAction() {
358      @Override
359      public Object run() throws Exception {
360        Delete delete = new Delete(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q1);
361        try(Connection connection = ConnectionFactory.createConnection(conf);
362            Table t = connection.getTable(TEST_TABLE.getTableName())) {
363          t.delete(delete);
364        }
365        return null;
366      }
367    };
368
369    verifyDenied(deleteFamily, USER_OTHER, GROUP_USER);
370    verifyDenied(deleteQ1, USER_OTHER, GROUP_USER);
371    verifyAllowed(deleteQ1, USER_OWNER);
372  }
373
374  /**
375   * Insure we are not granting access in the absence of any cells found
376   * when scanning for covered cells.
377   */
378  @Test
379  public void testCoveringCheck() throws Exception {
380    // Grant read access to USER_OTHER
381    grantOnTable(TEST_UTIL, USER_OTHER.getShortName(), TEST_TABLE.getTableName(), TEST_FAMILY,
382      null, Action.READ);
383    // Grant read access to GROUP
384    grantOnTable(TEST_UTIL, AuthUtil.toGroupEntry(GROUP), TEST_TABLE.getTableName(), TEST_FAMILY,
385      null, Action.READ);
386
387    // A write by USER_OTHER should be denied.
388    // This is where we could have a big problem if there is an error in the
389    // covering check logic.
390    verifyUserDeniedForWrite(USER_OTHER, ZERO);
391    // A write by GROUP_USER from group GROUP should be denied.
392    verifyUserDeniedForWrite(GROUP_USER, ZERO);
393
394    // Add the cell
395    verifyAllowed(new AccessTestAction() {
396      @Override
397      public Object run() throws Exception {
398        try(Connection connection = ConnectionFactory.createConnection(conf);
399            Table t = connection.getTable(TEST_TABLE.getTableName())) {
400          Put p;
401          p = new Put(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q1, ZERO);
402          t.put(p);
403        }
404        return null;
405      }
406    }, USER_OWNER);
407
408    // A write by USER_OTHER should still be denied, just to make sure
409    verifyUserDeniedForWrite(USER_OTHER, ONE);
410    // A write by GROUP_USER from group GROUP should still be denied
411    verifyUserDeniedForWrite(GROUP_USER, ONE);
412
413    // A read by USER_OTHER should be allowed, just to make sure
414    verifyUserAllowedForRead(USER_OTHER);
415    // A read by GROUP_USER from group GROUP should be allowed
416    verifyUserAllowedForRead(GROUP_USER);
417  }
418
419  private void verifyUserDeniedForWrite(final User user, final byte[] value) throws Exception {
420    verifyDenied(new AccessTestAction() {
421      @Override
422      public Object run() throws Exception {
423        try (Connection connection = ConnectionFactory.createConnection(conf);
424            Table t = connection.getTable(TEST_TABLE.getTableName())) {
425          Put p;
426          p = new Put(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q1, value);
427          t.put(p);
428        }
429        return null;
430      }
431    }, user);
432  }
433
434  private void verifyUserAllowedForRead(final User user) throws Exception {
435    verifyAllowed(new AccessTestAction() {
436      @Override
437      public Object run() throws Exception {
438        try (Connection connection = ConnectionFactory.createConnection(conf);
439            Table t = connection.getTable(TEST_TABLE.getTableName())) {
440          return t.get(new Get(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q1));
441        }
442      }
443    }, user);
444  }
445
446  private Map<String, Permission> prepareCellPermissions(String[] users, Action... action) {
447    Map<String, Permission> perms = new HashMap<>(2);
448    for (String user : users) {
449      perms.put(user, new Permission(action));
450    }
451    return perms;
452  }
453
454  @After
455  public void tearDown() throws Exception {
456    // Clean the _acl_ table
457    try {
458      TEST_UTIL.deleteTable(TEST_TABLE.getTableName());
459    } catch (TableNotFoundException ex) {
460      // Test deleted the table, no problem
461      LOG.info("Test deleted table " + TEST_TABLE.getTableName());
462    }
463    assertEquals(0, AccessControlLists.getTablePermissions(conf, TEST_TABLE.getTableName()).size());
464  }
465}