/**
 * 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.storm.security.auth.authorizer;

import java.io.IOException;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.storm.Config;
import org.apache.storm.security.auth.ClientAuthUtils;
import org.apache.storm.security.auth.IAuthorizer;
import org.apache.storm.security.auth.IGroupMappingServiceProvider;
import org.apache.storm.security.auth.IPrincipalToLocal;
import org.apache.storm.security.auth.ReqContext;
import org.apache.storm.shade.com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class ImpersonationAuthorizer implements IAuthorizer {
    protected static final String WILD_CARD = "*";
    private static final Logger LOG = LoggerFactory.getLogger(ImpersonationAuthorizer.class);
    @SuppressWarnings("checkstyle:AbbreviationAsWordInName")
    protected Map<String, ImpersonationACL> userImpersonationACL;
    protected IPrincipalToLocal ptol;
    protected IGroupMappingServiceProvider groupMappingProvider;

    @Override
    public void prepare(Map<String, Object> conf) {
        userImpersonationACL = new HashMap<>();

        Map<String, Map<String, List<String>>> userToHostAndGroup =
            (Map<String, Map<String, List<String>>>) conf.get(Config.NIMBUS_IMPERSONATION_ACL);

        if (userToHostAndGroup != null) {
            for (Map.Entry<String, Map<String, List<String>>> entry : userToHostAndGroup.entrySet()) {
                String user = entry.getKey();
                Set<String> groups = ImmutableSet.copyOf(entry.getValue().get("groups"));
                Set<String> hosts = ImmutableSet.copyOf(entry.getValue().get("hosts"));
                userImpersonationACL.put(user, new ImpersonationACL(user, groups, hosts));
            }
        }

        ptol = ClientAuthUtils.getPrincipalToLocalPlugin(conf);
        groupMappingProvider = ClientAuthUtils.getGroupMappingServiceProviderPlugin(conf);
    }

    @Override
    public boolean permit(ReqContext context, String operation, Map<String, Object> topoConf) {
        if (!context.isImpersonating()) {
            LOG.debug("Not an impersonation attempt.");
            return true;
        }

        String impersonatingPrincipal = context.realPrincipal().getName();
        String impersonatingUser = ptol.toLocal(context.realPrincipal());
        String userBeingImpersonated = ptol.toLocal(context.principal());
        InetAddress remoteAddress = context.remoteAddress();

        LOG.info("user = {}, principal = {} is attempting to impersonate user = {} for operation = {} from host = {}",
                 impersonatingUser, impersonatingPrincipal, userBeingImpersonated, operation, remoteAddress);

        /*
         * no config is present for impersonating principal or user, do not permit impersonation.
         */
        if (!userImpersonationACL.containsKey(impersonatingPrincipal) && !userImpersonationACL.containsKey(impersonatingUser)) {
            LOG.info("user = {}, principal = {} is trying to impersonate user {}, but config {} does not have entry "
                            + "for impersonating user or principal."
                            + "Please see SECURITY.MD to learn how to configure users for impersonation.",
                    impersonatingUser,
                    impersonatingPrincipal,
                    userBeingImpersonated,
                    Config.NIMBUS_IMPERSONATION_ACL);
            return false;
        }

        @SuppressWarnings("checkstyle:AbbreviationAsWordInName")
        ImpersonationACL principalACL = userImpersonationACL.get(impersonatingPrincipal);
        @SuppressWarnings("checkstyle:AbbreviationAsWordInName")
        ImpersonationACL userACL = userImpersonationACL.get(impersonatingUser);

        Set<String> authorizedHosts = new HashSet<>();
        Set<String> authorizedGroups = new HashSet<>();

        if (principalACL != null) {
            authorizedHosts.addAll(principalACL.authorizedHosts);
            authorizedGroups.addAll(principalACL.authorizedGroups);
        }

        if (userACL != null) {
            authorizedHosts.addAll(userACL.authorizedHosts);
            authorizedGroups.addAll(userACL.authorizedGroups);
        }

        LOG.debug("user = {}, principal = {} is allowed to impersonate groups = {} from hosts = {} ",
                  impersonatingUser, impersonatingPrincipal, authorizedGroups, authorizedHosts);

        if (!isAllowedToImpersonateFromHost(authorizedHosts, remoteAddress)) {
            LOG.info("user = {}, principal = {} is not allowed to impersonate from host {} ",
                     impersonatingUser, impersonatingPrincipal, remoteAddress);
            return false;
        }

        if (!isAllowedToImpersonateUser(authorizedGroups, userBeingImpersonated)) {
            LOG.info("user = {}, principal = {} is not allowed to impersonate any group that user {} is part of.",
                     impersonatingUser, impersonatingPrincipal, userBeingImpersonated);
            return false;
        }

        LOG.info("Allowing impersonation of user {} by user {}", userBeingImpersonated, impersonatingUser);
        return true;
    }

    private boolean isAllowedToImpersonateFromHost(Set<String> authorizedHosts, InetAddress remoteAddress) {
        return authorizedHosts.contains(WILD_CARD)
                || authorizedHosts.contains(remoteAddress.getCanonicalHostName())
                || authorizedHosts.contains(remoteAddress.getHostName())
                || authorizedHosts.contains(remoteAddress.getHostAddress());
    }

    private boolean isAllowedToImpersonateUser(Set<String> authorizedGroups, String userBeingImpersonated) {
        if (authorizedGroups.contains(WILD_CARD)) {
            return true;
        }

        Set<String> groups;
        try {
            groups = groupMappingProvider.getGroups(userBeingImpersonated);
        } catch (IOException e) {
            throw new RuntimeException("failed to get groups for user " + userBeingImpersonated);
        }

        if (groups == null || groups.isEmpty()) {
            return false;
        }

        for (String group : groups) {
            if (authorizedGroups.contains(group)) {
                return true;
            }
        }

        return false;
    }

    @SuppressWarnings("checkstyle:AbbreviationAsWordInName")
    protected static class ImpersonationACL {
        public String impersonatingUser;
        //Groups this user is authorized to impersonate.
        public Set<String> authorizedGroups;
        //Hosts this user is authorized to impersonate from.
        public Set<String> authorizedHosts;

        private ImpersonationACL(String impersonatingUser, Set<String> authorizedGroups, Set<String> authorizedHosts) {
            this.impersonatingUser = impersonatingUser;
            this.authorizedGroups = authorizedGroups;
            this.authorizedHosts = authorizedHosts;
        }

        @Override
        public String toString() {
            return "ImpersonationACL{"
                    + "impersonatingUser='" + impersonatingUser + '\''
                    + ", authorizedGroups=" + authorizedGroups
                    + ", authorizedHosts=" + authorizedHosts
                    + '}';
        }
    }
}
