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}