View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.apache.hadoop.hbase.security.access;
20  
21  import static org.junit.Assert.assertEquals;
22  import static org.junit.Assert.fail;
23  
24  import java.io.IOException;
25  import java.lang.reflect.UndeclaredThrowableException;
26  import java.security.PrivilegedActionException;
27  import java.security.PrivilegedExceptionAction;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.concurrent.Callable;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.hadoop.conf.Configuration;
35  import org.apache.hadoop.hbase.Coprocessor;
36  import org.apache.hadoop.hbase.HBaseTestingUtility;
37  import org.apache.hadoop.hbase.HConstants;
38  import org.apache.hadoop.hbase.MiniHBaseCluster;
39  import org.apache.hadoop.hbase.TableName;
40  import org.apache.hadoop.hbase.Waiter.Predicate;
41  import org.apache.hadoop.hbase.client.Connection;
42  import org.apache.hadoop.hbase.client.ConnectionFactory;
43  import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException;
44  import org.apache.hadoop.hbase.client.Table;
45  import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
46  import org.apache.hadoop.hbase.io.hfile.HFile;
47  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
48  import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos.AccessControlService;
49  import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos.CheckPermissionsRequest;
50  import org.apache.hadoop.hbase.regionserver.HRegion;
51  import org.apache.hadoop.hbase.security.AccessDeniedException;
52  import org.apache.hadoop.hbase.security.User;
53  import org.apache.hadoop.hbase.util.JVMClusterUtil.RegionServerThread;
54  
55  import com.google.common.collect.Lists;
56  import com.google.common.collect.Maps;
57  import com.google.protobuf.BlockingRpcChannel;
58  import com.google.protobuf.ServiceException;
59  
60  /**
61   * Utility methods for testing security
62   */
63  public class SecureTestUtil {
64    
65    private static final Log LOG = LogFactory.getLog(SecureTestUtil.class);
66    private static final int WAIT_TIME = 10000;
67  
68    public static void enableSecurity(Configuration conf) throws IOException {
69      conf.set("hadoop.security.authorization", "false");
70      conf.set("hadoop.security.authentication", "simple");
71      conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, AccessController.class.getName());
72      conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, AccessController.class.getName() +
73        "," + SecureBulkLoadEndpoint.class.getName());
74      conf.set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY, AccessController.class.getName());
75      // The secure minicluster creates separate service principals based on the
76      // current user's name, one for each slave. We need to add all of these to
77      // the superuser list or security won't function properly. We expect the
78      // HBase service account(s) to have superuser privilege.
79      String currentUser = User.getCurrent().getName();
80      StringBuffer sb = new StringBuffer();
81      sb.append("admin,");
82      sb.append(currentUser);
83      // Assumes we won't ever have a minicluster with more than 5 slaves
84      for (int i = 0; i < 5; i++) {
85        sb.append(',');
86        sb.append(currentUser); sb.append(".hfs."); sb.append(i);
87      }
88      conf.set("hbase.superuser", sb.toString());
89      // Need HFile V3 for tags for security features
90      conf.setInt(HFile.FORMAT_VERSION_KEY, 3);
91    }
92  
93    public static void verifyConfiguration(Configuration conf) {
94      if (!(conf.get(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY).contains(
95          AccessController.class.getName())
96          && conf.get(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY).contains(
97              AccessController.class.getName()) && conf.get(
98          CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY).contains(
99          AccessController.class.getName()))) {
100       throw new RuntimeException("AccessController is missing from a system coprocessor list");
101     }
102     if (conf.getInt(HFile.FORMAT_VERSION_KEY, 2) < HFile.MIN_FORMAT_VERSION_WITH_TAGS) {
103       throw new RuntimeException("Post 0.96 security features require HFile version >= 3");
104     }
105   }
106 
107   public static void checkTablePerms(Configuration conf, TableName table, byte[] family, byte[] column,
108       Permission.Action... actions) throws IOException {
109     Permission[] perms = new Permission[actions.length];
110     for (int i = 0; i < actions.length; i++) {
111       perms[i] = new TablePermission(table, family, column, actions[i]);
112     }
113 
114     checkTablePerms(conf, table, perms);
115   }
116 
117   public static void checkTablePerms(Configuration conf, TableName table, Permission... perms)
118   throws IOException {
119     CheckPermissionsRequest.Builder request = CheckPermissionsRequest.newBuilder();
120     for (Permission p : perms) {
121       request.addPermission(ProtobufUtil.toPermission(p));
122     }
123     try (Connection connection = ConnectionFactory.createConnection(conf)) {
124       try (Table acl = connection.getTable(table)) {
125         AccessControlService.BlockingInterface protocol =
126             AccessControlService.newBlockingStub(acl.coprocessorService(new byte[0]));
127         try {
128           protocol.checkPermissions(null, request.build());
129         } catch (ServiceException se) {
130           ProtobufUtil.toIOException(se);
131         }
132       }
133     }
134   }
135 
136   /**
137    * An AccessTestAction performs an action that will be examined to confirm
138    * the results conform to expected access rights.
139    * <p>
140    * To indicate an action was allowed, return null or a non empty list of
141    * KeyValues.
142    * <p>
143    * To indicate the action was not allowed, either throw an AccessDeniedException
144    * or return an empty list of KeyValues.
145    */
146   static interface AccessTestAction extends PrivilegedExceptionAction<Object> { }
147 
148   public static void verifyAllowed(User user, AccessTestAction... actions) throws Exception {
149     for (AccessTestAction action : actions) {
150       try {
151         Object obj = user.runAs(action);
152         if (obj != null && obj instanceof List<?>) {
153           List<?> results = (List<?>) obj;
154           if (results != null && results.isEmpty()) {
155             fail("Empty non null results from action for user '" + user.getShortName() + "'");
156           }
157         }
158       } catch (AccessDeniedException ade) {
159         fail("Expected action to pass for user '" + user.getShortName() + "' but was denied");
160       }
161     }
162   }
163 
164   public static void verifyAllowed(AccessTestAction action, User... users) throws Exception {
165     for (User user : users) {
166       verifyAllowed(user, action);
167     }
168   }
169 
170   public static void verifyAllowed(User user, AccessTestAction action, int count) throws Exception {
171     try {
172       Object obj = user.runAs(action);
173       if (obj != null && obj instanceof List<?>) {
174         List<?> results = (List<?>) obj;
175         if (results != null && results.isEmpty()) {
176           fail("Empty non null results from action for user '" + user.getShortName() + "'");
177         }
178         assertEquals(count, results.size());
179       }
180     } catch (AccessDeniedException ade) {
181       fail("Expected action to pass for user '" + user.getShortName() + "' but was denied");
182     }
183   }
184 
185   public static void verifyDeniedWithException(User user, AccessTestAction... actions)
186       throws Exception {
187     verifyDenied(user, true, actions);
188   }
189 
190   public static void verifyDeniedWithException(AccessTestAction action, User... users)
191       throws Exception {
192     for (User user : users) {
193       verifyDenied(user, true, action);
194     }
195   }
196 
197   public static void verifyDenied(User user, AccessTestAction... actions) throws Exception {
198     verifyDenied(user, false, actions);
199   }
200 
201   public static void verifyDenied(User user, boolean requireException,
202       AccessTestAction... actions) throws Exception {
203     for (AccessTestAction action : actions) {
204       try {
205         Object obj = user.runAs(action);
206         if (requireException) {
207           fail("Expected exception was not thrown for user '" + user.getShortName() + "'");
208         }
209         if (obj != null && obj instanceof List<?>) {
210           List<?> results = (List<?>) obj;
211           if (results != null && !results.isEmpty()) {
212             fail("Unexpected results for user '" + user.getShortName() + "'");
213           }
214         }
215       } catch (IOException e) {
216         boolean isAccessDeniedException = false;
217         if(e instanceof RetriesExhaustedWithDetailsException) {
218           // in case of batch operations, and put, the client assembles a
219           // RetriesExhaustedWithDetailsException instead of throwing an
220           // AccessDeniedException
221           for(Throwable ex : ((RetriesExhaustedWithDetailsException) e).getCauses()) {
222             if (ex instanceof AccessDeniedException) {
223               isAccessDeniedException = true;
224               break;
225             }
226           }
227         }
228         else {
229           // For doBulkLoad calls AccessDeniedException
230           // is buried in the stack trace
231           Throwable ex = e;
232           do {
233             if (ex instanceof AccessDeniedException) {
234               isAccessDeniedException = true;
235               break;
236             }
237           } while((ex = ex.getCause()) != null);
238         }
239         if (!isAccessDeniedException) {
240           fail("Expected exception was not thrown for user '" + user.getShortName() + "'");
241         }
242       } catch (UndeclaredThrowableException ute) {
243         // TODO why we get a PrivilegedActionException, which is unexpected?
244         Throwable ex = ute.getUndeclaredThrowable();
245         if (ex instanceof PrivilegedActionException) {
246           ex = ((PrivilegedActionException) ex).getException();
247         }
248         if (ex instanceof ServiceException) {
249           ServiceException se = (ServiceException)ex;
250           if (se.getCause() != null && se.getCause() instanceof AccessDeniedException) {
251             // expected result
252             return;
253           }
254         }
255         fail("Expected exception was not thrown for user '" + user.getShortName() + "'");
256       }
257     }
258   }
259 
260   public static void verifyDenied(AccessTestAction action, User... users) throws Exception {
261     for (User user : users) {
262       verifyDenied(user, action);
263     }
264   }
265 
266   private static List<AccessController> getAccessControllers(MiniHBaseCluster cluster) {
267     List<AccessController> result = Lists.newArrayList();
268     for (RegionServerThread t: cluster.getLiveRegionServerThreads()) {
269       for (HRegion region: t.getRegionServer().getOnlineRegionsLocalContext()) {
270         Coprocessor cp = region.getCoprocessorHost()
271           .findCoprocessor(AccessController.class.getName());
272         if (cp != null) {
273           result.add((AccessController)cp);
274         }
275       }
276     }
277     return result;
278   }
279 
280   private static Map<AccessController,Long> getAuthManagerMTimes(MiniHBaseCluster cluster) {
281     Map<AccessController,Long> result = Maps.newHashMap();
282     for (AccessController ac: getAccessControllers(cluster)) {
283       result.put(ac, ac.getAuthManager().getMTime());
284     }
285     return result;
286   }
287 
288   @SuppressWarnings("rawtypes")
289   private static void updateACLs(final HBaseTestingUtility util, Callable c) throws Exception {
290     // Get the current mtimes for all access controllers
291     final Map<AccessController,Long> oldMTimes = getAuthManagerMTimes(util.getHBaseCluster());
292 
293     // Run the update action
294     c.call();
295 
296     // Wait until mtimes for all access controllers have incremented
297     util.waitFor(WAIT_TIME, 100, new Predicate<IOException>() {
298       @Override
299       public boolean evaluate() throws IOException {
300         Map<AccessController,Long> mtimes = getAuthManagerMTimes(util.getHBaseCluster());
301         for (Map.Entry<AccessController,Long> e: mtimes.entrySet()) {
302           if (!oldMTimes.containsKey(e.getKey())) {
303             LOG.error("Snapshot of AccessController state does not include instance on region " +
304               e.getKey().getRegion().getRegionNameAsString());
305             // Error out the predicate, we will try again
306             return false;
307           }
308           long old = oldMTimes.get(e.getKey());
309           long now = e.getValue();
310           if (now <= old) {
311             LOG.info("AccessController on region " +
312               e.getKey().getRegion().getRegionNameAsString() + " has not updated: mtime=" +
313               now);
314             return false;
315           }
316         }
317         return true;
318       }
319     });
320   }
321 
322   /**
323    * Grant permissions globally to the given user. Will wait until all active
324    * AccessController instances have updated their permissions caches or will
325    * throw an exception upon timeout (10 seconds).
326    */
327   public static void grantGlobal(final HBaseTestingUtility util, final String user,
328       final Permission.Action... actions) throws Exception {
329     SecureTestUtil.updateACLs(util, new Callable<Void>() {
330       @Override
331       public Void call() throws Exception {
332         try (Connection connection = ConnectionFactory.createConnection(util.getConfiguration())) {
333           try (Table acl = connection.getTable(AccessControlLists.ACL_TABLE_NAME)) {
334             BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
335             AccessControlService.BlockingInterface protocol =
336                 AccessControlService.newBlockingStub(service);
337             ProtobufUtil.grant(protocol, user, actions);
338           }
339         }
340         return null;
341       }
342     });
343   }
344 
345   /**
346    * Revoke permissions globally from the given user. Will wait until all active
347    * AccessController instances have updated their permissions caches or will
348    * throw an exception upon timeout (10 seconds).
349    */
350   public static void revokeGlobal(final HBaseTestingUtility util, final String user,
351       final Permission.Action... actions) throws Exception {
352     SecureTestUtil.updateACLs(util, new Callable<Void>() {
353       @Override
354       public Void call() throws Exception {
355         try (Connection connection = ConnectionFactory.createConnection(util.getConfiguration())) {
356           try (Table acl = connection.getTable(AccessControlLists.ACL_TABLE_NAME)) {
357             BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
358             AccessControlService.BlockingInterface protocol =
359                 AccessControlService.newBlockingStub(service);
360             ProtobufUtil.revoke(protocol, user, actions);
361           }
362         }
363         return null;
364       }
365     });
366   }
367 
368   /**
369    * Grant permissions on a namespace to the given user. Will wait until all active
370    * AccessController instances have updated their permissions caches or will
371    * throw an exception upon timeout (10 seconds).
372    */
373   public static void grantOnNamespace(final HBaseTestingUtility util, final String user,
374       final String namespace, final Permission.Action... actions) throws Exception {
375     SecureTestUtil.updateACLs(util, new Callable<Void>() {
376       @Override
377       public Void call() throws Exception {
378         try (Connection connection = ConnectionFactory.createConnection(util.getConfiguration())) {
379           try (Table acl = connection.getTable(AccessControlLists.ACL_TABLE_NAME)) {
380             BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
381             AccessControlService.BlockingInterface protocol =
382                 AccessControlService.newBlockingStub(service);
383             ProtobufUtil.grant(protocol, user, namespace, actions);
384           }
385         }
386         return null;
387       }
388     });
389   }
390 
391   /**
392    * Grant permissions on a namespace to the given user using AccessControl Client.
393    * Will wait until all active AccessController instances have updated their permissions caches
394    * or will throw an exception upon timeout (10 seconds).
395    */
396   public static void grantOnNamespaceUsingAccessControlClient(final HBaseTestingUtility util,
397       final Configuration conf, final String user, final String namespace,
398       final Permission.Action... actions) throws Exception {
399     SecureTestUtil.updateACLs(util, new Callable<Void>() {
400       @Override
401       public Void call() throws Exception {
402         try {
403           AccessControlClient.grant(conf, namespace, user, actions);
404         } catch (Throwable t) {
405           t.printStackTrace();
406         }
407         return null;
408       }
409     });
410   }
411 
412   /**
413    * Revoke permissions on a namespace from the given user using AccessControl Client.
414    * Will wait until all active AccessController instances have updated their permissions caches
415    * or will throw an exception upon timeout (10 seconds).
416    */
417   public static void revokeFromNamespaceUsingAccessControlClient(final HBaseTestingUtility util,
418       final Configuration conf, final String user, final String namespace,
419       final Permission.Action... actions) throws Exception {
420     SecureTestUtil.updateACLs(util, new Callable<Void>() {
421       @Override
422       public Void call() throws Exception {
423         try {
424           AccessControlClient.revoke(conf, namespace, user, actions);
425         } catch (Throwable t) {
426           t.printStackTrace();
427         }
428         return null;
429       }
430     });
431   }
432 
433   /**
434    * Revoke permissions on a namespace from the given user. Will wait until all active
435    * AccessController instances have updated their permissions caches or will
436    * throw an exception upon timeout (10 seconds).
437    */
438   public static void revokeFromNamespace(final HBaseTestingUtility util, final String user,
439       final String namespace, final Permission.Action... actions) throws Exception {
440     SecureTestUtil.updateACLs(util, new Callable<Void>() {
441       @Override
442       public Void call() throws Exception {
443         try (Connection connection = ConnectionFactory.createConnection(util.getConfiguration())) {
444           try (Table acl = connection.getTable(AccessControlLists.ACL_TABLE_NAME)) {
445             BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
446             AccessControlService.BlockingInterface protocol =
447                 AccessControlService.newBlockingStub(service);
448             ProtobufUtil.revoke(protocol, user, namespace, actions);
449           }
450         }
451         return null;
452       }
453     });
454   }
455 
456   /**
457    * Grant permissions on a table to the given user. Will wait until all active
458    * AccessController instances have updated their permissions caches or will
459    * throw an exception upon timeout (10 seconds).
460    */
461   public static void grantOnTable(final HBaseTestingUtility util, final String user,
462       final TableName table, final byte[] family, final byte[] qualifier,
463       final Permission.Action... actions) throws Exception {
464     SecureTestUtil.updateACLs(util, new Callable<Void>() {
465       @Override
466       public Void call() throws Exception {
467         try (Connection connection = ConnectionFactory.createConnection(util.getConfiguration())) {
468           try (Table acl = connection.getTable(AccessControlLists.ACL_TABLE_NAME)) {
469             BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
470             AccessControlService.BlockingInterface protocol =
471                 AccessControlService.newBlockingStub(service);
472             ProtobufUtil.grant(protocol, user, table, family, qualifier, actions);
473           }
474         }
475         return null;
476       }
477     });
478   }
479 
480   /**
481    * Grant permissions on a table to the given user using AccessControlClient. Will wait until all
482    * active AccessController instances have updated their permissions caches or will
483    * throw an exception upon timeout (10 seconds).
484    */
485   public static void grantOnTableUsingAccessControlClient(final HBaseTestingUtility util,
486       final Configuration conf, final String user, final TableName table, final byte[] family,
487       final byte[] qualifier, final Permission.Action... actions) throws Exception {
488     SecureTestUtil.updateACLs(util, new Callable<Void>() {
489       @Override
490       public Void call() throws Exception {
491         try {
492           AccessControlClient.grant(conf, table, user, family, qualifier, actions);
493         } catch (Throwable t) {
494           t.printStackTrace();
495         }
496         return null;
497       }
498     });
499   }
500 
501   /**
502    * Grant global permissions to the given user using AccessControlClient. Will wait until all
503    * active AccessController instances have updated their permissions caches or will
504    * throw an exception upon timeout (10 seconds).
505    */
506   public static void grantGlobalUsingAccessControlClient(final HBaseTestingUtility util,
507       final Configuration conf, final String user, final Permission.Action... actions)
508       throws Exception {
509     SecureTestUtil.updateACLs(util, new Callable<Void>() {
510       @Override
511       public Void call() throws Exception {
512         try {
513           AccessControlClient.grant(conf, user, actions);
514         } catch (Throwable t) {
515           t.printStackTrace();
516         }
517         return null;
518       }
519     });
520   }
521 
522   /**
523    * Revoke permissions on a table from the given user. Will wait until all active
524    * AccessController instances have updated their permissions caches or will
525    * throw an exception upon timeout (10 seconds).
526    */
527   public static void revokeFromTable(final HBaseTestingUtility util, final String user,
528       final TableName table, final byte[] family, final byte[] qualifier,
529       final Permission.Action... actions) throws Exception {
530     SecureTestUtil.updateACLs(util, new Callable<Void>() {
531       @Override
532       public Void call() throws Exception {
533         try (Connection connection = ConnectionFactory.createConnection(util.getConfiguration())) {
534           try (Table acl = connection.getTable(AccessControlLists.ACL_TABLE_NAME)) {
535             BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
536             AccessControlService.BlockingInterface protocol =
537                 AccessControlService.newBlockingStub(service);
538             ProtobufUtil.revoke(protocol, user, table, family, qualifier, actions);
539           }
540         }
541         return null;
542       }
543     });
544   }
545 
546   /**
547    * Revoke permissions on a table from the given user using AccessControlClient. Will wait until
548    * all active AccessController instances have updated their permissions caches or will
549    * throw an exception upon timeout (10 seconds).
550    */
551   public static void revokeFromTableUsingAccessControlClient(final HBaseTestingUtility util,
552       final Configuration conf, final String user, final TableName table, final byte[] family,
553       final byte[] qualifier, final Permission.Action... actions) throws Exception {
554     SecureTestUtil.updateACLs(util, new Callable<Void>() {
555       @Override
556       public Void call() throws Exception {
557         try {
558           AccessControlClient.revoke(conf, table, user, family, qualifier, actions);
559         } catch (Throwable t) {
560           t.printStackTrace();
561         }
562         return null;
563       }
564     });
565   }
566 
567   /**
568    * Revoke global permissions from the given user using AccessControlClient. Will wait until
569    * all active AccessController instances have updated their permissions caches or will
570    * throw an exception upon timeout (10 seconds).
571    */
572   public static void revokeGlobalUsingAccessControlClient(final HBaseTestingUtility util,
573       final Configuration conf, final String user,final Permission.Action... actions)
574       throws Exception {
575     SecureTestUtil.updateACLs(util, new Callable<Void>() {
576       @Override
577       public Void call() throws Exception {
578         try {
579           AccessControlClient.revoke(conf, user, actions);
580         } catch (Throwable t) {
581           t.printStackTrace();
582         }
583         return null;
584       }
585     });
586   }
587 }