<commit_before>package io.corbel.lib.ws.auth;

import static java.util.stream.StreamSupport.stream;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.*;
import java.util.regex.Pattern;

import javax.annotation.Priority;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Priorities;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Sets;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import io.corbel.lib.token.TokenInfo;
import io.corbel.lib.ws.api.error.ErrorResponseFactory;
import io.dropwizard.auth.oauth.OAuthFactory;

/**
 * This class is a bit of a hack to Dropwizard(Jersey 2.17). It uses the {@link io.dropwizard.auth.oauth.OAuthFactory} class to obtain an
 * instance of {@link AuthorizationInfo}. The filter is configured to only verify the set of request whose path matches the specified
 * pattern. It validates only access rules of type <b>http_access</b>. If request cannot proceed, it returns a HTTP error 401 without any
 * error information.
 * 
 * @author <NAME>
 * 
 */
@Priority(Priorities.AUTHORIZATION) public class AuthorizationRequestFilter implements ContainerRequestFilter {

    public static final String AUTHORIZATION_INFO_PROPERTIES_KEY = "AuthorizationInfo";

    private static final Response UNAUTHORIZED_TOKEN_RESPONSE = ErrorResponseFactory.getInstance().unauthorized("unauthorized_token", "The Authorization token can not perform the request");
    private static final Response INVALID_TOKEN_RESPONSE = ErrorResponseFactory.getInstance().unauthorized("invalid_token", "The Authorization token is invalid");

    private static final Pattern REQUEST_WITH_DOMAIN_PATTERN = Pattern.compile("v[0-9]+\\.[0-9]+/[\\w\\-:\\.]+/\\w+/\\w+:.+");
    private static final Logger LOG = LoggerFactory.getLogger(AuthorizationRequestFilter.class);

    private final OAuthFactory<AuthorizationInfo> oAuthProvider;
    private final CookieOAuthFactory<AuthorizationInfo> cookieOAuthProvider;
    private final String unAuthenticatedPathPattern;
    private final boolean checkDomain;

    @Context private HttpServletRequest request;

    public AuthorizationRequestFilter(OAuthFactory<AuthorizationInfo> provider, CookieOAuthFactory<AuthorizationInfo> cookieOAuthProvider,
            String unAuthenticatedPathPattern, boolean checkDomain) {
        this.oAuthProvider = provider;
        this.cookieOAuthProvider = cookieOAuthProvider;
        this.unAuthenticatedPathPattern = unAuthenticatedPathPattern;
        this.checkDomain = checkDomain;
    }

