/**
* 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 org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.springframework.util.PathMatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.*;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.apache.commons.lang3.StringUtils.removeStart;
public class SecurityConfiguration {
private final List<UrlMatcher> urlMatchers = new ArrayList<>();
private boolean loginUrlDisabled;
private LoginUrlMatcher loginUrlMatcher;
private LogoutUrlMatcher logoutUrlMatcher;
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(sessionHandler, "sessionHandler must be defined or login url must be disabled");
r.add(loginUrlMatcher);
r.add(logoutUrlMatcher);
}
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 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 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.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 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;
}
}
}
// 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 SessionHandler {
void invalidate(HttpServletRequest req, HttpServletResponse resp);
boolean isUserAuthenticated(HttpServletRequest req);
boolean isUserAnonymous(HttpServletRequest req);
void setUser(int userId, boolean isUserAnonymous, HttpServletRequest req, HttpServletResponse resp);
void setUser(int userId, boolean isUserAnonymous, HttpServletRequest req, HttpServletResponse resp, boolean addRememberMeCookie);
}
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;
}
}
}