/*
 * 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.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.HBaseInterfaceAudience;
import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.TableNotFoundException;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.SnapshotDescription;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.client.TableDescriptor;
import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
import org.apache.hadoop.hbase.coprocessor.CoreCoprocessor;
import org.apache.hadoop.hbase.coprocessor.HasMasterServices;
import org.apache.hadoop.hbase.coprocessor.MasterCoprocessor;
import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment;
import org.apache.hadoop.hbase.coprocessor.MasterObserver;
import org.apache.hadoop.hbase.coprocessor.ObserverContext;
import org.apache.hadoop.hbase.master.MasterServices;
import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.UserProvider;
import org.apache.hadoop.hbase.security.access.SnapshotScannerHDFSAclHelper.PathHelper;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.yetus.audience.InterfaceAudience;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.hbase.thirdparty.com.google.common.collect.Sets;

/**
 * Set HDFS ACLs to hFiles to make HBase granted users have permission to scan snapshot
 * <p>
 * To use this feature, please mask sure HDFS config:
 * <ul>
 * <li>dfs.namenode.acls.enabled = true</li>
 * <li>fs.permissions.umask-mode = 027 (or smaller umask than 027)</li>
 * </ul>
 * </p>
 * <p>
 * The implementation of this feature is as followings:
 * <ul>
 * <li>For common directories such as 'data' and 'archive', set other permission to '--x' to make
 * everyone have the permission to access the directory.</li>
 * <li>For namespace or table directories such as 'data/ns/table', 'archive/ns/table' and
 * '.hbase-snapshot/snapshotName', set user 'r-x' access acl and 'r-x' default acl when following
 * operations happen:
 * <ul>
 * <li>grant user with global, namespace or table permission;</li>
 * <li>revoke user from global, namespace or table;</li>
 * <li>snapshot table;</li>
 * <li>truncate table;</li>
 * </ul>
 * </li>
 * <li>Note: Because snapshots are at table level, so this feature just considers users with global,
 * namespace or table permissions, ignores users with table CF or cell permissions.</li>
 * </ul>
 * </p>
 */
@CoreCoprocessor
@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
public class SnapshotScannerHDFSAclController implements MasterCoprocessor, MasterObserver {
  private static final Logger LOG = LoggerFactory.getLogger(SnapshotScannerHDFSAclController.class);

  private SnapshotScannerHDFSAclHelper hdfsAclHelper = null;
  private PathHelper pathHelper = null;
  private MasterServices masterServices = null;
  private volatile boolean initialized = false;
  private volatile boolean aclTableInitialized = false;
  /** Provider for mapping principal names to Users */
  private UserProvider userProvider;

  @Override
  public Optional<MasterObserver> getMasterObserver() {
    return Optional.of(this);
  }

  @Override
  public void preMasterInitialization(ObserverContext<MasterCoprocessorEnvironment> c)
    throws IOException {
    if (
      c.getEnvironment().getConfiguration()
        .getBoolean(SnapshotScannerHDFSAclHelper.ACL_SYNC_TO_HDFS_ENABLE, false)
    ) {
      MasterCoprocessorEnvironment mEnv = c.getEnvironment();
      if (!(mEnv instanceof HasMasterServices)) {
        throw new IOException("Does not implement HMasterServices");
      }
      masterServices = ((HasMasterServices) mEnv).getMasterServices();
      hdfsAclHelper = new SnapshotScannerHDFSAclHelper(masterServices.getConfiguration(),
        masterServices.getConnection());
      pathHelper = hdfsAclHelper.getPathHelper();
      hdfsAclHelper.setCommonDirectoryPermission();
      initialized = true;
      userProvider = UserProvider.instantiate(c.getEnvironment().getConfiguration());
    } else {
      LOG.warn("Try to initialize the coprocessor SnapshotScannerHDFSAclController but failure "
        + "because the config " + SnapshotScannerHDFSAclHelper.ACL_SYNC_TO_HDFS_ENABLE
        + " is false.");
    }
  }

