io.lavagna.web.security.PathConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for io.lavagna.web.security.PathConfiguration.java

Source

/**
 * This file is part of lavagna.
 *
 * lavagna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * lavagna is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with lavagna.  If not, see <http://www.gnu.org/licenses/>.
 */
package io.lavagna.web.security;

import static io.lavagna.web.security.login.LoginHandler.AbstractLoginHandler.logout;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.apache.commons.lang3.StringUtils.removeStart;
import static org.springframework.web.context.support.WebApplicationContextUtils.getWebApplicationContext;
import io.lavagna.common.Json;
import io.lavagna.model.Key;
import io.lavagna.service.ConfigurationRepository;
import io.lavagna.service.UserRepository;
import io.lavagna.web.helper.Redirector;
import io.lavagna.web.helper.UserSession;
import io.lavagna.web.security.login.DemoLogin;
import io.lavagna.web.security.login.LdapLogin;
import io.lavagna.web.security.login.LoginHandler;
import io.lavagna.web.security.login.OAuthLogin;
import io.lavagna.web.security.login.PersonaLogin;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.springframework.util.PathMatcher;
import org.springframework.web.context.WebApplicationContext;

import com.samskivert.mustache.Mustache;

public class PathConfiguration {

    private final List<UrlMatcher> urlMatchers = new ArrayList<>();
    private boolean loginUrlDisabled;
    private LoginUrlMatcher loginUrlMatcher;
    private LogoutUrlMatcher logoutUrlMatcher;

    List<UrlMatcher> buildMatcherList() {
        List<UrlMatcher> r = new ArrayList<>();

        if (!loginUrlDisabled) {
            Objects.requireNonNull(loginUrlMatcher, "login urls must be configured or disabled");
            Objects.requireNonNull(logoutUrlMatcher, "logout urls must be configured or disabled");

            r.add(loginUrlMatcher);
            r.add(logoutUrlMatcher);
        }

        r.addAll(urlMatchers);
        return r;
    }

    public PathConfiguration disableLogin() {
        loginUrlDisabled = true;
        return this;
    }

    //
    public LogoutConfigurer login(String loginUrlMatcher, String loginPageUrl, String loginPage) {
        Validate.isTrue(!loginUrlDisabled, "login has been disabled");
        this.loginUrlMatcher = new LoginUrlMatcher(loginUrlMatcher, loginPageUrl, loginPage);
        return new LogoutConfigurer(this);
    }

    public BasicUrlMatcher request(String url) {
        BasicUrlMatcher urlMatcher = new BasicUrlMatcher(this, url);
        urlMatchers.add(urlMatcher);
        return urlMatcher;
    }

    public static class LogoutConfigurer {

        private final PathConfiguration conf;

        private LogoutConfigurer(PathConfiguration conf) {
            this.conf = conf;
        }

        public PathConfiguration logout(String logoutUrlMatcher, String logoutBaseUrl) {
            conf.logoutUrlMatcher = new LogoutUrlMatcher(logoutUrlMatcher, logoutBaseUrl);
            return conf;
        }
    }

    public interface UrlMatcher {
        boolean match(String url, PathMatcher pathMatcher);

        boolean doAction(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException;
    }

    public static class BasicUrlMatcher implements UrlMatcher {
        private final String urlMatcher;
        private final PathConfiguration conf;
        private boolean redirect;
        private Mode mode;

        BasicUrlMatcher(PathConfiguration conf, String urlMatcher) {
            this.conf = conf;
            this.urlMatcher = urlMatcher;
        }

        public PathConfiguration denyAll() {
            mode = Mode.DENY_ALL;
            return conf;
        }

        public PathConfiguration requireAuthenticated() {
            return requireAuthenticated(true);
        }

        public PathConfiguration requireAuthenticated(boolean redirect) {
            mode = Mode.REQUIRE_AUTHENTICATED;
            this.redirect = redirect;
            return conf;
        }

        public PathConfiguration permitAll() {
            mode = Mode.PERMIT_ALL;
            return conf;
        }

        @Override
        public boolean match(String url, PathMatcher pathMatcher) {
            return pathMatcher.match(urlMatcher, url);
        }

        @Override
        public boolean doAction(HttpServletRequest req, HttpServletResponse resp) throws IOException {
            if (mode == Mode.REQUIRE_AUTHENTICATED && !UserSession.isUserAuthenticated(req)) {
                if (redirect) {
                    String requestedUrl = extractRequestedUrl(req);
                    Redirector.sendRedirect(req, resp, conf.loginUrlMatcher.loginPageUrl,
                            singletonMap("reqUrl", singletonList(URLEncoder.encode(requestedUrl, "UTF-8"))));
                } else {
                    resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                }
                return true;
            } else if (mode == Mode.DENY_ALL) {
                resp.sendError(HttpServletResponse.SC_FORBIDDEN);
                return true;
            } else {
                return false;
            }
        }
    }

