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.fail;
22  
23  import java.io.IOException;
24  import java.lang.reflect.UndeclaredThrowableException;
25  import java.security.PrivilegedActionException;
26  import java.security.PrivilegedExceptionAction;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.concurrent.Callable;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.apache.hadoop.conf.Configuration;
34  import org.apache.hadoop.hbase.Coprocessor;
35  import org.apache.hadoop.hbase.HBaseTestingUtility;
36  import org.apache.hadoop.hbase.HConstants;
37  import org.apache.hadoop.hbase.MiniHBaseCluster;
38  import org.apache.hadoop.hbase.TableName;
39  import org.apache.hadoop.hbase.Waiter.Predicate;
40  import org.apache.hadoop.hbase.client.HTable;
41  import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException;
42  import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
43  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
44  import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos.AccessControlService;
45  import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos.CheckPermissionsRequest;
46  import org.apache.hadoop.hbase.regionserver.HRegion;
47  import org.apache.hadoop.hbase.security.AccessDeniedException;
48  import org.apache.hadoop.hbase.security.User;
49  import org.apache.hadoop.hbase.util.JVMClusterUtil.RegionServerThread;
50  
51  import com.google.common.collect.Lists;
52  import com.google.common.collect.Maps;
53  import com.google.protobuf.BlockingRpcChannel;
54  import com.google.protobuf.ServiceException;
55  
56  /**
57   * Utility methods for testing security
58   */
59  public class SecureTestUtil {
60    private static final Log LOG = LogFactory.getLog(SecureTestUtil.class);
61    private static final int WAIT_TIME = 10000;
62  
63    public static void enableSecurity(Configuration conf) throws IOException {
64      conf.set("hadoop.security.authorization", "false");
65      conf.set("hadoop.security.authentication", "simple");
66      conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, AccessController.class.getName());
67      conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, AccessController.class.getName() +
68        "," + SecureBulkLoadEndpoint.class.getName());
69      conf.set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY, AccessController.class.getName());
70      // The secure minicluster creates separate service principals based on the
71      // current user's name, one for each slave. We need to add all of these to
72      // the superuser list or security won't function properly. We expect the
73      // HBase service account(s) to have superuser privilege.
74      String currentUser = User.getCurrent().getName();
75      StringBuffer sb = new StringBuffer();
76      sb.append("admin,");
77      sb.append(currentUser);
78      // Assumes we won't ever have a minicluster with more than 5 slaves
79      for (int i = 0; i < 5; i++) {
80        sb.append(',');
81        sb.append(currentUser); sb.append(".hfs."); sb.append(i);
82      }
83      conf.set("hbase.superuser", sb.toString());
84      // Need HFile V3 for tags for security features
85      conf.setInt("hfile.format.version", 3);
86    }
87  
88    public void checkTablePerms(Configuration conf, byte[] table, byte[] family, byte[] column,
89        Permission.Action... actions) throws IOException {
90      Permission[] perms = new Permission[actions.length];
91      for (int i = 0; i < actions.length; i++) {
92        perms[i] = new TablePermission(TableName.valueOf(table), family, column, actions[i]);
93      }
94  
95      checkTablePerms(conf, table, perms);
96    }
97  
98    public void checkTablePerms(Configuration conf, byte[] table, Permission... perms) throws IOException {
99      CheckPermissionsRequest.Builder request = CheckPermissionsRequest.newBuilder();
100     for (Permission p : perms) {
101       request.addPermission(ProtobufUtil.toPermission(p));
102     }
103     HTable acl = new HTable(conf, table);
104     try {
105       AccessControlService.BlockingInterface protocol =
106         AccessControlService.newBlockingStub(acl.coprocessorService(new byte[0]));
107       try {
108         protocol.checkPermissions(null, request.build());
109       } catch (ServiceException se) {
110         ProtobufUtil.toIOException(se);
111       }
112     } finally {
113       acl.close();
114     }
115   }
116 
117   /**
118    * An AccessTestAction performs an action that will be examined to confirm
119    * the results conform to expected access rights.
120    * <p>
121    * To indicate an action was allowed, return null or a non empty list of
122    * KeyValues.
123    * <p>
124    * To indicate the action was not allowed, either throw an AccessDeniedException
125    * or return an empty list of KeyValues.
126    */
127   static interface AccessTestAction extends PrivilegedExceptionAction<Object> { }
128 
129   public void verifyAllowed(User user, AccessTestAction... actions) throws Exception {
130     for (AccessTestAction action : actions) {
131       try {
132         Object obj = user.runAs(action);
133         if (obj != null && obj instanceof List<?>) {
134           List<?> results = (List<?>) obj;
135           if (results != null && results.isEmpty()) {
136             fail("Empty non null results from action for user '" + user.getShortName() + "'");
137           }
138         }
139       } catch (AccessDeniedException ade) {
140         fail("Expected action to pass for user '" + user.getShortName() + "' but was denied");
141       }
142     }
143   }
144 
145   public void verifyAllowed(AccessTestAction action, User... users) throws Exception {
146     for (User user : users) {
147       verifyAllowed(user, action);
148     }
149   }
150 
151   public void verifyDenied(User user, AccessTestAction... actions) throws Exception {
152     for (AccessTestAction action : actions) {
153       try {
154         Object obj = user.runAs(action);
155         if (obj != null && obj instanceof List<?>) {
156           List<?> results = (List<?>) obj;
157           if (results != null && !results.isEmpty()) {
158             fail("Expected no results for user '" + user.getShortName() + "'");
159           }
160         }
161       } catch (IOException e) {
162         boolean isAccessDeniedException = false;
163         if(e instanceof RetriesExhaustedWithDetailsException) {
164           // in case of batch operations, and put, the client assembles a
165           // RetriesExhaustedWithDetailsException instead of throwing an
166           // AccessDeniedException
167           for(Throwable ex : ((RetriesExhaustedWithDetailsException) e).getCauses()) {
168             if (ex instanceof AccessDeniedException) {
169               isAccessDeniedException = true;
170               break;
171             }
172           }
173         }
174         else {
175           // For doBulkLoad calls AccessDeniedException
176           // is buried in the stack trace
177           Throwable ex = e;
178           do {
179             if (ex instanceof AccessDeniedException) {
180               isAccessDeniedException = true;
181               break;
182             }
183           } while((ex = ex.getCause()) != null);
184         }
185         if (!isAccessDeniedException) {
186           fail("Not receiving AccessDeniedException for user '" + user.getShortName() + "'");
187         }
188       } catch (UndeclaredThrowableException ute) {
189         // TODO why we get a PrivilegedActionException, which is unexpected?
190         Throwable ex = ute.getUndeclaredThrowable();
191         if (ex instanceof PrivilegedActionException) {
192           ex = ((PrivilegedActionException) ex).getException();
193         }
194         if (ex instanceof ServiceException) {
195           ServiceException se = (ServiceException)ex;
196           if (se.getCause() != null && se.getCause() instanceof AccessDeniedException) {
197             // expected result
198             return;
199           }
200         }
201         fail("Not receiving AccessDeniedException for user '" + user.getShortName() + "'");
202       }
203     }
204   }
205 
206   public void verifyDenied(AccessTestAction action, User... users) throws Exception {
207     for (User user : users) {
208       verifyDenied(user, action);
209     }
210   }
211 
212   private static List<AccessController> getAccessControllers(MiniHBaseCluster cluster) {
213     List<AccessController> result = Lists.newArrayList();
214     for (RegionServerThread t: cluster.getLiveRegionServerThreads()) {
215       for (HRegion region: t.getRegionServer().getOnlineRegionsLocalContext()) {
216         Coprocessor cp = region.getCoprocessorHost()
217           .findCoprocessor(AccessController.class.getName());
218         if (cp != null) {
219           result.add((AccessController)cp);
220         }
221       }
222     }
223     return result;
224   }
225 
226   private static Map<AccessController,Long> getAuthManagerMTimes(MiniHBaseCluster cluster) {
227     Map<AccessController,Long> result = Maps.newHashMap();
228     for (AccessController ac: getAccessControllers(cluster)) {
229       result.put(ac, ac.getAuthManager().getMTime());
230     }
231     return result;
232   }
233 
234   @SuppressWarnings("rawtypes")
235   private static void updateACLs(final HBaseTestingUtility util, Callable c) throws Exception {
236     // Get the current mtimes for all access controllers
237     final Map<AccessController,Long> oldMTimes = getAuthManagerMTimes(util.getHBaseCluster());
238 
239     // Run the update action
240     c.call();
241 
242     // Wait until mtimes for all access controllers have incremented
243     util.waitFor(WAIT_TIME, 100, new Predicate<IOException>() {
244       @Override
245       public boolean evaluate() throws IOException {
246         Map<AccessController,Long> mtimes = getAuthManagerMTimes(util.getHBaseCluster());
247         for (Map.Entry<AccessController,Long> e: mtimes.entrySet()) {
248           if (!oldMTimes.containsKey(e.getKey())) {
249             LOG.error("Snapshot of AccessController state does not include instance on region " +
250               e.getKey().getRegion().getRegionNameAsString());
251             // Error out the predicate, we will try again
252             return false;
253           }
254           long old = oldMTimes.get(e.getKey());
255           long now = e.getValue();
256           if (now <= old) {
257             LOG.info("AccessController on region " +
258               e.getKey().getRegion().getRegionNameAsString() + " has not updated: mtime=" +
259               now);
260             return false;
261           }
262         }
263         return true;
264       }
265     });
266   }
267 
268   /**
269    * Grant permissions globally to the given user. Will wait until all active
270    * AccessController instances have updated their permissions caches or will
271    * throw an exception upon timeout (10 seconds).
272    */
273   public static void grantGlobal(final HBaseTestingUtility util, final String user,
274       final Permission.Action... actions) throws Exception {
275     SecureTestUtil.updateACLs(util, new Callable<Void>() {
276       @Override
277       public Void call() throws Exception {
278         HTable acl = new HTable(util.getConfiguration(), AccessControlLists.ACL_TABLE_NAME);
279         try {
280           BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
281           AccessControlService.BlockingInterface protocol =
282               AccessControlService.newBlockingStub(service);
283           ProtobufUtil.grant(protocol, user, actions);
284         } finally {
285           acl.close();
286         }
287         return null;
288       }
289     });
290   }
291 
292   /**
293    * Revoke permissions globally from the given user. Will wait until all active
294    * AccessController instances have updated their permissions caches or will
295    * throw an exception upon timeout (10 seconds).
296    */
297   public static void revokeGlobal(final HBaseTestingUtility util, final String user,
298       final Permission.Action... actions) throws Exception {
299     SecureTestUtil.updateACLs(util, new Callable<Void>() {
300       @Override
301       public Void call() throws Exception {
302         HTable acl = new HTable(util.getConfiguration(), AccessControlLists.ACL_TABLE_NAME);
303         try {
304           BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
305           AccessControlService.BlockingInterface protocol =
306               AccessControlService.newBlockingStub(service);
307           ProtobufUtil.revoke(protocol, user, actions);
308         } finally {
309           acl.close();
310         }
311         return null;
312       }
313     });
314   }
315 
316   /**
317    * Grant permissions on a namespace to the given user. Will wait until all active
318    * AccessController instances have updated their permissions caches or will
319    * throw an exception upon timeout (10 seconds).
320    */
321   public static void grantOnNamespace(final HBaseTestingUtility util, final String user,
322       final String namespace, final Permission.Action... actions) throws Exception {
323     SecureTestUtil.updateACLs(util, new Callable<Void>() {
324       @Override
325       public Void call() throws Exception {
326         HTable acl = new HTable(util.getConfiguration(), AccessControlLists.ACL_TABLE_NAME);
327         try {
328           BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
329           AccessControlService.BlockingInterface protocol =
330               AccessControlService.newBlockingStub(service);
331           ProtobufUtil.grant(protocol, user, namespace, actions);
332         } finally {
333           acl.close();
334         }
335         return null;
336       }
337     });
338   }
339 
340   /**
341    * Revoke permissions on a namespace from the given user. Will wait until all active
342    * AccessController instances have updated their permissions caches or will
343    * throw an exception upon timeout (10 seconds).
344    */
345   public static void revokeFromNamespace(final HBaseTestingUtility util, final String user,
346       final String namespace, final Permission.Action... actions) throws Exception {
347     SecureTestUtil.updateACLs(util, new Callable<Void>() {
348       @Override
349       public Void call() throws Exception {
350         HTable acl = new HTable(util.getConfiguration(), AccessControlLists.ACL_TABLE_NAME);
351         try {
352           BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
353           AccessControlService.BlockingInterface protocol =
354               AccessControlService.newBlockingStub(service);
355           ProtobufUtil.revoke(protocol, user, namespace, actions);
356         } finally {
357           acl.close();
358         }
359         return null;
360       }
361     });
362   }
363 
364   /**
365    * Grant permissions on a table to the given user. Will wait until all active
366    * AccessController instances have updated their permissions caches or will
367    * throw an exception upon timeout (10 seconds).
368    */
369   public static void grantOnTable(final HBaseTestingUtility util, final String user,
370       final TableName table, final byte[] family, final byte[] qualifier,
371       final Permission.Action... actions) throws Exception {
372     SecureTestUtil.updateACLs(util, new Callable<Void>() {
373       @Override
374       public Void call() throws Exception {
375         HTable acl = new HTable(util.getConfiguration(), AccessControlLists.ACL_TABLE_NAME);
376         try {
377           BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
378           AccessControlService.BlockingInterface protocol =
379               AccessControlService.newBlockingStub(service);
380           ProtobufUtil.grant(protocol, user, table, family, qualifier, actions);
381         } finally {
382           acl.close();
383         }
384         return null;
385       }
386     });
387   }
388 
389   /**
390    * Revoke permissions on a table from the given user. Will wait until all active
391    * AccessController instances have updated their permissions caches or will
392    * throw an exception upon timeout (10 seconds).
393    */
394   public static void revokeFromTable(final HBaseTestingUtility util, final String user,
395       final TableName table, final byte[] family, final byte[] qualifier,
396       final Permission.Action... actions) throws Exception {
397     SecureTestUtil.updateACLs(util, new Callable<Void>() {
398       @Override
399       public Void call() throws Exception {
400         HTable acl = new HTable(util.getConfiguration(), AccessControlLists.ACL_TABLE_NAME);
401         try {
402           BlockingRpcChannel service = acl.coprocessorService(HConstants.EMPTY_START_ROW);
403           AccessControlService.BlockingInterface protocol =
404               AccessControlService.newBlockingStub(service);
405           ProtobufUtil.revoke(protocol, user, table, family, qualifier, actions);
406         } finally {
407           acl.close();
408         }
409         return null;
410       }
411     });
412   }
413 }