  @Override
  public void postStartMaster(ObserverContext<MasterCoprocessorEnvironment> c) throws IOException {
    if (!initialized) {
      return;
    }
    try (Admin admin = c.getEnvironment().getConnection().getAdmin()) {
      if (admin.tableExists(PermissionStorage.ACL_TABLE_NAME)) {
        // Check if acl table has 'm' CF, if not, add 'm' CF
        TableDescriptor tableDescriptor = admin.getDescriptor(PermissionStorage.ACL_TABLE_NAME);
        boolean containHdfsAclFamily = Arrays.stream(tableDescriptor.getColumnFamilies()).anyMatch(
          family -> Bytes.equals(family.getName(), SnapshotScannerHDFSAclStorage.HDFS_ACL_FAMILY));
        if (!containHdfsAclFamily) {
          TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(tableDescriptor)
            .setColumnFamily(ColumnFamilyDescriptorBuilder
              .newBuilder(SnapshotScannerHDFSAclStorage.HDFS_ACL_FAMILY).build());
          admin.modifyTable(builder.build());
        }
        aclTableInitialized = true;
      } else {
        throw new TableNotFoundException(
          "Table " + PermissionStorage.ACL_TABLE_NAME + " is not created yet. Please check if "
            + getClass().getName() + " is configured after " + AccessController.class.getName());
      }
    }
  }

  @Override
  public void preStopMaster(ObserverContext<MasterCoprocessorEnvironment> c) {
    if (initialized) {
      hdfsAclHelper.close();
    }
  }

  @Override
  public void postCompletedCreateTableAction(ObserverContext<MasterCoprocessorEnvironment> c,
    TableDescriptor desc, RegionInfo[] regions) throws IOException {
    if (needHandleTableHdfsAcl(desc, "createTable " + desc.getTableName())) {
      TableName tableName = desc.getTableName();
      // 1. Create table directories to make HDFS acls can be inherited
      hdfsAclHelper.createTableDirectories(tableName);
      // 2. Add table owner HDFS acls
      String owner = getActiveUser(c).getShortName();
      hdfsAclHelper.addTableAcl(tableName, Sets.newHashSet(owner), "create");
      // 3. Record table owner permission is synced to HDFS in acl table
      SnapshotScannerHDFSAclStorage.addUserTableHdfsAcl(c.getEnvironment().getConnection(), owner,
        tableName);
    }
  }

  @Override
  public void postCreateNamespace(ObserverContext<MasterCoprocessorEnvironment> c,
    NamespaceDescriptor ns) throws IOException {
    if (checkInitialized("createNamespace " + ns.getName())) {
      // Create namespace directories to make HDFS acls can be inherited
      List<Path> paths = hdfsAclHelper.getNamespaceRootPaths(ns.getName());
      for (Path path : paths) {
        hdfsAclHelper.createDirIfNotExist(path);
      }
    }
  }

  @Override
  public void postCompletedSnapshotAction(ObserverContext<MasterCoprocessorEnvironment> c,
    SnapshotDescription snapshot, TableDescriptor tableDescriptor) throws IOException {
    if (needHandleTableHdfsAcl(tableDescriptor, "snapshot " + snapshot.getName())) {
      // Add HDFS acls of users with table read permission to snapshot files
      hdfsAclHelper.snapshotAcl(snapshot);
    }
  }

  @Override
  public void postCompletedTruncateTableAction(ObserverContext<MasterCoprocessorEnvironment> c,
    TableName tableName) throws IOException {
    if (needHandleTableHdfsAcl(tableName, "truncateTable " + tableName)) {
      // 1. create tmp table directories
      hdfsAclHelper.createTableDirectories(tableName);
      // 2. Since the table directories is recreated, so add HDFS acls again
      Set<String> users = hdfsAclHelper.getUsersWithTableReadAction(tableName, false, false);
      hdfsAclHelper.addTableAcl(tableName, users, "truncate");
    }
  }

  @Override
  public void postCompletedDeleteTableAction(ObserverContext<MasterCoprocessorEnvironment> ctx,
    TableName tableName) throws IOException {
    if (!tableName.isSystemTable() && checkInitialized("deleteTable " + tableName)) {
      /*
       * Remove table user access HDFS acl from namespace directory if the user has no permissions
       * of global, ns of the table or other tables of the ns, eg: Bob has 'ns1:t1' read permission,
       * when delete 'ns1:t1', if Bob has global read permission, '@ns1' read permission or
       * 'ns1:other_tables' read permission, then skip remove Bob access acl in ns1Dirs, otherwise,
       * remove Bob access acl.
       */
      try (Table aclTable =
        ctx.getEnvironment().getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) {
        Set<String> users = SnapshotScannerHDFSAclStorage.getTableUsers(aclTable, tableName);
        if (users.size() > 0) {
          // 1. Remove table archive directory default ACLs
          hdfsAclHelper.removeTableDefaultAcl(tableName, users);
          // 2. Delete table owner permission is synced to HDFS in acl table
          SnapshotScannerHDFSAclStorage.deleteTableHdfsAcl(aclTable, tableName);
          // 3. Remove namespace access acls
          Set<String> removeUsers = filterUsersToRemoveNsAccessAcl(aclTable, tableName, users);
          if (removeUsers.size() > 0) {
            hdfsAclHelper.removeNamespaceAccessAcl(tableName, removeUsers, "delete");
          }
        }
      }
    }
  }

