All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.hadoop.hbase.security.access.TableAuthManager Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.hadoop.hbase.security.access;

import java.io.Closeable;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.hbase.AuthUtil;
import org.apache.hadoop.hbase.classification.InterfaceAudience;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.exceptions.DeserializationException;
import org.apache.hadoop.hbase.security.Superusers;
import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.UserProvider;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
import org.apache.zookeeper.KeeperException;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;

/**
 * Performs authorization checks for a given user's assigned permissions
 */
@InterfaceAudience.Private
public class TableAuthManager implements Closeable {
  private static class PermissionCache {
    /** Cache of user permissions */
    private ListMultimap userCache = ArrayListMultimap.create();
    /** Cache of group permissions */
    private ListMultimap groupCache = ArrayListMultimap.create();

    public List getUser(String user) {
      return userCache.get(user);
    }

    public void putUser(String user, T perm) {
      userCache.put(user, perm);
    }

    public List replaceUser(String user, Iterable perms) {
      return userCache.replaceValues(user, perms);
    }

    public List getGroup(String group) {
      return groupCache.get(group);
    }

    public void putGroup(String group, T perm) {
      groupCache.put(group, perm);
    }

    public List replaceGroup(String group, Iterable perms) {
      return groupCache.replaceValues(group, perms);
    }

    /**
     * Returns a combined map of user and group permissions, with group names
     * distinguished according to {@link AuthUtil.isGroupPrincipal}
     */
    public ListMultimap getAllPermissions() {
      ListMultimap tmp = ArrayListMultimap.create();
      tmp.putAll(userCache);
      for (String group : groupCache.keySet()) {
        tmp.putAll(AuthUtil.toGroupEntry(group), groupCache.get(group));
      }
      return tmp;
    }
  }

  private static final Log LOG = LogFactory.getLog(TableAuthManager.class);

  /** Cache of global permissions */
  private volatile PermissionCache globalCache;

  private ConcurrentSkipListMap> tableCache =
      new ConcurrentSkipListMap>();

  private ConcurrentSkipListMap> nsCache =
    new ConcurrentSkipListMap>();

  private Configuration conf;
  private ZKPermissionWatcher zkperms;
  private AtomicLong mtime = new AtomicLong(0);

  private TableAuthManager(ZooKeeperWatcher watcher, Configuration conf)
      throws IOException {
    this.conf = conf;

    // initialize global permissions based on configuration
    globalCache = initGlobal(conf);

    this.zkperms = new ZKPermissionWatcher(watcher, this, conf);
    try {
      this.zkperms.start();
    } catch (KeeperException ke) {
      LOG.error("ZooKeeper initialization failed", ke);
    }
  }

  @Override
  public void close() {
    this.zkperms.close();
  }

  /**
   * Returns a new {@code PermissionCache} initialized with permission assignments
   * from the {@code hbase.superuser} configuration key.
   */
  private PermissionCache initGlobal(Configuration conf) throws IOException {
    UserProvider userProvider = UserProvider.instantiate(conf);
    User user = userProvider.getCurrent();
    if (user == null) {
      throw new IOException("Unable to obtain the current user, " +
          "authorization checks for internal operations will not work correctly!");
    }
    PermissionCache newCache = new PermissionCache();
    String currentUser = user.getShortName();

    // the system user is always included
    List superusers = Lists.asList(currentUser, conf.getStrings(
        Superusers.SUPERUSER_CONF_KEY, new String[0]));
    if (superusers != null) {
      for (String name : superusers) {
        if (AuthUtil.isGroupPrincipal(name)) {
          newCache.putGroup(AuthUtil.getGroupName(name),
              new Permission(Permission.Action.values()));
        } else {
          newCache.putUser(name, new Permission(Permission.Action.values()));
        }
      }
    }
    return newCache;
  }

  public ZKPermissionWatcher getZKPermissionWatcher() {
    return this.zkperms;
  }

  public void refreshTableCacheFromWritable(TableName table,
                                       byte[] data) throws IOException {
    if (data != null && data.length > 0) {
      ListMultimap perms;
      try {
        perms = AccessControlLists.readPermissions(data, conf);
      } catch (DeserializationException e) {
        throw new IOException(e);
      }

      if (perms != null) {
        if (Bytes.equals(table.getName(), AccessControlLists.ACL_GLOBAL_NAME)) {
          updateGlobalCache(perms);
        } else {
          updateTableCache(table, perms);
        }
      }
    } else {
      LOG.debug("Skipping permission cache refresh because writable data is empty");
    }
  }

