/**
 * 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.atlas.web.filters;

import org.apache.atlas.ApplicationProperties;
import org.apache.atlas.RequestContext;
import org.apache.atlas.security.SecurityProperties;
import org.apache.atlas.utils.AuthenticationUtil;
import org.apache.atlas.web.util.Servlets;
import org.apache.commons.collections.iterators.IteratorEnumeration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
import org.apache.hadoop.security.authentication.client.AuthenticationException;
import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
import org.apache.hadoop.security.authentication.server.AuthenticationHandler;
import org.apache.hadoop.security.authentication.server.AuthenticationToken;
import org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler;
import org.apache.hadoop.security.authentication.util.Signer;
import org.apache.hadoop.security.authentication.util.SignerException;
import org.apache.hadoop.security.authentication.util.SignerSecretProvider;
import org.apache.log4j.NDC;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.Principal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This enforces authentication as part of the filter before processing the request.
 * todo: Subclass of {@link org.apache.hadoop.security.authentication.server.AuthenticationFilter}.
 */

public class AtlasAuthenticationFilter extends AuthenticationFilter {
    private static final Logger LOG = LoggerFactory.getLogger(AtlasAuthenticationFilter.class);
    static final String PREFIX = "atlas.authentication.method";
    protected static ServletContext nullContext = new NullServletContext();
    private Signer signer;
    private SignerSecretProvider secretProvider;
    public final boolean isKerberos = AuthenticationUtil.isKerberosAuthenticationEnabled();
    private boolean isInitializedByTomcat;
    private Set<Pattern> browserUserAgents;

    public AtlasAuthenticationFilter() {
        try {
            LOG.info("AtlasAuthenticationFilter initialization started");
            init(null);
        } catch (ServletException e) {
            LOG.error("Error while initializing AtlasAuthenticationFilter : {}", e.getMessage());
        }
    }

    private HttpServlet optionsServlet;