  @Override
  public void postModifyTable(ObserverContext<MasterCoprocessorEnvironment> ctx,
    TableName tableName, TableDescriptor oldDescriptor, TableDescriptor currentDescriptor)
    throws IOException {
    try (Table aclTable =
      ctx.getEnvironment().getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) {
      if (
        needHandleTableHdfsAcl(currentDescriptor, "modifyTable " + tableName)
          && !hdfsAclHelper.isAclSyncToHdfsEnabled(oldDescriptor)
      ) {
        // 1. Create table directories used for acl inherited
        hdfsAclHelper.createTableDirectories(tableName);
        // 2. Add table users HDFS acls
        Set<String> tableUsers = hdfsAclHelper.getUsersWithTableReadAction(tableName, false, false);
        Set<String> users =
          hdfsAclHelper.getUsersWithNamespaceReadAction(tableName.getNamespaceAsString(), true);
        users.addAll(tableUsers);
        hdfsAclHelper.addTableAcl(tableName, users, "modify");
        // 3. Record table user acls are synced to HDFS in acl table
        SnapshotScannerHDFSAclStorage.addUserTableHdfsAcl(ctx.getEnvironment().getConnection(),
          tableUsers, tableName);
      } else if (
        needHandleTableHdfsAcl(oldDescriptor, "modifyTable " + tableName)
          && !hdfsAclHelper.isAclSyncToHdfsEnabled(currentDescriptor)
      ) {
        // 1. Remove empty table directories
        List<Path> tableRootPaths = hdfsAclHelper.getTableRootPaths(tableName, false);
        for (Path path : tableRootPaths) {
          hdfsAclHelper.deleteEmptyDir(path);
        }
        // 2. Remove all table HDFS acls
        Set<String> tableUsers = hdfsAclHelper.getUsersWithTableReadAction(tableName, false, false);
        Set<String> users =
          hdfsAclHelper.getUsersWithNamespaceReadAction(tableName.getNamespaceAsString(), true);
        users.addAll(tableUsers);
        hdfsAclHelper.removeTableAcl(tableName, users);
        // 3. Remove namespace access HDFS acls for users who only own permission for this table
        hdfsAclHelper.removeNamespaceAccessAcl(tableName,
          filterUsersToRemoveNsAccessAcl(aclTable, tableName, tableUsers), "modify");
        // 4. Record table user acl is not synced to HDFS
        SnapshotScannerHDFSAclStorage.deleteUserTableHdfsAcl(ctx.getEnvironment().getConnection(),
          tableUsers, tableName);
      }
    }
  }

  @Override
  public void postDeleteNamespace(ObserverContext<MasterCoprocessorEnvironment> ctx,
    String namespace) throws IOException {
    if (checkInitialized("deleteNamespace " + namespace)) {
      try (Table aclTable =
        ctx.getEnvironment().getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) {
        // 1. Delete namespace archive dir default ACLs
        Set<String> users = SnapshotScannerHDFSAclStorage.getEntryUsers(aclTable,
          PermissionStorage.toNamespaceEntry(Bytes.toBytes(namespace)));
        hdfsAclHelper.removeNamespaceDefaultAcl(namespace, users);
        // 2. Record namespace user acl is not synced to HDFS
        SnapshotScannerHDFSAclStorage.deleteNamespaceHdfsAcl(ctx.getEnvironment().getConnection(),
          namespace);
        // 3. Delete tmp namespace directory
        /**
         * Delete namespace tmp directory because it's created by this coprocessor when namespace is
         * created to make namespace default acl can be inherited by tables. The namespace data
         * directory is deleted by DeleteNamespaceProcedure, the namespace archive directory is
         * deleted by HFileCleaner.
         */
        hdfsAclHelper.deleteEmptyDir(pathHelper.getTmpNsDir(namespace));
      }
    }
  }

