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

Java tutorial

Introduction

Here is the source code for io.lavagna.web.security.SecurityConfiguration.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 java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.apache.commons.lang3.StringUtils.removeStart;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
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 io.lavagna.model.User;

public class SecurityConfiguration {

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

    private LockPageUrlMatcher lockPageUrlMatcher;
    private LockUrlMatcher lockUrlMatcher;

    private RequestMatcher requestMatcher = new AlwaysTrueRequestMatcher();

    private LoginHandlerFinder loginHandlerFinder = new LoginHandlerFinder() {
        @Override
        public Map<String, LoginHandler> find() {
            return Collections.emptyMap();
        }
    };

    private SessionHandler sessionHandler;

    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");
            Objects.requireNonNull(lockUrlMatcher, "lock urls must be configured or disabled");
            Objects.requireNonNull(lockPageUrlMatcher, "lock page urls must be configured or disabled");
            Objects.requireNonNull(sessionHandler, "sessionHandler must be defined or login url must be disabled");
            r.add(loginUrlMatcher);
            r.add(logoutUrlMatcher);

            r.add(lockUrlMatcher);
            r.add(lockPageUrlMatcher);
        }
        r.addAll(urlMatchers);
        return r;
    }

    public SecurityConfiguration requestMatcher(RequestMatcher requestMatcher) {
        this.requestMatcher = requestMatcher;
        return this;
    }

    public SecurityConfiguration loginHandlerFinder(LoginHandlerFinder loginHandlerFinder) {
        this.loginHandlerFinder = loginHandlerFinder;
        return this;
    }

    public SecurityConfiguration sessionHandler(SessionHandler sessionHandler) {
        this.sessionHandler = sessionHandler;
        return this;
    }

    public boolean matchRequest(HttpServletRequest request) {
        return requestMatcher.match(request);
    }

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

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

    //
    public LockConfigurer lockPage(String lockPageUrlMatcher, String lockPageUrl,
            LockPageGenerator lockPageGenerator) {
        Validate.isTrue(!lockPageUrlDisabled, "lock page has been disabled");
        this.lockPageUrlMatcher = new LockPageUrlMatcher(lockPageUrlMatcher, lockPageUrl, lockPageGenerator, this);
        return new LockConfigurer(this);
    }

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

    public static class LogoutConfigurer {

        private final SecurityConfiguration conf;

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

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

    public static class LockConfigurer {

        private final SecurityConfiguration conf;

        private LockConfigurer(SecurityConfiguration conf) {
            this.conf = conf;
        }

        public SecurityConfiguration lock(String lockUrlMatcher, String lockBaseUrl) {
            conf.lockUrlMatcher = new LockUrlMatcher(lockUrlMatcher, lockBaseUrl, conf);
            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 SecurityConfiguration conf;
        private boolean redirect;
        private String redirectTo;
        private Mode mode;

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

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

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

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

        public SecurityConfiguration redirectTo(String redirectTo) {
            mode = Mode.REDIRECT;
            this.redirectTo = redirectTo;
            return conf;
        }

        public SecurityConfiguration 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 && !conf.sessionHandler.isUserAuthenticated(req)) {
                if (redirect) {
                    String requestedUrl = extractRequestedUrl(req);
                    Redirector.sendRedirect(req, resp,
                            req.getContextPath() + "/" + removeStart(conf.loginUrlMatcher.loginPageUrl, "/"),
                            singletonMap("reqUrl", singletonList(URLEncoder.encode(requestedUrl, "UTF-8"))));
                } else {
                    resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                }
                return true;
            } else if (mode == Mode.REQUIRE_AUTHENTICATED && conf.sessionHandler.isUserAuthenticated(req)
                    && conf.sessionHandler.isLocked(req)) {
                if (redirect) {
                    String requestedUrl = extractRequestedUrl(req);
                    Redirector.sendRedirect(req, resp,
                            req.getContextPath() + "/" + removeStart(conf.lockPageUrlMatcher.lockPageUrl, "/"),
                            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 if (mode == Mode.REDIRECT) {
                Redirector.sendRedirect(req, resp, req.getContextPath() + "/" + removeStart(redirectTo, "/"),
                        Collections.<String, List<String>>emptyMap());
                return true;
            } else {
                return false;
            }
        }
    }

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

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

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

        @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 = conf.loginHandlerFinder.find();
            String subPath = findSubpath(req, logoutBaseUrl);
            if (handlers.containsKey(subPath)) {
                return handlers.get(subPath).handleLogout(req, resp);
            } else {
                // fallback to default logout handler
                conf.sessionHandler.invalidate(req, resp);
                return true;
            }
        }
    }

    public static class LockUrlMatcher implements UrlMatcher {
        private final String lockUrlMatcher;
        private final String lockBaseUrl;
        private final SecurityConfiguration conf;

        LockUrlMatcher(String lockUrlMatcher, String lockBaseUrl, SecurityConfiguration conf) {
            this.lockBaseUrl = lockBaseUrl;
            this.lockUrlMatcher = lockUrlMatcher;
            this.conf = conf;
        }

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

        @Override
        public boolean doAction(HttpServletRequest req, HttpServletResponse resp)
                throws IOException, ServletException {
            Map<String, LoginHandler> handlers = conf.loginHandlerFinder.find();
            String subPath = findSubpath(req, lockBaseUrl);
            if (handlers.containsKey(subPath)) {
                return handlers.get(subPath).handleLock(req, resp);
            } else {
                // fallback to default logout handler
                conf.sessionHandler.lock(req, resp);
                return true;
            }
        }
    }

    public static class LoginUrlMatcher implements UrlMatcher {
        private final String urlMatcher;
        private final String loginPageUrl;
        private final LoginPageGenerator loginPageGenerator;
        private final SecurityConfiguration conf;

        LoginUrlMatcher(String urlMatcher, String loginPageUrl, LoginPageGenerator loginPageGenerator,
                SecurityConfiguration conf) {
            this.urlMatcher = urlMatcher;
            this.loginPageUrl = loginPageUrl;
            this.loginPageGenerator = loginPageGenerator;
            this.conf = conf;
        }

        @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 = conf.loginHandlerFinder.find();

            // 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())) {
                loginPageGenerator.generate(req, resp, handlers);
                return true;
            }
            // -------------------------------
            // given /login/demo/ -> return demo
            // subPath will be demo/ldap/oauth
            String subPath = findSubpath(req, loginPageUrl);

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

    }

    public static class LockPageUrlMatcher implements UrlMatcher {
        private final String urlMatcher;
        private final String lockPageUrl;
        private final LockPageGenerator lockPageGenerator;
        private final SecurityConfiguration conf;

        LockPageUrlMatcher(String urlMatcher, String lockPageUrl, LockPageGenerator lockPageGenerator,
                SecurityConfiguration conf) {
            this.urlMatcher = urlMatcher;
            this.lockPageUrl = lockPageUrl;
            this.lockPageGenerator = lockPageGenerator;
            this.conf = conf;
        }

        @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 = conf.loginHandlerFinder.find();

            // 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()) && lockPageUrl.equals(req.getServletPath())) {
                lockPageGenerator.generate(req, resp, handlers);
                return true;
            }
            // -------------------------------
            // given /lockScreen/demo/ -> return demo
            // subPath will be demo/ldap/oauth
            String subPath = findSubpath(req, lockPageUrl);

            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 enum Mode {
        DENY_ALL, UNAUTHENTICATED, REQUIRE_AUTHENTICATED, PERMIT_ALL, LOGIN, LOGOUT, REDIRECT
    }

    public interface RequestMatcher {
        boolean match(HttpServletRequest request);
    }

    public interface LoginHandlerFinder {
        Map<String, LoginHandler> find();
    }

    public interface LoginPageGenerator {
        void generate(HttpServletRequest req, HttpServletResponse resp, Map<String, LoginHandler> handlers)
                throws IOException;
    }

    public interface LockPageGenerator {
        void generate(HttpServletRequest req, HttpServletResponse resp, Map<String, LoginHandler> handlers)
                throws IOException;
    }

    public interface SessionHandler {
        void invalidate(HttpServletRequest req, HttpServletResponse resp);

        boolean isUserAuthenticated(HttpServletRequest req);

        boolean isUserAnonymous(HttpServletRequest req);

        boolean isLocked(HttpServletRequest req);

        void setUser(int userId, boolean isUserAnonymous, HttpServletRequest req, HttpServletResponse resp);

        void setUser(int userId, boolean isUserAnonymous, HttpServletRequest req, HttpServletResponse resp,
                boolean addRememberMeCookie);

        void lock(HttpServletRequest req, HttpServletResponse resp);

        io.lavagna.model.User getCurrentUser(HttpServletRequest req);
    }

    public interface Users {
        boolean userExistsAndEnabled(String provider, String name);

        User findUserByName(String provider, String name);
    }

    public interface User {
        int getId();

        boolean isAnonymous();
    }

    public static class AlwaysTrueRequestMatcher implements RequestMatcher {
        @Override
        public boolean match(HttpServletRequest request) {
            return true;
        }
    }
}