View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to you under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.hadoop.hbase.quotas;
18  
19  import static org.junit.Assert.assertEquals;
20  import static org.junit.Assert.assertNull;
21  import static org.junit.Assert.fail;
22  
23  import java.io.IOException;
24  import java.security.PrivilegedExceptionAction;
25  import java.util.Map;
26  import java.util.concurrent.Callable;
27  import java.util.concurrent.atomic.AtomicLong;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.apache.hadoop.conf.Configuration;
32  import org.apache.hadoop.hbase.DoNotRetryIOException;
33  import org.apache.hadoop.hbase.HBaseTestingUtility;
34  import org.apache.hadoop.hbase.TableName;
35  import org.apache.hadoop.hbase.Waiter;
36  import org.apache.hadoop.hbase.Waiter.Predicate;
37  import org.apache.hadoop.hbase.client.Admin;
38  import org.apache.hadoop.hbase.client.Connection;
39  import org.apache.hadoop.hbase.client.ConnectionFactory;
40  import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
41  import org.apache.hadoop.hbase.regionserver.HRegionServer;
42  import org.apache.hadoop.hbase.security.access.AccessControlClient;
43  import org.apache.hadoop.hbase.security.access.AccessController;
44  import org.apache.hadoop.hbase.security.access.Permission.Action;
45  import org.apache.hadoop.hbase.testclassification.MediumTests;
46  import org.apache.hadoop.security.UserGroupInformation;
47  import org.junit.AfterClass;
48  import org.junit.Before;
49  import org.junit.BeforeClass;
50  import org.junit.Rule;
51  import org.junit.Test;
52  import org.junit.experimental.categories.Category;
53  import org.junit.rules.TestName;
54  
55  /**
56   * Test class to verify that the HBase superuser can override quotas.
57   */
58  @Category(MediumTests.class)
59  public class TestSuperUserQuotaPermissions {
60    private static final Log LOG = LogFactory.getLog(TestSuperUserQuotaPermissions.class);
61    private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
62    // Default to the user running the tests
63    private static final String SUPERUSER_NAME = System.getProperty("user.name");
64    private static final UserGroupInformation SUPERUSER_UGI =
65        UserGroupInformation.createUserForTesting(SUPERUSER_NAME, new String[0]);
66    private static final String REGULARUSER_NAME = "quota_regularuser";
67    private static final UserGroupInformation REGULARUSER_UGI =
68        UserGroupInformation.createUserForTesting(REGULARUSER_NAME, new String[0]);
69    private static final AtomicLong COUNTER = new AtomicLong(0);
70  
71    @Rule
72    public TestName testName = new TestName();
73    private SpaceQuotaHelperForTests helper;
74  
75    @BeforeClass
76    public static void setupMiniCluster() throws Exception {
77      Configuration conf = TEST_UTIL.getConfiguration();
78      // Increase the frequency of some of the chores for responsiveness of the test
79      conf.setInt(FileSystemUtilizationChore.FS_UTILIZATION_CHORE_DELAY_KEY, 1000);
80      conf.setInt(FileSystemUtilizationChore.FS_UTILIZATION_CHORE_PERIOD_KEY, 1000);
81      conf.setInt(QuotaObserverChore.QUOTA_OBSERVER_CHORE_DELAY_KEY, 1000);
82      conf.setInt(QuotaObserverChore.QUOTA_OBSERVER_CHORE_PERIOD_KEY, 1000);
83      conf.setInt(SpaceQuotaRefresherChore.POLICY_REFRESHER_CHORE_DELAY_KEY, 1000);
84      conf.setInt(SpaceQuotaRefresherChore.POLICY_REFRESHER_CHORE_PERIOD_KEY, 1000);
85      conf.setBoolean(QuotaUtil.QUOTA_CONF_KEY, true);
86  
87      conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, AccessController.class.getName());
88      conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, AccessController.class.getName());
89      conf.set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY, AccessController.class.getName());
90      conf.setBoolean("hbase.security.exec.permission.checks", true);
91      conf.setBoolean("hbase.security.authorization", true);
92      conf.set("hbase.superuser", SUPERUSER_NAME);
93  
94      TEST_UTIL.startMiniCluster(1);
95    }
96  
97    @AfterClass
98    public static void tearDown() throws Exception {
99      TEST_UTIL.shutdownMiniCluster();
100   }
101 
102   @Before
103   public void removeAllQuotas() throws Exception {
104     final Connection conn = TEST_UTIL.getConnection();
105     if (helper == null) {
106       helper = new SpaceQuotaHelperForTests(TEST_UTIL, testName, COUNTER);
107     }
108     // Wait for the quota table to be created
109     if (!conn.getAdmin().tableExists(QuotaUtil.QUOTA_TABLE_NAME)) {
110       helper.waitForQuotaTable(conn);
111     } else {
112       // Or, clean up any quotas from previous test runs.
113       helper.removeAllQuotas(conn);
114       assertEquals(0, helper.listNumDefinedQuotas(conn));
115     }
116   }
117 
118   @Test
119   public void testSuperUserCanStillCompact() throws Exception {
120     // Create a table and write enough data to push it into quota violation
121     final TableName tn = doAsSuperUser(new Callable<TableName>() {
122       @Override
123       public TableName call() throws Exception {
124         try (Connection conn = getConnection()) {
125           Admin admin = conn.getAdmin();
126           final TableName tn = helper.createTableWithRegions(admin, 5);
127           final long sizeLimit = 2L * SpaceQuotaHelperForTests.ONE_MEGABYTE;
128           QuotaSettings settings = QuotaSettingsFactory.limitTableSpace(
129               tn, sizeLimit, SpaceViolationPolicy.NO_WRITES_COMPACTIONS);
130           admin.setQuota(settings);
131           // Grant the normal user permissions
132           try {
133             AccessControlClient.grant(
134                 conn, tn, REGULARUSER_NAME, null, null, Action.READ, Action.WRITE);
135           } catch (Throwable t) {
136             if (t instanceof Exception) {
137               throw (Exception) t;
138             }
139             throw new Exception(t);
140           }
141           return tn;
142         }
143       }
144     });
145 
146     // Write a bunch of data as our end-user
147     doAsRegularUser(new Callable<Void>() {
148       @Override
149       public Void call() throws Exception {
150         try (Connection conn = getConnection()) {
151           helper.writeData(tn, 3L * SpaceQuotaHelperForTests.ONE_MEGABYTE);
152           return null;
153         }
154       }
155     });
156 
157     waitForTableToEnterQuotaViolation(tn);
158 
159     // Should throw an exception, unprivileged users cannot compact due to the quota
160     try {
161       doAsRegularUser(new Callable<Void>() {
162         @Override
163         public Void call() throws Exception {
164           try (Connection conn = getConnection()) {
165             conn.getAdmin().majorCompact(tn);
166             return null;
167           }
168         }
169       });
170       fail("Expected an exception trying to compact a table with a quota violation");
171     } catch (DoNotRetryIOException e) {
172       // Expected
173     }
174 
175     // Should not throw an exception (superuser can do anything)
176     doAsSuperUser(new Callable<Void>() {
177       @Override
178       public Void call() throws Exception {
179         try (Connection conn = getConnection()) {
180           conn.getAdmin().majorCompact(tn);
181           return null;
182         }
183       }
184     });
185   }
186 
187   @Test
188   public void testSuperuserCanRemoveQuota() throws Exception {
189     // Create a table and write enough data to push it into quota violation
190     final TableName tn = doAsSuperUser(new Callable<TableName>() {
191       @Override
192       public TableName call() throws Exception {
193         try (Connection conn = getConnection()) {
194           final Admin admin = conn.getAdmin();
195           final TableName tn = helper.createTableWithRegions(admin, 5);
196           final long sizeLimit = 2L * SpaceQuotaHelperForTests.ONE_MEGABYTE;
197           QuotaSettings settings = QuotaSettingsFactory.limitTableSpace(
198               tn, sizeLimit, SpaceViolationPolicy.NO_WRITES_COMPACTIONS);
199           admin.setQuota(settings);
200           // Grant the normal user permission to create a table and set a quota
201           try {
202             AccessControlClient.grant(
203                 conn, tn, REGULARUSER_NAME, null, null, Action.READ, Action.WRITE);
204           } catch (Throwable t) {
205             if (t instanceof Exception) {
206               throw (Exception) t;
207             }
208             throw new Exception(t);
209           }
210           return tn;
211         }
212       }
213     });
214 
215     // Write a bunch of data as our end-user
216     doAsRegularUser(new Callable<Void>() {
217       @Override
218       public Void call() throws Exception {
219         try (Connection conn = getConnection()) {
220           helper.writeData(tn, 3L * SpaceQuotaHelperForTests.ONE_MEGABYTE);
221           return null;
222         }
223       }
224     });
225 
226     // Wait for the table to hit quota violation
227     waitForTableToEnterQuotaViolation(tn);
228 
229     // Try to be "bad" and remove the quota as the end user (we want to write more data!)
230     doAsRegularUser(new Callable<Void>() {
231       @Override
232       public Void call() throws Exception {
233         try (Connection conn = getConnection()) {
234           final Admin admin = conn.getAdmin();
235           QuotaSettings qs = QuotaSettingsFactory.removeTableSpaceLimit(tn);
236           try {
237             admin.setQuota(qs);
238             fail("Expected that an unprivileged user should not be allowed to remove a quota");
239           } catch (Exception e) {
240             // pass
241           }
242           return null;
243         }
244       }
245     });
246 
247     // Verify that the superuser can remove the quota
248     doAsSuperUser(new Callable<Void>() {
249       @Override
250       public Void call() throws Exception {
251         try (Connection conn = getConnection()) {
252           final Admin admin = conn.getAdmin();
253           QuotaSettings qs = QuotaSettingsFactory.removeTableSpaceLimit(tn);
254           admin.setQuota(qs);
255           assertNull(helper.getTableSpaceQuota(conn, tn));
256           return null;
257         }
258       }
259     });
260   }
261 
262   private Connection getConnection() throws IOException {
263     return ConnectionFactory.createConnection(TEST_UTIL.getConfiguration());
264   }
265 
266   private <T> T doAsSuperUser(Callable<T> task) throws Exception {
267     return doAsUser(SUPERUSER_UGI, task);
268   }
269 
270   private <T> T doAsRegularUser(Callable<T> task) throws Exception {
271     return doAsUser(REGULARUSER_UGI, task);
272   }
273 
274   private <T> T doAsUser(UserGroupInformation ugi, final Callable<T> task) throws Exception {
275     return ugi.doAs(new PrivilegedExceptionAction<T>() {
276       public T run() throws Exception {
277         return task.call();
278       }
279     });
280   }
281 
282   private void waitForTableToEnterQuotaViolation(final TableName tn) throws Exception {
283     // Verify that the RegionServer has the quota in violation
284     final HRegionServer rs = TEST_UTIL.getHBaseCluster().getRegionServer(0);
285     Waiter.waitFor(TEST_UTIL.getConfiguration(), 30 * 1000, 1000, new Predicate<Exception>() {
286       @Override
287       public boolean evaluate() throws Exception {
288         Map<TableName,SpaceQuotaSnapshot> snapshots =
289             rs.getRegionServerSpaceQuotaManager().copyQuotaSnapshots();
290         SpaceQuotaSnapshot snapshot = snapshots.get(tn);
291         if (snapshot == null) {
292           LOG.info("Found no snapshot for " + tn);
293           return false;
294         }
295         LOG.info("Found snapshot " + snapshot);
296         return snapshot.getQuotaStatus().isInViolation();
297       }
298     });
299   }
300 }