/*
 * 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.service.auth.ldap;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.naming.NamingException;
import javax.security.sasl.AuthenticationException;
import org.apache.hadoop.hive.conf.HiveConf;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A factory for a {@link Filter} based on a list of allowed groups.
 * <br>
 * The produced filter object filters out all users that are not members of at least one of
 * the groups provided in Hive configuration.
 * @see HiveConf.ConfVars#HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER
 */
public final class GroupFilterFactory implements FilterFactory {

  /**
   * {@inheritDoc}
   */
  @Override
  public Filter getInstance(HiveConf conf) {
    Collection<String> groupFilter = conf.getStringCollection(
        HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER.varname);

    if (groupFilter.isEmpty()) {
      return null;
    }

    if (conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERMEMBERSHIP_KEY) == null) {
      return new GroupMembershipKeyFilter(groupFilter);
    } else {
      return new UserMembershipKeyFilter(groupFilter);
    }
  }

  @VisibleForTesting
  static final class GroupMembershipKeyFilter implements Filter {

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

    private final Set<String> groupFilter = new HashSet<>();

    GroupMembershipKeyFilter(Collection<String> groupFilter) {
      this.groupFilter.addAll(groupFilter);
    }

    @Override
    public void apply(DirSearch ldap, String user) throws AuthenticationException {
      LOG.info("Authenticating user '{}' using {}", user,
          GroupMembershipKeyFilter.class.getSimpleName());

      List<String> memberOf = null;

      try {
        String userDn = ldap.findUserDn(user);
        memberOf = ldap.findGroupsForUser(userDn);
        LOG.debug("User {} member of : {}", userDn, memberOf);
      } catch (NamingException e) {
        throw new AuthenticationException("LDAP Authentication failed for user", e);
      }

      for (String groupDn : memberOf) {
        String shortName = LdapUtils.getShortName(groupDn);
        if (groupFilter.stream().anyMatch(shortName::equalsIgnoreCase)) {
          LOG.debug("GroupMembershipKeyFilter passes: user '{}' is a member of '{}' group",
              user, groupDn);
          LOG.info("Authentication succeeded based on group membership");
          return;
        }
      }
      LOG.info("Authentication failed based on user membership");
      throw new AuthenticationException("Authentication failed: "
          + "User not a member of specified list");
    }
  }

  @VisibleForTesting
  static final class UserMembershipKeyFilter implements Filter {

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

    private final Collection<String> groupFilter;

    UserMembershipKeyFilter(Collection<String> groupFilter) {
      this.groupFilter = groupFilter;
    }

    @Override
    public void apply(DirSearch ldap, String user) throws AuthenticationException {
      LOG.info("Authenticating user '{}' using {}", user,
          UserMembershipKeyFilter.class.getSimpleName());

      List<String> groupDns = new ArrayList<>();
      for (String groupId : groupFilter) {
        try {
          String groupDn = ldap.findGroupDn(groupId);
          groupDns.add(groupDn);
        } catch (NamingException e) {
          LOG.warn("Cannot find DN for group", e);
          LOG.debug("Cannot find DN for group " + groupId, e);
        }
      }

      if (groupDns.isEmpty()) {
        String msg = String.format("No DN(s) has been found for any of group(s): %s",
            Joiner.on(',').join(groupFilter));
        LOG.debug(msg);
        throw new AuthenticationException("No DN(s) has been found for any of specified group(s)");
      }

      for (String groupDn : groupDns) {
        try {
          if (ldap.isUserMemberOfGroup(user, groupDn)) {
            LOG.debug("UserMembershipKeyFilter passes: user '{}' is a member of '{}' group",
                user, groupDn);
            LOG.info("Authentication succeeded based on user membership");
            return;
          }
        } catch (NamingException e) {
          LOG.warn("Cannot match user and group", e);
          LOG.debug("Cannot match user '{}' and group '{}'", user, groupDn, e);
        }
      }
      throw new AuthenticationException(String.format(
          "Authentication failed: User '%s' is not a member of listed groups", user));
    }
  }
}
