/*
 * 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.hive.hcatalog.templeton;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hadoop.security.Groups;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * When WebHCat is run with doAs query parameter this class ensures that user making the
 * call is allowed to impersonate doAs user and is making a call from authorized host.
 */
final class ProxyUserSupport {
  private static final Logger LOG = LoggerFactory.getLogger(ProxyUserSupport.class);
  private static final String CONF_PROXYUSER_PREFIX = "webhcat.proxyuser.";
  private static final String CONF_GROUPS_SUFFIX = ".groups";
  private static final String CONF_HOSTS_SUFFIX = ".hosts";
  private static final Set<String> WILD_CARD = Collections.unmodifiableSet(new HashSet<String>(0));
  private static final Map<String, Set<String>> proxyUserGroups = new HashMap<String, Set<String>>();
  private static final Map<String, Set<String>> proxyUserHosts = new HashMap<String, Set<String>>();

  static void processProxyuserConfig(AppConfig conf) {
    for(Map.Entry<String, String> confEnt : conf) {
      if(confEnt.getKey().startsWith(CONF_PROXYUSER_PREFIX)
          && confEnt.getKey().endsWith(CONF_GROUPS_SUFFIX)) {
        //process user groups for which doAs is authorized
        String proxyUser = 
            confEnt.getKey().substring(CONF_PROXYUSER_PREFIX.length(), 
            confEnt.getKey().lastIndexOf(CONF_GROUPS_SUFFIX));
        Set<String> groups;
        if("*".equals(confEnt.getValue())) {
          groups = WILD_CARD;
          if(LOG.isDebugEnabled()) {
            LOG.debug("User [" + proxyUser + "] is authorized to do doAs any user.");
          }
        }
        else if(confEnt.getValue() != null && confEnt.getValue().trim().length() > 0) {
          groups = new HashSet<String>(Arrays.asList(confEnt.getValue().trim().split(",")));
          if(LOG.isDebugEnabled()) {
            LOG.debug("User [" + proxyUser + 
                "] is authorized to do doAs for users in the following groups: ["
                + confEnt.getValue().trim() + "]");
          }
        }
        else {
          groups = Collections.emptySet();
          if(LOG.isDebugEnabled()) {
            LOG.debug("User [" + proxyUser + 
                "] is authorized to do doAs for users in the following groups: []");
          }
        }
        proxyUserGroups.put(proxyUser, groups);
      }
      else if(confEnt.getKey().startsWith(CONF_PROXYUSER_PREFIX)
          && confEnt.getKey().endsWith(CONF_HOSTS_SUFFIX)) {
        //process hosts from which doAs requests are authorized
        String proxyUser = confEnt.getKey().substring(CONF_PROXYUSER_PREFIX.length(), 
            confEnt.getKey().lastIndexOf(CONF_HOSTS_SUFFIX));
        Set<String> hosts;
        if("*".equals(confEnt.getValue())) {
          hosts = WILD_CARD;
          if(LOG.isDebugEnabled()) {
            LOG.debug("User [" + proxyUser + "] is authorized to do doAs from any host.");
          }
        }
        else if(confEnt.getValue() != null && confEnt.getValue().trim().length() > 0) {
          String[] hostValues = confEnt.getValue().trim().split(",");
          hosts = new HashSet<String>();
          for(String hostname : hostValues) {
            String nhn = normalizeHostname(hostname);
            if(nhn != null) {
              hosts.add(nhn);
            }
          }
          if(LOG.isDebugEnabled()) {
            LOG.debug("User [" + proxyUser + 
                "] is authorized to do doAs from the following hosts: ["
                + confEnt.getValue().trim() + "]");
          }
        }
        else {
          hosts = Collections.emptySet();
          if(LOG.isDebugEnabled()) {
            LOG.debug("User [" + proxyUser
                + "] is authorized to do doAs from the following hosts: []");
          }
        }
        proxyUserHosts.put(proxyUser, hosts);
      }
    }
  }
  /**
   * Verifies a that proxyUser is making the request from authorized host and that doAs user
   * belongs to one of the groups for which proxyUser is allowed to impersonate users.
   *
   * @param proxyUser user name of the proxy (logged in) user.
   * @param proxyHost host the proxy user is making the request from.
   * @param doAsUser user the proxy user is impersonating.
   * @throws NotAuthorizedException thrown if the user is not allowed to perform the proxyuser request.
   */
  static void validate(String proxyUser, String proxyHost, String doAsUser) throws 
      NotAuthorizedException {
    assertNotEmpty(proxyUser, "proxyUser",
        "If you're attempting to use user-impersonation via a proxy user, please make sure that "
            + CONF_PROXYUSER_PREFIX + "#USER#" + CONF_HOSTS_SUFFIX + " and "
            + CONF_PROXYUSER_PREFIX + "#USER#" + CONF_GROUPS_SUFFIX
            + " are configured correctly");
    assertNotEmpty(proxyHost, "proxyHost",
        "If you're attempting to use user-impersonation via a proxy user, please make sure that "
            + CONF_PROXYUSER_PREFIX + proxyUser + CONF_HOSTS_SUFFIX + " and "
            + CONF_PROXYUSER_PREFIX + proxyUser + CONF_GROUPS_SUFFIX
            + " are configured correctly");
    assertNotEmpty(doAsUser, Server.DO_AS_PARAM);
    LOG.debug(MessageFormat.format("Authorization check proxyuser [{0}] host [{1}] doAs [{2}]",
        proxyUser, proxyHost, doAsUser));
    if (proxyUserHosts.containsKey(proxyUser)) {
      proxyHost = normalizeHostname(proxyHost);
      validateRequestorHost(proxyUser, proxyHost);
      validateGroup(proxyUser, doAsUser);
    }
    else {
      throw new NotAuthorizedException(MessageFormat.format(
          "User [{0}] not defined as proxyuser", proxyUser));
    }
  }

