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 java.io.IOException;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.concurrent.ConcurrentSkipListMap;
26  
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  import org.apache.hadoop.conf.Configuration;
30  import org.apache.hadoop.hbase.Cell;
31  import org.apache.hadoop.hbase.TableName;
32  import org.apache.hadoop.hbase.exceptions.DeserializationException;
33  import org.apache.hadoop.hbase.security.User;
34  import org.apache.hadoop.hbase.security.UserProvider;
35  import org.apache.hadoop.hbase.util.Bytes;
36  import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
37  import org.apache.zookeeper.KeeperException;
38  
39  import com.google.common.collect.ArrayListMultimap;
40  import com.google.common.collect.ListMultimap;
41  import com.google.common.collect.Lists;
42  
43  /**
44   * Performs authorization checks for a given user's assigned permissions
45   */
46  public class TableAuthManager {
47    private static class PermissionCache<T extends Permission> {
48      /** Cache of user permissions */
49      private ListMultimap<String,T> userCache = ArrayListMultimap.create();
50      /** Cache of group permissions */
51      private ListMultimap<String,T> groupCache = ArrayListMultimap.create();
52  
53      public List<T> getUser(String user) {
54        return userCache.get(user);
55      }
56  
57      public void putUser(String user, T perm) {
58        userCache.put(user, perm);
59      }
60  
61      public List<T> replaceUser(String user, Iterable<? extends T> perms) {
62        return userCache.replaceValues(user, perms);
63      }
64  
65      public List<T> getGroup(String group) {
66        return groupCache.get(group);
67      }
68  
69      public void putGroup(String group, T perm) {
70        groupCache.put(group, perm);
71      }
72  
73      public List<T> replaceGroup(String group, Iterable<? extends T> perms) {
74        return groupCache.replaceValues(group, perms);
75      }
76  
77      /**
78       * Returns a combined map of user and group permissions, with group names prefixed by
79       * {@link AccessControlLists#GROUP_PREFIX}.
80       */
81      public ListMultimap<String,T> getAllPermissions() {
82        ListMultimap<String,T> tmp = ArrayListMultimap.create();
83        tmp.putAll(userCache);
84        for (String group : groupCache.keySet()) {
85          tmp.putAll(AccessControlLists.GROUP_PREFIX + group, groupCache.get(group));
86        }
87        return tmp;
88      }
89    }
90  
91    private static Log LOG = LogFactory.getLog(TableAuthManager.class);
92  
93    private static TableAuthManager instance;
94  
95    /** Cache of global permissions */
96    private volatile PermissionCache<Permission> globalCache;
97  
98    private ConcurrentSkipListMap<TableName, PermissionCache<TablePermission>> tableCache =
99        new ConcurrentSkipListMap<TableName, PermissionCache<TablePermission>>();
100 
101   private ConcurrentSkipListMap<String, PermissionCache<TablePermission>> nsCache =
102     new ConcurrentSkipListMap<String, PermissionCache<TablePermission>>();
103 
104   private Configuration conf;
105   private ZKPermissionWatcher zkperms;
106   private volatile long mtime;
107 
108   private TableAuthManager(ZooKeeperWatcher watcher, Configuration conf)
109       throws IOException {
110     this.conf = conf;
111 
112     // initialize global permissions based on configuration
113     globalCache = initGlobal(conf);
114 
115     this.zkperms = new ZKPermissionWatcher(watcher, this, conf);
116     try {
117       this.zkperms.start();
118     } catch (KeeperException ke) {
119       LOG.error("ZooKeeper initialization failed", ke);
120     }
121   }
122 
123   /**
124    * Returns a new {@code PermissionCache} initialized with permission assignments
125    * from the {@code hbase.superuser} configuration key.
126    */
127   private PermissionCache<Permission> initGlobal(Configuration conf) throws IOException {
128     UserProvider userProvider = UserProvider.instantiate(conf);
129     User user = userProvider.getCurrent();
130     if (user == null) {
131       throw new IOException("Unable to obtain the current user, " +
132           "authorization checks for internal operations will not work correctly!");
133     }
134     PermissionCache<Permission> newCache = new PermissionCache<Permission>();
135     String currentUser = user.getShortName();
136 
137     // the system user is always included
138     List<String> superusers = Lists.asList(currentUser, conf.getStrings(
139         AccessControlLists.SUPERUSER_CONF_KEY, new String[0]));
140     if (superusers != null) {
141       for (String name : superusers) {
142         if (AccessControlLists.isGroupPrincipal(name)) {
143           newCache.putGroup(AccessControlLists.getGroupName(name),
144               new Permission(Permission.Action.values()));
145         } else {
146           newCache.putUser(name, new Permission(Permission.Action.values()));
147         }
148       }
149     }
150     return newCache;
151   }
152 
153   public ZKPermissionWatcher getZKPermissionWatcher() {
154     return this.zkperms;
155   }
156 
157   public void refreshTableCacheFromWritable(TableName table,
158                                        byte[] data) throws IOException {
159     if (data != null && data.length > 0) {
160       ListMultimap<String,TablePermission> perms;
161       try {
162         perms = AccessControlLists.readPermissions(data, conf);
163       } catch (DeserializationException e) {
164         throw new IOException(e);
165       }
166 
167       if (perms != null) {
168         if (Bytes.equals(table.getName(), AccessControlLists.ACL_GLOBAL_NAME)) {
169           updateGlobalCache(perms);
170         } else {
171           updateTableCache(table, perms);
172         }
173       }
174     } else {
175       LOG.debug("Skipping permission cache refresh because writable data is empty");
176     }
177   }
178 
179   public void refreshNamespaceCacheFromWritable(String namespace, byte[] data) throws IOException {
180     if (data != null && data.length > 0) {
181       ListMultimap<String,TablePermission> perms;
182       try {
183         perms = AccessControlLists.readPermissions(data, conf);
184       } catch (DeserializationException e) {
185         throw new IOException(e);
186       }
187       if (perms != null) {
188         updateNsCache(namespace, perms);
189       }
190     } else {
191       LOG.debug("Skipping permission cache refresh because writable data is empty");
192     }
193   }
194 
195   /**
196    * Updates the internal global permissions cache
197    *
198    * @param userPerms
199    */
200   private void updateGlobalCache(ListMultimap<String,TablePermission> userPerms) {
201     PermissionCache<Permission> newCache = null;
202     try {
203       newCache = initGlobal(conf);
204       for (Map.Entry<String,TablePermission> entry : userPerms.entries()) {
205         if (AccessControlLists.isGroupPrincipal(entry.getKey())) {
206           newCache.putGroup(AccessControlLists.getGroupName(entry.getKey()),
207               new Permission(entry.getValue().getActions()));
208         } else {
209           newCache.putUser(entry.getKey(), new Permission(entry.getValue().getActions()));
210         }
211       }
212       globalCache = newCache;
213       mtime++;
214     } catch (IOException e) {
215       // Never happens
216       LOG.error("Error occured while updating the global cache", e);
217     }
218   }
219 
220   /**
221    * Updates the internal permissions cache for a single table, splitting
222    * the permissions listed into separate caches for users and groups to optimize
223    * group lookups.
224    * 
225    * @param table
226    * @param tablePerms
227    */
228   private void updateTableCache(TableName table,
229                                 ListMultimap<String,TablePermission> tablePerms) {
230     PermissionCache<TablePermission> newTablePerms = new PermissionCache<TablePermission>();
231 
232     for (Map.Entry<String,TablePermission> entry : tablePerms.entries()) {
233       if (AccessControlLists.isGroupPrincipal(entry.getKey())) {
234         newTablePerms.putGroup(AccessControlLists.getGroupName(entry.getKey()), entry.getValue());
235       } else {
236         newTablePerms.putUser(entry.getKey(), entry.getValue());
237       }
238     }
239 
240     tableCache.put(table, newTablePerms);
241     mtime++;
242   }
243 
244   /**
245    * Updates the internal permissions cache for a single table, splitting
246    * the permissions listed into separate caches for users and groups to optimize
247    * group lookups.
248    *
249    * @param namespace
250    * @param tablePerms
251    */
252   private void updateNsCache(String namespace,
253                              ListMultimap<String, TablePermission> tablePerms) {
254     PermissionCache<TablePermission> newTablePerms = new PermissionCache<TablePermission>();
255 
256     for (Map.Entry<String, TablePermission> entry : tablePerms.entries()) {
257       if (AccessControlLists.isGroupPrincipal(entry.getKey())) {
258         newTablePerms.putGroup(AccessControlLists.getGroupName(entry.getKey()), entry.getValue());
259       } else {
260         newTablePerms.putUser(entry.getKey(), entry.getValue());
261       }
262     }
263 
264     nsCache.put(namespace, newTablePerms);
265     mtime++;
266   }
267 
268   private PermissionCache<TablePermission> getTablePermissions(TableName table) {
269     if (!tableCache.containsKey(table)) {
270       tableCache.putIfAbsent(table, new PermissionCache<TablePermission>());
271     }
272     return tableCache.get(table);
273   }
274 
275   private PermissionCache<TablePermission> getNamespacePermissions(String namespace) {
276     if (!nsCache.containsKey(namespace)) {
277       nsCache.putIfAbsent(namespace, new PermissionCache<TablePermission>());
278     }
279     return nsCache.get(namespace);
280   }
281 
282   /**
283    * Authorizes a global permission
284    * @param perms
285    * @param action
286    * @return true if authorized, false otherwise
287    */
288   private boolean authorize(List<Permission> perms, Permission.Action action) {
289     if (perms != null) {
290       for (Permission p : perms) {
291         if (p.implies(action)) {
292           return true;
293         }
294       }
295     } else if (LOG.isDebugEnabled()) {
296       LOG.debug("No permissions found");
297     }
298 
299     return false;
300   }
301 
302   /**
303    * Authorize a global permission based on ACLs for the given user and the
304    * user's groups.
305    * @param user
306    * @param action
307    * @return true if known and authorized, false otherwise
308    */
309   public boolean authorize(User user, Permission.Action action) {
310     if (user == null) {
311       return false;
312     }
313 
314     if (authorize(globalCache.getUser(user.getShortName()), action)) {
315       return true;
316     }
317 
318     String[] groups = user.getGroupNames();
319     if (groups != null) {
320       for (String group : groups) {
321         if (authorize(globalCache.getGroup(group), action)) {
322           return true;
323         }
324       }
325     }
326     return false;
327   }
328 
329   private boolean authorize(List<TablePermission> perms,
330                             TableName table, byte[] family,
331                             Permission.Action action) {
332     return authorize(perms, table, family, null, action);
333   }
334 
335   private boolean authorize(List<TablePermission> perms,
336                             TableName table, byte[] family,
337                             byte[] qualifier, Permission.Action action) {
338     if (perms != null) {
339       for (TablePermission p : perms) {
340         if (p.implies(table, family, qualifier, action)) {
341           return true;
342         }
343       }
344     } else if (LOG.isDebugEnabled()) {
345       LOG.debug("No permissions found for table="+table);
346     }
347     return false;
348   }
349 
350   /**
351    * Authorize a user for a given KV. This is called from AccessControlFilter.
352    */
353   public boolean authorize(User user, TableName table, Cell cell, Permission.Action action) {
354     try {
355       List<Permission> perms = AccessControlLists.getCellPermissionsForUser(user, cell);
356       if (LOG.isTraceEnabled()) {
357         LOG.trace("Perms for user " + user.getShortName() + " in cell " + cell + ": " + perms);
358       }
359       for (Permission p: perms) {
360         if (p.implies(action)) {
361           return true;
362         }
363       }
364     } catch (IOException e) {
365       // We failed to parse the KV tag
366       LOG.error("Failed parse of ACL tag in cell " + cell);
367       // Fall through to check with the table and CF perms we were able
368       // to collect regardless
369     }
370     return false;
371   }
372 
373   public boolean authorize(User user, String namespace, Permission.Action action) {
374     // Global authorizations supercede namespace level
375     if (authorizeUser(user, action)) {
376       return true;
377     }
378     // Check namespace permissions
379     PermissionCache<TablePermission> tablePerms = nsCache.get(namespace);
380     if (tablePerms != null) {
381       List<TablePermission> userPerms = tablePerms.getUser(user.getShortName());
382       if (authorize(userPerms, namespace, action)) {
383         return true;
384       }
385       String[] groupNames = user.getGroupNames();
386       if (groupNames != null) {
387         for (String group : groupNames) {
388           List<TablePermission> groupPerms = tablePerms.getGroup(group);
389           if (authorize(groupPerms, namespace, action)) {
390             return true;
391           }
392         }
393       }
394     }
395     return false;
396   }
397 
398   private boolean authorize(List<TablePermission> perms, String namespace,
399                             Permission.Action action) {
400     if (perms != null) {
401       for (TablePermission p : perms) {
402         if (p.implies(namespace, action)) {
403           return true;
404         }
405       }
406     } else if (LOG.isDebugEnabled()) {
407       LOG.debug("No permissions for authorize() check, table=" + namespace);
408     }
409 
410     return false;
411   }
412 
413   /**
414    * Checks global authorization for a specific action for a user, based on the
415    * stored user permissions.
416    */
417   public boolean authorizeUser(User user, Permission.Action action) {
418     return authorize(globalCache.getUser(user.getShortName()), action);
419   }
420 
421   /**
422    * Checks authorization to a given table and column family for a user, based on the
423    * stored user permissions.
424    *
425    * @param user
426    * @param table
427    * @param family
428    * @param action
429    * @return true if known and authorized, false otherwise
430    */
431   public boolean authorizeUser(User user, TableName table, byte[] family,
432       Permission.Action action) {
433     return authorizeUser(user, table, family, null, action);
434   }
435 
436   public boolean authorizeUser(User user, TableName table, byte[] family,
437       byte[] qualifier, Permission.Action action) {
438     if (table == null) table = AccessControlLists.ACL_TABLE_NAME;
439     // Global and namespace authorizations supercede table level
440     if (authorize(user, table.getNamespaceAsString(), action)) {    
441       return true;
442     }
443     // Check table permissions
444     return authorize(getTablePermissions(table).getUser(user.getShortName()), table, family,
445         qualifier, action);
446   }
447 
448   /**
449    * Checks global authorization for a given action for a group, based on the stored
450    * permissions.
451    */
452   public boolean authorizeGroup(String groupName, Permission.Action action) {
453     return authorize(globalCache.getGroup(groupName), action);
454   }
455 
456   /**
457    * Checks authorization to a given table and column family for a group, based
458    * on the stored permissions. 
459    * @param groupName
460    * @param table
461    * @param family
462    * @param action
463    * @return true if known and authorized, false otherwise
464    */
465   public boolean authorizeGroup(String groupName, TableName table, byte[] family,
466       Permission.Action action) {
467     // Global authorization supercedes table level
468     if (authorizeGroup(groupName, action)) {
469       return true;
470     }
471     if (table == null) table = AccessControlLists.ACL_TABLE_NAME;
472     // Namespace authorization supercedes table level
473     if (authorize(getNamespacePermissions(table.getNamespaceAsString()).getGroup(groupName),
474         table, family, action)) {
475       return true;
476     }
477     // Check table level
478     return authorize(getTablePermissions(table).getGroup(groupName), table, family, action);
479   }
480 
481   public boolean authorize(User user, TableName table, byte[] family,
482       byte[] qualifier, Permission.Action action) {
483     if (authorizeUser(user, table, family, qualifier, action)) {
484       return true;
485     }
486 
487     String[] groups = user.getGroupNames();
488     if (groups != null) {
489       for (String group : groups) {
490         if (authorizeGroup(group, table, family, action)) {
491           return true;
492         }
493       }
494     }
495     return false;
496   }
497 
498   public boolean authorize(User user, TableName table, byte[] family,
499       Permission.Action action) {
500     return authorize(user, table, family, null, action);
501   }
502 
503   /**
504    * Returns true if the given user has a {@link TablePermission} matching up
505    * to the column family portion of a permission.  Note that this permission
506    * may be scoped to a given column qualifier and does not guarantee that
507    * authorize() on the same column family would return true.
508    */
509   public boolean matchPermission(User user,
510       TableName table, byte[] family, Permission.Action action) {
511     PermissionCache<TablePermission> tablePerms = tableCache.get(table);
512     if (tablePerms != null) {
513       List<TablePermission> userPerms = tablePerms.getUser(user.getShortName());
514       if (userPerms != null) {
515         for (TablePermission p : userPerms) {
516           if (p.matchesFamily(table, family, action)) {
517             return true;
518           }
519         }
520       }
521 
522       String[] groups = user.getGroupNames();
523       if (groups != null) {
524         for (String group : groups) {
525           List<TablePermission> groupPerms = tablePerms.getGroup(group);
526           if (groupPerms != null) {
527             for (TablePermission p : groupPerms) {
528               if (p.matchesFamily(table, family, action)) {
529                 return true;
530               }
531             }
532           }
533         }
534       }
535     }
536 
537     return false;
538   }
539 
540   public boolean matchPermission(User user,
541       TableName table, byte[] family, byte[] qualifier,
542       Permission.Action action) {
543     PermissionCache<TablePermission> tablePerms = tableCache.get(table);
544     if (tablePerms != null) {
545       List<TablePermission> userPerms = tablePerms.getUser(user.getShortName());
546       if (userPerms != null) {
547         for (TablePermission p : userPerms) {
548           if (p.matchesFamilyQualifier(table, family, qualifier, action)) {
549             return true;
550           }
551         }
552       }
553 
554       String[] groups = user.getGroupNames();
555       if (groups != null) {
556         for (String group : groups) {
557           List<TablePermission> groupPerms = tablePerms.getGroup(group);
558           if (groupPerms != null) {
559             for (TablePermission p : groupPerms) {
560               if (p.matchesFamilyQualifier(table, family, qualifier, action)) {
561                 return true;
562               }
563             }
564           }
565         }
566       }
567     }
568     return false;
569   }
570 
571   public void removeNamespace(byte[] ns) {
572     nsCache.remove(Bytes.toString(ns));
573   }
574 
575   public void removeTable(TableName table) {
576     tableCache.remove(table);
577   }
578 
579   /**
580    * Overwrites the existing permission set for a given user for a table, and
581    * triggers an update for zookeeper synchronization.
582    * @param username
583    * @param table
584    * @param perms
585    */
586   public void setTableUserPermissions(String username, TableName table,
587       List<TablePermission> perms) {
588     PermissionCache<TablePermission> tablePerms = getTablePermissions(table);
589     tablePerms.replaceUser(username, perms);
590     writeTableToZooKeeper(table, tablePerms);
591   }
592 
593   /**
594    * Overwrites the existing permission set for a group and triggers an update
595    * for zookeeper synchronization.
596    * @param group
597    * @param table
598    * @param perms
599    */
600   public void setTableGroupPermissions(String group, TableName table,
601       List<TablePermission> perms) {
602     PermissionCache<TablePermission> tablePerms = getTablePermissions(table);
603     tablePerms.replaceGroup(group, perms);
604     writeTableToZooKeeper(table, tablePerms);
605   }
606 
607   /**
608    * Overwrites the existing permission set for a given user for a table, and
609    * triggers an update for zookeeper synchronization.
610    * @param username
611    * @param namespace
612    * @param perms
613    */
614   public void setNamespaceUserPermissions(String username, String namespace,
615       List<TablePermission> perms) {
616     PermissionCache<TablePermission> tablePerms = getNamespacePermissions(namespace);
617     tablePerms.replaceUser(username, perms);
618     writeNamespaceToZooKeeper(namespace, tablePerms);
619   }
620 
621   /**
622    * Overwrites the existing permission set for a group and triggers an update
623    * for zookeeper synchronization.
624    * @param group
625    * @param namespace
626    * @param perms
627    */
628   public void setNamespaceGroupPermissions(String group, String namespace,
629       List<TablePermission> perms) {
630     PermissionCache<TablePermission> tablePerms = getNamespacePermissions(namespace);
631     tablePerms.replaceGroup(group, perms);
632     writeNamespaceToZooKeeper(namespace, tablePerms);
633   }
634 
635   public void writeTableToZooKeeper(TableName table,
636       PermissionCache<TablePermission> tablePerms) {
637     byte[] serialized = new byte[0];
638     if (tablePerms != null) {
639       serialized = AccessControlLists.writePermissionsAsBytes(tablePerms.getAllPermissions(), conf);
640     }
641     zkperms.writeToZookeeper(table.getName(), serialized);
642   }
643 
644   public void writeNamespaceToZooKeeper(String namespace,
645       PermissionCache<TablePermission> tablePerms) {
646     byte[] serialized = new byte[0];
647     if (tablePerms != null) {
648       serialized = AccessControlLists.writePermissionsAsBytes(tablePerms.getAllPermissions(), conf);
649     }
650     zkperms.writeToZookeeper(Bytes.toBytes(AccessControlLists.toNamespaceEntry(namespace)),
651         serialized);
652   }
653 
654   public long getMTime() {
655     return mtime;
656   }
657 
658   static Map<ZooKeeperWatcher,TableAuthManager> managerMap =
659     new HashMap<ZooKeeperWatcher,TableAuthManager>();
660 
661   public synchronized static TableAuthManager get(
662       ZooKeeperWatcher watcher, Configuration conf) throws IOException {
663     instance = managerMap.get(watcher);
664     if (instance == null) {
665       instance = new TableAuthManager(watcher, conf);
666       managerMap.put(watcher, instance);
667     }
668     return instance;
669   }
670 }