/*
 * 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;

import java.util.Iterator;
import java.util.List;

import com.google.common.base.Function;
import com.google.common.collect.Iterators;
import org.apache.hadoop.hive.metastore.utils.MetaStoreUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hive.common.classification.InterfaceAudience.Private;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
import org.apache.hadoop.hive.metastore.MetaStorePreEventListener;
import org.apache.hadoop.hive.metastore.TableType;
import org.apache.hadoop.hive.metastore.api.Database;
import org.apache.hadoop.hive.metastore.api.InvalidOperationException;
import org.apache.hadoop.hive.metastore.api.MetaException;
import org.apache.hadoop.hive.metastore.api.NoSuchObjectException;
import org.apache.hadoop.hive.metastore.api.Partition;
import org.apache.hadoop.hive.metastore.events.PreAddPartitionEvent;
import org.apache.hadoop.hive.metastore.events.PreAlterDatabaseEvent;
import org.apache.hadoop.hive.metastore.events.PreAlterPartitionEvent;
import org.apache.hadoop.hive.metastore.events.PreAlterTableEvent;
import org.apache.hadoop.hive.metastore.events.PreCreateDatabaseEvent;
import org.apache.hadoop.hive.metastore.events.PreCreateFunctionEvent;
import org.apache.hadoop.hive.metastore.events.PreCreateTableEvent;
import org.apache.hadoop.hive.metastore.events.PreDropDatabaseEvent;
import org.apache.hadoop.hive.metastore.events.PreDropFunctionEvent;
import org.apache.hadoop.hive.metastore.events.PreDropPartitionEvent;
import org.apache.hadoop.hive.metastore.events.PreDropTableEvent;
import org.apache.hadoop.hive.metastore.events.PreEventContext;
import org.apache.hadoop.hive.metastore.events.PreReadDatabaseEvent;
import org.apache.hadoop.hive.metastore.events.PreReadTableEvent;
import org.apache.hadoop.hive.ql.metadata.AuthorizationException;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.metadata.HiveUtils;
import org.apache.hadoop.hive.ql.plan.HiveOperation;
import org.apache.hadoop.hive.ql.security.HiveMetastoreAuthenticationProvider;

/**
 * AuthorizationPreEventListener : A MetaStorePreEventListener that
 * performs authorization/authentication checks on the metastore-side.
 *
 * Note that this can only perform authorization checks on defined
 * metastore PreEventContexts, such as the adding/dropping and altering
 * of databases, tables and partitions.
 */
@Private
public class AuthorizationPreEventListener extends MetaStorePreEventListener {

  public static final Logger LOG = LoggerFactory.getLogger(
      AuthorizationPreEventListener.class);

  private static final ThreadLocal<Configuration> tConfig = new ThreadLocal<Configuration>() {
    @Override
    protected Configuration initialValue() {
      return new HiveConf(AuthorizationPreEventListener.class);
    }
  };

  private static final ThreadLocal<HiveMetastoreAuthenticationProvider> tAuthenticator
      = new ThreadLocal<HiveMetastoreAuthenticationProvider>() {
    @Override
    protected HiveMetastoreAuthenticationProvider initialValue() {
      try {
        return  (HiveMetastoreAuthenticationProvider) HiveUtils.getAuthenticator(
            tConfig.get(), HiveConf.ConfVars.HIVE_METASTORE_AUTHENTICATOR_MANAGER);
      } catch (HiveException he) {
        throw new IllegalStateException("Authentication provider instantiation failure",he);
      }
    }
  };

  private static final ThreadLocal<List<HiveMetastoreAuthorizationProvider>> tAuthorizers
      = new ThreadLocal<List<HiveMetastoreAuthorizationProvider>>() {
    @Override
    protected List<HiveMetastoreAuthorizationProvider> initialValue() {
      try {
        return  HiveUtils.getMetaStoreAuthorizeProviderManagers(
            tConfig.get(), HiveConf.ConfVars.HIVE_METASTORE_AUTHORIZATION_MANAGER, tAuthenticator.get());
      } catch (HiveException he) {
        throw new IllegalStateException("Authorization provider instantiation failure",he);
      }
    }
  };

  private static final ThreadLocal<Boolean> tConfigSetOnAuths = new ThreadLocal<Boolean>() {
    @Override
    protected Boolean initialValue() {
      return false;
    }
  };

  public AuthorizationPreEventListener(Configuration config) throws HiveException {
    super(config);
  }