  @Override
  public void postGrant(ObserverContext<MasterCoprocessorEnvironment> c,
    UserPermission userPermission, boolean mergeExistingPermissions) throws IOException {
    if (
      !checkInitialized(
        "grant " + userPermission + ", merge existing permissions " + mergeExistingPermissions)
    ) {
      return;
    }
    try (Table aclTable =
      c.getEnvironment().getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) {
      Configuration conf = c.getEnvironment().getConfiguration();
      String userName = userPermission.getUser();
      switch (userPermission.getAccessScope()) {
        case GLOBAL:
          UserPermission perm = getUserGlobalPermission(conf, userName);
          if (perm != null && hdfsAclHelper.containReadAction(perm)) {
            if (!isHdfsAclSet(aclTable, userName)) {
              // 1. Get namespaces and tables which global user acls are already synced
              Pair<Set<String>, Set<TableName>> skipNamespaceAndTables =
                SnapshotScannerHDFSAclStorage.getUserNamespaceAndTable(aclTable, userName);
              Set<String> skipNamespaces = skipNamespaceAndTables.getFirst();
              Set<TableName> skipTables = skipNamespaceAndTables.getSecond().stream()
                .filter(t -> !skipNamespaces.contains(t.getNamespaceAsString()))
                .collect(Collectors.toSet());
              // 2. Add HDFS acl(skip namespaces and tables directories whose acl is set)
              hdfsAclHelper.grantAcl(userPermission, skipNamespaces, skipTables);
              // 3. Record global acl is sync to HDFS
              SnapshotScannerHDFSAclStorage.addUserGlobalHdfsAcl(aclTable, userName);
            }
          } else {
            // The merged user permission doesn't contain READ, so remove user global HDFS acls if
            // it's set
            removeUserGlobalHdfsAcl(aclTable, userName, userPermission);
          }
          break;
        case NAMESPACE:
          String namespace = ((NamespacePermission) userPermission.getPermission()).getNamespace();
          UserPermission nsPerm = getUserNamespacePermission(conf, userName, namespace);
          if (nsPerm != null && hdfsAclHelper.containReadAction(nsPerm)) {
            if (!isHdfsAclSet(aclTable, userName, namespace)) {
              // 1. Get tables which namespace user acls are already synced
              Set<TableName> skipTables = SnapshotScannerHDFSAclStorage
                .getUserNamespaceAndTable(aclTable, userName).getSecond();
              // 2. Add HDFS acl(skip tables directories whose acl is set)
              hdfsAclHelper.grantAcl(userPermission, new HashSet<>(0), skipTables);
            }
            // 3. Record namespace acl is synced to HDFS
            SnapshotScannerHDFSAclStorage.addUserNamespaceHdfsAcl(aclTable, userName, namespace);
          } else {
            // The merged user permission doesn't contain READ, so remove user namespace HDFS acls
            // if it's set
            removeUserNamespaceHdfsAcl(aclTable, userName, namespace, userPermission);
          }
          break;
        case TABLE:
          TablePermission tablePerm = (TablePermission) userPermission.getPermission();
          if (needHandleTableHdfsAcl(tablePerm)) {
            TableName tableName = tablePerm.getTableName();
            UserPermission tPerm = getUserTablePermission(conf, userName, tableName);
            if (tPerm != null && hdfsAclHelper.containReadAction(tPerm)) {
              if (!isHdfsAclSet(aclTable, userName, tableName)) {
                // 1. create table dirs
                hdfsAclHelper.createTableDirectories(tableName);
                // 2. Add HDFS acl
                hdfsAclHelper.grantAcl(userPermission, new HashSet<>(0), new HashSet<>(0));
              }
              // 2. Record table acl is synced to HDFS
              SnapshotScannerHDFSAclStorage.addUserTableHdfsAcl(aclTable, userName, tableName);
            } else {
              // The merged user permission doesn't contain READ, so remove user table HDFS acls if
              // it's set
              removeUserTableHdfsAcl(aclTable, userName, tableName, userPermission);
            }
          }
          break;
        default:
          throw new IllegalArgumentException(
            "Illegal user permission scope " + userPermission.getAccessScope());
      }
    }
  }

