/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License;
 * you may not use this file except in compliance with the Elastic License.
 */
package org.elasticsearch.xpack.security.authz;

import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.util.concurrent.CountDown;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.xpack.ClientHelper;
import org.elasticsearch.xpack.security.SecurityContext;
import org.elasticsearch.xpack.security.authc.Authentication;
import org.elasticsearch.xpack.security.authz.permission.Role;
import org.elasticsearch.xpack.security.support.Automatons;
import org.elasticsearch.xpack.security.user.SystemUser;
import org.elasticsearch.xpack.security.user.XPackSecurityUser;
import org.elasticsearch.xpack.security.user.XPackUser;

import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;

import static org.elasticsearch.xpack.ClientHelper.DEPRECATION_ORIGIN;
import static org.elasticsearch.xpack.ClientHelper.ML_ORIGIN;
import static org.elasticsearch.xpack.ClientHelper.MONITORING_ORIGIN;
import static org.elasticsearch.xpack.ClientHelper.PERSISTENT_TASK_ORIGIN;
import static org.elasticsearch.xpack.ClientHelper.SECURITY_ORIGIN;
import static org.elasticsearch.xpack.ClientHelper.WATCHER_ORIGIN;

public final class AuthorizationUtils {

    private static final Predicate<String> INTERNAL_PREDICATE = Automatons.predicate("internal:*");

    private AuthorizationUtils() {}

    /**
     * This method is used to determine if a request should be executed as the system user, even if the request already
     * has a user associated with it.
     *
     * In order for the user to be replaced by the system user one of the following conditions must be true:
     *
     * <ul>
     *     <li>the action is an internal action and no user is associated with the request</li>
     *     <li>the action is an internal action and the thread context contains a non-internal action as the originating action</li>
     * </ul>
     *
     * @param threadContext the {@link ThreadContext} that contains the headers and context associated with the request
     * @param action the action name that is being executed
     * @return true if the system user should be used to execute a request
     */
    public static boolean shouldReplaceUserWithSystem(ThreadContext threadContext, String action) {
        // the action must be internal OR the thread context must be a system context.
        if (threadContext.isSystemContext() == false && isInternalAction(action) == false) {
            return false;
        }

        // there is no authentication object AND we are executing in a system context OR an internal action
        // AND there
        Authentication authentication = threadContext.getTransient(Authentication.AUTHENTICATION_KEY);
        if (authentication == null && threadContext.getTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME) == null) {
            return true;
        }

        // we have a internal action being executed by a user other than the system user, lets verify that there is a
        // originating action that is not a internal action. We verify that there must be a originating action as an
        // internal action should never be called by user code from a client
        final String originatingAction = threadContext.getTransient(AuthorizationService.ORIGINATING_ACTION_KEY);
        if (originatingAction != null && isInternalAction(originatingAction) == false) {
            return true;
        }

        // either there was no originating action or the originating action was an internal action,
        // we should not replace under these circumstances
        return false;
    }

    /**
     * Returns true if the thread context contains the origin of the action and does not have any authentication
     */
    public static boolean shouldSetUserBasedOnActionOrigin(ThreadContext context) {
        final String actionOrigin = context.getTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME);
        final Authentication authentication = context.getTransient(Authentication.AUTHENTICATION_KEY);
        return actionOrigin != null && authentication == null;
    }

    /**
     * Stashes the current context and executes the consumer as the proper user based on the origin of the action.
     *
     * This method knows nothing about listeners so it is important that callers ensure their listeners preserve their
     * context and restore it appropriately.
     */
    public static void switchUserBasedOnActionOriginAndExecute(ThreadContext threadContext, SecurityContext securityContext,
                                                               Consumer<ThreadContext.StoredContext> consumer) {
        final String actionOrigin = threadContext.getTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME);
        if (actionOrigin == null) {
            assert false : "cannot switch user if there is no action origin";
            throw new IllegalStateException("cannot switch user if there is no action origin");
        }

        switch (actionOrigin) {
            case SECURITY_ORIGIN:
                securityContext.executeAsUser(XPackSecurityUser.INSTANCE, consumer, Version.CURRENT);
                break;
            case WATCHER_ORIGIN:
            case ML_ORIGIN:
            case MONITORING_ORIGIN:
            case DEPRECATION_ORIGIN:
            case PERSISTENT_TASK_ORIGIN:
                securityContext.executeAsUser(XPackUser.INSTANCE, consumer, Version.CURRENT);
                break;
            default:
                assert false : "action.origin [" + actionOrigin + "] is unknown!";
                throw new IllegalStateException("action.origin [" + actionOrigin + "] should always be a known value");
        }
    }

    private static boolean isInternalAction(String action) {
        return INTERNAL_PREDICATE.test(action);
    }

    /**
     * A base class to authorize authorize a given {@link Authentication} against it's users or run-as users roles.
     * This class fetches the roles for the users asynchronously and then authenticates the in the callback.
     */
    public static class AsyncAuthorizer {

        private final ActionListener listener;
        private final BiConsumer<Role, Role> consumer;
        private final Authentication authentication;
        private volatile Role userRoles;
        private volatile Role runAsRoles;
        private CountDown countDown = new CountDown(2); // we expect only two responses!!

        public AsyncAuthorizer(Authentication authentication, ActionListener listener, BiConsumer<Role, Role> consumer) {
            this.consumer = consumer;
            this.listener = listener;
            this.authentication = authentication;
        }

        public void authorize(AuthorizationService service) {
            if (SystemUser.is(authentication.getUser().authenticatedUser())) {
                assert authentication.getUser().isRunAs() == false;
                setUserRoles(null); // we can inform the listener immediately - nothing to fetch for us on system user
                setRunAsRoles(null);
            } else {
                service.roles(authentication.getUser().authenticatedUser(), ActionListener.wrap(this::setUserRoles, listener::onFailure));
                if (authentication.getUser().isRunAs()) {
                    service.roles(authentication.getUser(), ActionListener.wrap(this::setRunAsRoles, listener::onFailure));
                } else {
                    setRunAsRoles(null);
                }
            }
        }

        private void setUserRoles(Role roles) {
            this.userRoles = roles;
            maybeRun();
        }

        private void setRunAsRoles(Role roles) {
            this.runAsRoles = roles;
            maybeRun();
        }

        private void maybeRun() {
            if (countDown.countDown()) {
                try {
                    consumer.accept(userRoles, runAsRoles);
                } catch (Exception e) {
                    listener.onFailure(e);
                }
            }
        }

    }
}
