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.security.access;
019
020import static org.apache.hadoop.hbase.AuthUtil.toGroupEntry;
021import static org.junit.Assert.assertArrayEquals;
022import static org.junit.Assert.assertFalse;
023import static org.junit.Assert.assertTrue;
024import static org.junit.Assert.fail;
025import static org.mockito.Mockito.mock;
026
027import com.google.protobuf.Service;
028import com.google.protobuf.ServiceException;
029import java.io.IOException;
030import java.security.PrivilegedExceptionAction;
031import java.util.Collections;
032import java.util.HashMap;
033import org.apache.hadoop.conf.Configuration;
034import org.apache.hadoop.hbase.AuthUtil;
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. It doesn't test all
070 * operations which require ADMIN access, only those which get vetted within MasterRpcServices at
071 * the point of entry itself (unlike old approach of using hooks in AccessController). Sidenote:
072 * There is one big difference between how security tests for AccessController hooks work, and how
073 * the tests in this class for security in MasterRpcServices work. The difference arises because of
074 * the way AC & MasterRpcServices get the user. In AccessController, it first checks if there is an
075 * active rpc user in ObserverContext. If not, it uses UserProvider for current user. This *might*
076 * make sense in the context of coprocessors, because they can be called outside the context of
077 * RPCs. But in the context of MasterRpcServices, only one way makes sense -
078 * RPCServer.getRequestUser(). In AC tests, when we do FooUser.runAs on AccessController instance
079 * directly, it bypasses the rpc framework completely, but works because UserProvider provides the
080 * correct user, i.e. FooUser in this case. But this doesn't work for the tests here, so we go
081 * around by doing complete RPCs.
082 */
083@Category({ SecurityTests.class, MediumTests.class })
084public class TestRpcAccessChecks {
085  @ClassRule
086  public static final HBaseClassTestRule CLASS_RULE =
087    HBaseClassTestRule.forClass(TestRpcAccessChecks.class);
088
089  @Rule
090  public final TestName TEST_NAME = new TestName();
091
092  private final static HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
093  private static Configuration conf;
094
095  // user granted with all global permission
096  private static User USER_ADMIN;
097  // user without admin permissions
098  private static User USER_NON_ADMIN;
099  // user in supergroup
100  private static User USER_IN_SUPERGROUPS;
101  // user with global permission but not a superuser
102  private static User USER_ADMIN_NOT_SUPER;
103
104  private static final String GROUP_ADMIN = "admin_group";
105  private static User USER_GROUP_ADMIN;
106
107  // Dummy service to test execService calls. Needs to be public so can be loaded as Coprocessor.
108  public static class DummyCpService implements MasterCoprocessor, RegionServerCoprocessor {
109    public DummyCpService() {
110    }
111
112    @Override
113    public Iterable<Service> getServices() {
114      return Collections.singleton(mock(TestRpcServiceProtos.TestProtobufRpcProto.class));
115    }
116  }
117
118  private static void enableSecurity(Configuration conf) throws IOException {
119    conf.set("hadoop.security.authorization", "false");
120    conf.set("hadoop.security.authentication", "simple");
121    conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
122      AccessController.class.getName() + "," + DummyCpService.class.getName());
123    conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, AccessController.class.getName());
124    conf.set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY,
125      AccessController.class.getName() + "," + DummyCpService.class.getName());
126    conf.set(User.HBASE_SECURITY_AUTHORIZATION_CONF_KEY, "true");
127    SecureTestUtil.configureSuperuser(conf);
128  }
129
130  @BeforeClass
131  public static void setup() throws Exception {
132    conf = TEST_UTIL.getConfiguration();
133
134    // Enable security
135    enableSecurity(conf);
136
137    // Create users
138    // admin is superuser as well.
139    USER_ADMIN = User.createUserForTesting(conf, "admin", new String[0]);
140    USER_NON_ADMIN = User.createUserForTesting(conf, "non_admin", new String[0]);
141    USER_GROUP_ADMIN =
142      User.createUserForTesting(conf, "user_group_admin", new String[] { GROUP_ADMIN });
143    USER_IN_SUPERGROUPS =
144      User.createUserForTesting(conf, "user_in_supergroup", new String[] { "supergroup" });
145    USER_ADMIN_NOT_SUPER = User.createUserForTesting(conf, "normal_admin", new String[0]);
146
147    TEST_UTIL.startMiniCluster();
148    // Wait for the ACL table to become available
149    TEST_UTIL.waitUntilAllRegionsAssigned(PermissionStorage.ACL_TABLE_NAME);
150
151    // Assign permissions to groups
152    SecureTestUtil.grantGlobal(TEST_UTIL, toGroupEntry(GROUP_ADMIN), Permission.Action.ADMIN,
153      Permission.Action.CREATE);
154    SecureTestUtil.grantGlobal(TEST_UTIL, USER_ADMIN_NOT_SUPER.getShortName(),
155      Permission.Action.ADMIN);
156  }
157
158  interface Action {
159    void run(Admin admin) throws Exception;
160  }
161
162  private void verifyAllowed(User user, Action action) throws Exception {
163    user.runAs((PrivilegedExceptionAction<?>) () -> {
164      try (Connection conn = ConnectionFactory.createConnection(conf);
165        Admin admin = conn.getAdmin()) {
166        action.run(admin);
167      } catch (IOException e) {
168        fail(e.toString());
169      }
170      return null;
171    });
172  }
173
174  private void verifyDenied(User user, Action action) throws Exception {
175    user.runAs((PrivilegedExceptionAction<?>) () -> {
176      boolean accessDenied = false;
177      try (Connection conn = ConnectionFactory.createConnection(conf);
178        Admin admin = conn.getAdmin()) {
179        action.run(admin);
180      } catch (AccessDeniedException e) {
181        accessDenied = true;
182      }
183      assertTrue("Expected access to be denied", accessDenied);
184      return null;
185    });
186  }
187
188  private void verifiedDeniedServiceException(User user, Action action) throws Exception {
189    user.runAs((PrivilegedExceptionAction<?>) () -> {
190      boolean accessDenied = false;
191      try (Connection conn = ConnectionFactory.createConnection(conf);
192        Admin admin = conn.getAdmin()) {
193        action.run(admin);
194      } catch (ServiceException e) {
195        // For MasterRpcServices.execService.
196        if (e.getCause() instanceof AccessDeniedException) {
197          accessDenied = true;
198        }
199      }
200      assertTrue("Expected access to be denied", accessDenied);
201      return null;
202    });
203
204  }
205
206  private void verifyAdminCheckForAction(Action action) throws Exception {
207    verifyAllowed(USER_ADMIN, action);
208    verifyAllowed(USER_GROUP_ADMIN, action);
209    verifyDenied(USER_NON_ADMIN, action);
210  }
211
212  @Test
213  public void testEnableCatalogJanitor() throws Exception {
214    verifyAdminCheckForAction((admin) -> admin.enableCatalogJanitor(true));
215  }
216
217  @Test
218  public void testRunCatalogJanitor() throws Exception {
219    verifyAdminCheckForAction((admin) -> admin.runCatalogJanitor());
220  }
221
222  @Test
223  public void testCleanerChoreRunning() throws Exception {
224    verifyAdminCheckForAction((admin) -> admin.cleanerChoreSwitch(true));
225  }
226
227  @Test
228  public void testRunCleanerChore() throws Exception {
229    verifyAdminCheckForAction((admin) -> admin.runCleanerChore());
230  }
231
232  @Test
233  public void testExecProcedure() throws Exception {
234    verifyAdminCheckForAction((admin) -> {
235      // Using existing table instead of creating a new one.
236      admin.execProcedure("flush-table-proc", TableName.META_TABLE_NAME.getNameAsString(),
237        new HashMap<>());
238    });
239  }
240
241  @Test
242  public void testExecService() throws Exception {
243    Action action = (admin) -> {
244      TestRpcServiceProtos.TestProtobufRpcProto.BlockingInterface service =
245        TestRpcServiceProtos.TestProtobufRpcProto.newBlockingStub(admin.coprocessorService());
246      service.ping(null, TestProtos.EmptyRequestProto.getDefaultInstance());
247    };
248
249    verifyAllowed(USER_ADMIN, action);
250    verifyAllowed(USER_GROUP_ADMIN, action);
251    // This is same as above verifyAccessDenied
252    verifiedDeniedServiceException(USER_NON_ADMIN, action);
253  }
254
255  @Test
256  public void testExecProcedureWithRet() throws Exception {
257    verifyAdminCheckForAction((admin) -> {
258      // Using existing table instead of creating a new one.
259      admin.execProcedureWithReturn("flush-table-proc", TableName.META_TABLE_NAME.getNameAsString(),
260        new HashMap<>());
261    });
262  }
263
264  @Test
265  public void testNormalize() throws Exception {
266    verifyAdminCheckForAction((admin) -> admin.normalize());
267  }
268
269  @Test
270  public void testSetNormalizerRunning() throws Exception {
271    verifyAdminCheckForAction((admin) -> admin.normalizerSwitch(true));
272  }
273
274  @Test
275  public void testExecRegionServerService() throws Exception {
276    Action action = (admin) -> {
277      ServerName serverName = TEST_UTIL.getHBaseCluster().getRegionServer(0).getServerName();
278      TestRpcServiceProtos.TestProtobufRpcProto.BlockingInterface service =
279        TestRpcServiceProtos.TestProtobufRpcProto
280          .newBlockingStub(admin.coprocessorService(serverName));
281      service.ping(null, TestProtos.EmptyRequestProto.getDefaultInstance());
282    };
283
284    verifyAllowed(USER_ADMIN, action);
285    verifyAllowed(USER_GROUP_ADMIN, action);
286    verifiedDeniedServiceException(USER_NON_ADMIN, action);
287  }
288
289  @Test
290  public void testTableFlush() throws Exception {
291    TableName tn = TableName.valueOf(TEST_NAME.getMethodName());
292    TableDescriptor desc = TableDescriptorBuilder.newBuilder(tn)
293      .setColumnFamily(ColumnFamilyDescriptorBuilder.of("f1")).build();
294    Action adminAction = (admin) -> {
295      admin.createTable(desc);
296      // Avoid giving a global permission which may screw up other tests
297      SecureTestUtil.grantOnTable(TEST_UTIL, USER_NON_ADMIN.getShortName(), tn, null, null,
298        Permission.Action.READ, Permission.Action.WRITE, Permission.Action.CREATE);
299    };
300    verifyAllowed(USER_ADMIN, adminAction);
301
302    Action userAction = (admin) -> {
303      Connection conn = admin.getConnection();
304      final byte[] rowKey = Bytes.toBytes("row1");
305      final byte[] col = Bytes.toBytes("q1");
306      final byte[] val = Bytes.toBytes("v1");
307      try (Table table = conn.getTable(tn)) {
308        // Write a value
309        Put p = new Put(rowKey);
310        p.addColumn(Bytes.toBytes("f1"), col, val);
311        table.put(p);
312        // Flush should not require ADMIN permission
313        admin.flush(tn);
314        // Nb: ideally, we would verify snapshot permission too (as that was fixed in the
315        // regression HBASE-20185) but taking a snapshot requires ADMIN permission which
316        // masks the root issue.
317        // Make sure we read the value
318        Result result = table.get(new Get(rowKey));
319        assertFalse(result.isEmpty());
320        Cell c = result.getColumnLatestCell(Bytes.toBytes("f1"), col);
321        assertArrayEquals(val, CellUtil.cloneValue(c));
322      }
323    };
324    verifyAllowed(USER_NON_ADMIN, userAction);
325  }
326
327  @Test
328  public void testTableFlushAndSnapshot() throws Exception {
329    TableName tn = TableName.valueOf(TEST_NAME.getMethodName());
330    TableDescriptor desc = TableDescriptorBuilder.newBuilder(tn)
331      .setColumnFamily(ColumnFamilyDescriptorBuilder.of("f1")).build();
332    Action adminAction = (admin) -> {
333      admin.createTable(desc);
334      // Giving ADMIN here, but only on this table, *not* globally
335      SecureTestUtil.grantOnTable(TEST_UTIL, USER_NON_ADMIN.getShortName(), tn, null, null,
336        Permission.Action.READ, Permission.Action.WRITE, Permission.Action.CREATE,
337        Permission.Action.ADMIN);
338    };
339    verifyAllowed(USER_ADMIN, adminAction);
340
341    Action userAction = (admin) -> {
342      Connection conn = admin.getConnection();
343      final byte[] rowKey = Bytes.toBytes("row1");
344      final byte[] col = Bytes.toBytes("q1");
345      final byte[] val = Bytes.toBytes("v1");
346      try (Table table = conn.getTable(tn)) {
347        // Write a value
348        Put p = new Put(rowKey);
349        p.addColumn(Bytes.toBytes("f1"), col, val);
350        table.put(p);
351        // Flush should not require ADMIN permission
352        admin.flush(tn);
353        // Table admin should be sufficient to snapshot this table
354        admin.snapshot(tn.getNameAsString() + "_snapshot1", tn);
355        // Read the value just because
356        Result result = table.get(new Get(rowKey));
357        assertFalse(result.isEmpty());
358        Cell c = result.getColumnLatestCell(Bytes.toBytes("f1"), col);
359        assertArrayEquals(val, CellUtil.cloneValue(c));
360      }
361    };
362    verifyAllowed(USER_NON_ADMIN, userAction);
363  }
364
365  @Test
366  public void testGrantDeniedOnSuperUsersGroups() {
367    /** User */
368    try {
369      // Global
370      SecureTestUtil.grantGlobal(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getShortName(),
371        Permission.Action.ADMIN, Permission.Action.CREATE);
372      fail("Granting superuser's global permissions is not allowed.");
373    } catch (Exception e) {
374    }
375    try {
376      // Namespace
377      SecureTestUtil.grantOnNamespace(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getShortName(),
378        TEST_NAME.getMethodName(), Permission.Action.ADMIN, Permission.Action.CREATE);
379      fail("Granting superuser's namespace permissions is not allowed.");
380    } catch (Exception e) {
381    }
382    try {
383      // Table
384      SecureTestUtil.grantOnTable(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getName(),
385        TableName.valueOf(TEST_NAME.getMethodName()), null, null, Permission.Action.ADMIN,
386        Permission.Action.CREATE);
387      fail("Granting superuser's table permissions is not allowed.");
388    } catch (Exception e) {
389    }
390
391    /** Group */
392    try {
393      SecureTestUtil.grantGlobal(USER_ADMIN_NOT_SUPER, TEST_UTIL,
394        USER_IN_SUPERGROUPS.getShortName(), Permission.Action.ADMIN, Permission.Action.CREATE);
395      fail("Granting superuser's global permissions is not allowed.");
396    } catch (Exception e) {
397    }
398  }
399
400  @Test
401  public void testRevokeDeniedOnSuperUsersGroups() {
402    /** User */
403    try {
404      // Global
405      SecureTestUtil.revokeGlobal(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getShortName(),
406        Permission.Action.ADMIN);
407      fail("Revoking superuser's global permissions is not allowed.");
408    } catch (Exception e) {
409    }
410    try {
411      // Namespace
412      SecureTestUtil.revokeFromNamespace(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getShortName(),
413        TEST_NAME.getMethodName(), Permission.Action.ADMIN);
414      fail("Revoking superuser's namespace permissions is not allowed.");
415    } catch (Exception e) {
416    }
417    try {
418      // Table
419      SecureTestUtil.revokeFromTable(USER_ADMIN_NOT_SUPER, TEST_UTIL, USER_ADMIN.getName(),
420        TableName.valueOf(TEST_NAME.getMethodName()), null, null, Permission.Action.ADMIN);
421      fail("Revoking superuser's table permissions is not allowed.");
422    } catch (Exception e) {
423    }
424
425    /** Group */
426    try {
427      // Global revoke
428      SecureTestUtil.revokeGlobal(USER_ADMIN_NOT_SUPER, TEST_UTIL,
429        AuthUtil.toGroupEntry("supergroup"), Permission.Action.ADMIN, Permission.Action.CREATE);
430      fail("Revoking supergroup's permissions is not allowed.");
431    } catch (Exception e) {
432    }
433  }
434}