  @Override
  public void postRevoke(ObserverContext<MasterCoprocessorEnvironment> c,
    UserPermission userPermission) throws IOException {
    if (checkInitialized("revoke " + userPermission)) {
      try (Table aclTable =
        c.getEnvironment().getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) {
        String userName = userPermission.getUser();
        Configuration conf = c.getEnvironment().getConfiguration();
        switch (userPermission.getAccessScope()) {
          case GLOBAL:
            UserPermission userGlobalPerm = getUserGlobalPermission(conf, userName);
            if (userGlobalPerm == null || !hdfsAclHelper.containReadAction(userGlobalPerm)) {
              removeUserGlobalHdfsAcl(aclTable, userName, userPermission);
            }
            break;
          case NAMESPACE:
            NamespacePermission nsPerm = (NamespacePermission) userPermission.getPermission();
            UserPermission userNsPerm =
              getUserNamespacePermission(conf, userName, nsPerm.getNamespace());
            if (userNsPerm == null || !hdfsAclHelper.containReadAction(userNsPerm)) {
              removeUserNamespaceHdfsAcl(aclTable, userName, nsPerm.getNamespace(), userPermission);
            }
            break;
          case TABLE:
            TablePermission tPerm = (TablePermission) userPermission.getPermission();
            if (needHandleTableHdfsAcl(tPerm)) {
              TableName tableName = tPerm.getTableName();
              UserPermission userTablePerm = getUserTablePermission(conf, userName, tableName);
              if (userTablePerm == null || !hdfsAclHelper.containReadAction(userTablePerm)) {
                removeUserTableHdfsAcl(aclTable, userName, tableName, userPermission);
              }
            }
            break;
          default:
            throw new IllegalArgumentException(
              "Illegal user permission scope " + userPermission.getAccessScope());
        }
      }
    }
  }

  private void removeUserGlobalHdfsAcl(Table aclTable, String userName,
    UserPermission userPermission) throws IOException {
    if (SnapshotScannerHDFSAclStorage.hasUserGlobalHdfsAcl(aclTable, userName)) {
      // 1. Get namespaces and tables which global user acls are already synced
      Pair<Set<String>, Set<TableName>> namespaceAndTable =
        SnapshotScannerHDFSAclStorage.getUserNamespaceAndTable(aclTable, userName);
      Set<String> skipNamespaces = namespaceAndTable.getFirst();
      Set<TableName> skipTables = namespaceAndTable.getSecond().stream()
        .filter(t -> !skipNamespaces.contains(t.getNamespaceAsString()))
        .collect(Collectors.toSet());
      // 2. Remove user HDFS acls(skip namespaces and tables directories
      // whose acl must be reversed)
      hdfsAclHelper.revokeAcl(userPermission, skipNamespaces, skipTables);
      // 3. Remove global user acl is synced to HDFS in acl table
      SnapshotScannerHDFSAclStorage.deleteUserGlobalHdfsAcl(aclTable, userName);
    }
  }

  private void removeUserNamespaceHdfsAcl(Table aclTable, String userName, String namespace,
    UserPermission userPermission) throws IOException {
    if (SnapshotScannerHDFSAclStorage.hasUserNamespaceHdfsAcl(aclTable, userName, namespace)) {
      if (!SnapshotScannerHDFSAclStorage.hasUserGlobalHdfsAcl(aclTable, userName)) {
        // 1. Get tables whose namespace user acls are already synced
        Set<TableName> skipTables =
          SnapshotScannerHDFSAclStorage.getUserNamespaceAndTable(aclTable, userName).getSecond();
        // 2. Remove user HDFS acls(skip tables directories whose acl must be reversed)
        hdfsAclHelper.revokeAcl(userPermission, new HashSet<>(), skipTables);
      }
      // 3. Remove namespace user acl is synced to HDFS in acl table
      SnapshotScannerHDFSAclStorage.deleteUserNamespaceHdfsAcl(aclTable, userName, namespace);
    }
  }

  private void removeUserTableHdfsAcl(Table aclTable, String userName, TableName tableName,
    UserPermission userPermission) throws IOException {
    if (SnapshotScannerHDFSAclStorage.hasUserTableHdfsAcl(aclTable, userName, tableName)) {
      if (
        !SnapshotScannerHDFSAclStorage.hasUserGlobalHdfsAcl(aclTable, userName)
          && !SnapshotScannerHDFSAclStorage.hasUserNamespaceHdfsAcl(aclTable, userName,
            tableName.getNamespaceAsString())
      ) {
        // 1. Remove table acls
        hdfsAclHelper.revokeAcl(userPermission, new HashSet<>(0), new HashSet<>(0));
      }
      // 2. Remove table user acl is synced to HDFS in acl table
      SnapshotScannerHDFSAclStorage.deleteUserTableHdfsAcl(aclTable, userName, tableName);
    }
  }