    public AuthorizationRequestFilter() {
        this.oAuthProvider = null;
        this.cookieOAuthProvider = null;
        this.unAuthenticatedPathPattern = null;
        this.checkDomain = false;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void filter(ContainerRequestContext request) {
        if (!request.getUriInfo().getPath().matches(unAuthenticatedPathPattern)) {
            // OPTIONS is always allowed (for CORS)
            if (!request.getMethod().equals(HttpMethod.OPTIONS)) {

                CustomRequest customRequest = new CustomRequest(getRequest(), request);
                oAuthProvider.setRequest(customRequest);
                AuthorizationInfo info = oAuthProvider.provide();
                if (info == null) {
                    cookieOAuthProvider.setRequest(customRequest);
                    info = cookieOAuthProvider.provide();
                }

                if (info != null) {
                    String domainId = extractDomainId(info, request);
                    checkAccessRules(info, request, domainId);
                    storeAuthorizationInfoInRequestProperties(info, request);
                } else {
                    throw new WebApplicationException(INVALID_TOKEN_RESPONSE);
                }
            }
        }
    }

    private String extractDomainId(AuthorizationInfo info, ContainerRequestContext request) {
        if (checkDomain && REQUEST_WITH_DOMAIN_PATTERN.matcher(request.getUriInfo().getPath()).matches()) {
            String domainId = request.getUriInfo().getPath().split("/")[1];
            if (!info.getDomainId().equals(domainId)) {
                throw new WebApplicationException(UNAUTHORIZED_TOKEN_RESPONSE);
            }
            return domainId;
        }
        return null;
    }

    public void checkAccessRules(final AuthorizationInfo info, final ContainerRequestContext request, String domainId) {
        String scopeUrl = extractScopeUrl(domainId, request.getUriInfo().getPath());
        Set<JsonObject> applicableRules = Sets.filter(info.getAccessRules(),
                rule -> matchesMethod(request.getMethod(), rule) && matchesUriPath(scopeUrl, rule) && matchesMediaTypes(request, rule)
                        && matchesTokenType(info.getTokenReader().getInfo(), rule));
        // If no rules apply then by default access is denied
        if (applicableRules.isEmpty()) {
            throw new WebApplicationException(UNAUTHORIZED_TOKEN_RESPONSE);
        }
    }

    private String extractScopeUrl(String domainId, String path) {
        if (domainId != null)
            path = path.replace(domainId + "/", "");
        return path.substring(path.indexOf("/") + 1);
    }

    private void storeAuthorizationInfoInRequestProperties(AuthorizationInfo info, ContainerRequestContext request) {<commit_msg>Fix problem with final unauthorized responses.<commit_after>package io.corbel.lib.ws.auth;

import static java.util.stream.StreamSupport.stream;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.*;
import java.util.regex.Pattern;

import javax.annotation.Priority;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Priorities;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Sets;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import io.corbel.lib.token.TokenInfo;
import io.corbel.lib.ws.api.error.ErrorResponseFactory;
import io.dropwizard.auth.oauth.OAuthFactory;

/**
 * This class is a bit of a hack to Dropwizard(Jersey 2.17). It uses the {@link io.dropwizard.auth.oauth.OAuthFactory} class to obtain an
 * instance of {@link AuthorizationInfo}. The filter is configured to only verify the set of request whose path matches the specified
 * pattern. It validates only access rules of type <b>http_access</b>. If request cannot proceed, it returns a HTTP error 401 without any
 * error information.
 * 
 * @author <NAME>
 * 
 */
@Priority(Priorities.AUTHORIZATION) public class AuthorizationRequestFilter implements ContainerRequestFilter {

    public static final String AUTHORIZATION_INFO_PROPERTIES_KEY = "AuthorizationInfo";

    private static final Pattern REQUEST_WITH_DOMAIN_PATTERN = Pattern.compile("v[0-9]+\\.[0-9]+/[\\w\\-:\\.]+/\\w+/\\w+:.+");
    private static final Logger LOG = LoggerFactory.getLogger(AuthorizationRequestFilter.class);

    private final OAuthFactory<AuthorizationInfo> oAuthProvider;
    private final CookieOAuthFactory<AuthorizationInfo> cookieOAuthProvider;
    private final String unAuthenticatedPathPattern;
    private final boolean checkDomain;

    @Context private HttpServletRequest request;

    public AuthorizationRequestFilter(OAuthFactory<AuthorizationInfo> provider, CookieOAuthFactory<AuthorizationInfo> cookieOAuthProvider,
            String unAuthenticatedPathPattern, boolean checkDomain) {
        this.oAuthProvider = provider;
        this.cookieOAuthProvider = cookieOAuthProvider;
        this.unAuthenticatedPathPattern = unAuthenticatedPathPattern;
        this.checkDomain = checkDomain;
    }

    public AuthorizationRequestFilter() {
        this.oAuthProvider = null;
        this.cookieOAuthProvider = null;
        this.unAuthenticatedPathPattern = null;
        this.checkDomain = false;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void filter(ContainerRequestContext request) {
        if (!request.getUriInfo().getPath().matches(unAuthenticatedPathPattern)) {
            // OPTIONS is always allowed (for CORS)
            if (!request.getMethod().equals(HttpMethod.OPTIONS)) {

                CustomRequest customRequest = new CustomRequest(getRequest(), request);
                oAuthProvider.setRequest(customRequest);
                AuthorizationInfo info = oAuthProvider.provide();
                if (info == null) {
                    cookieOAuthProvider.setRequest(customRequest);
                    info = cookieOAuthProvider.provide();
                }

                if (info != null) {
                    String domainId = extractDomainId(info, request);
                    checkAccessRules(info, request, domainId);
                    storeAuthorizationInfoInRequestProperties(info, request);
                } else {
                    throw new WebApplicationException(generateInvalidTokenResponse());
                }
            }
        }
    }

    private String extractDomainId(AuthorizationInfo info, ContainerRequestContext request) {
        if (checkDomain && REQUEST_WITH_DOMAIN_PATTERN.matcher(request.getUriInfo().getPath()).matches()) {
            String domainId = request.getUriInfo().getPath().split("/")[1];
            if (!info.getDomainId().equals(domainId)) {
                throw new WebApplicationException(generateUnauthorizedTokenResponse());
            }
            return domainId;
        }
        return null;
    }

    public void checkAccessRules(final AuthorizationInfo info, final ContainerRequestContext request, String domainId) {
        String scopeUrl = extractScopeUrl(domainId, request.getUriInfo().getPath());
        Set<JsonObject> applicableRules = Sets.filter(info.getAccessRules(),
                rule -> matchesMethod(request.getMethod(), rule) && matchesUriPath(scopeUrl, rule) && matchesMediaTypes(request, rule)
                        && matchesTokenType(info.getTokenReader().getInfo(), rule));
        // If no rules apply then by default access is denied
        if (applicableRules.isEmpty()) {
            throw new WebApplicationException(generateUnauthorizedTokenResponse());
        }
    }

    private Response generateInvalidTokenResponse() {
        return ErrorResponseFactory.getInstance().unauthorized("invalid_token", "The Authorization token is invalid");
    }

    private Response generateUnauthorizedTokenResponse() {
        return ErrorResponseFactory.getInstance().unauthorized("unauthorized_token", "The Authorization token can not perform the request");
    }

    private String extractScopeUrl(String domainId, String path) {
        if (domainId != null)
            path = path.replace(domainId + "/", "");
        return path.substring(path.indexOf("/") + 1);
    }

    private void storeAuthorizationInfoInRequestProperties(AuthorizationInfo info, ContainerRequestContext request) {
{'fraction_non_alphanumeric': 0.09469504983949992, 'fraction_numerical': 0.0023652644027707384, 'mean_word_length': 3.440735183795949, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2653786', 'n_tokens_mistral': 3114, 'n_tokens_neox': 3029, 'n_words': 703}