  public void refreshNamespaceCacheFromWritable(String namespace, byte[] data) throws IOException {
    if (data != null && data.length > 0) {
      ListMultimap perms;
      try {
        perms = AccessControlLists.readPermissions(data, conf);
      } catch (DeserializationException e) {
        throw new IOException(e);
      }
      if (perms != null) {
        updateNsCache(namespace, perms);
      }
    } else {
      LOG.debug("Skipping permission cache refresh because writable data is empty");
    }
  }

  /**
   * Updates the internal global permissions cache
   *
   * @param userPerms
   */
  private void updateGlobalCache(ListMultimap userPerms) {
    PermissionCache newCache = null;
    try {
      newCache = initGlobal(conf);
      for (Map.Entry entry : userPerms.entries()) {
        if (AuthUtil.isGroupPrincipal(entry.getKey())) {
          newCache.putGroup(AuthUtil.getGroupName(entry.getKey()),
              new Permission(entry.getValue().getActions()));
        } else {
          newCache.putUser(entry.getKey(), new Permission(entry.getValue().getActions()));
        }
      }
      globalCache = newCache;
      mtime.incrementAndGet();
    } catch (IOException e) {
      // Never happens
      LOG.error("Error occurred while updating the global cache", e);
    }
  }

  /**
   * Updates the internal permissions cache for a single table, splitting
   * the permissions listed into separate caches for users and groups to optimize
   * group lookups.
   *
   * @param table
   * @param tablePerms
   */
  private void updateTableCache(TableName table,
                                ListMultimap tablePerms) {
    PermissionCache newTablePerms = new PermissionCache();

    for (Map.Entry entry : tablePerms.entries()) {
      if (AuthUtil.isGroupPrincipal(entry.getKey())) {
        newTablePerms.putGroup(AuthUtil.getGroupName(entry.getKey()), entry.getValue());
      } else {
        newTablePerms.putUser(entry.getKey(), entry.getValue());
      }
    }

    tableCache.put(table, newTablePerms);
    mtime.incrementAndGet();
  }

  /**
   * Updates the internal permissions cache for a single table, splitting
   * the permissions listed into separate caches for users and groups to optimize
   * group lookups.
   *
   * @param namespace
   * @param tablePerms
   */
  private void updateNsCache(String namespace,
                             ListMultimap tablePerms) {
    PermissionCache newTablePerms = new PermissionCache();

    for (Map.Entry entry : tablePerms.entries()) {
      if (AuthUtil.isGroupPrincipal(entry.getKey())) {
        newTablePerms.putGroup(AuthUtil.getGroupName(entry.getKey()), entry.getValue());
      } else {
        newTablePerms.putUser(entry.getKey(), entry.getValue());
      }
    }

    nsCache.put(namespace, newTablePerms);
    mtime.incrementAndGet();
  }

  private PermissionCache getTablePermissions(TableName table) {
    if (!tableCache.containsKey(table)) {
      tableCache.putIfAbsent(table, new PermissionCache());
    }
    return tableCache.get(table);
  }

  private PermissionCache getNamespacePermissions(String namespace) {
    if (!nsCache.containsKey(namespace)) {
      nsCache.putIfAbsent(namespace, new PermissionCache());
    }
    return nsCache.get(namespace);
  }

  /**
   * Authorizes a global permission
   * @param perms
   * @param action
   * @return true if authorized, false otherwise
   */
  private boolean authorize(List perms, Permission.Action action) {
    if (perms != null) {
      for (Permission p : perms) {
        if (p.implies(action)) {
          return true;
        }
      }
    } else if (LOG.isDebugEnabled()) {
      LOG.debug("No permissions found for " + action);
    }

    return false;
  }

  /**
   * Authorize a global permission based on ACLs for the given user and the
   * user's groups.
   * @param user
   * @param action
   * @return true if known and authorized, false otherwise
   */
  public boolean authorize(User user, Permission.Action action) {
    if (user == null) {
      return false;
    }

    if (authorize(globalCache.getUser(user.getShortName()), action)) {
      return true;
    }

    String[] groups = user.getGroupNames();
    if (groups != null) {
      for (String group : groups) {
        if (authorize(globalCache.getGroup(group), action)) {
          return true;
        }
      }
    }
    return false;
  }

  private boolean authorize(List perms,
                            TableName table, byte[] family,
                            byte[] qualifier, Permission.Action action) {
    if (perms != null) {
      for (TablePermission p : perms) {
        if (p.implies(table, family, qualifier, action)) {
          return true;
        }
      }
    } else if (LOG.isDebugEnabled()) {
      LOG.debug("No permissions found for table="+table);
    }
    return false;
  }

