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