  @Override
  public void onEvent(PreEventContext context) throws MetaException, NoSuchObjectException,
      InvalidOperationException {

    if (!tConfigSetOnAuths.get()){
      // The reason we do this guard is because when we do not have a good way of initializing
      // the config to the handler's thread local config until this call, so we do it then.
      // Once done, though, we need not repeat this linking, we simply call setMetaStoreHandler
      // and let the AuthorizationProvider and AuthenticationProvider do what they want.
      tConfig.set(context.getHandler().getConf());
      // Warning note : HMSHandler.getHiveConf() is not thread-unique, .getConf() is.
      tAuthenticator.get().setConf(tConfig.get());
      for(HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()){
        authorizer.setConf(tConfig.get());
      }
      tConfigSetOnAuths.set(true); // set so we don't repeat this initialization
    }

    tAuthenticator.get().setMetaStoreHandler(context.getHandler());
    for(HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()){
      authorizer.setMetaStoreHandler(context.getHandler());
    }

    switch (context.getEventType()) {
    case CREATE_TABLE:
      authorizeCreateTable((PreCreateTableEvent)context);
      break;
    case DROP_TABLE:
      authorizeDropTable((PreDropTableEvent)context);
      break;
    case ALTER_TABLE:
      authorizeAlterTable((PreAlterTableEvent)context);
      break;
    case READ_TABLE:
      authorizeReadTable((PreReadTableEvent)context);
      break;
    case READ_DATABASE:
      authorizeReadDatabase((PreReadDatabaseEvent)context);
      break;
    case ADD_PARTITION:
      authorizeAddPartition((PreAddPartitionEvent)context);
      break;
    case DROP_PARTITION:
      authorizeDropPartition((PreDropPartitionEvent)context);
      break;
    case ALTER_PARTITION:
      authorizeAlterPartition((PreAlterPartitionEvent)context);
      break;
    case CREATE_DATABASE:
      authorizeCreateDatabase((PreCreateDatabaseEvent)context);
      break;
    case ALTER_DATABASE:
        authorizeAlterDatabase((PreAlterDatabaseEvent) context);
        break;
    case DROP_DATABASE:
      authorizeDropDatabase((PreDropDatabaseEvent)context);
      break;
    case CREATE_FUNCTION:
      authorizeCreateFunction((PreCreateFunctionEvent)context);
      break;
    case DROP_FUNCTION:
      authorizeDropFunction((PreDropFunctionEvent)context);
      break;
    case LOAD_PARTITION_DONE:
      // noop for now
      break;
    case AUTHORIZATION_API_CALL:
      authorizeAuthorizationAPICall();
    default:
      break;
    }

  }

