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