/*
* Copyright 2015 herd contributors
*
* Licensed 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.finra.herd.app.config;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.env.Environment;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;

import org.finra.herd.app.security.HerdUserDetailsService;
import org.finra.herd.app.security.HttpHeaderApplicationUserBuilder;
import org.finra.herd.app.security.HttpHeaderAuthenticationFilter;
import org.finra.herd.app.security.TrustedApplicationUserBuilder;
import org.finra.herd.app.security.TrustedUserAuthenticationFilter;
import org.finra.herd.core.ApplicationContextHolder;
import org.finra.herd.core.helper.ConfigurationHelper;
import org.finra.herd.dao.Log4jOverridableConfigurer;
import org.finra.herd.dao.config.DaoSpringModuleConfig;
import org.finra.herd.model.dto.ConfigurationValue;
import org.finra.herd.model.jpa.ConfigurationEntity;

/**
 * This is the Spring root configuration for the web application.
 */
@Configuration
// Component scan all packages, but exclude the configuration ones since they are explicitly specified.
@ComponentScan(value = "org.finra.herd.app",
    excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "org\\.finra\\.herd\\.app\\.config\\..*"))
@EnableGlobalMethodSecurity(securedEnabled = true)
public class AppSpringModuleConfig extends GlobalMethodSecurityConfiguration
{
    @Autowired
    private ConfigurationHelper configurationHelper;

    @Autowired
    private HerdUserDetailsService herdUserDetailsService;

    /**
     * The Log4J overridable configurer that will handle our Log4J initialization for the herd application.
     * <p/>
     * IMPORTANT: Ensure this method is static since the returned Log4jOverridableConfigurer is a bean factory post processor (BFPP). If it weren't static,
     * autowiring and injection on this @Configuration class won't work due to lifecycle issues. See "Bootstrapping" comment in @Bean annotation for more
     * details.
     *
     * @return the Log4J overridable configurer.
     */
    @Bean
    public static Log4jOverridableConfigurer log4jConfigurer()
    {
        // Access the environment using the application context holder since we're in a static method that doesn't have access to the environment in any
        // other way.
        Environment environment = ApplicationContextHolder.getApplicationContext().getEnvironment();

        // Create the Log4J configurer.
        Log4jOverridableConfigurer log4jConfigurer = new Log4jOverridableConfigurer();

        // This is the primary database override location (if present). An entry needs to be present in the database at application startup for this
        // configuration to be used.
        log4jConfigurer.setDataSource(DaoSpringModuleConfig.getHerdDataSource());
        log4jConfigurer.setTableName(ConfigurationEntity.TABLE_NAME);
        log4jConfigurer.setSelectColumn(ConfigurationEntity.COLUMN_VALUE_CLOB);
        log4jConfigurer.setWhereColumn(ConfigurationEntity.COLUMN_KEY);
        log4jConfigurer.setWhereValue(ConfigurationValue.LOG4J_OVERRIDE_CONFIGURATION.getKey());

        // This is the secondary override location (if present and if the primary location isn't present). This resource needs to be present at application
        // startup for this configuration to be used.
        log4jConfigurer.setOverrideResourceLocation(ConfigurationHelper.getProperty(ConfigurationValue.LOG4J_OVERRIDE_RESOURCE_LOCATION, environment));

        // This is the default fallback location which is bundled in the WAR and should always be present in case the override locations aren't present.
        // Note that the herd-log4j.xml file must be in the herd-war project so it is accessible on the classpath as a file. Placing it within another
        // project's JAR will result in the resource not being found as a "file".
        log4jConfigurer.setDefaultResourceLocation("classpath:herd-log4j.xml");

        // Return the Log4J configurer.
        return log4jConfigurer;
    }

    /**
     * Gets a filter chain proxy.
     *
     * @param trustedUserAuthenticationFilter the trusted user authentication filter.
     * @param httpHeaderAuthenticationFilter the HTTP header authentication filter.
     *
     * @return the filter chain proxy.
     */
    @Bean
    public FilterChainProxy filterChainProxy(final TrustedUserAuthenticationFilter trustedUserAuthenticationFilter,
        final HttpHeaderAuthenticationFilter httpHeaderAuthenticationFilter)
    {
        return new FilterChainProxy(new SecurityFilterChain()
        {
            @Override
            public boolean matches(HttpServletRequest request)
            {
                // Match all URLs.
                return true;
            }

            @Override
            public List<Filter> getFilters()
            {
                List<Filter> filters = new ArrayList<>();

                // Required filter to store session information between HTTP requests.
                filters.add(new SecurityContextPersistenceFilter());

                // Trusted user filter to bypass security based on SpEL expression environment property.
                filters.add(trustedUserAuthenticationFilter);

                // Filter that authenticates based on http headers.
                if (Boolean.valueOf(configurationHelper.getProperty(ConfigurationValue.SECURITY_HTTP_HEADER_ENABLED)))
                {
                    filters.add(httpHeaderAuthenticationFilter);
                }

                // Anonymous user filter.
                filters.add(new AnonymousAuthenticationFilter("AnonymousFilterKey"));

                return filters;
            }
        });
    }

    @Bean
    public TrustedUserAuthenticationFilter trustedUserAuthenticationFilter(AuthenticationManager authenticationManager,
        TrustedApplicationUserBuilder trustedApplicationUserBuilder)
    {
        return new TrustedUserAuthenticationFilter(authenticationManager, trustedApplicationUserBuilder);
    }

    @Bean
    public TrustedApplicationUserBuilder trustedApplicationUserBuilder()
    {
        return new TrustedApplicationUserBuilder();
    }

    @Bean
    public HttpHeaderAuthenticationFilter httpHeaderAuthenticationFilter(AuthenticationManager authenticationManager,
        HttpHeaderApplicationUserBuilder httpHeaderApplicationUserBuilder)
    {
        return new HttpHeaderAuthenticationFilter(authenticationManager, httpHeaderApplicationUserBuilder);
    }

    @Bean
    public HttpHeaderApplicationUserBuilder httpHeaderApplicationUserBuilder()
    {
        return new HttpHeaderApplicationUserBuilder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManager()
    {
        PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
        authenticationProvider.setPreAuthenticatedUserDetailsService(herdUserDetailsService);
        List<AuthenticationProvider> providers = new ArrayList<>();
        providers.add(authenticationProvider);
        return new ProviderManager(providers);
    }

    /**
     * Overridden to remove role prefix for the role voter. The application does not require any other access decision voters in the default configuration.
     */
    /*
     * rawtypes must be suppressed because AffirmativeBased constructor takes in a raw typed list of AccessDecisionVoters
     */
    @SuppressWarnings("rawtypes")
    @Override
    protected AccessDecisionManager accessDecisionManager()
    {
        List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
        RoleVoter decisionVoter = new RoleVoter();
        decisionVoter.setRolePrefix("");
        decisionVoters.add(decisionVoter);
        return new AffirmativeBased(decisionVoters);
    }
}