  private UserPermission getUserGlobalPermission(Configuration conf, String userName)
    throws IOException {
    List<UserPermission> permissions = PermissionStorage.getUserPermissions(conf,
      PermissionStorage.ACL_GLOBAL_NAME, null, null, userName, true);
    return permissions.size() > 0 ? permissions.get(0) : null;
  }

  private UserPermission getUserNamespacePermission(Configuration conf, String userName,
    String namespace) throws IOException {
    List<UserPermission> permissions =
      PermissionStorage.getUserNamespacePermissions(conf, namespace, userName, true);
    return permissions.size() > 0 ? permissions.get(0) : null;
  }

  private UserPermission getUserTablePermission(Configuration conf, String userName,
    TableName tableName) throws IOException {
    List<UserPermission> permissions = PermissionStorage
      .getUserTablePermissions(conf, tableName, null, null, userName, true).stream()
      .filter(userPermission -> hdfsAclHelper
        .isNotFamilyOrQualifierPermission((TablePermission) userPermission.getPermission()))
      .collect(Collectors.toList());
    return permissions.size() > 0 ? permissions.get(0) : null;
  }

  private boolean isHdfsAclSet(Table aclTable, String userName) throws IOException {
    return isHdfsAclSet(aclTable, userName, null, null);
  }

  private boolean isHdfsAclSet(Table aclTable, String userName, String namespace)
    throws IOException {
    return isHdfsAclSet(aclTable, userName, namespace, null);
  }

  private boolean isHdfsAclSet(Table aclTable, String userName, TableName tableName)
    throws IOException {
    return isHdfsAclSet(aclTable, userName, null, tableName);
  }

  /**
   * Check if user global/namespace/table HDFS acls is already set
   */
  private boolean isHdfsAclSet(Table aclTable, String userName, String namespace,
    TableName tableName) throws IOException {
    boolean isSet = SnapshotScannerHDFSAclStorage.hasUserGlobalHdfsAcl(aclTable, userName);
    if (namespace != null) {
      isSet = isSet
        || SnapshotScannerHDFSAclStorage.hasUserNamespaceHdfsAcl(aclTable, userName, namespace);
    }
    if (tableName != null) {
      isSet = isSet
        || SnapshotScannerHDFSAclStorage.hasUserNamespaceHdfsAcl(aclTable, userName,
          tableName.getNamespaceAsString())
        || SnapshotScannerHDFSAclStorage.hasUserTableHdfsAcl(aclTable, userName, tableName);
    }
    return isSet;
  }

  @InterfaceAudience.Private
  boolean checkInitialized(String operation) {
    if (initialized) {
      if (aclTableInitialized) {
        return true;
      } else {
        LOG.warn("Skip set HDFS acls because acl table is not initialized when {}", operation);
      }
    }
    return false;
  }

  private boolean needHandleTableHdfsAcl(TablePermission tablePermission) throws IOException {
    return needHandleTableHdfsAcl(tablePermission.getTableName(), "")
      && hdfsAclHelper.isNotFamilyOrQualifierPermission(tablePermission);
  }

  private boolean needHandleTableHdfsAcl(TableName tableName, String operation) throws IOException {
    return !tableName.isSystemTable() && checkInitialized(operation)
      && hdfsAclHelper.isAclSyncToHdfsEnabled(masterServices.getTableDescriptors().get(tableName));
  }

  private boolean needHandleTableHdfsAcl(TableDescriptor tableDescriptor, String operation) {
    TableName tableName = tableDescriptor.getTableName();
    return !tableName.isSystemTable() && checkInitialized(operation)
      && hdfsAclHelper.isAclSyncToHdfsEnabled(tableDescriptor);
  }

  private User getActiveUser(ObserverContext<?> ctx) throws IOException {
    // for non-rpc handling, fallback to system user
    Optional<User> optionalUser = ctx.getCaller();
    if (optionalUser.isPresent()) {
      return optionalUser.get();
    }
    return userProvider.getCurrent();
  }

