001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.security.access;
019
020import java.io.IOException;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.concurrent.atomic.AtomicLong;
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.hbase.AuthUtil;
029import org.apache.hadoop.hbase.Cell;
030import org.apache.hadoop.hbase.TableName;
031import org.apache.hadoop.hbase.exceptions.DeserializationException;
032import org.apache.hadoop.hbase.security.Superusers;
033import org.apache.hadoop.hbase.security.User;
034import org.apache.hadoop.hbase.util.Bytes;
035import org.apache.yetus.audience.InterfaceAudience;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039import org.apache.hbase.thirdparty.com.google.common.collect.ListMultimap;
040
041/**
042 * Performs authorization checks for a given user's assigned permissions.
043 * <p>
044 * There're following scopes: <b>Global</b>, <b>Namespace</b>, <b>Table</b>, <b>Family</b>,
045 * <b>Qualifier</b>, <b>Cell</b>. Generally speaking, higher scopes can overrides lower scopes,
046 * except for Cell permission can be granted even a user has not permission on specified table,
047 * which means the user can get/scan only those granted cells parts.
048 * </p>
049 * e.g, if user A has global permission R(ead), he can read table T without checking table scope
050 * permission, so authorization checks alway starts from Global scope.
051 * <p>
052 * For each scope, not only user but also groups he belongs to will be checked.
053 * </p>
054 */
055@InterfaceAudience.Private
056public final class AuthManager {
057
058  /**
059   * Cache of permissions, it is thread safe.
060   * @param <T> T extends Permission
061   */
062  private static class PermissionCache<T extends Permission> {
063    private final Object mutex = new Object();
064    private Map<String, Set<T>> cache = new HashMap<>();
065
066    void put(String name, T perm) {
067      synchronized (mutex) {
068        Set<T> perms = cache.getOrDefault(name, ConcurrentHashMap.newKeySet());
069        perms.add(perm);
070        cache.put(name, perms);
071      }
072    }
073
074    Set<T> get(String name) {
075      synchronized (mutex) {
076        return cache.get(name);
077      }
078    }
079
080    void clear() {
081      synchronized (mutex) {
082        for (Map.Entry<String, Set<T>> entry : cache.entrySet()) {
083          entry.getValue().clear();
084        }
085        cache.clear();
086      }
087    }
088  }
089
090  PermissionCache<NamespacePermission> NS_NO_PERMISSION = new PermissionCache<>();
091  PermissionCache<TablePermission> TBL_NO_PERMISSION = new PermissionCache<>();
092
093  /**
094   * Cache for global permission excluding superuser and supergroup. Since every user/group can only
095   * have one global permission, no need to use PermissionCache.
096   */
097  private Map<String, GlobalPermission> globalCache = new ConcurrentHashMap<>();
098  /** Cache for namespace permission. */
099  private ConcurrentHashMap<String, PermissionCache<NamespacePermission>> namespaceCache =
100    new ConcurrentHashMap<>();
101  /** Cache for table permission. */
102  private ConcurrentHashMap<TableName, PermissionCache<TablePermission>> tableCache =
103    new ConcurrentHashMap<>();
104
105  private static final Logger LOG = LoggerFactory.getLogger(AuthManager.class);
106
107  private Configuration conf;
108  private final AtomicLong mtime = new AtomicLong(0L);
109
110  AuthManager(Configuration conf) {
111    this.conf = conf;
112  }
113
114  /**
115   * Update acl info for table.
116   * @param table name of table
117   * @param data  updated acl data
118   * @throws IOException exception when deserialize data
119   */
120  public void refreshTableCacheFromWritable(TableName table, byte[] data) throws IOException {
121    if (data != null && data.length > 0) {
122      try {
123        ListMultimap<String, Permission> perms = PermissionStorage.readPermissions(data, conf);
124        if (perms != null) {
125          if (Bytes.equals(table.getName(), PermissionStorage.ACL_GLOBAL_NAME)) {
126            updateGlobalCache(perms);
127          } else {
128            updateTableCache(table, perms);
129          }
130        }
131      } catch (DeserializationException e) {
132        throw new IOException(e);
133      }
134    } else {
135      LOG.info("Skipping permission cache refresh because writable data is empty");
136    }
137  }
138
139  /**
140   * Update acl info for namespace.
141   * @param namespace namespace
142   * @param data      updated acl data
143   * @throws IOException exception when deserialize data
144   */
145  public void refreshNamespaceCacheFromWritable(String namespace, byte[] data) throws IOException {
146    if (data != null && data.length > 0) {
147      try {
148        ListMultimap<String, Permission> perms = PermissionStorage.readPermissions(data, conf);
149        if (perms != null) {
150          updateNamespaceCache(namespace, perms);
151        }
152      } catch (DeserializationException e) {
153        throw new IOException(e);
154      }
155    } else {
156      LOG.debug("Skipping permission cache refresh because writable data is empty");
157    }
158  }
159
160  /**
161   * Updates the internal global permissions cache.
162   * @param globalPerms new global permissions
163   */
164  private void updateGlobalCache(ListMultimap<String, Permission> globalPerms) {
165    globalCache.clear();
166    for (String name : globalPerms.keySet()) {
167      for (Permission permission : globalPerms.get(name)) {
168        // Before 2.2, the global permission which storage in zk is not right. It was saved as a
169        // table permission. So here need to handle this for compatibility. See HBASE-22503.
170        if (permission instanceof TablePermission) {
171          globalCache.put(name, new GlobalPermission(permission.getActions()));
172        } else {
173          globalCache.put(name, (GlobalPermission) permission);
174        }
175      }
176    }
177    mtime.incrementAndGet();
178  }
179
180  /**
181   * Updates the internal table permissions cache for specified table.
182   * @param table      updated table name
183   * @param tablePerms new table permissions
184   */
185  private void updateTableCache(TableName table, ListMultimap<String, Permission> tablePerms) {
186    PermissionCache<TablePermission> cacheToUpdate =
187      tableCache.getOrDefault(table, new PermissionCache<>());
188    clearCache(cacheToUpdate);
189    updateCache(tablePerms, cacheToUpdate);
190    tableCache.put(table, cacheToUpdate);
191    mtime.incrementAndGet();
192  }
193
194  /**
195   * Updates the internal namespace permissions cache for specified namespace.
196   * @param namespace updated namespace
197   * @param nsPerms   new namespace permissions
198   */
199  private void updateNamespaceCache(String namespace, ListMultimap<String, Permission> nsPerms) {
200    PermissionCache<NamespacePermission> cacheToUpdate =
201      namespaceCache.getOrDefault(namespace, new PermissionCache<>());
202    clearCache(cacheToUpdate);
203    updateCache(nsPerms, cacheToUpdate);
204    namespaceCache.put(namespace, cacheToUpdate);
205    mtime.incrementAndGet();
206  }
207
208  private void clearCache(PermissionCache cacheToUpdate) {
209    cacheToUpdate.clear();
210  }
211
212  @SuppressWarnings("unchecked")
213  private void updateCache(ListMultimap<String, ? extends Permission> newPermissions,
214    PermissionCache cacheToUpdate) {
215    for (String name : newPermissions.keySet()) {
216      for (Permission permission : newPermissions.get(name)) {
217        cacheToUpdate.put(name, permission);
218      }
219    }
220  }
221
222  /**
223   * Check if user has given action privilige in global scope.
224   * @param user   user name
225   * @param action one of action in [Read, Write, Create, Exec, Admin]
226   * @return true if user has, false otherwise
227   */
228  public boolean authorizeUserGlobal(User user, Permission.Action action) {
229    if (user == null) {
230      return false;
231    }
232    if (Superusers.isSuperUser(user)) {
233      return true;
234    }
235    if (authorizeGlobal(globalCache.get(user.getShortName()), action)) {
236      return true;
237    }
238    for (String group : user.getGroupNames()) {
239      if (authorizeGlobal(globalCache.get(AuthUtil.toGroupEntry(group)), action)) {
240        return true;
241      }
242    }
243    return false;
244  }
245
246  private boolean authorizeGlobal(GlobalPermission permissions, Permission.Action action) {
247    return permissions != null && permissions.implies(action);
248  }
249
250  /**
251   * Check if user has given action privilige in namespace scope.
252   * @param user      user name
253   * @param namespace namespace
254   * @param action    one of action in [Read, Write, Create, Exec, Admin]
255   * @return true if user has, false otherwise
256   */
257  public boolean authorizeUserNamespace(User user, String namespace, Permission.Action action) {
258    if (user == null) {
259      return false;
260    }
261    if (authorizeUserGlobal(user, action)) {
262      return true;
263    }
264    PermissionCache<NamespacePermission> nsPermissions =
265      namespaceCache.getOrDefault(namespace, NS_NO_PERMISSION);
266    if (authorizeNamespace(nsPermissions.get(user.getShortName()), namespace, action)) {
267      return true;
268    }
269    for (String group : user.getGroupNames()) {
270      if (authorizeNamespace(nsPermissions.get(AuthUtil.toGroupEntry(group)), namespace, action)) {
271        return true;
272      }
273    }
274    return false;
275  }
276
277  private boolean authorizeNamespace(Set<NamespacePermission> permissions, String namespace,
278    Permission.Action action) {
279    if (permissions == null) {
280      return false;
281    }
282    for (NamespacePermission permission : permissions) {
283      if (permission.implies(namespace, action)) {
284        return true;
285      }
286    }
287    return false;
288  }
289
290  /**
291   * Checks if the user has access to the full table or at least a family/qualifier for the
292   * specified action.
293   * @param user   user name
294   * @param table  table name
295   * @param action action in one of [Read, Write, Create, Exec, Admin]
296   * @return true if the user has access to the table, false otherwise
297   */
298  public boolean accessUserTable(User user, TableName table, Permission.Action action) {
299    if (user == null) {
300      return false;
301    }
302    if (table == null) {
303      table = PermissionStorage.ACL_TABLE_NAME;
304    }
305    if (authorizeUserNamespace(user, table.getNamespaceAsString(), action)) {
306      return true;
307    }
308    PermissionCache<TablePermission> tblPermissions =
309      tableCache.getOrDefault(table, TBL_NO_PERMISSION);
310    if (hasAccessTable(tblPermissions.get(user.getShortName()), action)) {
311      return true;
312    }
313    for (String group : user.getGroupNames()) {
314      if (hasAccessTable(tblPermissions.get(AuthUtil.toGroupEntry(group)), action)) {
315        return true;
316      }
317    }
318    return false;
319  }
320
321  private boolean hasAccessTable(Set<TablePermission> permissions, Permission.Action action) {
322    if (permissions == null) {
323      return false;
324    }
325    for (TablePermission permission : permissions) {
326      if (permission.implies(action)) {
327        return true;
328      }
329    }
330    return false;
331  }
332
333  /**
334   * Check if user has given action privilige in table scope.
335   * @param user   user name
336   * @param table  table name
337   * @param action one of action in [Read, Write, Create, Exec, Admin]
338   * @return true if user has, false otherwise
339   */
340  public boolean authorizeUserTable(User user, TableName table, Permission.Action action) {
341    return authorizeUserTable(user, table, null, null, action);
342  }
343
344  /**
345   * Check if user has given action privilige in table:family scope.
346   * @param user   user name
347   * @param table  table name
348   * @param family family name
349   * @param action one of action in [Read, Write, Create, Exec, Admin]
350   * @return true if user has, false otherwise
351   */
352  public boolean authorizeUserTable(User user, TableName table, byte[] family,
353    Permission.Action action) {
354    return authorizeUserTable(user, table, family, null, action);
355  }
356
357  /**
358   * Check if user has given action privilige in table:family:qualifier scope.
359   * @param user      user name
360   * @param table     table name
361   * @param family    family name
362   * @param qualifier qualifier name
363   * @param action    one of action in [Read, Write, Create, Exec, Admin]
364   * @return true if user has, false otherwise
365   */
366  public boolean authorizeUserTable(User user, TableName table, byte[] family, byte[] qualifier,
367    Permission.Action action) {
368    if (user == null) {
369      return false;
370    }
371    if (table == null) {
372      table = PermissionStorage.ACL_TABLE_NAME;
373    }
374    if (authorizeUserNamespace(user, table.getNamespaceAsString(), action)) {
375      return true;
376    }
377    PermissionCache<TablePermission> tblPermissions =
378      tableCache.getOrDefault(table, TBL_NO_PERMISSION);
379    if (authorizeTable(tblPermissions.get(user.getShortName()), table, family, qualifier, action)) {
380      return true;
381    }
382    for (String group : user.getGroupNames()) {
383      if (
384        authorizeTable(tblPermissions.get(AuthUtil.toGroupEntry(group)), table, family, qualifier,
385          action)
386      ) {
387        return true;
388      }
389    }
390    return false;
391  }
392
393  private boolean authorizeTable(Set<TablePermission> permissions, TableName table, byte[] family,
394    byte[] qualifier, Permission.Action action) {
395    if (permissions == null) {
396      return false;
397    }
398    for (TablePermission permission : permissions) {
399      if (permission.implies(table, family, qualifier, action)) {
400        return true;
401      }
402    }
403    return false;
404  }
405
406  /**
407   * Check if user has given action privilige in table:family scope. This method is for backward
408   * compatibility.
409   * @param user   user name
410   * @param table  table name
411   * @param family family names
412   * @param action one of action in [Read, Write, Create, Exec, Admin]
413   * @return true if user has, false otherwise
414   */
415  public boolean authorizeUserFamily(User user, TableName table, byte[] family,
416    Permission.Action action) {
417    PermissionCache<TablePermission> tblPermissions =
418      tableCache.getOrDefault(table, TBL_NO_PERMISSION);
419    if (authorizeFamily(tblPermissions.get(user.getShortName()), table, family, action)) {
420      return true;
421    }
422    for (String group : user.getGroupNames()) {
423      if (
424        authorizeFamily(tblPermissions.get(AuthUtil.toGroupEntry(group)), table, family, action)
425      ) {
426        return true;
427      }
428    }
429    return false;
430  }
431
432  private boolean authorizeFamily(Set<TablePermission> permissions, TableName table, byte[] family,
433    Permission.Action action) {
434    if (permissions == null) {
435      return false;
436    }
437    for (TablePermission permission : permissions) {
438      if (permission.implies(table, family, action)) {
439        return true;
440      }
441    }
442    return false;
443  }
444
445  /**
446   * Check if user has given action privilige in cell scope.
447   * @param user   user name
448   * @param table  table name
449   * @param cell   cell to be checked
450   * @param action one of action in [Read, Write, Create, Exec, Admin]
451   * @return true if user has, false otherwise
452   */
453  public boolean authorizeCell(User user, TableName table, Cell cell, Permission.Action action) {
454    try {
455      List<Permission> perms = PermissionStorage.getCellPermissionsForUser(user, cell);
456      if (LOG.isTraceEnabled()) {
457        LOG.trace("Perms for user {} in table {} in cell {}: {}", user.getShortName(), table, cell,
458          (perms != null ? perms : ""));
459      }
460      if (perms != null) {
461        for (Permission p : perms) {
462          if (p.implies(action)) {
463            return true;
464          }
465        }
466      }
467    } catch (IOException e) {
468      // We failed to parse the KV tag
469      LOG.error("Failed parse of ACL tag in cell " + cell);
470      // Fall through to check with the table and CF perms we were able
471      // to collect regardless
472    }
473    return false;
474  }
475
476  /**
477   * Remove given namespace from AuthManager's namespace cache.
478   * @param ns namespace
479   */
480  public void removeNamespace(byte[] ns) {
481    namespaceCache.remove(Bytes.toString(ns));
482  }
483
484  /**
485   * Remove given table from AuthManager's table cache.
486   * @param table table name
487   */
488  public void removeTable(TableName table) {
489    tableCache.remove(table);
490  }
491
492  /**
493   * Last modification logical time
494   */
495  public long getMTime() {
496    return mtime.get();
497  }
498}