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