  private void authorizeReadTable(PreReadTableEvent context) throws InvalidOperationException,
      MetaException {
    if (!isReadAuthzEnabled()) {
      return;
    }
    try {
      org.apache.hadoop.hive.ql.metadata.Table wrappedTable = new TableWrapper(context.getTable());
      for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
        authorizer.authorize(wrappedTable, new Privilege[] { Privilege.SELECT }, null);
      }
    } catch (AuthorizationException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private void authorizeReadDatabase(PreReadDatabaseEvent context)
      throws InvalidOperationException, MetaException {
    if (!isReadAuthzEnabled()) {
      return;
    }
    try {
      for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
        authorizer.authorize(new Database(context.getDatabase()),
            new Privilege[] { Privilege.SELECT }, null);
      }
    } catch (AuthorizationException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private boolean isReadAuthzEnabled() {
    return tConfig.get().getBoolean(ConfVars.HIVE_METASTORE_AUTHORIZATION_AUTH_READS.varname, true);
  }

  private void authorizeAuthorizationAPICall() throws InvalidOperationException, MetaException {
    for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
      try {
        authorizer.authorizeAuthorizationApiInvocation();
      } catch (AuthorizationException e) {
        throw invalidOperationException(e);
      } catch (HiveException e) {
        throw metaException(e);
      }
    }
  }

  private void authorizeCreateDatabase(PreCreateDatabaseEvent context)
      throws InvalidOperationException, MetaException {
    try {
      for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
        authorizer.authorize(new Database(context.getDatabase()),
            HiveOperation.CREATEDATABASE.getInputRequiredPrivileges(),
            HiveOperation.CREATEDATABASE.getOutputRequiredPrivileges());
      }
    } catch (AuthorizationException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private void authorizeDropDatabase(PreDropDatabaseEvent context)
      throws InvalidOperationException, MetaException {
    try {
      for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
        authorizer.authorize(new Database(context.getDatabase()),
            HiveOperation.DROPDATABASE.getInputRequiredPrivileges(),
            HiveOperation.DROPDATABASE.getOutputRequiredPrivileges());
      }
    } catch (AuthorizationException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private void authorizeAlterDatabase(PreAlterDatabaseEvent context)
      throws InvalidOperationException, MetaException {
    try {
      for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
        authorizer.authorize(new Database(context.getOldDatabase()),
            HiveOperation.ALTERDATABASE_LOCATION.getInputRequiredPrivileges(),
            HiveOperation.ALTERDATABASE_LOCATION.getOutputRequiredPrivileges());
      }
    } catch (AuthorizationException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private void authorizeCreateTable(PreCreateTableEvent context)
      throws InvalidOperationException, MetaException {
    try {
      org.apache.hadoop.hive.ql.metadata.Table wrappedTable = new TableWrapper(context.getTable());
      for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
        authorizer.authorize(wrappedTable,
            HiveOperation.CREATETABLE.getInputRequiredPrivileges(),
            HiveOperation.CREATETABLE.getOutputRequiredPrivileges());
      }
    } catch (AuthorizationException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private void authorizeDropTable(PreDropTableEvent context)
      throws InvalidOperationException, MetaException {
    try {
      org.apache.hadoop.hive.ql.metadata.Table wrappedTable = new TableWrapper(context.getTable());
      for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
        authorizer.authorize(wrappedTable,
            HiveOperation.DROPTABLE.getInputRequiredPrivileges(),
            HiveOperation.DROPTABLE.getOutputRequiredPrivileges());
      }
    } catch (AuthorizationException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private void authorizeAlterTable(PreAlterTableEvent context)
      throws InvalidOperationException, MetaException {

    try {
      org.apache.hadoop.hive.ql.metadata.Table wrappedTable = new TableWrapper(context.getOldTable());
      for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
        authorizer.authorize(wrappedTable,
            null,
            new Privilege[]{Privilege.ALTER_METADATA});
      }
    } catch (AuthorizationException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private void authorizeAddPartition(PreAddPartitionEvent context)
      throws InvalidOperationException, MetaException {
    try {
      for (org.apache.hadoop.hive.metastore.api.Partition mapiPart : context.getPartitions()) {
        org.apache.hadoop.hive.ql.metadata.Partition wrappedPartiton = new PartitionWrapper(
            mapiPart, context);
    for(HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()){
          authorizer.authorize(wrappedPartiton,
              HiveOperation.ALTERTABLE_ADDPARTS.getInputRequiredPrivileges(),
              HiveOperation.ALTERTABLE_ADDPARTS.getOutputRequiredPrivileges());
        }
      }
    } catch (AuthorizationException | NoSuchObjectException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private void authorizeDropMultiPartition(HiveMultiPartitionAuthorizationProviderBase authorizer,
                                           final PreDropPartitionEvent context)
      throws AuthorizationException, HiveException {
    Iterator<Partition> partitionIterator = context.getPartitionIterator();

    final TableWrapper table = new TableWrapper(context.getTable());
    final Iterator<org.apache.hadoop.hive.ql.metadata.Partition> qlPartitionIterator =
        Iterators.transform(partitionIterator, new Function<Partition, org.apache.hadoop.hive.ql.metadata.Partition>() {
          @Override
          public org.apache.hadoop.hive.ql.metadata.Partition apply(Partition partition) {
            try {
              return new PartitionWrapper(table, partition);
            } catch (Exception exception) {
              LOG.error("Could not construct partition-object for: " + partition, exception);
              throw new RuntimeException(exception);
            }
          }
        });

    authorizer.authorize(new TableWrapper(context.getTable()),
                         new Iterable<org.apache.hadoop.hive.ql.metadata.Partition>() {
                           @Override
                           public Iterator<org.apache.hadoop.hive.ql.metadata.Partition> iterator() {
                             return qlPartitionIterator;
                           }
                         },
                         HiveOperation.ALTERTABLE_DROPPARTS.getInputRequiredPrivileges(),
                         HiveOperation.ALTERTABLE_DROPPARTS.getOutputRequiredPrivileges());
  }

  private void authorizeDropPartition(PreDropPartitionEvent context)
      throws InvalidOperationException, MetaException {
    try {
      for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
        if (authorizer instanceof HiveMultiPartitionAuthorizationProviderBase) {
          // Authorize all dropped-partitions in one shot.
          authorizeDropMultiPartition((HiveMultiPartitionAuthorizationProviderBase)authorizer, context);
        }
        else {
          // Authorize individually.
          TableWrapper table = new TableWrapper(context.getTable());
          Iterator<Partition> partitionIterator = context.getPartitionIterator();
          while (partitionIterator.hasNext()) {
            authorizer.authorize(
                new PartitionWrapper(table, partitionIterator.next()),
                HiveOperation.ALTERTABLE_DROPPARTS.getInputRequiredPrivileges(),
                HiveOperation.ALTERTABLE_DROPPARTS.getOutputRequiredPrivileges()
            );
          }
        }
      }
    } catch (AuthorizationException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private void authorizeCreateFunction(PreCreateFunctionEvent context)
          throws InvalidOperationException, MetaException {
    try {
      for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
        authorizer.authorizeDbLevelOperations(
                HiveOperation.CREATEFUNCTION.getInputRequiredPrivileges(),
                HiveOperation.CREATEFUNCTION.getOutputRequiredPrivileges(), null, null);
      }
    } catch (AuthorizationException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private void authorizeDropFunction(PreDropFunctionEvent context)
          throws InvalidOperationException, MetaException {
    try {
      for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
        authorizer.authorizeDbLevelOperations(
                HiveOperation.DROPFUNCTION.getInputRequiredPrivileges(),
                HiveOperation.DROPFUNCTION.getOutputRequiredPrivileges(), null, null);
      }
    } catch (AuthorizationException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private void authorizeAlterPartition(PreAlterPartitionEvent context)
      throws InvalidOperationException, MetaException {
    try {
      org.apache.hadoop.hive.metastore.api.Partition mapiPart = context.getNewPartition();
      org.apache.hadoop.hive.ql.metadata.Partition wrappedPartition = new PartitionWrapper(
          mapiPart, context);
    for (HiveMetastoreAuthorizationProvider authorizer : tAuthorizers.get()) {
       authorizer.authorize(wrappedPartition,
            null,
            new Privilege[]{Privilege.ALTER_METADATA});
      }
    } catch (AuthorizationException | NoSuchObjectException e) {
      throw invalidOperationException(e);
    } catch (HiveException e) {
      throw metaException(e);
    }
  }

  private InvalidOperationException invalidOperationException(Exception e) {
    InvalidOperationException ex = new InvalidOperationException(e.getMessage());
    ex.initCause(e.getCause());
    return ex;
  }

  private MetaException metaException(HiveException e) {
    MetaException ex =  new MetaException(e.getMessage());
    ex.initCause(e);
    return ex;
  }

  // Wrapper extends ql.metadata.Table for easy construction syntax
  public static class TableWrapper extends org.apache.hadoop.hive.ql.metadata.Table {

    public TableWrapper(org.apache.hadoop.hive.metastore.api.Table apiTable) {
      org.apache.hadoop.hive.metastore.api.Table wrapperApiTable = apiTable.deepCopy();
      if (wrapperApiTable.getTableType() == null){
        // TableType specified was null, we need to figure out what type it was.
        if (MetaStoreUtils.isExternalTable(wrapperApiTable)){
          wrapperApiTable.setTableType(TableType.EXTERNAL_TABLE.toString());
        } else if (MetaStoreUtils.isMaterializedViewTable(wrapperApiTable)) {
          wrapperApiTable.setTableType(TableType.MATERIALIZED_VIEW.toString());
        } else if ((wrapperApiTable.getSd() == null) || (wrapperApiTable.getSd().getLocation() == null)) {
          wrapperApiTable.setTableType(TableType.VIRTUAL_VIEW.toString());
        } else {
          wrapperApiTable.setTableType(TableType.MANAGED_TABLE.toString());
        }
      }
      initialize(wrapperApiTable);
    }
  }

  // Wrapper extends ql.metadata.Partition for easy construction syntax
  public static class PartitionWrapper extends org.apache.hadoop.hive.ql.metadata.Partition {

    public PartitionWrapper(org.apache.hadoop.hive.ql.metadata.Table table,
        org.apache.hadoop.hive.metastore.api.Partition mapiPart) throws HiveException {
      initialize(table,mapiPart);
    }

    public PartitionWrapper(org.apache.hadoop.hive.metastore.api.Partition mapiPart,
        PreEventContext context) throws HiveException, NoSuchObjectException, MetaException {
      org.apache.hadoop.hive.metastore.api.Partition wrapperApiPart = mapiPart.deepCopy();
      String catName = mapiPart.isSetCatName() ? mapiPart.getCatName() :
          MetaStoreUtils.getDefaultCatalog(context.getHandler().getConf());
      org.apache.hadoop.hive.metastore.api.Table t = context.getHandler().get_table_core(
          catName, mapiPart.getDbName(), mapiPart.getTableName());
      if (wrapperApiPart.getSd() == null){
        // In the cases of create partition, by the time this event fires, the partition
        // object has not yet come into existence, and thus will not yet have a
        // location or an SD, but these are needed to create a ql.metadata.Partition,
        // so we use the table's SD. The only place this is used is by the
        // authorization hooks, so we will not affect code flow in the metastore itself.
        wrapperApiPart.setSd(t.getSd().deepCopy());
      }
      initialize(new TableWrapper(t),wrapperApiPart);
    }
  }

}
