/*
 * 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.hive.ql.security.authorization.plugin;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.metastore.Warehouse;
import org.apache.hadoop.hive.metastore.api.Database;
import org.apache.hadoop.hive.metastore.api.HiveObjectPrivilege;
import org.apache.hadoop.hive.metastore.api.HiveObjectRef;
import org.apache.hadoop.hive.metastore.api.HiveObjectType;
import org.apache.hadoop.hive.metastore.api.Partition;
import org.apache.hadoop.hive.metastore.api.PrincipalType;
import org.apache.hadoop.hive.metastore.api.PrivilegeBag;
import org.apache.hadoop.hive.metastore.api.PrivilegeGrantInfo;
import org.apache.hadoop.hive.metastore.api.Role;
import org.apache.hadoop.hive.metastore.api.RolePrincipalGrant;
import org.apache.hadoop.hive.ql.metadata.Hive;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.metadata.Table;
import org.apache.hadoop.hive.ql.parse.SemanticException;
import org.apache.hadoop.hive.ql.security.authorization.AuthorizationUtils;
import org.apache.hadoop.hive.ql.security.authorization.PrivilegeScope;
import org.apache.hadoop.hive.ql.security.authorization.plugin.sqlstd.SQLStdHiveAccessController;
import org.apache.hadoop.hive.ql.session.SessionState;

public class HiveV1Authorizer extends AbstractHiveAuthorizer {

  private final HiveConf conf;
  static private final String AUTHORIZER = "v1";

  public HiveV1Authorizer(HiveConf conf) {
    this.conf = conf;
  }


  // Leave this ctor around for backward compat.
  @Deprecated
  public HiveV1Authorizer(HiveConf conf, Hive hive) {
    this(conf);
  }

  @Override
  public VERSION getVersion() {
    return VERSION.V1;
  }

  @Override
  public void checkPrivileges(HiveOperationType hiveOpType, List<HivePrivilegeObject> inputsHObjs,
      List<HivePrivilegeObject> outputHObjs, HiveAuthzContext context)
      throws HiveAuthzPluginException, HiveAccessControlException {
    throw new UnsupportedOperationException("Should not be called for v1 authorizer");
  }

  @Override
  public void grantPrivileges(
      List<HivePrincipal> principals, List<HivePrivilege> privileges, HivePrivilegeObject privObject,
      HivePrincipal grantor, boolean grantOption)
      throws HiveAuthzPluginException, HiveAccessControlException {
    try {
      PrivilegeBag privBag = toPrivilegeBag(privileges, privObject, grantor, grantOption, AUTHORIZER);
      grantOrRevokePrivs(principals, privBag, true, grantOption);
    } catch (Exception e) {
      throw new HiveAuthzPluginException(e);
    }
  }

  @Override
  public void revokePrivileges(
      List<HivePrincipal> principals, List<HivePrivilege> privileges, HivePrivilegeObject privObject,
      HivePrincipal grantor, boolean grantOption)
      throws HiveAuthzPluginException, HiveAccessControlException {
    try {
      PrivilegeBag privBag = toPrivilegeBag(privileges, privObject, grantor, grantOption, AUTHORIZER);
      grantOrRevokePrivs(principals, privBag, false, grantOption);
    } catch (Exception e) {
      throw new HiveAuthzPluginException(e);
    }
  }

  private void grantOrRevokePrivs(List<HivePrincipal> principals, PrivilegeBag privBag,
      boolean isGrant, boolean grantOption) throws HiveException {
    for (HivePrincipal principal : principals) {
      PrincipalType type = AuthorizationUtils.getThriftPrincipalType(principal.getType());
      for (HiveObjectPrivilege priv : privBag.getPrivileges()) {
        priv.setPrincipalName(principal.getName());
        priv.setPrincipalType(type);
      }
      Hive hive = Hive.getWithFastCheck(this.conf);
      if (isGrant) {
        hive.grantPrivileges(privBag);
      } else {
        hive.revokePrivileges(privBag, grantOption);
      }
    }
  }

  private PrivilegeBag toPrivilegeBag(List<HivePrivilege> privileges,
      HivePrivilegeObject privObject, HivePrincipal grantor, boolean grantOption, String authorizer)
      throws HiveException {

    PrivilegeBag privBag = new PrivilegeBag();
    if (privileges.isEmpty()) {
      return privBag;
    }
    String grantorName = grantor.getName();
    PrincipalType grantorType = AuthorizationUtils.getThriftPrincipalType(grantor.getType());
    if (privObject.getType() == null ||
        privObject.getType() == HivePrivilegeObject.HivePrivilegeObjectType.GLOBAL) {
      for (HivePrivilege priv : privileges) {
        List<String> columns = priv.getColumns();
        if (columns != null && !columns.isEmpty()) {
          throw new HiveException(
              "For user-level privileges, column sets should be null. columns=" +
                  columns.toString());
        }
        privBag.addToPrivileges(new HiveObjectPrivilege(new HiveObjectRef(
            HiveObjectType.GLOBAL, null, null, null, null), null, null,
            new PrivilegeGrantInfo(priv.getName(), 0, grantor.getName(), grantorType,
                grantOption), authorizer));
      }
      return privBag;
    }

    if (privObject.getPartKeys() != null && grantOption) {
      throw new HiveException("Grant does not support partition level.");
    }
    Hive hive = Hive.getWithFastCheck(this.conf);
    Database dbObj = hive.getDatabase(privObject.getDbname());
    if (dbObj == null) {
      throw new HiveException("Database " + privObject.getDbname() + " does not exists");
    }
    Table tableObj = null;
    if (privObject.getObjectName() != null) {
      tableObj = hive.getTable(dbObj.getName(), privObject.getObjectName());
    }

    List<String> partValues = null;
    if (tableObj != null) {
      if ((!tableObj.isPartitioned())
          && privObject.getPartKeys() != null) {
        throw new HiveException(
            "Table is not partitioned, but partition name is present: partSpec="
                + privObject.getPartKeys());
      }

      if (privObject.getPartKeys() != null) {
        Map<String, String> partSpec =
            Warehouse.makeSpecFromValues(tableObj.getPartitionKeys(), privObject.getPartKeys());
        Partition partObj = hive.getPartition(tableObj, partSpec, false).getTPartition();
        partValues = partObj.getValues();
      }
    }

    for (HivePrivilege priv : privileges) {
      List<String> columns = priv.getColumns();
      if (columns != null && !columns.isEmpty()) {
        if (!priv.supportsScope(PrivilegeScope.COLUMN_LEVEL_SCOPE)) {
          throw new HiveException(priv.getName() + " does not support column level privilege.");
        }
        if (tableObj == null) {
          throw new HiveException(
              "For user-level/database-level privileges, column sets should be null. columns="
                  + columns);
        }
        for (int i = 0; i < columns.size(); i++) {
          privBag.addToPrivileges(new HiveObjectPrivilege(
              new HiveObjectRef(HiveObjectType.COLUMN, dbObj.getName(), tableObj.getTableName(),
                  partValues, columns.get(i)), null, null,
              new PrivilegeGrantInfo(priv.getName(), 0, grantorName, grantorType, grantOption), authorizer));
        }
      } else if (tableObj == null) {
        privBag.addToPrivileges(new HiveObjectPrivilege(
            new HiveObjectRef(HiveObjectType.DATABASE, dbObj.getName(), null,
                null, null), null, null,
            new PrivilegeGrantInfo(priv.getName(), 0, grantorName, grantorType, grantOption), authorizer));
      } else if (partValues == null) {
        privBag.addToPrivileges(new HiveObjectPrivilege(
            new HiveObjectRef(HiveObjectType.TABLE, dbObj.getName(), tableObj.getTableName(),
                null, null), null, null,
            new PrivilegeGrantInfo(priv.getName(), 0, grantorName, grantorType, grantOption), authorizer));
      } else {
        privBag.addToPrivileges(new HiveObjectPrivilege(
            new HiveObjectRef(HiveObjectType.PARTITION, dbObj.getName(), tableObj.getTableName(),
                partValues, null), null, null,
            new PrivilegeGrantInfo(priv.getName(), 0, grantorName, grantorType, grantOption), authorizer));
      }
    }
    return privBag;
  }

  @Override
  public void createRole(String roleName, HivePrincipal adminGrantor) throws HiveAuthzPluginException, HiveAccessControlException {
    try {
      Hive hive = Hive.getWithFastCheck(this.conf);
      hive.createRole(roleName, adminGrantor == null ? null : adminGrantor.getName());
    } catch (HiveException e) {
      throw new HiveAuthzPluginException(e);
    }
  }

  @Override
  public void dropRole(String roleName) throws HiveAuthzPluginException, HiveAccessControlException {
    try {
      Hive hive = Hive.getWithFastCheck(this.conf);
      hive.dropRole(roleName);
    } catch (HiveException e) {
      throw new HiveAuthzPluginException(e);
    }
  }

  @Override
  public List<HiveRoleGrant> getPrincipalGrantInfoForRole(String roleName) throws HiveAuthzPluginException, HiveAccessControlException {
    try {
      Hive hive = Hive.getWithFastCheck(this.conf);
      return SQLStdHiveAccessController.getHiveRoleGrants(hive.getMSC(), roleName);
    } catch (Exception e) {
      throw new HiveAuthzPluginException(e);
    }
  }

  @Override
  public List<HiveRoleGrant> getRoleGrantInfoForPrincipal(HivePrincipal principal) throws HiveAuthzPluginException, HiveAccessControlException {
    PrincipalType type = AuthorizationUtils.getThriftPrincipalType(principal.getType());
    try {
      List<HiveRoleGrant> grants = new ArrayList<HiveRoleGrant>();
      Hive hive = Hive.getWithFastCheck(this.conf);
      for (RolePrincipalGrant grant : hive.getRoleGrantInfoForPrincipal(principal.getName(), type)) {
        grants.add(new HiveRoleGrant(grant));
      }
      return grants;
    } catch (HiveException e) {
      throw new HiveAuthzPluginException(e);
    }
  }

  @Override
  public void grantRole(List<HivePrincipal> principals, List<String> roles, boolean grantOption,
      HivePrincipal grantor) throws HiveAuthzPluginException, HiveAccessControlException {
    try {
      grantOrRevokeRole(principals, roles, grantOption, grantor, true);
    } catch (HiveException e) {
      throw new HiveAuthzPluginException(e);
    }
  }

  @Override
  public void revokeRole(List<HivePrincipal> principals, List<String> roles, boolean grantOption,
      HivePrincipal grantor) throws HiveAuthzPluginException, HiveAccessControlException {
    try {
      grantOrRevokeRole(principals, roles, grantOption, grantor, false);
    } catch (HiveException e) {
      throw new HiveAuthzPluginException(e);
    }
  }

  private void grantOrRevokeRole(List<HivePrincipal> principals, List<String> roles,
      boolean grantOption, HivePrincipal grantor, boolean isGrant) throws HiveException {
    PrincipalType grantorType = AuthorizationUtils.getThriftPrincipalType(grantor.getType());
    Hive hive = Hive.getWithFastCheck(this.conf);
    for (HivePrincipal principal : principals) {
      PrincipalType principalType = AuthorizationUtils.getThriftPrincipalType(principal.getType());
      String userName = principal.getName();
      for (String roleName : roles) {
        if (isGrant) {
          hive.grantRole(roleName, userName, principalType,
              grantor.getName(), grantorType, grantOption);
        } else {
          hive.revokeRole(roleName, userName, principalType, grantOption);
        }
      }
    }
  }

  @Override
  public List<String> getAllRoles() throws HiveAuthzPluginException, HiveAccessControlException {
    try {
      Hive hive = Hive.getWithFastCheck(this.conf);
      return hive.getAllRoleNames();
    } catch (HiveException e) {
      throw new HiveAuthzPluginException(e);
    }
  }

  @Override
  public List<HivePrivilegeInfo> showPrivileges(HivePrincipal principal, HivePrivilegeObject privObj)
      throws HiveAuthzPluginException, HiveAccessControlException {
    String name = principal == null ? null : principal.getName();
    PrincipalType type =
        AuthorizationUtils.getThriftPrincipalType(principal == null ? null : principal.getType());

    List<HiveObjectPrivilege> privs = new ArrayList<HiveObjectPrivilege>();
    try {
      Hive hive = Hive.getWithFastCheck(this.conf);
      if (privObj == null) {
        // show user level privileges
        privs.addAll(hive.showPrivilegeGrant(HiveObjectType.GLOBAL, name, type,
            null, null, null, null));
      } else if (privObj.getDbname() == null) {
        // show all privileges
        privs.addAll(hive.showPrivilegeGrant(null, name, type, null, null, null, null));
      } else {
        Database dbObj = hive.getDatabase(privObj.getDbname());;
        if (dbObj == null) {
          throw new HiveException("Database " + privObj.getDbname() + " does not exists");
        }
        Table tableObj = null;
        if (privObj.getObjectName() != null) {
          tableObj = hive.getTable(dbObj.getName(), privObj.getObjectName());
        }
        List<String> partValues = privObj.getPartKeys();

        if (tableObj == null) {
          // show database level privileges
          privs.addAll(hive.showPrivilegeGrant(HiveObjectType.DATABASE,
              name, type, dbObj.getName(), null, null, null));
        } else {
          List<String> columns = privObj.getColumns();
          if (columns != null && !columns.isEmpty()) {
            // show column level privileges
            for (String columnName : columns) {
              privs.addAll(hive.showPrivilegeGrant(HiveObjectType.COLUMN, name, type,
                  dbObj.getName(), tableObj.getTableName(), partValues, columnName));
            }
          } else if (partValues == null) {
            // show table level privileges
            privs.addAll(hive.showPrivilegeGrant(HiveObjectType.TABLE, name, type,
                dbObj.getName(), tableObj.getTableName(), null, null));
          } else {
            // show partition level privileges
            privs.addAll(hive.showPrivilegeGrant(HiveObjectType.PARTITION, name, type,
                dbObj.getName(), tableObj.getTableName(), partValues, null));
          }
        }
      }
      return AuthorizationUtils.getPrivilegeInfos(privs);
    } catch (Exception ex) {
      throw new HiveAuthzPluginException(ex);
    }
  }

  @Override
  public void setCurrentRole(String roleName) throws HiveAccessControlException, HiveAuthzPluginException {
    throw new HiveAuthzPluginException("Unsupported operation 'setCurrentRole' for V1 auth");
  }

  @Override
  public List<String> getCurrentRoleNames() throws HiveAuthzPluginException {

    String userName = SessionState.get().getUserName();
    if (userName == null) {
      userName = SessionState.getUserFromAuthenticator();
    }
    if (userName == null) {
      throw new HiveAuthzPluginException("Cannot resolve current user name");
    }
    try {
      Hive hive = Hive.getWithFastCheck(this.conf);
      List<String> roleNames = new ArrayList<String>();
      for (Role role : hive.listRoles(userName, PrincipalType.USER)) {
        roleNames.add(role.getRoleName());
      }
      return roleNames;
    } catch (HiveException e) {
      throw new HiveAuthzPluginException(e);
    }
  }

  @Override
  public void applyAuthorizationConfigPolicy(HiveConf hiveConf) {
  }

  @Override
  public List<HivePrivilegeObject> filterListCmdObjects(List<HivePrivilegeObject> listObjs,
      HiveAuthzContext context) throws HiveAuthzPluginException, HiveAccessControlException {
    // do no filtering in old authorizer
    return listObjs;
  }

  @Override
  public boolean needTransform() {
    return false;
  }

  @Override
  public List<HivePrivilegeObject> applyRowFilterAndColumnMasking(HiveAuthzContext context,
      List<HivePrivilegeObject> privObjs) throws SemanticException {
    return null;
  }

}