  private boolean hasAccess(List perms,
                            TableName table, Permission.Action action) {
    if (perms != null) {
      for (TablePermission p : perms) {
        if (p.implies(action)) {
          return true;
        }
      }
    } else if (LOG.isDebugEnabled()) {
      LOG.debug("No permissions found for table="+table);
    }
    return false;
  }

  /**
   * Authorize a user for a given KV. This is called from AccessControlFilter.
   */
  public boolean authorize(User user, TableName table, Cell cell, Permission.Action action) {
    try {
      List perms = AccessControlLists.getCellPermissionsForUser(user, cell);
      if (LOG.isTraceEnabled()) {
        LOG.trace("Perms for user " + user.getShortName() + " in cell " + cell + ": " +
          (perms != null ? perms : ""));
      }
      if (perms != null) {
        for (Permission p: perms) {
          if (p.implies(action)) {
            return true;
          }
        }
      }
    } catch (IOException e) {
      // We failed to parse the KV tag
      LOG.error("Failed parse of ACL tag in cell " + cell);
      // Fall through to check with the table and CF perms we were able
      // to collect regardless
    }
    return false;
  }

  public boolean authorize(User user, String namespace, Permission.Action action) {
    // Global authorizations supercede namespace level
    if (authorize(user, action)) {
      return true;
    }
    // Check namespace permissions
    PermissionCache tablePerms = nsCache.get(namespace);
    if (tablePerms != null) {
      List userPerms = tablePerms.getUser(user.getShortName());
      if (authorize(userPerms, namespace, action)) {
        return true;
      }
      String[] groupNames = user.getGroupNames();
      if (groupNames != null) {
        for (String group : groupNames) {
          List groupPerms = tablePerms.getGroup(group);
          if (authorize(groupPerms, namespace, action)) {
            return true;
          }
        }
      }
    }
    return false;
  }

  private boolean authorize(List perms, String namespace,
                            Permission.Action action) {
    if (perms != null) {
      for (TablePermission p : perms) {
        if (p.implies(namespace, action)) {
          return true;
        }
      }
    } else if (LOG.isDebugEnabled()) {
      LOG.debug("No permissions for authorize() check, table=" + namespace);
    }

    return false;
  }

  /**
   * Checks authorization to a given table and column family for a user, based on the
   * stored user permissions.
   *
   * @param user
   * @param table
   * @param family
   * @param action
   * @return true if known and authorized, false otherwise
   */
  public boolean authorizeUser(User user, TableName table, byte[] family,
      Permission.Action action) {
    return authorizeUser(user, table, family, null, action);
  }

  public boolean authorizeUser(User user, TableName table, byte[] family,
      byte[] qualifier, Permission.Action action) {
    if (table == null) table = AccessControlLists.ACL_TABLE_NAME;
    // Global and namespace authorizations supercede table level
    if (authorize(user, table.getNamespaceAsString(), action)) {
      return true;
    }
    // Check table permissions
    return authorize(getTablePermissions(table).getUser(user.getShortName()), table, family,
        qualifier, action);
  }

  /**
   * Checks if the user has access to the full table or at least a family/qualifier
   * for the specified action.
   *
   * @param user
   * @param table
   * @param action
   * @return true if the user has access to the table, false otherwise
   */
  public boolean userHasAccess(User user, TableName table, Permission.Action action) {
    if (table == null) table = AccessControlLists.ACL_TABLE_NAME;
    // Global and namespace authorizations supercede table level
    if (authorize(user, table.getNamespaceAsString(), action)) {
      return true;
    }
    // Check table permissions
    return hasAccess(getTablePermissions(table).getUser(user.getShortName()), table, action);
  }

  /**
   * Checks global authorization for a given action for a group, based on the stored
   * permissions.
   */
  public boolean authorizeGroup(String groupName, Permission.Action action) {
    List perms = globalCache.getGroup(groupName);
    if (LOG.isDebugEnabled()) {
      LOG.debug("authorizing " + (perms != null && !perms.isEmpty() ? perms.get(0) : "") +
        " for " + action);
    }
    return authorize(perms, action);
  }