  /**
   * Remove table user access HDFS acl from namespace directory if the user has no permissions of
   * global, ns of the table or other tables of the ns, eg: Bob has 'ns1:t1' read permission, when
   * delete 'ns1:t1', if Bob has global read permission, '@ns1' read permission or
   * 'ns1:other_tables' read permission, then skip remove Bob access acl in ns1Dirs, otherwise,
   * remove Bob access acl.
   * @param aclTable    acl table
   * @param tableName   the name of the table
   * @param tablesUsers table users set
   * @return users whose access acl will be removed from the namespace of the table
   * @throws IOException if an error occurred
   */
  private Set<String> filterUsersToRemoveNsAccessAcl(Table aclTable, TableName tableName,
    Set<String> tablesUsers) throws IOException {
    Set<String> removeUsers = new HashSet<>();
    byte[] namespace = tableName.getNamespace();
    for (String user : tablesUsers) {
      List<byte[]> userEntries = SnapshotScannerHDFSAclStorage.getUserEntries(aclTable, user);
      boolean remove = true;
      for (byte[] entry : userEntries) {
        if (
          PermissionStorage.isGlobalEntry(entry)
            || (PermissionStorage.isNamespaceEntry(entry)
              && Bytes.equals(PermissionStorage.fromNamespaceEntry(entry), namespace))
            || (PermissionStorage.isTableEntry(entry) && !Bytes.equals(tableName.getName(), entry)
              && Bytes.equals(TableName.valueOf(entry).getNamespace(), namespace))
        ) {
          remove = false;
          break;
        }
      }
      if (remove) {
        removeUsers.add(user);
      }
    }
    return removeUsers;
  }

  static final class SnapshotScannerHDFSAclStorage {
    /**
     * Add a new CF in HBase acl table to record if the HBase read permission is synchronized to
     * related hfiles. The record has two usages: 1. check if we need to remove HDFS acls for a
     * grant without READ permission(eg: grant user table read permission and then grant user table
     * write permission without merging the existing permissions, in this case, need to remove HDFS
     * acls); 2. skip some HDFS acl sync because it may be already set(eg: grant user table read
     * permission and then grant user ns read permission; grant user table read permission and then
     * grant user table write permission with merging the existing permissions).
     */
    static final byte[] HDFS_ACL_FAMILY = Bytes.toBytes("m");
    // The value 'R' has no specific meaning, if cell value is not null, it means that the user HDFS
    // acls is set to hfiles.
    private static final byte[] HDFS_ACL_VALUE = Bytes.toBytes("R");

    static void addUserGlobalHdfsAcl(Table aclTable, String user) throws IOException {
      addUserEntry(aclTable, user, PermissionStorage.ACL_GLOBAL_NAME);
    }

    static void addUserNamespaceHdfsAcl(Table aclTable, String user, String namespace)
      throws IOException {
      addUserEntry(aclTable, user, Bytes.toBytes(PermissionStorage.toNamespaceEntry(namespace)));
    }

    static void addUserTableHdfsAcl(Connection connection, Set<String> users, TableName tableName)
      throws IOException {
      try (Table aclTable = connection.getTable(PermissionStorage.ACL_TABLE_NAME)) {
        for (String user : users) {
          addUserTableHdfsAcl(aclTable, user, tableName);
        }
      }
    }

    static void addUserTableHdfsAcl(Connection connection, String user, TableName tableName)
      throws IOException {
      try (Table aclTable = connection.getTable(PermissionStorage.ACL_TABLE_NAME)) {
        addUserTableHdfsAcl(aclTable, user, tableName);
      }
    }

    static void addUserTableHdfsAcl(Table aclTable, String user, TableName tableName)
      throws IOException {
      addUserEntry(aclTable, user, tableName.getName());
    }

    private static void addUserEntry(Table t, String user, byte[] entry) throws IOException {
      Put p = new Put(entry);
      p.addColumn(HDFS_ACL_FAMILY, Bytes.toBytes(user), HDFS_ACL_VALUE);
      t.put(p);
    }

    static void deleteUserGlobalHdfsAcl(Table aclTable, String user) throws IOException {
      deleteUserEntry(aclTable, user, PermissionStorage.ACL_GLOBAL_NAME);
    }

    static void deleteUserNamespaceHdfsAcl(Table aclTable, String user, String namespace)
      throws IOException {
      deleteUserEntry(aclTable, user, Bytes.toBytes(PermissionStorage.toNamespaceEntry(namespace)));
    }

