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