  /**
   * Checks authorization to a given table, column family and column for a group, based
   * on the stored permissions.
   * @param groupName
   * @param table
   * @param family
   * @param qualifier
   * @param action
   * @return true if known and authorized, false otherwise
   */
  public boolean authorizeGroup(String groupName, TableName table, byte[] family,
      byte[] qualifier, Permission.Action action) {
    // Global authorization supercedes table level
    if (authorizeGroup(groupName, action)) {
      return true;
    }
    if (table == null) table = AccessControlLists.ACL_TABLE_NAME;
    // Namespace authorization supercedes table level
    String namespace = table.getNamespaceAsString();
    if (authorize(getNamespacePermissions(namespace).getGroup(groupName), namespace, action)) {
      return true;
    }
    // Check table level
    List tblPerms = getTablePermissions(table).getGroup(groupName);
    if (LOG.isDebugEnabled()) {
      LOG.debug("authorizing " + (tblPerms != null && !tblPerms.isEmpty() ? tblPerms.get(0) : "") +
        " for " +groupName + " on " + table + "." + Bytes.toString(family) + "." +
        Bytes.toString(qualifier) + " with " + action);
    }
    return authorize(tblPerms, table, family, qualifier, action);
  }

  /**
   * Checks if the user has access to the full table or at least a family/qualifier
   * for the specified action.
   * @param groupName
   * @param table
   * @param action
   * @return true if the group has access to the table, false otherwise
   */
  public boolean groupHasAccess(String groupName, TableName table, Permission.Action action) {
    // Global authorization supercedes table level
    if (authorizeGroup(groupName, action)) {
      return true;
    }
    if (table == null) table = AccessControlLists.ACL_TABLE_NAME;
    // Namespace authorization supercedes table level
    if (hasAccess(getNamespacePermissions(table.getNamespaceAsString()).getGroup(groupName),
        table, action)) {
      return true;
    }
    // Check table level
    return hasAccess(getTablePermissions(table).getGroup(groupName), table, action);
  }

  public boolean authorize(User user, TableName table, byte[] family,
      byte[] qualifier, Permission.Action action) {
    if (authorizeUser(user, table, family, qualifier, action)) {
      return true;
    }

    String[] groups = user.getGroupNames();
    if (groups != null) {
      for (String group : groups) {
        if (authorizeGroup(group, table, family, qualifier, action)) {
          return true;
        }
      }
    }
    return false;
  }

  public boolean hasAccess(User user, TableName table, Permission.Action action) {
    if (userHasAccess(user, table, action)) {
      return true;
    }

    String[] groups = user.getGroupNames();
    if (groups != null) {
      for (String group : groups) {
        if (groupHasAccess(group, table, action)) {
          return true;
        }
      }
    }
    return false;
  }

  public boolean authorize(User user, TableName table, byte[] family,
      Permission.Action action) {
    return authorize(user, table, family, null, action);
  }

  /**
   * Returns true if the given user has a {@link TablePermission} matching up
   * to the column family portion of a permission.  Note that this permission
   * may be scoped to a given column qualifier and does not guarantee that
   * authorize() on the same column family would return true.
   */
  public boolean matchPermission(User user,
      TableName table, byte[] family, Permission.Action action) {
    PermissionCache tablePerms = tableCache.get(table);
    if (tablePerms != null) {
      List userPerms = tablePerms.getUser(user.getShortName());
      if (userPerms != null) {
        for (TablePermission p : userPerms) {
          if (p.matchesFamily(table, family, action)) {
            return true;
          }
        }
      }

      String[] groups = user.getGroupNames();
      if (groups != null) {
        for (String group : groups) {
          List groupPerms = tablePerms.getGroup(group);
          if (groupPerms != null) {
            for (TablePermission p : groupPerms) {
              if (p.matchesFamily(table, family, action)) {
                return true;
              }
            }
          }
        }
      }
    }

    return false;
  }

  public boolean matchPermission(User user,
      TableName table, byte[] family, byte[] qualifier,
      Permission.Action action) {
    PermissionCache tablePerms = tableCache.get(table);
    if (tablePerms != null) {
      List userPerms = tablePerms.getUser(user.getShortName());
      if (userPerms != null) {
        for (TablePermission p : userPerms) {
          if (p.matchesFamilyQualifier(table, family, qualifier, action)) {
            return true;
          }
        }
      }

      String[] groups = user.getGroupNames();
      if (groups != null) {
        for (String group : groups) {
          List groupPerms = tablePerms.getGroup(group);
          if (groupPerms != null) {
            for (TablePermission p : groupPerms) {
              if (p.matchesFamilyQualifier(table, family, qualifier, action)) {
                return true;
              }
            }
          }
        }
      }
    }
    return false;
  }

