001 002/** 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * or more contributor license agreements. See the NOTICE file 005 * distributed with this work for additional information 006 * regarding copyright ownership. The ASF licenses this file 007 * to you under the Apache License, Version 2.0 (the 008 * "License"); you may not use this file except in compliance 009 * with the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 */ 019package org.apache.hadoop.hbase.security.access; 020 021import static org.apache.hadoop.hbase.AuthUtil.toGroupEntry; 022import static org.junit.Assert.assertArrayEquals; 023import static org.junit.Assert.assertFalse; 024import static org.junit.Assert.assertTrue; 025import static org.junit.Assert.fail; 026import static org.mockito.Mockito.mock; 027 028import com.google.protobuf.Service; 029import com.google.protobuf.ServiceException; 030import java.io.IOException; 031import java.security.PrivilegedExceptionAction; 032import java.util.Collections; 033import java.util.HashMap; 034import org.apache.hadoop.conf.Configuration; 035import org.apache.hadoop.hbase.Cell; 036import org.apache.hadoop.hbase.CellUtil; 037import org.apache.hadoop.hbase.HBaseClassTestRule; 038import org.apache.hadoop.hbase.HBaseTestingUtility; 039import org.apache.hadoop.hbase.ServerName; 040import org.apache.hadoop.hbase.TableName; 041import org.apache.hadoop.hbase.client.Admin; 042import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 043import org.apache.hadoop.hbase.client.Connection; 044import org.apache.hadoop.hbase.client.ConnectionFactory; 045import org.apache.hadoop.hbase.client.Get; 046import org.apache.hadoop.hbase.client.Put; 047import org.apache.hadoop.hbase.client.Result; 048import org.apache.hadoop.hbase.client.Table; 049import org.apache.hadoop.hbase.client.TableDescriptor; 050import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 051import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; 052import org.apache.hadoop.hbase.coprocessor.MasterCoprocessor; 053import org.apache.hadoop.hbase.coprocessor.RegionServerCoprocessor; 054import org.apache.hadoop.hbase.ipc.protobuf.generated.TestProtos; 055import org.apache.hadoop.hbase.ipc.protobuf.generated.TestRpcServiceProtos; 056import org.apache.hadoop.hbase.security.AccessDeniedException; 057import org.apache.hadoop.hbase.security.User; 058import org.apache.hadoop.hbase.testclassification.MediumTests; 059import org.apache.hadoop.hbase.testclassification.SecurityTests; 060import org.apache.hadoop.hbase.util.Bytes; 061import org.junit.BeforeClass; 062import org.junit.ClassRule; 063import org.junit.Rule; 064import org.junit.Test; 065import org.junit.experimental.categories.Category; 066import org.junit.rules.TestName; 067 068/** 069 * This class tests operations in MasterRpcServices which require ADMIN access. 070 * It doesn't test all operations which require ADMIN access, only those which get vetted within 071 * MasterRpcServices at the point of entry itself (unlike old approach of using 072 * hooks in AccessController). 073 * 074 * Sidenote: 075 * There is one big difference between how security tests for AccessController hooks work, and how 076 * the tests in this class for security in MasterRpcServices work. 077 * The difference arises because of the way AC & MasterRpcServices get the user. 078 * 079 * In AccessController, it first checks if there is an active rpc user in ObserverContext. If not, 080 * it uses UserProvider for current user. This *might* make sense in the context of coprocessors, 081 * because they can be called outside the context of RPCs. 082 * But in the context of MasterRpcServices, only one way makes sense - RPCServer.getRequestUser(). 083 * 084 * In AC tests, when we do FooUser.runAs on AccessController instance directly, it bypasses 085 * the rpc framework completely, but works because UserProvider provides the correct user, i.e. 086 * FooUser in this case. 087 * 088 * But this doesn't work for the tests here, so we go around by doing complete RPCs. 089 */ 090@Category({SecurityTests.class, MediumTests.class}) 091public class TestRpcAccessChecks { 092 @ClassRule 093 public static final HBaseClassTestRule CLASS_RULE = 094 HBaseClassTestRule.forClass(TestRpcAccessChecks.class); 095 096 @Rule 097 public final TestName TEST_NAME = new TestName(); 098 099 private final static HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); 100 private static Configuration conf; 101 102 // user granted with all global permission 103 private static User USER_ADMIN; 104 // user without admin permissions 105 private static User USER_NON_ADMIN; 106 107 private static final String GROUP_ADMIN = "admin_group"; 108 private static User USER_GROUP_ADMIN; 109 110 // Dummy service to test execService calls. Needs to be public so can be loaded as Coprocessor. 111 public static class DummyCpService implements MasterCoprocessor, RegionServerCoprocessor { 112 public DummyCpService() {} 113 114 @Override 115 public Iterable<Service> getServices() { 116 return Collections.singleton(mock(TestRpcServiceProtos.TestProtobufRpcProto.class)); 117 } 118 } 119 120 private static void enableSecurity(Configuration conf) throws IOException { 121 conf.set("hadoop.security.authorization", "false"); 122 conf.set("hadoop.security.authentication", "simple"); 123 conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, AccessController.class.getName() + 124 "," + DummyCpService.class.getName()); 125 conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, AccessController.class.getName()); 126 conf.set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY, AccessController.class.getName() + 127 "," + DummyCpService.class.getName()); 128 conf.set(User.HBASE_SECURITY_AUTHORIZATION_CONF_KEY, "true"); 129 SecureTestUtil.configureSuperuser(conf); 130 } 131 132 @BeforeClass 133 public static void setup() throws Exception { 134 conf = TEST_UTIL.getConfiguration(); 135 136 // Enable security 137 enableSecurity(conf); 138 TEST_UTIL.startMiniCluster(); 139 140 // Wait for the ACL table to become available 141 TEST_UTIL.waitUntilAllRegionsAssigned(AccessControlLists.ACL_TABLE_NAME); 142 143 // Create users 144 USER_ADMIN = User.createUserForTesting(conf, "admin", new String[0]); 145 USER_NON_ADMIN = User.createUserForTesting(conf, "non_admin", new String[0]); 146 USER_GROUP_ADMIN = 147 User.createUserForTesting(conf, "user_group_admin", new String[] { GROUP_ADMIN }); 148 149 // Assign permissions to users and groups 150 SecureTestUtil.grantGlobal(TEST_UTIL, USER_ADMIN.getShortName(), Permission.Action.ADMIN); 151 SecureTestUtil.grantGlobal(TEST_UTIL, toGroupEntry(GROUP_ADMIN), Permission.Action.ADMIN); 152 // No permissions to USER_NON_ADMIN 153 } 154 155 interface Action { 156 void run(Admin admin) throws Exception; 157 } 158 159 private void verifyAllowed(User user, Action action) throws Exception { 160 user.runAs((PrivilegedExceptionAction<?>) () -> { 161 try (Connection conn = ConnectionFactory.createConnection(conf); 162 Admin admin = conn.getAdmin()) { 163 action.run(admin); 164 } catch (IOException e) { 165 fail(e.toString()); 166 } 167 return null; 168 }); 169 } 170 171 private void verifyDenied(User user, Action action) throws Exception { 172 user.runAs((PrivilegedExceptionAction<?>) () -> { 173 boolean accessDenied = false; 174 try (Connection conn = ConnectionFactory.createConnection(conf); 175 Admin admin = conn.getAdmin()) { 176 action.run(admin); 177 } catch (AccessDeniedException e) { 178 accessDenied = true; 179 } 180 assertTrue("Expected access to be denied", accessDenied); 181 return null; 182 }); 183 } 184 185 private void verifiedDeniedServiceException(User user, Action action) throws Exception { 186 user.runAs((PrivilegedExceptionAction<?>) () -> { 187 boolean accessDenied = false; 188 try (Connection conn = ConnectionFactory.createConnection(conf); 189 Admin admin = conn.getAdmin()) { 190 action.run(admin); 191 } catch (ServiceException e) { 192 // For MasterRpcServices.execService. 193 if (e.getCause() instanceof AccessDeniedException) { 194 accessDenied = true; 195 } 196 } 197 assertTrue("Expected access to be denied", accessDenied); 198 return null; 199 }); 200 201 } 202 203 private void verifyAdminCheckForAction(Action action) throws Exception { 204 verifyAllowed(USER_ADMIN, action); 205 verifyAllowed(USER_GROUP_ADMIN, action); 206 verifyDenied(USER_NON_ADMIN, action); 207 } 208 209 @Test 210 public void testEnableCatalogJanitor() throws Exception { 211 verifyAdminCheckForAction((admin) -> admin.enableCatalogJanitor(true)); 212 } 213 214 @Test 215 public void testRunCatalogJanitor() throws Exception { 216 verifyAdminCheckForAction((admin) -> admin.runCatalogJanitor()); 217 } 218 219 @Test 220 public void testCleanerChoreRunning() throws Exception { 221 verifyAdminCheckForAction((admin) -> admin.cleanerChoreSwitch(true)); 222 } 223 224 @Test 225 public void testRunCleanerChore() throws Exception { 226 verifyAdminCheckForAction((admin) -> admin.runCleanerChore()); 227 } 228 229 @Test 230 public void testExecProcedure() throws Exception { 231 verifyAdminCheckForAction((admin) -> { 232 // Using existing table instead of creating a new one. 233 admin.execProcedure("flush-table-proc", TableName.META_TABLE_NAME.getNameAsString(), 234 new HashMap<>()); 235 }); 236 } 237 238 @Test 239 public void testExecService() throws Exception { 240 Action action = (admin) -> { 241 TestRpcServiceProtos.TestProtobufRpcProto.BlockingInterface service = 242 TestRpcServiceProtos.TestProtobufRpcProto.newBlockingStub(admin.coprocessorService()); 243 service.ping(null, TestProtos.EmptyRequestProto.getDefaultInstance()); 244 }; 245 246 verifyAllowed(USER_ADMIN, action); 247 verifyAllowed(USER_GROUP_ADMIN, action); 248 // This is same as above verifyAccessDenied 249 verifiedDeniedServiceException(USER_NON_ADMIN, action); 250 } 251 252 @Test 253 public void testExecProcedureWithRet() throws Exception { 254 verifyAdminCheckForAction((admin) -> { 255 // Using existing table instead of creating a new one. 256 admin.execProcedureWithReturn("flush-table-proc", TableName.META_TABLE_NAME.getNameAsString(), 257 new HashMap<>()); 258 }); 259 } 260 261 @Test 262 public void testNormalize() throws Exception { 263 verifyAdminCheckForAction((admin) -> admin.normalize()); 264 } 265 266 @Test 267 public void testSetNormalizerRunning() throws Exception { 268 verifyAdminCheckForAction((admin) -> admin.normalizerSwitch(true)); 269 } 270 271 @Test 272 public void testExecRegionServerService() throws Exception { 273 Action action = (admin) -> { 274 ServerName serverName = TEST_UTIL.getHBaseCluster().getRegionServer(0).getServerName(); 275 TestRpcServiceProtos.TestProtobufRpcProto.BlockingInterface service = 276 TestRpcServiceProtos.TestProtobufRpcProto.newBlockingStub( 277 admin.coprocessorService(serverName)); 278 service.ping(null, TestProtos.EmptyRequestProto.getDefaultInstance()); 279 }; 280 281 verifyAllowed(USER_ADMIN, action); 282 verifyAllowed(USER_GROUP_ADMIN, action); 283 verifiedDeniedServiceException(USER_NON_ADMIN, action); 284 } 285 286 @Test 287 public void testTableFlush() throws Exception { 288 TableName tn = TableName.valueOf(TEST_NAME.getMethodName()); 289 TableDescriptor desc = TableDescriptorBuilder.newBuilder(tn) 290 .setColumnFamily(ColumnFamilyDescriptorBuilder.of("f1")).build(); 291 Action adminAction = (admin) -> { 292 admin.createTable(desc); 293 // Avoid giving a global permission which may screw up other tests 294 SecureTestUtil.grantOnTable( 295 TEST_UTIL, USER_NON_ADMIN.getShortName(), tn, null, null, Permission.Action.READ, 296 Permission.Action.WRITE, Permission.Action.CREATE); 297 }; 298 verifyAllowed(USER_ADMIN, adminAction); 299 300 Action userAction = (admin) -> { 301 Connection conn = admin.getConnection(); 302 final byte[] rowKey = Bytes.toBytes("row1"); 303 final byte[] col = Bytes.toBytes("q1"); 304 final byte[] val = Bytes.toBytes("v1"); 305 try (Table table = conn.getTable(tn)) { 306 // Write a value 307 Put p = new Put(rowKey); 308 p.addColumn(Bytes.toBytes("f1"), col, val); 309 table.put(p); 310 // Flush should not require ADMIN permission 311 admin.flush(tn); 312 // Nb: ideally, we would verify snapshot permission too (as that was fixed in the 313 // regression HBASE-20185) but taking a snapshot requires ADMIN permission which 314 // masks the root issue. 315 // Make sure we read the value 316 Result result = table.get(new Get(rowKey)); 317 assertFalse(result.isEmpty()); 318 Cell c = result.getColumnLatestCell(Bytes.toBytes("f1"), col); 319 assertArrayEquals(val, CellUtil.cloneValue(c)); 320 } 321 }; 322 verifyAllowed(USER_NON_ADMIN, userAction); 323 } 324 325 @Test 326 public void testTableFlushAndSnapshot() throws Exception { 327 TableName tn = TableName.valueOf(TEST_NAME.getMethodName()); 328 TableDescriptor desc = TableDescriptorBuilder.newBuilder(tn) 329 .setColumnFamily(ColumnFamilyDescriptorBuilder.of("f1")).build(); 330 Action adminAction = (admin) -> { 331 admin.createTable(desc); 332 // Giving ADMIN here, but only on this table, *not* globally 333 SecureTestUtil.grantOnTable( 334 TEST_UTIL, USER_NON_ADMIN.getShortName(), tn, null, null, Permission.Action.READ, 335 Permission.Action.WRITE, Permission.Action.CREATE, Permission.Action.ADMIN); 336 }; 337 verifyAllowed(USER_ADMIN, adminAction); 338 339 Action userAction = (admin) -> { 340 Connection conn = admin.getConnection(); 341 final byte[] rowKey = Bytes.toBytes("row1"); 342 final byte[] col = Bytes.toBytes("q1"); 343 final byte[] val = Bytes.toBytes("v1"); 344 try (Table table = conn.getTable(tn)) { 345 // Write a value 346 Put p = new Put(rowKey); 347 p.addColumn(Bytes.toBytes("f1"), col, val); 348 table.put(p); 349 // Flush should not require ADMIN permission 350 admin.flush(tn); 351 // Table admin should be sufficient to snapshot this table 352 admin.snapshot(tn.getNameAsString() + "_snapshot1", tn); 353 // Read the value just because 354 Result result = table.get(new Get(rowKey)); 355 assertFalse(result.isEmpty()); 356 Cell c = result.getColumnLatestCell(Bytes.toBytes("f1"), col); 357 assertArrayEquals(val, CellUtil.cloneValue(c)); 358 } 359 }; 360 verifyAllowed(USER_NON_ADMIN, userAction); 361 } 362}