package org.apereo.cas.config;

import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.CasProtocolConstants;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.configuration.model.core.web.security.AdminPagesSecurityProperties;
import org.apereo.cas.util.ResourceUtils;
import org.pac4j.cas.authorization.DefaultCasAuthorizationGenerator;
import org.pac4j.cas.client.direct.DirectCasClient;
import org.pac4j.cas.config.CasConfiguration;
import org.pac4j.core.authorization.authorizer.IsAuthenticatedAuthorizer;
import org.pac4j.core.authorization.authorizer.RequireAnyRoleAuthorizer;
import org.pac4j.core.authorization.generator.SpringSecurityPropertiesAuthorizationGenerator;
import org.pac4j.core.config.Config;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.engine.DefaultSecurityLogic;
import org.pac4j.core.exception.HttpAction;
import org.pac4j.http.client.direct.IpClient;
import org.pac4j.http.credentials.authenticator.IpRegexpAuthenticator;
import org.pac4j.springframework.web.SecurityInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.mvc.WebContentInterceptor;
import org.springframework.web.servlet.view.RedirectView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;

/**
 * This is {@link CasSecurityContextConfiguration} that attempts to create Spring-managed beans
 * backed by external configuration.
 *
 * @author Misagh Moayyed
 * @since 5.0.0
 */
@Configuration("casSecurityContextConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CasSecurityContextConfiguration extends WebMvcConfigurerAdapter {
    private static final Logger LOGGER = LoggerFactory.getLogger(CasSecurityContextConfiguration.class);

    private static final String CAS_CLIENT_NAME = "CasClient";

    @Autowired
    private CasConfigurationProperties casProperties;

    @Bean
    public WebContentInterceptor webContentInterceptor() {
        final WebContentInterceptor interceptor = new WebContentInterceptor();
        interceptor.setCacheSeconds(0);
        interceptor.setAlwaysUseFullPath(true);
        return interceptor;
    }

    @RefreshScope
    @Bean
    public SecurityInterceptor requiresAuthenticationStatusInterceptor() {
        return new SecurityInterceptor(new
                Config(new IpClient(new IpRegexpAuthenticator(casProperties.getAdminPagesSecurity().getIp()))),
                "IpClient");
    }

    @RefreshScope
    @Bean
    public Config config() {
        try {
            final AdminPagesSecurityProperties adminProps = casProperties.getAdminPagesSecurity();
            if (StringUtils.isNotBlank(adminProps.getLoginUrl())
                    && StringUtils.isNotBlank(adminProps.getService())) {

                final CasConfiguration casConfig = new CasConfiguration(adminProps.getLoginUrl());
                final DirectCasClient client = new DirectCasClient(casConfig);
                client.setName(CAS_CLIENT_NAME);

                final Config cfg = new Config(adminProps.getService(), client);
                if (adminProps.getUsers() == null) {
                    LOGGER.warn("List of authorized users for admin pages security is not defined. "
                            + "Allowing access for all authenticated users");
                    client.setAuthorizationGenerator(new DefaultCasAuthorizationGenerator<>());
                    cfg.setAuthorizer(new IsAuthenticatedAuthorizer());
                } else {
                    final Resource file = ResourceUtils.prepareClasspathResourceIfNeeded(adminProps.getUsers());
                    if (file != null && file.exists()) {
                        LOGGER.debug("Loading list of authorized users from [{}]", file);
                        final Properties properties = new Properties();
                        properties.load(file.getInputStream());
                        client.setAuthorizationGenerator(new SpringSecurityPropertiesAuthorizationGenerator(properties));
                        cfg.setAuthorizer(new RequireAnyRoleAuthorizer(adminProps.getAdminRoles()));
                    }
                }
                return cfg;
            }
        } catch (final Exception e) {
            LOGGER.warn(e.getMessage(), e);
        }
        return new Config();
    }

    @RefreshScope
    @Bean
    public SecurityInterceptor requiresAuthenticationStatusAdminEndpointsInterceptor() {
        final Config cfg = config();
        if (cfg.getClients() == null) {
            return requiresAuthenticationStatusInterceptor();
        }
        final CasAdminPagesSecurityInterceptor interceptor = new CasAdminPagesSecurityInterceptor(cfg,
                CAS_CLIENT_NAME, "securityHeaders,csrfToken,".concat(getAuthorizerName()));
        return interceptor;
    }

    @Override
    public void addInterceptors(final InterceptorRegistry registry) {
        registry.addInterceptor(statusInterceptor()).addPathPatterns("/status/**");
        registry.addInterceptor(webContentInterceptor()).addPathPatterns("/*");
    }

    @Bean
    public HandlerInterceptorAdapter statusInterceptor() {
        return new CasAdminStatusInterceptor();
    }

    @RefreshScope
    @Bean
    public EndpointHandlerMappingCustomizer mappingCustomizer() {
        return mapping -> mapping.setInterceptors(new Object[]{statusInterceptor()});
    }

    private String getAuthorizerName() {
        if (this.casProperties.getAdminPagesSecurity().getUsers() == null) {
            return IsAuthenticatedAuthorizer.class.getSimpleName();
        }
        return RequireAnyRoleAuthorizer.class.getSimpleName();
    }

    /**
     * The Cas admin status interceptor.
     */
    public class CasAdminStatusInterceptor extends HandlerInterceptorAdapter {
        @Override
        public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response,
                                 final Object handler) throws Exception {
            final String requestPath = request.getRequestURI();
            final Pattern pattern = Pattern.compile("/status(/)*$");

            if (pattern.matcher(requestPath).find()) {
                return requiresAuthenticationStatusInterceptor().preHandle(request, response, handler);
            }
            return requiresAuthenticationStatusAdminEndpointsInterceptor().preHandle(request, response, handler);
        }

        @Override
        public void postHandle(final HttpServletRequest request, final HttpServletResponse response,
                               final Object handler, final ModelAndView modelAndView) throws Exception {
            if (StringUtils.isNotBlank(request.getQueryString())
                    && request.getQueryString().contains(CasProtocolConstants.PARAMETER_TICKET)
                    && modelAndView != null) {
                final RedirectView v = new RedirectView(request.getRequestURL().toString());
                v.setExposeModelAttributes(false);
                v.setExposePathVariables(false);
                modelAndView.setView(v);
            }
        }
    }

    /**
     * The Cas admin pages security interceptor.
     */
    public static class CasAdminPagesSecurityInterceptor extends SecurityInterceptor {

        public CasAdminPagesSecurityInterceptor(final Config config, final String clients, final String authorizers) {
            super(config, clients, authorizers);

            final DefaultSecurityLogic secLogic = new DefaultSecurityLogic() {
                @Override
                protected HttpAction unauthorized(final WebContext context, final List currentClients) {
                    return HttpAction.forbidden("Access Denied", context);
                }

                @Override
                protected boolean loadProfilesFromSession(final WebContext context, final List currentClients) {
                    return true;
                }
            };
            secLogic.setSaveProfileInSession(true);
            setSecurityLogic(secLogic);
        }
    }
}
