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}