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