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