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}