  private static void validateRequestorHost(String proxyUser, String hostname) throws 
      NotAuthorizedException {
    Set<String> validHosts = proxyUserHosts.get(proxyUser);
    if (validHosts == WILD_CARD) {
      return;
    }
    if (validHosts == null || !validHosts.contains(hostname)) {
      throw new NotAuthorizedException(MessageFormat.format(
          "Unauthorized host [{0}] for proxyuser [{1}]", hostname, proxyUser));
    }
  }

  private static void validateGroup(String proxyUser, String doAsUser) throws 
      NotAuthorizedException {
    Set<String> validGroups = proxyUserGroups.get(proxyUser);
    if(validGroups == WILD_CARD) {
      return;
    }
    else if(validGroups == null || validGroups.isEmpty()) {
      throw new NotAuthorizedException(
          MessageFormat.format(
            "Unauthorized proxyuser [{0}] for doAsUser [{1}], not in proxyuser groups",
            proxyUser, doAsUser));
    }
    Groups groupsInfo = new Groups(Main.getAppConfigInstance());
    try {
      List<String> userGroups = groupsInfo.getGroups(doAsUser);
      for (String g : validGroups) {
        if (userGroups.contains(g)) {
          return;
        }
      }
    }
    catch (IOException ex) {//thrown, for example, if there is no such user on the system
      LOG.warn(MessageFormat.format("Unable to get list of groups for doAsUser [{0}].",
          doAsUser), ex);
    }
    throw new NotAuthorizedException(
      MessageFormat.format(
        "Unauthorized proxyuser [{0}] for doAsUser [{1}], not in proxyuser groups",
            proxyUser, doAsUser));
  }

  private static String normalizeHostname(String name) {
    try {
      InetAddress address = InetAddress.getByName( 
          "localhost".equalsIgnoreCase(name) ? null : name);
      return address.getCanonicalHostName();
    }
    catch (UnknownHostException ex) {
      LOG.warn(MessageFormat.format("Unable to normalize hostname [{0}]", name));
      return null;
    }
  }
  /**
   * Check that a string is not null and not empty. If null or empty 
   * throws an IllegalArgumentException.
   *
   * @param str value.
   * @param name parameter name for the exception message.
   * @return the given value.
   */
  private static String assertNotEmpty(String str, String name) {
    return assertNotEmpty(str, name, null);
  }

  /**
   * Check that a string is not null and not empty. If null or empty 
   * throws an IllegalArgumentException.
   *
   * @param str value.
   * @param name parameter name for the exception message.
   * @param info additional information to be printed with the exception message
   * @return the given value.
   */
  private static String assertNotEmpty(String str, String name, String info) {
    if (str == null) {
      throw new IllegalArgumentException(
          name + " cannot be null" + (info == null ? "" : ", " + info));
    }
    if (str.length() == 0) {
      throw new IllegalArgumentException(
          name + " cannot be empty" + (info == null ? "" : ", " + info));
    }
    return str;
  }
}
