/* * Weblounge: Web Content Management System * Copyright (c) 2003 - 2011 The Weblounge Team * http://entwinemedia.com/weblounge * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package ch.entwine.weblounge.kernel.security; import ch.entwine.weblounge.common.impl.security.PasswordEncoder; import ch.entwine.weblounge.common.impl.security.SystemRole; import ch.entwine.weblounge.common.security.DigestType; import ch.entwine.weblounge.common.security.DirectoryProvider; import ch.entwine.weblounge.common.security.DirectoryService; import ch.entwine.weblounge.common.security.LoginListener; import ch.entwine.weblounge.common.security.Password; import ch.entwine.weblounge.common.security.Role; import ch.entwine.weblounge.common.security.SecurityService; import ch.entwine.weblounge.common.security.SiteDirectory; import ch.entwine.weblounge.common.security.User; import ch.entwine.weblounge.common.site.Site; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; /** * Federated user and role providers, and exposes a spring UserDetailsService so * user lookups can be used by spring security. */ public class DirectoryServiceImpl implements DirectoryService, UserDetailsService { /** The logger */ private static final Logger logger = LoggerFactory.getLogger(DirectoryServiceImpl.class); /** The list of directories */ protected Map<String, List<DirectoryProvider>> siteDirectories = new HashMap<String, List<DirectoryProvider>>(); /** The list of system directories */ protected List<DirectoryProvider> systemDirectories = new ArrayList<DirectoryProvider>(); /** The list of login listeners */ protected List<LoginListener> loginListeners = new ArrayList<LoginListener>(); /** The security service */ protected SecurityService securityService = null; /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.security.DirectoryService#getRoles() */ public Role[] getRoles() throws IllegalStateException { Site site = securityService.getSite(); if (site == null) throw new IllegalStateException("No site set in security context"); List<DirectoryProvider> providers = new ArrayList<DirectoryProvider>(); // Assemble a list of all possible directories List<DirectoryProvider> siteProviders = this.siteDirectories.get(site.getIdentifier()); if (siteProviders != null) providers.addAll(siteProviders); providers.addAll(systemDirectories); // Collect roles from all directories registered for this site SortedSet<Role> roles = new TreeSet<Role>(); for (DirectoryProvider directory : providers) { for (Role role : directory.getRoles()) { roles.add(role); } } return roles.toArray(new Role[roles.size()]); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.security.DirectoryService#loadUser(java.lang.String, * Site) */ public User loadUser(String login, Site site) throws IllegalStateException { List<DirectoryProvider> providers = new ArrayList<DirectoryProvider>(); // Assemble a list of all possible directories List<DirectoryProvider> siteProviders = this.siteDirectories.get(site.getIdentifier()); if (siteProviders != null) providers.addAll(siteProviders); providers.addAll(systemDirectories); // Find a user principal to use for login for (DirectoryProvider directory : providers) { try { User user = directory.loadUser(login, site); if (user != null) { logger.debug("User directory '{}' returned a user to login '{}' into site '{}'", new String[] { directory.getIdentifier(), login, site.getIdentifier() }); return user; } } catch (Throwable t) { logger.warn("Error looking up user from {}: {}", directory, t.getMessage()); } } return null; } /** * {@inheritDoc} * * @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String) */ public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException, DataAccessException { Site site = securityService.getSite(); if (site == null) { logger.error("Site context not available during user lookup"); throw new UsernameNotFoundException("No site context available"); } User user = loadUser(name, site); if (user == null) { throw new UsernameNotFoundException(name); } else { // By default, add the anonymous role so the user is able to access // publicly available resources user.addPublicCredentials(SystemRole.GUEST); // Collect the set of roles (granted authorities) for this users Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>(); for (Object o : user.getPublicCredentials(Role.class)) { Role masterRole = (Role) o; for (Role r : masterRole.getClosure()) { authorities.add(new SimpleGrantedAuthority(r.getContext() + ":" + r.getIdentifier())); // Every role may or may not be a system role or - in case of non- // system roles, may or may not be including one or more of those // roles. Let's ask for a translation and then add those roles // to the set of granted authorities Role[] systemEquivalents = getSystemRoles(r); for (Role systemRole : systemEquivalents) { authorities.add(new SimpleGrantedAuthority(systemRole.getContext() + ":" + systemRole.getIdentifier())); user.addPublicCredentials(systemRole); } } } // Make sure there is no ambiguous information with regards to passwords Set<Object> passwords = user.getPrivateCredentials(Password.class); if (passwords.size() > 1) { logger.warn("User '{}@{}' has more than one password'", name, site.getIdentifier()); throw new DataRetrievalFailureException("User '" + user + "' has more than one password"); } else if (passwords.size() == 0) { logger.warn("User '{}@{}' has no password", name, site.getIdentifier()); throw new DataRetrievalFailureException("User '" + user + "' has no password"); } // Create the password according to the site's and Spring Security's // digest policy Password p = (Password) passwords.iterator().next(); String password = null; switch (site.getDigestType()) { case md5: if (!DigestType.md5.equals(p.getDigestType())) { logger.debug("Creating digest password for '{}@{}'", name, site.getIdentifier()); password = PasswordEncoder.encode(p.getPassword()); } else { password = p.getPassword(); } break; case plain: if (!DigestType.plain.equals(p.getDigestType())) { logger.warn("User '{}@{}' does not have a plain text password'", name, site.getIdentifier()); return null; } password = p.getPassword(); break; default: throw new IllegalStateException("Unknown digest type '" + site.getDigestType() + "'"); } // Notifiy login listeners of the initiated login attempt for (LoginListener listener : loginListeners) { try { listener.beforeLogin(site, user); } catch (Throwable t) { logger.warn("Login listener '{}' failed to preoprly process before login callback", listener, t); } } // Provide the user to Spring Security return new SpringSecurityUser(user, password, true, true, true, true, authorities); } } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.security.DirectoryService#getLocalRole(ch.entwine.weblounge.common.security.Role) */ public Role getLocalRole(Role role) { Site site = securityService.getSite(); if (site == null) throw new IllegalStateException("No site set in security context"); List<DirectoryProvider> providers = new ArrayList<DirectoryProvider>(); // Assemble a list of all possible directories List<DirectoryProvider> siteProviders = this.siteDirectories.get(site.getIdentifier()); if (siteProviders != null) providers.addAll(siteProviders); providers.addAll(systemDirectories); for (DirectoryProvider directory : providers) { Role localRole = directory.getLocalRole(role); if (localRole != null) { return localRole; } } return null; } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.security.DirectoryService#getSystemRoles(ch.entwine.weblounge.common.security.Role) */ public Role[] getSystemRoles(Role role) { Site site = securityService.getSite(); if (site == null) throw new IllegalStateException("No site set in security context"); List<DirectoryProvider> providers = new ArrayList<DirectoryProvider>(); // Assemble a list of all possible directories List<DirectoryProvider> siteProviders = this.siteDirectories.get(site.getIdentifier()); if (siteProviders != null) providers.addAll(siteProviders); providers.addAll(systemDirectories); Set<Role> systemRoles = new HashSet<Role>(); for (DirectoryProvider directory : providers) { Role[] roleMappings = directory.getSystemRoles(role); if (roleMappings != null && roleMappings.length > 0) { systemRoles.addAll(Arrays.asList(roleMappings)); } } return systemRoles.toArray(new Role[systemRoles.size()]); } /** * Sets the security service. * * @param securityService * the security service */ void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** * Adds the directory to the list of site directories. * * @param directory * the site directory */ void addDirectoryProvider(DirectoryProvider directory) { logger.debug("Registering directory provider '{}'", directory.getIdentifier()); if (directory instanceof SiteDirectory) { List<DirectoryProvider> directoryProviders = siteDirectories.get(directory.getIdentifier()); if (directoryProviders == null) { directoryProviders = new ArrayList<DirectoryProvider>(); siteDirectories.put(directory.getIdentifier(), directoryProviders); } directoryProviders.add(directory); } else { systemDirectories.add(directory); } } /** * Removes the directory service provider from the list of providers. * * @param directory * the directory service provider */ void removeDirectoryProvider(DirectoryProvider directory) { logger.debug("Unregistering directory provider '{}'", directory.getIdentifier()); if (directory instanceof SiteDirectory) { List<DirectoryProvider> directoryProviders = this.siteDirectories.get(directory.getIdentifier()); if (directoryProviders != null) { directoryProviders.remove(directory); if (directoryProviders.size() == 0) { directoryProviders.remove(directory.getIdentifier()); } } } else { systemDirectories.remove(directory); } } /** * Adds the login listener to the list of login listeners. * * @param loginListener * the login listener */ void addLoginListener(LoginListener loginListener) { logger.debug("Registering login listener '{}'", loginListener); loginListeners.add(loginListener); } /** * Removes the login listener from the list of login listeners. * * @param loginListener * the login listener */ void removeLoginListener(LoginListener loginListener) { logger.debug("Removing login listener '{}'", loginListener); loginListeners.remove(loginListener); } }