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