/** * 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.config; import com.samskivert.mustache.Mustache; import io.lavagna.common.Json; import io.lavagna.common.Version; import io.lavagna.model.Key; import io.lavagna.model.Role; import io.lavagna.model.UserToCreate; import io.lavagna.service.ConfigurationRepository; import io.lavagna.service.Ldap; import io.lavagna.service.UserRepository; import io.lavagna.service.UserService; import io.lavagna.web.helper.GsonHttpMessageConverter; import io.lavagna.web.helper.UserSession; import io.lavagna.web.security.LoginHandler; import io.lavagna.web.security.SecurityConfiguration; import io.lavagna.web.security.SecurityConfiguration.*; import io.lavagna.web.security.login.DemoLogin; import io.lavagna.web.security.login.LdapLogin; import io.lavagna.web.security.login.PasswordLogin; import io.lavagna.web.security.login.LdapLogin.LdapAuthenticator; import io.lavagna.web.security.login.OAuthLogin; import io.lavagna.web.security.login.OAuthLogin.OAuthConfiguration; import io.lavagna.web.security.login.OAuthLogin.OauthConfigurationFetcher; import org.scribe.builder.ServiceBuilder; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Lazy; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.web.client.RestTemplate; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.Map; import static java.util.Arrays.asList; public class WebSecurityConfig { @Bean public SecurityConfiguration configuredApp(ConfigurationRepository configurationRepository, UserRepository userRepository, SessionHandler sessionHandler, ApplicationContext context) { return new SecurityConfiguration().requestMatcher(onlyWhenSetupComplete(configurationRepository)) .loginHandlerFinder(loginHandlerFinder(configurationRepository, context)) .sessionHandler(sessionHandler) .request("/favicon.ico").permitAll() .request("/favicon/**").permitAll() .request("/css/**").permitAll() .request("/fonts/**").permitAll() .request("/resource-login/**").permitAll() .request("/setup/**").denyAll() .request("/api/calendar/**").permitAll() .request("/api/**").requireAuthenticated(false) .request("/**").requireAuthenticated() .login("/login/**", "/login", loginPageGenerator()) .logout("/logout/**", "/logout"); } @Bean public SecurityConfiguration unconfiguredApp(ConfigurationRepository configurationRepository) { return new SecurityConfiguration().requestMatcher(onlyWhenSetupIsNotComplete(configurationRepository)) .request("/setup/**").permitAll() .request("/css/**").permitAll() .request("/js/**").permitAll() .request("/fonts/**").permitAll() .request("/help/**").permitAll() .request("/about/**").permitAll() .request("/favicon/**").permitAll() .request("/**").redirectTo("/setup/") .disableLogin(); } private LoginPageGenerator loginPageGenerator() { return new LoginPageGenerator() { @Override public void generate(HttpServletRequest req, HttpServletResponse resp, Map<String, LoginHandler> handlers) throws IOException { Map<String, Object> model = new HashMap<>(); model.put("version", Version.version()); for (LoginHandler lh : handlers.values()) { model.putAll(lh.modelForLoginPage(req)); } resp.setStatus(HttpServletResponse.SC_OK); resp.setContentType("text/html"); resp.setCharacterEncoding("UTF-8"); model.put("json", Json.GSON.toJson(model)); try (InputStream is = req.getServletContext().getResourceAsStream("/WEB-INF/views/login.html")) { Mustache.compiler().escapeHTML(false).defaultValue("").compile(new InputStreamReader(is, StandardCharsets.UTF_8)).execute(model, resp.getWriter()); } } }; } @Bean public SessionHandler sessionHandler(final UserRepository userRepository) { return new SessionHandler() { @Override public void invalidate(HttpServletRequest req, HttpServletResponse resp) { UserSession.invalidate(req, resp, userRepository); } @Override public boolean isUserAuthenticated(HttpServletRequest req) { return UserSession.isUserAuthenticated(req); } @Override public boolean isUserAnonymous(HttpServletRequest req) { return UserSession.isUserAnonymous(req); } @Override public void setUser(int userId, boolean isUserAnonymous, HttpServletRequest req, HttpServletResponse resp) { UserSession.setUser(userId, isUserAnonymous, req, resp, userRepository); } @Override public void setUser(int userId, boolean isUserAnonymous, HttpServletRequest req, HttpServletResponse resp, boolean addRememberMeCookie) { UserSession.setUser(userId, isUserAnonymous, req, resp, userRepository, addRememberMeCookie); } }; } private static class WebSecurityUser implements User { private final int id; private final boolean anonymous; private WebSecurityUser(io.lavagna.model.User user) { this.id = user.getId(); this.anonymous = user.getAnonymous(); } @Override public int getId() { return id; } @Override public boolean isAnonymous() { return anonymous; } } public static class AccountCreatorIfMissing { private final UserRepository userRepository; private final ConfigurationRepository configurationRepository; private final UserService userService; public AccountCreatorIfMissing(UserRepository userRepository, ConfigurationRepository configurationRepository, UserService userService) { this.userRepository = userRepository; this.configurationRepository = configurationRepository; this.userService = userService; } private void createDefaultUser(String provider, String name) { UserToCreate userToCreate = new UserToCreate(provider, name); userToCreate.setRoles(Collections.singletonList(Role.Companion.getDEFAULT_ROLE().getName())); userService.createUser(userToCreate); } private boolean canLdap(String provider, String name) { return "ldap".equals(provider) && !userRepository.userExists(provider, name) && "true".equals(configurationRepository.getValueOrNull(Key.LDAP_AUTOCREATE_MISSING_ACCOUNT)); } private boolean canCreateUserForOauthProvider(String provider) { OAuthConfiguration conf = Json.GSON.fromJson(configurationRepository.getValueOrNull(Key.OAUTH_CONFIGURATION), OAuthConfiguration.class); return conf != null && conf.hasProvider(provider) && conf.getProviderWithName(provider).getAutoCreateMissingAccount(); } private boolean canOauth(String provider, String name) { return provider.startsWith("oauth.") && !userRepository.userExists(provider, name) && canCreateUserForOauthProvider(provider); } public void createIfConfiguredAndMissing(String provider, String name) { if (canLdap(provider, name) || canOauth(provider, name)) { createDefaultUser(provider, name); } } } @Bean private AccountCreatorIfMissing accountCreatorIfMissing(UserRepository userRepository, ConfigurationRepository configurationRepository, UserService userService) { return new AccountCreatorIfMissing(userRepository, configurationRepository, userService); } @Bean private Users users(final UserRepository userRepository, final AccountCreatorIfMissing accountCreatorIfMissing) { return new Users() { @Override public boolean userExistsAndEnabled(String provider, String name) { accountCreatorIfMissing.createIfConfiguredAndMissing(provider, name); return userRepository.userExistsAndEnabled(provider, name); } @Override public User findUserByName(String provider, String name) { return new WebSecurityUser(userRepository.findUserByName(provider, name)); } }; } private LoginHandlerFinder loginHandlerFinder(final ConfigurationRepository configurationRepository, final ApplicationContext context) { return new LoginHandlerFinder() { @Override public Map<String, LoginHandler> find() { LoginHandlerType[] authMethods = Json.GSON.fromJson(configurationRepository.getValue(Key.AUTHENTICATION_METHOD), LoginHandlerType[].class); Map<String, LoginHandler> res = new HashMap<String, LoginHandler>(); for (LoginHandlerType m : authMethods) { res.put(m.pathAfterLogin, context.getBean(m.classHandler)); } return res; } }; } public enum LoginHandlerType { DEMO(DemoLogin.class, "demo"), LDAP(LdapLogin.class, "ldap"), OAUTH(OAuthLogin.class, "oauth"), PASSWORD(PasswordLogin.class, "password"); LoginHandlerType(Class<? extends LoginHandler> classHandler, String pathAfterLogin) { this.classHandler = classHandler; this.pathAfterLogin = pathAfterLogin; } private final Class<? extends LoginHandler> classHandler; private final String pathAfterLogin; } private static SecurityConfiguration.RequestMatcher onlyWhenSetupComplete(final ConfigurationRepository configurationRepository) { return new SecurityConfiguration.RequestMatcher() { @Override public boolean match(HttpServletRequest request) { return "true".equals(configurationRepository.getValueOrNull(Key.SETUP_COMPLETE)); } }; } private static SecurityConfiguration.RequestMatcher onlyWhenSetupIsNotComplete(final ConfigurationRepository configurationRepository) { return new SecurityConfiguration.RequestMatcher() { @Override public boolean match(HttpServletRequest request) { return !"true".equals(configurationRepository.getValueOrNull(Key.SETUP_COMPLETE)); } }; } @Lazy @Bean public DemoLogin demoLogin(Users users, SessionHandler sessionHandler) { return new DemoLogin(users, sessionHandler, "/login?error-demo"); } @Lazy @Bean public OAuthLogin oauthLogin(Users users, SessionHandler sessionHandler, final ConfigurationRepository configurationRepository) { OauthConfigurationFetcher configurationFetcher = new OauthConfigurationFetcher() { @Override public OAuthConfiguration fetch() { return Json.GSON.fromJson(configurationRepository.getValueOrNull(Key.OAUTH_CONFIGURATION), OAuthConfiguration.class); } }; return new OAuthLogin(users, sessionHandler, configurationFetcher, new ServiceBuilder(), "/login?error-oauth"); } @Lazy @Bean public LdapLogin ldapLogin(Users users, SessionHandler sessionHandler, final Ldap ldap) { LdapAuthenticator authenticator = new LdapAuthenticator() { @Override public boolean authenticate(String username, String password) { return ldap.authenticate(username, password); } }; return new LdapLogin(users, sessionHandler, authenticator, "/login?error-ldap"); } @Lazy @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setMessageConverters(asList(new FormHttpMessageConverter(), new GsonHttpMessageConverter())); return restTemplate; } @Lazy @Bean public PasswordLogin passwordLogin(Users users, SessionHandler sessionHandler, UserRepository userRepository) { return new PasswordLogin(users, sessionHandler, userRepository, "/login?error-password"); } }