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.apache.hadoop.hbase.AuthUtil.toGroupEntry; 021import static org.junit.jupiter.api.Assertions.assertArrayEquals; 022import static org.junit.jupiter.api.Assertions.assertFalse; 023import static org.junit.jupiter.api.Assertions.assertTrue; 024import static org.junit.jupiter.api.Assertions.fail; 025 026import java.io.IOException; 027import java.security.PrivilegedExceptionAction; 028import java.util.Collections; 029import java.util.HashMap; 030import org.apache.hadoop.conf.Configuration; 031import org.apache.hadoop.hbase.AuthUtil; 032import org.apache.hadoop.hbase.Cell; 033import org.apache.hadoop.hbase.CellUtil; 034import org.apache.hadoop.hbase.HBaseTestingUtil; 035import org.apache.hadoop.hbase.ServerName; 036import org.apache.hadoop.hbase.TableName; 037import org.apache.hadoop.hbase.client.Admin; 038import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 039import org.apache.hadoop.hbase.client.Connection; 040import org.apache.hadoop.hbase.client.ConnectionFactory; 041import org.apache.hadoop.hbase.client.Get; 042import org.apache.hadoop.hbase.client.Put; 043import org.apache.hadoop.hbase.client.Result; 044import org.apache.hadoop.hbase.client.Table; 045import org.apache.hadoop.hbase.client.TableDescriptor; 046import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 047import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; 048import org.apache.hadoop.hbase.coprocessor.MasterCoprocessor; 049import org.apache.hadoop.hbase.coprocessor.RegionServerCoprocessor; 050import org.apache.hadoop.hbase.ipc.TestProtobufRpcServiceImpl; 051import org.apache.hadoop.hbase.security.AccessDeniedException; 052import org.apache.hadoop.hbase.security.User; 053import org.apache.hadoop.hbase.testclassification.MediumTests; 054import org.apache.hadoop.hbase.testclassification.SecurityTests; 055import org.apache.hadoop.hbase.util.Bytes; 056import org.junit.jupiter.api.BeforeAll; 057import org.junit.jupiter.api.Tag; 058import org.junit.jupiter.api.Test; 059import org.junit.jupiter.api.TestInfo; 060 061import org.apache.hbase.thirdparty.com.google.protobuf.Service; 062import org.apache.hbase.thirdparty.com.google.protobuf.ServiceException; 063 064import org.apache.hadoop.hbase.shaded.ipc.protobuf.generated.TestProtos; 065import org.apache.hadoop.hbase.shaded.ipc.protobuf.generated.TestRpcServiceProtos; 066 067/** 068 * This class tests operations in MasterRpcServices which require ADMIN access. It doesn't test all 069 * operations which require ADMIN access, only those which get vetted within MasterRpcServices at 070 * the point of entry itself (unlike old approach of using hooks in AccessController). Sidenote: 071 * There is one big difference between how security tests for AccessController hooks work, and how 072 * the tests in this class for security in MasterRpcServices work. The difference arises because of 073 * the way AC & MasterRpcServices get the user. In AccessController, it first checks if there is an 074 * active rpc user in ObserverContext. If not, it uses UserProvider for current user. This *might* 075 * make sense in the context of coprocessors, because they can be called outside the context of 076 * RPCs. But in the context of MasterRpcServices, only one way makes sense - 077 * RPCServer.getRequestUser(). In AC tests, when we do FooUser.runAs on AccessController instance 078 * directly, it bypasses the rpc framework completely, but works because UserProvider provides the 079 * correct user, i.e. FooUser in this case. But this doesn't work for the tests here, so we go 080 * around by doing complete RPCs. 081 */ 082@Tag(SecurityTests.TAG) 083@Tag(MediumTests.TAG) 084public class TestRpcAccessChecks { 085 private final static HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 086 private static Configuration conf; 087 088 // user granted with all global permission 089 private static User USER_ADMIN; 090 // user without admin permissions 091 private static User USER_NON_ADMIN; 092 // user in supergroup 093 private static User USER_IN_SUPERGROUPS; 094 // user with global permission but not a superuser 095 private static User USER_ADMIN_NOT_SUPER; 096 097 private static final String GROUP_ADMIN = "admin_group"; 098 private static User USER_GROUP_ADMIN; 099 100 // Dummy service to test execService calls. Needs to be public so can be loaded as Coprocessor. 101 public static class DummyCpService implements MasterCoprocessor, RegionServerCoprocessor { 102 public DummyCpService() { 103 } 104 105 @Override 106 public Iterable<Service> getServices() { 107 return Collections.singleton(TestRpcServiceProtos.TestProtobufRpcProto 108 .newReflectiveService(new TestProtobufRpcServiceImpl())); 109 } 110 } 111 112 private static void enableSecurity(Configuration conf) throws IOException { 113 conf.set("hadoop.security.authorization", "false"); 114 conf.set("hadoop.security.authentication", "simple"); 115 conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, 116 AccessController.class.getName() + "," + DummyCpService.class.getName()); 117 conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, AccessController.class.getName()); 118 conf.set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY, 119 AccessController.class.getName() + "," + DummyCpService.class.getName()); 120 conf.set(User.HBASE_SECURITY_AUTHORIZATION_CONF_KEY, "true"); 121 SecureTestUtil.configureSuperuser(conf); 122 } 123 124 @BeforeAll 125 public static void setup() throws Exception { 126 conf = TEST_UTIL.getConfiguration(); 127 128 // Enable security 129 enableSecurity(conf); 130 131 // Create users 132 // admin is superuser as well. 133 USER_ADMIN = User.createUserForTesting(conf, "admin", new String[0]); 134 USER_NON_ADMIN = User.createUserForTesting(conf, "non_admin", new String[0]); 135 USER_GROUP_ADMIN = 136 User.createUserForTesting(conf, "user_group_admin", new String[] { GROUP_ADMIN }); 137 USER_IN_SUPERGROUPS = 138 User.createUserForTesting(conf, "user_in_supergroup", new String[] { "supergroup" }); 139 USER_ADMIN_NOT_SUPER = User.createUserForTesting(conf, "normal_admin", new String[0]); 140 141 TEST_UTIL.startMiniCluster(); 142 // Wait for the ACL table to become available 143 TEST_UTIL.waitUntilAllRegionsAssigned(PermissionStorage.ACL_TABLE_NAME); 144 145 // Assign permissions to groups 146 SecureTestUtil.grantGlobal(TEST_UTIL, toGroupEntry(GROUP_ADMIN), Permission.Action.ADMIN, 147 Permission.Action.CREATE); 148 SecureTestUtil.grantGlobal(TEST_UTIL, USER_ADMIN_NOT_SUPER.getShortName(), 149 Permission.Action.ADMIN); 150 } 151 152 interface Action { 153 void run(Admin admin) throws Exception; 154 } 155 156 private void verifyAllowed(User user, Action action) throws Exception { 157 user.runAs((PrivilegedExceptionAction<?>) () -> { 158 try (Connection conn = ConnectionFactory.createConnection(conf); 159 Admin admin = conn.getAdmin()) { 160 action.run(admin); 161 } catch (IOException e) { 162 fail(e.toString()); 163 } 164 return null; 165 }); 166 } 167 168 private void verifyDenied(User user, Action action) throws Exception { 169 user.runAs((PrivilegedExceptionAction<?>) () -> { 170 boolean accessDenied = false; 171 try (Connection conn = ConnectionFactory.createConnection(conf); 172 Admin admin = conn.getAdmin()) { 173 action.run(admin); 174 } catch (AccessDeniedException e) { 175 accessDenied = true; 176 } 177 assertTrue(accessDenied, "Expected access to be denied"); 178 return null; 179 }); 180 } 181 182 private void verifiedDeniedServiceException(User user, Action action) throws Exception { 183 user.runAs((PrivilegedExceptionAction<?>) () -> { 184 boolean accessDenied = false; 185 try (Connection conn = ConnectionFactory.createConnection(conf); 186 Admin admin = conn.getAdmin()) { 187 action.run(admin); 188 } catch (ServiceException e) { 189 // For MasterRpcServices.execService. 190 if (e.getCause() instanceof AccessDeniedException) { 191 accessDenied = true; 192 } 193 } 194 assertTrue(accessDenied, "Expected access to be denied"); 195 return null; 196 }); 197 198 } 199 200 private void verifyAdminCheckForAction(Action action) throws Exception { 201 verifyAllowed(USER_ADMIN, action); 202 verifyAllowed(USER_GROUP_ADMIN, action); 203 verifyDenied(USER_NON_ADMIN, action); 204 } 205 206 @Test 207 public void testEnableCatalogJanitor() throws Exception { 208 verifyAdminCheckForAction((admin) -> admin.catalogJanitorSwitch(true)); 209 } 210 211 @Test 212 public void testRunCatalogJanitor() throws Exception { 213 verifyAdminCheckForAction((admin) -> admin.runCatalogJanitor()); 214 } 215 216 @Test 217 public void testCleanerChoreRunning() throws Exception { 218 verifyAdminCheckForAction((admin) -> admin.cleanerChoreSwitch(true)); 219 } 220 221 @Test 222 public void testRunCleanerChore() throws Exception { 223 verifyAdminCheckForAction((admin) -> admin.runCleanerChore()); 224 } 225 226 @Test 227 public void testExecProcedure() throws Exception { 228 verifyAdminCheckForAction((admin) -> { 229 // Using existing table instead of creating a new one. 230 admin.execProcedure("flush-table-proc", TableName.META_TABLE_NAME.getNameAsString(), 231 new HashMap<>()); 232 }); 233 } 234 235 @Test 236 public void testExecService() throws Exception { 237 Action action = (admin) -> { 238 TestRpcServiceProtos.TestProtobufRpcProto.BlockingInterface service = 239 TestRpcServiceProtos.TestProtobufRpcProto.newBlockingStub(admin.coprocessorService()); 240 service.ping(null, TestProtos.EmptyRequestProto.getDefaultInstance()); 241 }; 242 243 verifyAllowed(USER_ADMIN, action); 244 verifyAllowed(USER_GROUP_ADMIN, action); 245 // This is same as above verifyAccessDenied 246 verifiedDeniedServiceException(USER_NON_ADMIN, action); 247 } 248 249 @Test 250 public void testExecProcedureWithRet() throws Exception { 251 verifyAdminCheckForAction((admin) -> { 252 // Using existing table instead of creating a new one. 253 admin.execProcedureWithReturn("flush-table-proc", TableName.META_TABLE_NAME.getNameAsString(), 254 new HashMap<>()); 255 }); 256 } 257 258 @Test 259 public void testNormalize() throws Exception { 260 verifyAdminCheckForAction((admin) -> admin.normalize()); 261 } 262 263 @Test 264 public void testSetNormalizerRunning() throws Exception { 265 verifyAdminCheckForAction((admin) -> admin.normalizerSwitch(true)); 266 } 267 268 @Test 269 public void testExecRegionServerService() throws Exception { 270 Action action = (admin) -> { 271 ServerName serverName = TEST_UTIL.getHBaseCluster().getRegionServer(0).getServerName(); 272 TestRpcServiceProtos.TestProtobufRpcProto.BlockingInterface service = 273 TestRpcServiceProtos.TestProtobufRpcProto 274 .newBlockingStub(admin.coprocessorService(serverName)); 275 service.ping(null, TestProtos.EmptyRequestProto.getDefaultInstance()); 276 }; 277 278 verifyAllowed(USER_ADMIN, action); 279 verifyAllowed(USER_GROUP_ADMIN, action); 280 verifiedDeniedServiceException(USER_NON_ADMIN, action); 281 } 282 283 @Test 284 public void testTableFlush(TestInfo testInfo) throws Exception { 285 TableName tn = TableName.valueOf(testInfo.getTestMethod().get().getName()); 286 TableDescriptor desc = TableDescriptorBuilder.newBuilder(tn) 287 .setColumnFamily(ColumnFamilyDescriptorBuilder.of("f1")).build(); 288 Action adminAction = (admin) -> { 289 admin.createTable(desc); 290 // Avoid giving a global permission which may screw up other tests 291 SecureTestUtil.grantOnTable(TEST_UTIL, USER_NON_ADMIN.getShortName(), tn, null, null, 292 Permission.Action.READ, Permission.Action.WRITE, Permission.Action.CREATE); 293 }; 294 verifyAllowed(USER_ADMIN, adminAction); 295 296 Action userAction = (admin) -> { 297 Connection conn = admin.getConnection(); 298 final byte[] rowKey = Bytes.toBytes("row1"); 299 final byte[] col = Bytes.toBytes("q1"); 300 final byte[] val = Bytes.toBytes("v1"); 301 try (Table table = conn.getTable(tn)) { 302 // Write a value 303 Put p = new Put(rowKey); 304 p.addColumn(Bytes.toBytes("f1"), col, val); 305 table.put(p); 306 // Flush should not require ADMIN permission 307 admin.flush(tn); 308 // Nb: ideally, we would verify snapshot permission too (as that was fixed in the 309 // regression HBASE-20185) but taking a snapshot requires ADMIN permission which 310 // masks the root issue. 311 // Make sure we read the value 312 Result result = table.get(new Get(rowKey)); 313 assertFalse(result.isEmpty()); 314 Cell c = result.getColumnLatestCell(Bytes.toBytes("f1"), col); 315 assertArrayEquals(val, CellUtil.cloneValue(c)); 316 } 317 }; 318 verifyAllowed(USER_NON_ADMIN, userAction); 319 } 320 321 @Test 322 public void testTableFlushAndSnapshot(TestInfo testInfo) throws Exception { 323 TableName tn = TableName.valueOf(testInfo.getTestMethod().get().getName()); 324 TableDescriptor desc = TableDescriptorBuilder.newBuilder(tn) 325 .setColumnFamily(ColumnFamilyDescriptorBuilder.of("f1")).build(); 326 Action adminAction = (admin) -> { 327 admin.createTable(desc); 328 // Giving ADMIN here, but only on this table, *not* globally 329 SecureTestUtil.grantOnTable(TEST_UTIL, USER_NON_ADMIN.getShortName(), tn, null, null, 330 Permission.Action.READ, Permission.Action.WRITE, Permission.Action.CREATE, 331 Permission.Action.ADMIN); 332 }; 333 verifyAllowed(USER_ADMIN, adminAction); 334 335 Action userAction = (admin) -> { 336 Connection conn = admin.getConnection(); 337 final byte[] rowKey = Bytes.toBytes("row1"); 338 final byte[] col = Bytes.toBytes("q1"); 339 final byte[] val = Bytes.toBytes("v1"); 340 try (Table table = conn.getTable(tn)) { 341 // Write a value 342 Put p = new Put(rowKey); 343 p.addColumn(Bytes.toBytes("f1"), col, val); 344 table.put(p); 345 // Flush should not require ADMIN permission 346 admin.flush(tn); 347 // Table admin should be sufficient to snapshot this table 348 admin.snapshot(tn.getNameAsString() + "_snapshot1", tn); 349 // Read the value just because 350 Result result = table.get(new Get(rowKey)); 351 assertFalse(result.isEmpty()); 352 Cell c = result.getColumnLatestCell(Bytes.toBytes("f1"), col); 353 assertArrayEquals(val, CellUtil.cloneValue(c)); 354 } 355 }; 356 verifyAllowed(USER_NON_ADMIN, userAction); 357 } 358 359 @Test 360 public void testGrantDeniedOnSuperUsersGroups(TestInfo testInfo) { 361 /** User */ 362 try { 363 // Global 364 SecureTestUtil.grantGlobal(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getShortName(), 365 Permission.Action.ADMIN, Permission.Action.CREATE); 366 fail("Granting superuser's global permissions is not allowed."); 367 } catch (Exception e) { 368 } 369 try { 370 // Namespace 371 SecureTestUtil.grantOnNamespace(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getShortName(), 372 testInfo.getTestMethod().get().getName(), Permission.Action.ADMIN, 373 Permission.Action.CREATE); 374 fail("Granting superuser's namespace permissions is not allowed."); 375 } catch (Exception e) { 376 } 377 try { 378 // Table 379 SecureTestUtil.grantOnTable(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getName(), 380 TableName.valueOf(testInfo.getTestMethod().get().getName()), null, null, 381 Permission.Action.ADMIN, Permission.Action.CREATE); 382 fail("Granting superuser's table permissions is not allowed."); 383 } catch (Exception e) { 384 } 385 386 /** Group */ 387 try { 388 SecureTestUtil.grantGlobal(USER_ADMIN_NOT_SUPER, TEST_UTIL, 389 USER_IN_SUPERGROUPS.getShortName(), Permission.Action.ADMIN, Permission.Action.CREATE); 390 fail("Granting superuser's global permissions is not allowed."); 391 } catch (Exception e) { 392 } 393 } 394 395 @Test 396 public void testRevokeDeniedOnSuperUsersGroups(TestInfo testInfo) { 397 /** User */ 398 try { 399 // Global 400 SecureTestUtil.revokeGlobal(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getShortName(), 401 Permission.Action.ADMIN); 402 fail("Revoking superuser's global permissions is not allowed."); 403 } catch (Exception e) { 404 } 405 try { 406 // Namespace 407 SecureTestUtil.revokeFromNamespace(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getShortName(), 408 testInfo.getTestMethod().get().getName(), Permission.Action.ADMIN); 409 fail("Revoking superuser's namespace permissions is not allowed."); 410 } catch (Exception e) { 411 } 412 try { 413 // Table 414 SecureTestUtil.revokeFromTable(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getName(), 415 TableName.valueOf(testInfo.getTestMethod().get().getName()), null, null, 416 Permission.Action.ADMIN); 417 fail("Revoking superuser's table permissions is not allowed."); 418 } catch (Exception e) { 419 } 420 421 /** Group */ 422 try { 423 // Global revoke 424 SecureTestUtil.revokeGlobal(USER_ADMIN_NOT_SUPER, TEST_UTIL, 425 AuthUtil.toGroupEntry("supergroup"), Permission.Action.ADMIN, Permission.Action.CREATE); 426 fail("Revoking supergroup's permissions is not allowed."); 427 } catch (Exception e) { 428 } 429 } 430}