    private static String extractRequestedUrl(HttpServletRequest req) {
        String queryString = req.getQueryString();
        return removeStart(req.getRequestURI(), req.getContextPath())
                + (queryString != null ? ("?" + queryString) : "");
    }

    public static class LogoutUrlMatcher implements UrlMatcher {
        private final String logoutUrlMatcher;
        private final String logoutBaseUrl;

        LogoutUrlMatcher(String logoutUrlMatcher, String logoutBaseUrl) {
            this.logoutBaseUrl = logoutBaseUrl;
            this.logoutUrlMatcher = logoutUrlMatcher;
        }

        @Override
        public boolean match(String url, PathMatcher pathMatcher) {
            return pathMatcher.match(logoutUrlMatcher, url);
        }

        @Override
        public boolean doAction(HttpServletRequest req, HttpServletResponse resp)
                throws IOException, ServletException {
            Map<String, LoginHandler> handlers = findActiveLoginHandlers(req);
            String subPath = findSubpath(req, logoutBaseUrl);
            if (handlers.containsKey(subPath)) {
                return handlers.get(subPath).handleLogout(req, resp);
            } else {
                // fallback to default logout handler
                return logout(req, resp,
                        getWebApplicationContext(req.getServletContext()).getBean(UserRepository.class));
            }
        }
    }

    public static class LoginUrlMatcher implements UrlMatcher {
        private final String urlMatcher;
        private final String loginPageUrl;
        private final String loginPage;

        LoginUrlMatcher(String urlMatcher, String loginPageUrl, String loginPage) {
            this.urlMatcher = urlMatcher;
            this.loginPageUrl = loginPageUrl;
            this.loginPage = loginPage;
        }

        @Override
        public boolean match(String url, PathMatcher pathMatcher) {
            return pathMatcher.match(urlMatcher, url);
        }

        @Override
        public boolean doAction(HttpServletRequest req, HttpServletResponse resp)
                throws IOException, ServletException {
            Map<String, LoginHandler> handlers = findActiveLoginHandlers(req);

            // handle the static page for the login, we expect that it's a GET
            // request _and_ it match the configured path (/login)
            if ("GET".equalsIgnoreCase(req.getMethod()) && loginPageUrl.equals(req.getServletPath())) {
                InputStream is = req.getServletContext().getResourceAsStream(loginPage);
                Map<String, Object> model = new HashMap<>();
                for (LoginHandler lh : handlers.values()) {
                    model.putAll(lh.modelForLoginPage(req));
                }
                resp.setStatus(HttpServletResponse.SC_OK);
                resp.setContentType("text/html");
                Mustache.compiler().defaultValue("").compile(new InputStreamReader(is, StandardCharsets.UTF_8))
                        .execute(model, resp.getWriter());
                return true;
            }
            // -------------------------------
            // given /login/demo/ -> return demo
            // subPath will be demo/ldap/oauth/persona
            String subPath = findSubpath(req, loginPageUrl);

            if (handlers.containsKey(subPath)) {
                return handlers.get(subPath).doAction(req, resp);
            } else {
                return false;
            }
        }

    }

    // given /login/demo/ -> return demo
    private static String findSubpath(HttpServletRequest req, String firstPath) {
        return StringUtils.substringBefore(StringUtils.substring(req.getServletPath(), firstPath.length() + 1),
                "/");
    }

    private static Map<String, LoginHandler> findActiveLoginHandlers(HttpServletRequest req) {
        Map<String, LoginHandler> res = new HashMap<String, LoginHandler>();
        WebApplicationContext ctx = getWebApplicationContext(req.getServletContext());
        LoginHandlerType[] authMethods = Json.GSON.fromJson(
                ctx.getBean(ConfigurationRepository.class).getValue(Key.AUTHENTICATION_METHOD),
                LoginHandlerType[].class);
        for (LoginHandlerType m : authMethods) {
            res.put(m.pathAfterLogin, ctx.getBean(m.classHandler));
        }
        return res;
    }

    public enum LoginHandlerType {

        DEMO(DemoLogin.class, "demo"), LDAP(LdapLogin.class, "ldap"), OAUTH(OAuthLogin.class,
                "oauth"), PERSONA(PersonaLogin.class, "persona");

        LoginHandlerType(Class<? extends LoginHandler> classHandler, String pathAfterLogin) {
            this.classHandler = classHandler;
            this.pathAfterLogin = pathAfterLogin;
        }

        private final Class<? extends LoginHandler> classHandler;
        private final String pathAfterLogin;

    }

    private enum Mode {
        DENY_ALL, UNAUTHENTICATED, REQUIRE_AUTHENTICATED, PERMIT_ALL, LOGIN, LOGOUT
    }
}