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