    /**
     * Initialize the filter.
     *
     * @param filterConfig filter configuration.
     * @throws ServletException thrown if the filter could not be initialized.
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        LOG.info("AtlasAuthenticationFilter initialization started");
        final FilterConfig globalConf = filterConfig;

        final Map<String, String> params = new HashMap<>();

        FilterConfig filterConfig1 = new FilterConfig() {
            @Override
            public ServletContext getServletContext() {
                if (globalConf != null) {
                    return globalConf.getServletContext();
                } else {
                    return nullContext;
                }
            }

            @SuppressWarnings("unchecked")
            @Override
            public Enumeration<String> getInitParameterNames() {
                return new IteratorEnumeration(params.keySet().iterator());
            }

            @Override
            public String getInitParameter(String param) {
                return params.get(param);
            }

            @Override
            public String getFilterName() {
                return "AtlasAuthenticationFilter";
            }
        };

        super.init(filterConfig1);

        optionsServlet = new HttpServlet() {
        };
        optionsServlet.init();

    }


    @Override
    public void initializeSecretProvider(FilterConfig filterConfig)
            throws ServletException {
        LOG.debug("AtlasAuthenticationFilter :: initializeSecretProvider {}", filterConfig);
        secretProvider = (SignerSecretProvider) filterConfig.getServletContext().
                getAttribute(AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE);
        if (secretProvider == null) {
            // As tomcat cannot specify the provider object in the configuration.
            // It'll go into this path
            String configPrefix = filterConfig.getInitParameter(CONFIG_PREFIX);
            configPrefix = (configPrefix != null) ? configPrefix + "." : "";
            try {
                secretProvider = AuthenticationFilter.constructSecretProvider(
                        filterConfig.getServletContext(),
                        super.getConfiguration(configPrefix, filterConfig), false);
                this.isInitializedByTomcat = true;
            } catch (Exception ex) {
                throw new ServletException(ex);
            }
        }
        signer = new Signer(secretProvider);
    }

    @Override
    protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) throws ServletException {
        Configuration configuration;
        try {
            configuration = ApplicationProperties.get();
        } catch (Exception e) {
            throw new ServletException(e);
        }

        Properties config = new Properties();

        String kerberosAuthEnabled = configuration != null ? configuration.getString("atlas.authentication.method.kerberos") : null;
        // getString may return null, and would like to log the nature of the default setting
        String authMethod = "";
        if (kerberosAuthEnabled == null || kerberosAuthEnabled.equalsIgnoreCase("false")) {
            LOG.info("No authentication method configured.  Defaulting to simple authentication");
            authMethod = "simple";
        } else if (kerberosAuthEnabled.equalsIgnoreCase("true")) {
            authMethod = "kerberos";
        }

        if(configuration.getString("atlas.authentication.method.kerberos.name.rules")!=null) {
            config.put("kerberos.name.rules", configuration.getString("atlas.authentication.method.kerberos.name.rules"));
        }
        if(configuration.getString("atlas.authentication.method.kerberos.keytab")!=null) {
            config.put("kerberos.keytab", configuration.getString("atlas.authentication.method.kerberos.keytab"));
        }
        if(configuration.getString("atlas.authentication.method.kerberos.principal")!=null) {
            config.put("kerberos.principal", configuration.getString("atlas.authentication.method.kerberos.principal"));
        }
        config.put(AuthenticationFilter.AUTH_TYPE, authMethod );
        config.put(AuthenticationFilter.COOKIE_PATH, "/");

        // add any config passed in as init parameters
        Enumeration<String> enumeration = filterConfig.getInitParameterNames();
        while (enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();
            config.put(name, filterConfig.getInitParameter(name));
        }

        //Resolve _HOST into bind address
        String bindAddress = configuration.getString(SecurityProperties.BIND_ADDRESS);
        if (bindAddress == null) {
            LOG.info("No host name configured.  Defaulting to local host name.");
            try {
                bindAddress = InetAddress.getLocalHost().getHostName();
            } catch (UnknownHostException e) {
                throw new ServletException("Unable to obtain host name", e);
            }
        }
        String principal = config.getProperty(KerberosAuthenticationHandler.PRINCIPAL);
        if (principal != null) {
            try {
                principal = SecurityUtil.getServerPrincipal(principal, bindAddress);
            } catch (IOException ex) {
                throw new RuntimeException("Could not resolve Kerberos principal name: " + ex.toString(), ex);
            }
            config.put(KerberosAuthenticationHandler.PRINCIPAL, principal);
        }

        LOG.debug(" AuthenticationFilterConfig: {}", config);

        String agents = configuration.getString(AtlasCSRFPreventionFilter.BROWSER_USER_AGENT_PARAM, AtlasCSRFPreventionFilter.BROWSER_USER_AGENTS_DEFAULT);

        if (agents == null) {
            agents = AtlasCSRFPreventionFilter.BROWSER_USER_AGENTS_DEFAULT;
        }

        parseBrowserUserAgents(agents);

        return config;
    }

    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response,
                         final FilterChain filterChain) throws IOException, ServletException {

        final HttpServletRequest httpRequest = (HttpServletRequest) request;
        FilterChain filterChainWrapper = new FilterChain() {
            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse)
                    throws IOException, ServletException {
                final HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
                final HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;

                if (isKerberos) {
                    Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
                    String userName = readUserFromCookie(httpResponse);

                    if (StringUtils.isEmpty(userName) && !StringUtils.isEmpty(httpRequest.getRemoteUser())) {
                        userName = httpRequest.getRemoteUser();
                    }

                    if ((existingAuth == null || !existingAuth.isAuthenticated()) && (!StringUtils.isEmpty(userName))) {
                        UserGroupInformation ugi = UserGroupInformation.getLoginUser();
                        String[] groupsName = ugi.getGroupNames();

                        final List<GrantedAuthority> grantedAuths = new ArrayList<>();
                        for (String group : groupsName) {
                            grantedAuths.add(new SimpleGrantedAuthority(group));
                        }

                        final UserDetails principal = new User(userName, "", grantedAuths);
                        final Authentication finalAuthentication = new UsernamePasswordAuthenticationToken(principal, "", grantedAuths);
                        WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpRequest);
                        ((AbstractAuthenticationToken) finalAuthentication).setDetails(webDetails);
                        SecurityContextHolder.getContext().setAuthentication(finalAuthentication);

                        request.setAttribute("atlas.http.authentication.type", true);
                        LOG.info("Logged into Atlas as = {}", userName);
                    }
                }
                // OPTIONS method is sent from quick start jersey atlas client
                if (httpRequest.getMethod().equals("OPTIONS")) {
                    optionsServlet.service(request, response);
                } else {
                    try {
                        String requestUser = httpRequest.getRemoteUser();
                        NDC.push(requestUser + ":" + httpRequest.getMethod() + httpRequest.getRequestURI());
                        RequestContext requestContext = RequestContext.get();
                        if(requestContext!=null) {
                            requestContext.setUser(requestUser);
                        }
                        LOG.info("Request from authenticated user: {}, URL={}", requestUser,
                                Servlets.getRequestURI(httpRequest));

                        filterChain.doFilter(servletRequest, servletResponse);
                    } finally {
                        NDC.pop();
                    }
                }
            }
        };


        try {
            Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
            HttpServletResponse httpResponse = (HttpServletResponse) response;

            AtlasResponseRequestWrapper responseWrapper = new AtlasResponseRequestWrapper(httpResponse);
            responseWrapper.setHeader("X-Frame-Options", "DENY");

            if (existingAuth == null) {
                String authHeader = httpRequest.getHeader("Authorization");
                if (authHeader != null && authHeader.startsWith("Basic")) {
                    filterChain.doFilter(request, response);
                } else if (isKerberos) {
                    doKerberosAuth(request, response, filterChainWrapper, filterChain);
                } else {
                    filterChain.doFilter(request, response);
                }
            } else {
                filterChain.doFilter(request, response);
            }
        } catch (NullPointerException e) {
            LOG.error("Exception in AtlasAuthenticationFilter ", e);
            //PseudoAuthenticationHandler.getUserName() from hadoop-auth throws NPE if user name is not specified
            ((HttpServletResponse) response).sendError(Response.Status.BAD_REQUEST.getStatusCode(),
                    "Authentication is enabled and user is not specified. Specify user.name parameter");
        }
    }



    /**
     * This method is copied from hadoop auth lib, code added for error handling and fallback to other auth methods
     *
     * If the request has a valid authentication token it allows the request to continue to the target resource,
     * otherwise it triggers an authentication sequence using the configured {@link org.apache.hadoop.security.authentication.server.AuthenticationHandler}.
     *
     * @param request the request object.
     * @param response the response object.
     * @param filterChain the filter chain object.
     *
     * @throws IOException thrown if an IO error occurred.
     * @throws ServletException thrown if a processing error occurred.
     */
    public void doKerberosAuth(ServletRequest request, ServletResponse response, FilterChain filterChainWrapper ,FilterChain filterChain )
            throws IOException, ServletException {
        boolean unauthorizedResponse = true;
        int errCode = HttpServletResponse.SC_UNAUTHORIZED;
        AuthenticationException authenticationEx = null;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        boolean isHttps = "https".equals(httpRequest.getScheme());
        AuthenticationHandler   authHandler = getAuthenticationHandler();
        try {
            boolean newToken = false;
            AuthenticationToken token;
            try {
                token = getToken(httpRequest);
            }
            catch (AuthenticationException ex) {
                LOG.warn("AuthenticationToken ignored: {}", ex.getMessage());
                // will be sent back in a 401 unless filter authenticates
                authenticationEx = ex;
                token = null;
            }
            if (authHandler.managementOperation(token, httpRequest, httpResponse)) {
                if (token == null) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Request [{}] triggering authentication", getRequestURL(httpRequest));
                    }
                    token = authHandler.authenticate(httpRequest, httpResponse);
                    if (token != null && token.getExpires() != 0 &&
                            token != AuthenticationToken.ANONYMOUS) {
                        token.setExpires(System.currentTimeMillis() + getValidity() * 1000);
                    }
                    newToken = true;
                }
                if (token != null) {
                    unauthorizedResponse = false;
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Request [{}] user [{}] authenticated", getRequestURL(httpRequest), token.getUserName());
                    }
                    final AuthenticationToken authToken = token;
                    httpRequest = new HttpServletRequestWrapper(httpRequest) {

                        @Override
                        public String getAuthType() {
                            return authToken.getType();
                        }

                        @Override
                        public String getRemoteUser() {
                            return authToken.getUserName();
                        }

                        @Override
                        public Principal getUserPrincipal() {
                            return (authToken != AuthenticationToken.ANONYMOUS) ? authToken : null;
                        }
                    };
                    if (newToken && !token.isExpired() && token != AuthenticationToken.ANONYMOUS) {
                        String signedToken = signer.sign(token.toString());
                        createAuthCookie(httpResponse, signedToken, getCookieDomain(),
                                getCookiePath(), token.getExpires(), isHttps);
                    }

                    filterChainWrapper.doFilter(httpRequest, httpResponse);
                }
            } else {
                unauthorizedResponse = false;
            }
        } catch (AuthenticationException ex) {
            // exception from the filter itself is fatal
            errCode = HttpServletResponse.SC_FORBIDDEN;
            authenticationEx = ex;
            LOG.warn("Authentication exception: {}", ex.getMessage(), ex);
        }
        if (unauthorizedResponse) {
            if (!httpResponse.isCommitted()) {
                createAuthCookie(httpResponse, "", getCookieDomain(),
                        getCookiePath(), 0, isHttps);
                // If response code is 401. Then WWW-Authenticate Header should be
                // present.. reset to 403 if not found..
                if ((errCode == HttpServletResponse.SC_UNAUTHORIZED)
                        && (!httpResponse.containsHeader(
                        KerberosAuthenticator.WWW_AUTHENTICATE))) {
                    errCode = HttpServletResponse.SC_FORBIDDEN;
                }
                if (authenticationEx == null) { // added this code for atlas error handling and fallback
                    if (isBrowser(httpRequest.getHeader("User-Agent"))) {
                        filterChain.doFilter(request, response);
                    } else {
                        boolean chk = true;
                        Collection<String> headerNames = httpResponse.getHeaderNames();
                        for (String headerName : headerNames) {
                            String value = httpResponse.getHeader(headerName);
                            if (headerName.equalsIgnoreCase("Set-Cookie") && value.startsWith("ATLASSESSIONID")) {
                                chk = false;
                                break;
                            }
                        }
                        String authHeader = httpRequest.getHeader("Authorization");
                        if (authHeader == null && chk) {
                            filterChain.doFilter(request, response);
                        } else if (authHeader != null && authHeader.startsWith("Basic")) {
                            filterChain.doFilter(request, response);
                        }
                    }
                } else {
                    httpResponse.sendError(errCode, authenticationEx.getMessage());
                }
            }
        }
    }


    @Override
    public void destroy() {

        if ((this.secretProvider != null) && (this.isInitializedByTomcat)) {
            this.secretProvider.destroy();
            this.secretProvider = null;
        }
        optionsServlet.destroy();
        super.destroy();
    }


    private static String readUserFromCookie(HttpServletResponse response1) {
        String userName = null;
        boolean isCookieSet = response1.containsHeader("Set-Cookie");
        if (isCookieSet) {
            Collection<String> authUserName = response1.getHeaders("Set-Cookie");
            if (authUserName != null) {
                for (String cookie : authUserName) {
                    if (!StringUtils.isEmpty(cookie)) {
                        if (cookie.toLowerCase().startsWith(AuthenticatedURL.AUTH_COOKIE.toLowerCase()) && cookie.contains("u=")) {
                            String[] split = cookie.split(";");
                            if (split != null) {
                                for (String s : split) {
                                    if (!StringUtils.isEmpty(s) && s.toLowerCase().startsWith(AuthenticatedURL.AUTH_COOKIE.toLowerCase())) {
                                        int ustr = s.indexOf("u=");
                                        if (ustr != -1) {
                                            int andStr = s.indexOf("&", ustr);
                                            if (andStr != -1) {
                                                try {
                                                    userName = s.substring(ustr + 2, andStr);
                                                    break;
                                                } catch (Exception e) {
                                                    userName = null;
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return userName;
    }

    public static void createAuthCookie(HttpServletResponse resp, String token, String domain, String path, long expires, boolean isSecure) {
        StringBuilder sb = (new StringBuilder(AuthenticatedURL.AUTH_COOKIE)).append("=");
        if(token != null && token.length() > 0) {
            sb.append("\"").append(token).append("\"");
        }

        sb.append("; Version=1");
        if(path != null) {
            sb.append("; Path=").append(path);
        }

        if(domain != null) {
            sb.append("; Domain=").append(domain);
        }

        if(expires >= 0L) {
            Date date = new Date(expires);
            SimpleDateFormat df = new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss zzz");
            df.setTimeZone(TimeZone.getTimeZone("GMT"));
            sb.append("; Expires=").append(df.format(date));
        }

        if(isSecure) {
            sb.append("; Secure");
        }

        sb.append("; HttpOnly");
        resp.addHeader("Set-Cookie", sb.toString());
    }

    @Override
    protected AuthenticationToken getToken(HttpServletRequest request)
            throws IOException, AuthenticationException {
        AuthenticationToken token = null;
        String tokenStr = null;
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(AuthenticatedURL.AUTH_COOKIE)) {
                    tokenStr = cookie.getValue();
                    try {
                        tokenStr = this.signer.verifyAndExtract(tokenStr);
                    } catch (SignerException ex) {
                        throw new AuthenticationException(ex);
                    }
                }
            }
        }

        if (tokenStr != null) {
            token = AuthenticationToken.parse(tokenStr);
            if(token != null) {
                AuthenticationHandler authHandler = getAuthenticationHandler();
                if (!token.getType().equals(authHandler.getType())) {
                    throw new AuthenticationException("Invalid AuthenticationToken type");
                }
                if (token.isExpired()) {
                    throw new AuthenticationException("AuthenticationToken expired");
                }
            }
        }
        return token;
    }

    void parseBrowserUserAgents(String userAgents) {
        String[] agentsArray = userAgents.split(",");
        browserUserAgents = new HashSet<>();
        for (String patternString : agentsArray) {
            browserUserAgents.add(Pattern.compile(patternString));
        }
    }

    boolean isBrowser(String userAgent) {
        if (userAgent == null) {
            return false;
        }
        if (browserUserAgents != null) {
            for (Pattern pattern : browserUserAgents) {
                Matcher matcher = pattern.matcher(userAgent);
                if (matcher.matches()) {
                    return true;
                }
            }
        }
        return false;
    }

}
