/*
 * 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.ozone.om.multitenant;

import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_KERBEROS_KEYTAB_FILE_KEY;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_KERBEROS_PRINCIPAL_KEY;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_RANGER_HTTPS_ADDRESS_KEY;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_RANGER_SERVICE;
import static org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod.KERBEROS;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.google.common.base.Preconditions;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.ozone.OmUtils;
import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.ranger.RangerServiceException;
import org.apache.ranger.plugin.model.RangerPolicy;
import org.apache.ranger.plugin.model.RangerRole;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.ranger.RangerClient;

/**
 * Implementation of {@link MultiTenantAccessController} using the
 * {@link RangerClient} to communicate with Ranger.
 */
public class RangerClientMultiTenantAccessController implements
    MultiTenantAccessController {

  private static final Logger LOG = LoggerFactory
      .getLogger(RangerClientMultiTenantAccessController.class);

  private final RangerClient client;
  private final String rangerServiceName;
  private final Map<IAccessAuthorizer.ACLType, String> aclToString;
  private final Map<String, IAccessAuthorizer.ACLType> stringToAcl;
  private final String omPrincipal;

  public RangerClientMultiTenantAccessController(OzoneConfiguration conf)
      throws IOException {
    aclToString = MultiTenantAccessController.getRangerAclStrings();
    stringToAcl = new HashMap<>();
    aclToString.forEach((type, string) -> stringToAcl.put(string, type));

    // Should have passed the check in OMMultiTenantManager
    String rangerHttpsAddress = conf.get(OZONE_RANGER_HTTPS_ADDRESS_KEY);
    Preconditions.checkNotNull(rangerHttpsAddress);
    rangerServiceName = conf.get(OZONE_RANGER_SERVICE);
    Preconditions.checkNotNull(rangerServiceName);

    String configuredOmPrincipal = conf.get(OZONE_OM_KERBEROS_PRINCIPAL_KEY);
    Preconditions.checkNotNull(configuredOmPrincipal);
    // Replace _HOST pattern with host name in the Kerberos principal. Ranger
    // client currently does not do this automatically.
    omPrincipal = SecurityUtil.getServerPrincipal(
        configuredOmPrincipal, OmUtils.getOmAddress(conf).getHostName());
    String keytabPath = conf.get(OZONE_OM_KERBEROS_KEYTAB_FILE_KEY);
    Preconditions.checkNotNull(keytabPath);

    client = new RangerClient(rangerHttpsAddress,
        KERBEROS.name().toLowerCase(), omPrincipal, keytabPath,
        rangerServiceName, OzoneConsts.OZONE);
  }

  @Override
  public void createPolicy(Policy policy) throws RangerServiceException {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Sending create request for policy {} to Ranger.",
          policy.getName());
    }
    client.createPolicy(toRangerPolicy(policy));
  }

  @Override
  public Policy getPolicy(String policyName) throws RangerServiceException {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Sending get request for policy {} to Ranger.",
          policyName);
    }
    return fromRangerPolicy(client.getPolicy(rangerServiceName, policyName));
  }

  @Override
  public List<Policy> getLabeledPolicies(String label)
      throws RangerServiceException {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Sending get request for policies with label {} to Ranger.",
          label);
    }
    Map<String, String> filterMap = new HashMap<>();
    filterMap.put("serviceName", rangerServiceName);
    filterMap.put("policyLabelsPartial", label);
    return client.findPolicies(filterMap).stream()
        .map(this::fromRangerPolicy)
        .collect(Collectors.toList());
  }

  @Override
  public void updatePolicy(Policy policy) throws RangerServiceException {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Sending update request for policy {} to Ranger.",
          policy.getName());
    }
    client.updatePolicy(rangerServiceName, policy.getName(),
        toRangerPolicy(policy));
  }

  @Override
  public void deletePolicy(String policyName) throws RangerServiceException {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Sending delete request for policy {} to Ranger.",
          policyName);
    }
    client.deletePolicy(rangerServiceName, policyName);
  }

  @Override
  public void createRole(Role role) throws RangerServiceException {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Sending create request for role {} to Ranger.",
          role.getName());
    }
    client.createRole(rangerServiceName, toRangerRole(role));
  }

  @Override
  public Role getRole(String roleName) throws RangerServiceException {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Sending get request for role {} to Ranger.",
          roleName);
    }
    return fromRangerRole(client.getRole(roleName, omPrincipal,
        rangerServiceName));
  }

  @Override
  public void updateRole(long roleID, Role role) throws RangerServiceException {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Sending update request for role ID {} to Ranger.",
          roleID);
    }
    client.updateRole(roleID, toRangerRole(role));
  }

  @Override
  public void deleteRole(String roleName) throws RangerServiceException {
    if (LOG.isDebugEnabled()) {
      LOG.debug("Sending delete request for role {} to Ranger.",
          roleName);
    }
    client.deleteRole(roleName, omPrincipal, rangerServiceName);
  }

  @Override
  public long getRangerServiceVersion() {
    throw new UnsupportedOperationException("Ranger client implementation " +
        "does not currently support this method. A workaround will be " +
        "implemented in HDDS-6755.");
  }

  private static List<RangerRole.RoleMember> toRangerRoleMembers(
      Collection<String> members) {
    return members.stream()
            .map(princ -> new RangerRole.RoleMember(princ, false))
            .collect(Collectors.toList());
  }

  private static List<String> fromRangerRoleMembers(
      Collection<RangerRole.RoleMember> members) {
    return members.stream()
        .map(rangerUser -> rangerUser.getName())
        .collect(Collectors.toList());
  }

  private static Role fromRangerRole(RangerRole rangerRole) {
    return new Role.Builder()
      .setID(rangerRole.getId())
      .setName(rangerRole.getName())
      .setDescription(rangerRole.getDescription())
      .addUsers(fromRangerRoleMembers(rangerRole.getUsers()))
      .build();
  }

  private static RangerRole toRangerRole(Role role) {
    RangerRole rangerRole = new RangerRole();
    rangerRole.setName(role.getName());
    rangerRole.setUsers(toRangerRoleMembers(role.getUsers()));
    if (role.getDescription().isPresent()) {
      rangerRole.setDescription(role.getDescription().get());
    }
    return rangerRole;
  }

  private Policy fromRangerPolicy(RangerPolicy rangerPolicy) {
    Policy.Builder policyBuilder = new Policy.Builder();

    // Get roles and their acls from the policy.
    for (RangerPolicy.RangerPolicyItem policyItem:
        rangerPolicy.getPolicyItems()) {
      Collection<Acl> acls = new ArrayList<>();
      for (RangerPolicy.RangerPolicyItemAccess access:
           policyItem.getAccesses()) {
        if (access.getIsAllowed()) {
          acls.add(Acl.allow(stringToAcl.get(access.getType())));
        } else {
          acls.add(Acl.deny(stringToAcl.get(access.getType())));
        }
      }

      for (String roleName: policyItem.getRoles()) {
        policyBuilder.addRoleAcl(roleName, acls);
      }
    }

    // Add resources.
    for (Map.Entry<String, RangerPolicy.RangerPolicyResource> resource:
        rangerPolicy.getResources().entrySet()) {
      String resourceType = resource.getKey();
      List<String> resourceNames = resource.getValue().getValues();
      switch (resourceType) {
      case "volume":
        policyBuilder.addVolumes(resourceNames);
        break;
      case "bucket":
        policyBuilder.addBuckets(resourceNames);
        break;
      case "key":
        policyBuilder.addKeys(resourceNames);
        break;
      default:
        LOG.warn("Pulled Ranger policy with unknown resource type '{}' with" +
            " names '{}'", resourceType, String.join(",", resourceNames));
      }
    }

    policyBuilder.setName(rangerPolicy.getName())
       .setDescription(rangerPolicy.getDescription())
        .addLabels(rangerPolicy.getPolicyLabels());

    return policyBuilder.build();
  }

  private RangerPolicy toRangerPolicy(Policy policy) {
    RangerPolicy rangerPolicy = new RangerPolicy();
    rangerPolicy.setName(policy.getName());
    rangerPolicy.setService(rangerServiceName);
    rangerPolicy.setPolicyLabels(new ArrayList<>(policy.getLabels()));

    // Add resources.
    Map<String, RangerPolicy.RangerPolicyResource> resource = new HashMap<>();
    // Add volumes.
    if (!policy.getVolumes().isEmpty()) {
      RangerPolicy.RangerPolicyResource volumeResources =
          new RangerPolicy.RangerPolicyResource();
      volumeResources.setValues(new ArrayList<>(policy.getVolumes()));
      resource.put("volume", volumeResources);
    }
    // Add buckets.
    if (!policy.getBuckets().isEmpty()) {
      RangerPolicy.RangerPolicyResource bucketResources =
          new RangerPolicy.RangerPolicyResource();
      bucketResources.setValues(new ArrayList<>(policy.getBuckets()));
      resource.put("bucket", bucketResources);
    }
    // Add keys.
    if (!policy.getKeys().isEmpty()) {
      RangerPolicy.RangerPolicyResource keyResources =
          new RangerPolicy.RangerPolicyResource();
      keyResources.setValues(new ArrayList<>(policy.getKeys()));
      resource.put("key", keyResources);
    }
    rangerPolicy.setService(rangerServiceName);
    rangerPolicy.setResources(resource);
    if (policy.getDescription().isPresent()) {
      rangerPolicy.setDescription(policy.getDescription().get());
    }

    // Add roles to the policy.
    for (Map.Entry<String, Collection<Acl>> roleAcls:
        policy.getRoleAcls().entrySet()) {
      RangerPolicy.RangerPolicyItem item = new RangerPolicy.RangerPolicyItem();
      item.setRoles(Collections.singletonList(roleAcls.getKey()));

      for (Acl acl: roleAcls.getValue()) {
        RangerPolicy.RangerPolicyItemAccess access =
            new RangerPolicy.RangerPolicyItemAccess();
        access.setIsAllowed(acl.isAllowed());
        access.setType(aclToString.get(acl.getAclType()));
        item.getAccesses().add(access);
      }

      rangerPolicy.getPolicyItems().add(item);
    }

    return rangerPolicy;
  }
}
