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.quotas; 019 020import static org.junit.jupiter.api.Assertions.assertEquals; 021import static org.junit.jupiter.api.Assertions.assertNull; 022import static org.junit.jupiter.api.Assertions.fail; 023 024import java.io.IOException; 025import java.security.PrivilegedExceptionAction; 026import java.util.Map; 027import java.util.concurrent.Callable; 028import java.util.concurrent.atomic.AtomicLong; 029import org.apache.hadoop.conf.Configuration; 030import org.apache.hadoop.hbase.DoNotRetryIOException; 031import org.apache.hadoop.hbase.HBaseTestingUtil; 032import org.apache.hadoop.hbase.TableName; 033import org.apache.hadoop.hbase.Waiter; 034import org.apache.hadoop.hbase.Waiter.Predicate; 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.coprocessor.CoprocessorHost; 039import org.apache.hadoop.hbase.regionserver.HRegionServer; 040import org.apache.hadoop.hbase.security.access.AccessControlClient; 041import org.apache.hadoop.hbase.security.access.AccessController; 042import org.apache.hadoop.hbase.security.access.Permission.Action; 043import org.apache.hadoop.hbase.testclassification.MediumTests; 044import org.apache.hadoop.hbase.util.Bytes; 045import org.apache.hadoop.security.UserGroupInformation; 046import org.junit.jupiter.api.AfterAll; 047import org.junit.jupiter.api.BeforeAll; 048import org.junit.jupiter.api.BeforeEach; 049import org.junit.jupiter.api.Tag; 050import org.junit.jupiter.api.Test; 051import org.junit.jupiter.api.TestInfo; 052import org.slf4j.Logger; 053import org.slf4j.LoggerFactory; 054 055/** 056 * Test class to verify that the HBase superuser can override quotas. 057 */ 058@Tag(MediumTests.TAG) 059public class TestSuperUserQuotaPermissions { 060 061 private static final Logger LOG = LoggerFactory.getLogger(TestSuperUserQuotaPermissions.class); 062 private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 063 // Default to the user running the tests 064 private static final String SUPERUSER_NAME = System.getProperty("user.name"); 065 private static final UserGroupInformation SUPERUSER_UGI = 066 UserGroupInformation.createUserForTesting(SUPERUSER_NAME, new String[0]); 067 private static final String REGULARUSER_NAME = "quota_regularuser"; 068 private static final UserGroupInformation REGULARUSER_UGI = 069 UserGroupInformation.createUserForTesting(REGULARUSER_NAME, new String[0]); 070 private static final AtomicLong COUNTER = new AtomicLong(0); 071 072 private SpaceQuotaHelperForTests helper; 073 074 @BeforeAll 075 public static void setupMiniCluster() throws Exception { 076 Configuration conf = TEST_UTIL.getConfiguration(); 077 // Increase the frequency of some of the chores for responsiveness of the test 078 SpaceQuotaHelperForTests.updateConfigForQuotas(conf); 079 080 conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, AccessController.class.getName()); 081 conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, AccessController.class.getName()); 082 conf.set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY, AccessController.class.getName()); 083 conf.setBoolean("hbase.security.exec.permission.checks", true); 084 conf.setBoolean("hbase.security.authorization", true); 085 conf.set("hbase.superuser", SUPERUSER_NAME); 086 087 TEST_UTIL.startMiniCluster(1); 088 } 089 090 @AfterAll 091 public static void tearDown() throws Exception { 092 TEST_UTIL.shutdownMiniCluster(); 093 } 094 095 @BeforeEach 096 public void removeAllQuotas(TestInfo testInfo) throws Exception { 097 final Connection conn = TEST_UTIL.getConnection(); 098 if (helper == null) { 099 helper = new SpaceQuotaHelperForTests(TEST_UTIL, 100 () -> testInfo.getTestMethod().get().getName(), COUNTER); 101 } 102 // Wait for the quota table to be created 103 if (!conn.getAdmin().tableExists(QuotaUtil.QUOTA_TABLE_NAME)) { 104 helper.waitForQuotaTable(conn); 105 } else { 106 // Or, clean up any quotas from previous test runs. 107 helper.removeAllQuotas(conn); 108 assertEquals(0, helper.listNumDefinedQuotas(conn)); 109 } 110 } 111 112 @Test 113 public void testSuperUserCanStillCompact() throws Exception { 114 // Create a table and write enough data to push it into quota violation 115 final TableName tn = doAsSuperUser(new Callable<TableName>() { 116 @Override 117 public TableName call() throws Exception { 118 try (Connection conn = getConnection()) { 119 Admin admin = conn.getAdmin(); 120 final TableName tn = helper.createTableWithRegions(admin, 5); 121 // Grant the normal user permissions 122 try { 123 AccessControlClient.grant(conn, tn, REGULARUSER_NAME, null, null, Action.READ, 124 Action.WRITE); 125 } catch (Throwable t) { 126 if (t instanceof Exception) { 127 throw (Exception) t; 128 } 129 throw new Exception(t); 130 } 131 return tn; 132 } 133 } 134 }); 135 136 // Write a bunch of data as our end-user 137 doAsRegularUser(new Callable<Void>() { 138 @Override 139 public Void call() throws Exception { 140 try (Connection conn = getConnection()) { 141 // Write way more data so that we have HFiles > numRegions after flushes 142 // helper.writeData flushes itself, so no need to flush explicitly 143 helper.writeData(tn, 2L * SpaceQuotaHelperForTests.ONE_MEGABYTE); 144 helper.writeData(tn, 2L * SpaceQuotaHelperForTests.ONE_MEGABYTE); 145 return null; 146 } 147 } 148 }); 149 150 final long sizeLimit = 2L * SpaceQuotaHelperForTests.ONE_MEGABYTE; 151 QuotaSettings settings = QuotaSettingsFactory.limitTableSpace(tn, sizeLimit, 152 SpaceViolationPolicy.NO_WRITES_COMPACTIONS); 153 154 try (Connection conn = getConnection()) { 155 conn.getAdmin().setQuota(settings); 156 } 157 158 waitForTableToEnterQuotaViolation(tn); 159 160 // Should throw an exception, unprivileged users cannot compact due to the quota 161 try { 162 doAsRegularUser(new Callable<Void>() { 163 @Override 164 public Void call() throws Exception { 165 try (Connection conn = getConnection()) { 166 conn.getAdmin().majorCompact(tn); 167 return null; 168 } 169 } 170 }); 171 fail("Expected an exception trying to compact a table with a quota violation"); 172 } catch (DoNotRetryIOException e) { 173 // Expected Exception. 174 LOG.debug("message", e); 175 } 176 177 try { 178 // Should not throw an exception (superuser can do anything) 179 doAsSuperUser(new Callable<Void>() { 180 @Override 181 public Void call() throws Exception { 182 try (Connection conn = getConnection()) { 183 conn.getAdmin().majorCompact(tn); 184 return null; 185 } 186 } 187 }); 188 } catch (Exception e) { 189 // Unexpected Exception. 190 LOG.debug("message", e); 191 fail("Did not expect an exception to be thrown while a super user tries " 192 + "to compact a table with a quota violation"); 193 } 194 int numberOfRegions = TEST_UTIL.getAdmin().getRegions(tn).size(); 195 waitForHFilesCountLessorEqual(tn, Bytes.toBytes("f1"), numberOfRegions); 196 } 197 198 @Test 199 public void testSuperuserCanRemoveQuota() throws Exception { 200 // Create a table and write enough data to push it into quota violation 201 final TableName tn = doAsSuperUser(new Callable<TableName>() { 202 @Override 203 public TableName call() throws Exception { 204 try (Connection conn = getConnection()) { 205 final Admin admin = conn.getAdmin(); 206 final TableName tn = helper.createTableWithRegions(admin, 5); 207 final long sizeLimit = 2L * SpaceQuotaHelperForTests.ONE_MEGABYTE; 208 QuotaSettings settings = QuotaSettingsFactory.limitTableSpace(tn, sizeLimit, 209 SpaceViolationPolicy.NO_WRITES_COMPACTIONS); 210 admin.setQuota(settings); 211 // Grant the normal user permission to create a table and set a quota 212 try { 213 AccessControlClient.grant(conn, tn, REGULARUSER_NAME, null, null, Action.READ, 214 Action.WRITE); 215 } catch (Throwable t) { 216 if (t instanceof Exception) { 217 throw (Exception) t; 218 } 219 throw new Exception(t); 220 } 221 return tn; 222 } 223 } 224 }); 225 226 // Write a bunch of data as our end-user 227 doAsRegularUser(new Callable<Void>() { 228 @Override 229 public Void call() throws Exception { 230 try (Connection conn = getConnection()) { 231 helper.writeData(tn, 3L * SpaceQuotaHelperForTests.ONE_MEGABYTE); 232 return null; 233 } 234 } 235 }); 236 237 // Wait for the table to hit quota violation 238 waitForTableToEnterQuotaViolation(tn); 239 240 // Try to be "bad" and remove the quota as the end user (we want to write more data!) 241 doAsRegularUser(new Callable<Void>() { 242 @Override 243 public Void call() throws Exception { 244 try (Connection conn = getConnection()) { 245 final Admin admin = conn.getAdmin(); 246 QuotaSettings qs = QuotaSettingsFactory.removeTableSpaceLimit(tn); 247 try { 248 admin.setQuota(qs); 249 fail("Expected that an unprivileged user should not be allowed to remove a quota"); 250 } catch (Exception e) { 251 // pass 252 } 253 return null; 254 } 255 } 256 }); 257 258 // Verify that the superuser can remove the quota 259 doAsSuperUser(new Callable<Void>() { 260 @Override 261 public Void call() throws Exception { 262 try (Connection conn = getConnection()) { 263 final Admin admin = conn.getAdmin(); 264 QuotaSettings qs = QuotaSettingsFactory.removeTableSpaceLimit(tn); 265 admin.setQuota(qs); 266 assertNull(helper.getTableSpaceQuota(conn, tn)); 267 return null; 268 } 269 } 270 }); 271 } 272 273 private Connection getConnection() throws IOException { 274 return ConnectionFactory.createConnection(TEST_UTIL.getConfiguration()); 275 } 276 277 private <T> T doAsSuperUser(Callable<T> task) throws Exception { 278 return doAsUser(SUPERUSER_UGI, task); 279 } 280 281 private <T> T doAsRegularUser(Callable<T> task) throws Exception { 282 return doAsUser(REGULARUSER_UGI, task); 283 } 284 285 private <T> T doAsUser(UserGroupInformation ugi, Callable<T> task) throws Exception { 286 return ugi.doAs(new PrivilegedExceptionAction<T>() { 287 @Override 288 public T run() throws Exception { 289 return task.call(); 290 } 291 }); 292 } 293 294 private void waitForTableToEnterQuotaViolation(TableName tn) throws Exception { 295 // Verify that the RegionServer has the quota in violation 296 final HRegionServer rs = TEST_UTIL.getHBaseCluster().getRegionServer(0); 297 Waiter.waitFor(TEST_UTIL.getConfiguration(), 30 * 1000, 1000, new Predicate<Exception>() { 298 @Override 299 public boolean evaluate() throws Exception { 300 Map<TableName, SpaceQuotaSnapshot> snapshots = 301 rs.getRegionServerSpaceQuotaManager().copyQuotaSnapshots(); 302 SpaceQuotaSnapshot snapshot = snapshots.get(tn); 303 if (snapshot == null) { 304 LOG.info("Found no snapshot for " + tn); 305 return false; 306 } 307 LOG.info("Found snapshot " + snapshot); 308 return snapshot.getQuotaStatus().isInViolation(); 309 } 310 }); 311 } 312 313 private void waitForHFilesCountLessorEqual(TableName tn, byte[] cf, int count) throws Exception { 314 Waiter.waitFor(TEST_UTIL.getConfiguration(), 30 * 1000, 1000, new Predicate<Exception>() { 315 @Override 316 public boolean evaluate() throws Exception { 317 return TEST_UTIL.getNumHFiles(tn, cf) <= count; 318 } 319 }); 320 } 321}