    static void deleteUserTableHdfsAcl(Table aclTable, String user, TableName tableName)
      throws IOException {
      deleteUserEntry(aclTable, user, tableName.getName());
    }

    static void deleteUserTableHdfsAcl(Connection connection, Set<String> users,
      TableName tableName) throws IOException {
      try (Table aclTable = connection.getTable(PermissionStorage.ACL_TABLE_NAME)) {
        for (String user : users) {
          deleteUserTableHdfsAcl(aclTable, user, tableName);
        }
      }
    }

    private static void deleteUserEntry(Table aclTable, String user, byte[] entry)
      throws IOException {
      Delete delete = new Delete(entry);
      delete.addColumns(HDFS_ACL_FAMILY, Bytes.toBytes(user));
      aclTable.delete(delete);
    }

    static void deleteNamespaceHdfsAcl(Connection connection, String namespace) throws IOException {
      try (Table aclTable = connection.getTable(PermissionStorage.ACL_TABLE_NAME)) {
        deleteEntry(aclTable, Bytes.toBytes(PermissionStorage.toNamespaceEntry(namespace)));
      }
    }

    static void deleteTableHdfsAcl(Table aclTable, TableName tableName) throws IOException {
      deleteEntry(aclTable, tableName.getName());
    }

    private static void deleteEntry(Table aclTable, byte[] entry) throws IOException {
      Delete delete = new Delete(entry);
      delete.addFamily(HDFS_ACL_FAMILY);
      aclTable.delete(delete);
    }

    static Set<String> getTableUsers(Table aclTable, TableName tableName) throws IOException {
      return getEntryUsers(aclTable, tableName.getName());
    }

    private static Set<String> getEntryUsers(Table aclTable, byte[] entry) throws IOException {
      Set<String> users = new HashSet<>();
      Get get = new Get(entry);
      get.addFamily(HDFS_ACL_FAMILY);
      Result result = aclTable.get(get);
      List<Cell> cells = result.listCells();
      if (cells != null) {
        for (Cell cell : cells) {
          if (cell != null) {
            users.add(Bytes.toString(CellUtil.cloneQualifier(cell)));
          }
        }
      }
      return users;
    }

    static Pair<Set<String>, Set<TableName>> getUserNamespaceAndTable(Table aclTable,
      String userName) throws IOException {
      Set<String> namespaces = new HashSet<>();
      Set<TableName> tables = new HashSet<>();
      List<byte[]> userEntries = getUserEntries(aclTable, userName);
      for (byte[] entry : userEntries) {
        if (PermissionStorage.isNamespaceEntry(entry)) {
          namespaces.add(Bytes.toString(PermissionStorage.fromNamespaceEntry(entry)));
        } else if (PermissionStorage.isTableEntry(entry)) {
          tables.add(TableName.valueOf(entry));
        }
      }
      return new Pair<>(namespaces, tables);
    }

    static List<byte[]> getUserEntries(Table aclTable, String userName) throws IOException {
      Scan scan = new Scan();
      scan.addColumn(HDFS_ACL_FAMILY, Bytes.toBytes(userName));
      ResultScanner scanner = aclTable.getScanner(scan);
      List<byte[]> entry = new ArrayList<>();
      for (Result result : scanner) {
        if (result != null && result.getRow() != null) {
          entry.add(result.getRow());
        }
      }
      return entry;
    }

    static boolean hasUserGlobalHdfsAcl(Table aclTable, String user) throws IOException {
      return hasUserEntry(aclTable, user, PermissionStorage.ACL_GLOBAL_NAME);
    }

    static boolean hasUserNamespaceHdfsAcl(Table aclTable, String user, String namespace)
      throws IOException {
      return hasUserEntry(aclTable, user,
        Bytes.toBytes(PermissionStorage.toNamespaceEntry(namespace)));
    }

    static boolean hasUserTableHdfsAcl(Table aclTable, String user, TableName tableName)
      throws IOException {
      return hasUserEntry(aclTable, user, tableName.getName());
    }

    private static boolean hasUserEntry(Table aclTable, String userName, byte[] entry)
      throws IOException {
      Get get = new Get(entry);
      get.addColumn(HDFS_ACL_FAMILY, Bytes.toBytes(userName));
      return aclTable.exists(get);
    }
  }
}
