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    * Revoke permissions on a namespace from the given user. Will wait until all active
394    * AccessController instances have updated their permissions caches or will
395    * throw an exception upon timeout (10 seconds).
396    */
397   public static void revokeFromNamespace(final HBaseTestingUtility util, final String user,
398       final String namespace, final Permission.Action... actions) throws Exception {
399     SecureTestUtil.updateACLs(util, new Callable<Void>() {
400       @Override
401       public Void call() throws Exception {
402         HTable acl = new HTable(util.getConfiguration(), AccessControlLists.ACL_TABLE_NAME);
403         try {
404           BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
405           AccessControlService.BlockingInterface protocol =
406               AccessControlService.newBlockingStub(service);
407           ProtobufUtil.revoke(protocol, user, namespace, actions);
408         } finally {
409           acl.close();
410         }
411         return null;
412       }
413     });
414   }
415 
416   /**
417    * Grant permissions on a table to the given user. Will wait until all active
418    * AccessController instances have updated their permissions caches or will
419    * throw an exception upon timeout (10 seconds).
420    */
421   public static void grantOnTable(final HBaseTestingUtility util, final String user,
422       final TableName table, final byte[] family, final byte[] qualifier,
423       final Permission.Action... actions) throws Exception {
424     SecureTestUtil.updateACLs(util, new Callable<Void>() {
425       @Override
426       public Void call() throws Exception {
427         HTable acl = new HTable(util.getConfiguration(), AccessControlLists.ACL_TABLE_NAME);
428         try {
429           BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
430           AccessControlService.BlockingInterface protocol =
431               AccessControlService.newBlockingStub(service);
432           ProtobufUtil.grant(protocol, user, table, family, qualifier, actions);
433         } finally {
434           acl.close();
435         }
436         return null;
437       }
438     });
439   }
440 
441   /**
442    * Revoke permissions on a table from the given user. Will wait until all active
443    * AccessController instances have updated their permissions caches or will
444    * throw an exception upon timeout (10 seconds).
445    */
446   public static void revokeFromTable(final HBaseTestingUtility util, final String user,
447       final TableName table, final byte[] family, final byte[] qualifier,
448       final Permission.Action... actions) throws Exception {
449     SecureTestUtil.updateACLs(util, new Callable<Void>() {
450       @Override
451       public Void call() throws Exception {
452         HTable acl = new HTable(util.getConfiguration(), AccessControlLists.ACL_TABLE_NAME);
453         try {
454           BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
455           AccessControlService.BlockingInterface protocol =
456               AccessControlService.newBlockingStub(service);
457           ProtobufUtil.revoke(protocol, user, table, family, qualifier, actions);
458         } finally {
459           acl.close();
460         }
461         return null;
462       }
463     });
464   }
465 }