  public void removeNamespace(byte[] ns) {
    nsCache.remove(Bytes.toString(ns));
  }

  public void removeTable(TableName table) {
    tableCache.remove(table);
  }

  /**
   * Overwrites the existing permission set for a given user for a table, and
   * triggers an update for zookeeper synchronization.
   * @param username
   * @param table
   * @param perms
   */
  public void setTableUserPermissions(String username, TableName table,
      List perms) {
    PermissionCache tablePerms = getTablePermissions(table);
    tablePerms.replaceUser(username, perms);
    writeTableToZooKeeper(table, tablePerms);
  }

  /**
   * Overwrites the existing permission set for a group and triggers an update
   * for zookeeper synchronization.
   * @param group
   * @param table
   * @param perms
   */
  public void setTableGroupPermissions(String group, TableName table,
      List perms) {
    PermissionCache tablePerms = getTablePermissions(table);
    tablePerms.replaceGroup(group, perms);
    writeTableToZooKeeper(table, tablePerms);
  }

  /**
   * Overwrites the existing permission set for a given user for a table, and
   * triggers an update for zookeeper synchronization.
   * @param username
   * @param namespace
   * @param perms
   */
  public void setNamespaceUserPermissions(String username, String namespace,
      List perms) {
    PermissionCache tablePerms = getNamespacePermissions(namespace);
    tablePerms.replaceUser(username, perms);
    writeNamespaceToZooKeeper(namespace, tablePerms);
  }

  /**
   * Overwrites the existing permission set for a group and triggers an update
   * for zookeeper synchronization.
   * @param group
   * @param namespace
   * @param perms
   */
  public void setNamespaceGroupPermissions(String group, String namespace,
      List perms) {
    PermissionCache tablePerms = getNamespacePermissions(namespace);
    tablePerms.replaceGroup(group, perms);
    writeNamespaceToZooKeeper(namespace, tablePerms);
  }

  public void writeTableToZooKeeper(TableName table,
      PermissionCache tablePerms) {
    byte[] serialized = new byte[0];
    if (tablePerms != null) {
      serialized = AccessControlLists.writePermissionsAsBytes(tablePerms.getAllPermissions(), conf);
    }
    zkperms.writeToZookeeper(table.getName(), serialized);
  }

  public void writeNamespaceToZooKeeper(String namespace,
      PermissionCache tablePerms) {
    byte[] serialized = new byte[0];
    if (tablePerms != null) {
      serialized = AccessControlLists.writePermissionsAsBytes(tablePerms.getAllPermissions(), conf);
    }
    zkperms.writeToZookeeper(Bytes.toBytes(AccessControlLists.toNamespaceEntry(namespace)),
        serialized);
  }

  public long getMTime() {
    return mtime.get();
  }

  private static Map managerMap =
    new HashMap();

  private static Map refCount = new HashMap<>();

  /** Returns a TableAuthManager from the cache. If not cached, constructs a new one. Returned
   * instance should be released back by calling {@link #release(TableAuthManager)}. */
  public synchronized static TableAuthManager getOrCreate(
      ZooKeeperWatcher watcher, Configuration conf) throws IOException {
    TableAuthManager instance = managerMap.get(watcher);
    if (instance == null) {
      instance = new TableAuthManager(watcher, conf);
      managerMap.put(watcher, instance);
    }
    int ref = refCount.get(instance) == null ? 0 : refCount.get(instance).intValue();
    refCount.put(instance, ref + 1);
    return instance;
  }

  @VisibleForTesting
  public static int getTotalRefCount() {
    int total = 0;
    for (int count : refCount.values()) {
      total += count;
    }
    return total;
  }

  /**
   * Releases the resources for the given TableAuthManager if the reference count is down to 0.
   * @param instance TableAuthManager to be released
   */
  public synchronized static void release(TableAuthManager instance) {
    if (refCount.get(instance) == null || refCount.get(instance) < 1) {
      String msg = "Something wrong with the TableAuthManager reference counting: " + instance
          + " whose count is " + refCount.get(instance);
      LOG.fatal(msg);
      instance.close();
      managerMap.remove(instance.getZKPermissionWatcher().getWatcher());
      instance.getZKPermissionWatcher().getWatcher().abort(msg, null);
    } else {
      int ref = refCount.get(instance);
      refCount.put(instance, ref-1);
      if (ref-1 == 0) {
        instance.close();
        managerMap.remove(instance.getZKPermissionWatcher().getWatcher());
        refCount.remove(instance);
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy