/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2010-2011 The OpenNMS Group, Inc.
* OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
* OpenNMS(R) 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.
*
* OpenNMS(R) 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 OpenNMS(R). If not, see:
* http://www.gnu.org/licenses/
*
* For more information contact:
* OpenNMS(R) Licensing <license@opennms.org>
* http://www.opennms.org/
* http://www.opennms.com/
*******************************************************************************/
package org.opennms.web.springframework.security;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.opennms.core.utils.BundleLists;
import org.opennms.core.utils.ThreadCategory;
import org.opennms.netmgt.config.GroupFactory;
import org.opennms.netmgt.config.GroupManager;
import org.opennms.netmgt.config.UserFactory;
import org.opennms.netmgt.config.UserManager;
import org.opennms.netmgt.config.groups.Role;
import org.opennms.netmgt.model.OnmsUser;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.util.Assert;
/**
* Implements the interface to allow Tomcat to check our users.xml file
* to authenticate users.
* <p/>
* <p>This class is Tomcat-specific and will not be portable to other
* servlet containers. It relies on packages supplied with Tomcat.</p>
*
* @author <A HREF="mailto:larry@opennms.org">Lawrence Karnowski</A>
* @author <A HREF="mailto:eric@tuxbot.com">Eric Molitor</A>
*/
public class SpringSecurityUserDaoImpl implements SpringSecurityUserDao, InitializingBean {
private UserManager m_userManager;
private GroupManager m_groupManager;
private String m_usersConfigurationFile;
private String m_groupsConfigurationFile;
/**
* The set of valid users from users.xml, keyed by userId
*/
private Map<String, OnmsUser> m_users = null;
private long m_usersLastModified;
private String m_magicUsersConfigurationFile;
/**
* The set of valid users from magic-users.properties, keyed by userId
*/
private Map<String, OnmsUser> m_magicUsers = null;
private Map<String, Collection<? extends GrantedAuthority>> m_roles = null;
private long m_magicUsersLastModified;
private long m_groupsLastModified;
private boolean m_useGroups;
/**
* <p>Constructor for SpringSecurityUserDaoImpl.</p>
*/
public SpringSecurityUserDaoImpl() {
}
/**
* Convenience method for parsing the users.xml file.
* <p/>
* <p>This method is synchronized so only one thread at a time
* can parse the users.xml file and create the <code>principal</code>
* instance variable.</p>
*/
private void parseUsers() throws DataRetrievalFailureException {
final HashMap<String, OnmsUser> users = new HashMap<String, OnmsUser>();
try {
for (final OnmsUser user : m_userManager.getOnmsUserList()) {
users.put(user.getUsername(), user);
}
} catch (final Throwable t) {
throw new DataRetrievalFailureException("Unable to get user list.", t);
}
log().debug("Loaded the users.xml file with " + users.size() + " users");
m_usersLastModified = m_userManager.getLastModified();
m_users = users;
}
/**
* Parses the groups.xml file into mapping roles to users of that role
* through group membership.
*/
private Map<String, LinkedList<String>> parseGroupRoles() throws DataRetrievalFailureException {
long lastModified = new File(m_groupsConfigurationFile).lastModified();
final Map<String, LinkedList<String>> roleMap = new HashMap<String, LinkedList<String>>();
final Collection<Role> roles = m_groupManager.getRoles();
for (final Role role : roles) {
final String groupname = role.getMembershipGroup();
final String securityRole = Authentication.getSpringSecurityRoleFromOldRoleName(role.getName());
if (securityRole != null) {
final List<String> users;
try {
users = m_groupManager.getGroup(groupname).getUserCollection();
} catch (Throwable e) {
throw new DataRetrievalFailureException("Error reading groups configuration file '" + m_groupsConfigurationFile + "': " + e.getMessage(), e);
}
for (final String user : users) {
if (roleMap.get(user) == null) {
roleMap.put(user, new LinkedList<String>());
}
final LinkedList<String> userRoleList = roleMap.get(user);
userRoleList.add(securityRole);
}
}
}
log().debug("Loaded roles from groups.xml file for " + roleMap.size() + " users");
m_groupsLastModified = lastModified;
return roleMap;
}
/**
* Parses the magic-users.properties file into two mappings: from magic
* username to password, and from magic role to authorized users of that
* role.
*/
private void parseMagicUsers() throws DataRetrievalFailureException {
HashMap<String, OnmsUser> magicUsers = new HashMap<String, OnmsUser>();
Map<String, Collection<? extends GrantedAuthority>> roles = new HashMap<String, Collection<? extends GrantedAuthority>>();
long lastModified = new File(m_magicUsersConfigurationFile).lastModified();
// read the file
Properties properties = new Properties();
try {
properties.load(new FileInputStream(m_magicUsersConfigurationFile));
} catch (FileNotFoundException e) {
throw new DataRetrievalFailureException("Magic users configuration file '" + m_magicUsersConfigurationFile + "' not found: " + e.getMessage(), e);
} catch (IOException e) {
throw new DataRetrievalFailureException("Error reading magic users configuration file '" + m_magicUsersConfigurationFile + "': " + e.getMessage(), e);
}
// look up users and their passwords
String[] configuredUsers = BundleLists.parseBundleList(properties.getProperty("users"));
for (String user : configuredUsers) {
String username = properties.getProperty("user." + user + ".username");
String password = properties.getProperty("user." + user + ".password");
OnmsUser newUser = null;
try {
newUser = m_userManager.getOnmsUser(user);
} catch (final Exception ioe) {
throw new DataRetrievalFailureException("Unable to read user " + user + " from users.xml", ioe);
}
if (newUser == null) {
newUser = new OnmsUser();
newUser.setUsername(username);
newUser.setPassword(m_userManager.encryptedPassword(password, true));
newUser.setPasswordSalted(true);
}
magicUsers.put(username, newUser);
}
String[] configuredRoles = BundleLists.parseBundleList(properties.getProperty("roles"));
// Use roles from the groups.xml file if specified in applicationContext-spring-security.xml
Map<String, LinkedList<String>> roleMap = m_useGroups ? parseGroupRoles()
: new HashMap<String, LinkedList<String>>();
Map<String, Boolean> roleAddDefaultMap = new HashMap<String, Boolean>();
for (String role : configuredRoles) {
String rolename = properties.getProperty("role." + role + ".name");
if (rolename == null) {
throw new DataRetrievalFailureException("Role configuration for '" + role + "' does not have 'name' parameter. Expecting a 'role." + role + ".name' property");
}
String userList = properties.getProperty("role." + role + ".users");
if (userList == null) {
throw new DataRetrievalFailureException("Role configuration for '" + role + "' does not have 'users' parameter. Expecting a 'role." + role + ".users' property");
}
String[] authUsers = BundleLists.parseBundleList(userList);
boolean notInDefaultGroup = "true".equals(properties.getProperty("role." + role + ".notInDefaultGroup"));
String securityRole = Authentication.getSpringSecurityRoleFromOldRoleName(rolename);
if (securityRole == null) {
throw new DataRetrievalFailureException("Could not find Spring Security role mapping for old role name '" + rolename + "' for role '" + role + "'");
}
for (String authUser : authUsers) {
if (roleMap.get(authUser) == null) {
roleMap.put(authUser, new LinkedList<String>());
}
LinkedList<String> userRoleList = roleMap.get(authUser);
userRoleList.add(securityRole);
}
roleAddDefaultMap.put(securityRole, !notInDefaultGroup);
}
for (String user : roleMap.keySet()) {
roles.put(user, getAuthorityListFromRoleList(roleMap.get(user), roleAddDefaultMap));
}
log().debug("Loaded the magic-users.properties file with " + magicUsers.size() + " magic users, " + configuredRoles.length + " roles, and " + roles.size() + " user roles");
m_magicUsersLastModified = lastModified;
m_magicUsers = magicUsers;
m_roles = roles;
}
private Collection<? extends GrantedAuthority> getAuthorityListFromRoleList(LinkedList<String> roleList, Map<String, Boolean> roleAddDefaultMap) {
boolean addToDefaultGroup = false;
for (String role : roleList) {
if (Boolean.TRUE.equals(roleAddDefaultMap.get(role))) {
addToDefaultGroup = true;
break;
}
}
List<GrantedAuthority> authorities = new LinkedList<GrantedAuthority>();
if (addToDefaultGroup) {
authorities.add(ROLE_USER);
}
for (String role : roleList) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
/**
* <p>getAuthoritiesByUsername</p>
*
* @param user a {@link java.lang.String} object.
* @return an array of {@link org.springframework.security.GrantedAuthority} objects.
*/
protected Collection<? extends GrantedAuthority> getAuthoritiesByUsername(String user) {
if (m_roles.containsKey(user)) {
return m_roles.get(user);
} else {
return Arrays.asList(new GrantedAuthority[] { ROLE_USER });
}
}
/**
* Checks the last modified time of the user file against
* the last known last modified time. If the times are different, then the
* file must be reparsed.
*
* <p>
* Note that the <code>lastModified</code> variables are not set here.
* This is in case there is a problem parsing either file. If we set the
* value here, and then try to parse and fail, then we will not try to parse
* again until the file changes again. Instead, when we see the file
* changes, we continue parsing attempts until the parsing succeeds.
* </p>
*/
private boolean isUsersParseNecessary() {
if (m_users == null) {
return true;
}
if (m_usersLastModified != new File(m_usersConfigurationFile).lastModified()) {
return true;
}
return false;
}
/**
* Checks the last modified time of the group file against
* the last known last modified time. If the times are different, then the
* file must be reparsed.
*
* <p>
* Note that the <code>lastModified</code> variables are not set here.
* This is in case there is a problem parsing either file. If we set the
* value here, and then try to parse and fail, then we will not try to parse
* again until the file changes again. Instead, when we see the file
* changes, we continue parsing attempts until the parsing succeeds.
* </p>
*/
private boolean isGroupsParseNecessary() {
if (m_groupsLastModified != new File(m_groupsConfigurationFile).lastModified()) {
return true;
}
return false;
}
/**
* Checks the last modified time of the magic-users file against
* the last known last modified time. If the times are different, then the
* file must be reparsed.
*
* <p>
* Note that the <code>lastModified</code> variables are not set here.
* This is in case there is a problem parsing either file. If we set the
* value here, and then try to parse and fail, then we will not try to parse
* again until the file changes again. Instead, when we see the file
* changes, we continue parsing attempts until the parsing succeeds.
* </p>
*/
private boolean isMagicUsersParseNecessary() {
if (m_magicUsers == null) {
return true;
}
if (m_magicUsersLastModified != new File(m_magicUsersConfigurationFile).lastModified()) {
return true;
}
return false;
}
/**
* <p>setUsersConfigurationFile</p>
*
* @param usersConfigurationFile a {@link java.lang.String} object.
*/
public void setUsersConfigurationFile(String usersConfigurationFile) {
m_usersConfigurationFile = usersConfigurationFile;
UserFactory.setInstance(null);
}
/**
* <p>setGroupsConfigurationFile</p>
*
* @param groupsConfigurationFile a {@link java.lang.String} object.
*/
public void setGroupsConfigurationFile(String groupsConfigurationFile) {
m_groupsConfigurationFile = groupsConfigurationFile;
GroupFactory.setInstance(null);
}
/**
* <p>setUseGroups</p>
*
* @param useGroups a boolean.
*/
public void setUseGroups(boolean useGroups){
m_useGroups = useGroups;
}
/**
* <p>getUsersConfigurationFile</p>
*
* @return a {@link java.lang.String} object.
*/
public String getUsersConfigurationFile() {
return m_usersConfigurationFile;
}
/**
* <p>setMagicUsersConfigurationFile</p>
*
* @param magicUsersConfigurationFile a {@link java.lang.String} object.
*/
public void setMagicUsersConfigurationFile(String magicUsersConfigurationFile) {
m_magicUsersConfigurationFile = magicUsersConfigurationFile;
}
/**
* <p>getMagicUsersConfigurationFile</p>
*
* @return a {@link java.lang.String} object.
*/
public String getMagicUsersConfigurationFile() {
return m_magicUsersConfigurationFile;
}
/** {@inheritDoc} */
public OnmsUser getByUsername(String username) {
reloadIfNecessary();
OnmsUser user;
if (m_magicUsers.containsKey(username)) {
user = m_magicUsers.get(username);
} else {
user = m_users.get(username);
}
if (user == null) {
return null;
}
user.setAuthorities(getAuthoritiesByUsername(username));
return user;
}
private void reloadIfNecessary() {
if (isUsersParseNecessary()) {
parseUsers();
}
if (isMagicUsersParseNecessary() || (m_useGroups && isGroupsParseNecessary())) {
parseMagicUsers();
}
}
/**
* Returns the Log4J category for logging web authentication messages.
*/
private final ThreadCategory log() {
return ThreadCategory.getInstance(getClass());
}
/**
* <p>getMagicUsersLastModified</p>
*
* @return a long.
*/
public long getMagicUsersLastModified() {
return m_magicUsersLastModified;
}
/**
* <p>getUsersLastModified</p>
*
* @return a long.
*/
public long getUsersLastModified() {
return m_usersLastModified;
}
/**
* <p>getGroupsLastModified</p>
*
* @return a long.
*/
public long getGroupsLastModified() {
return m_groupsLastModified;
}
/**
* <p>isUseGroups</p>
*
* @return a boolean.
*/
public boolean isUseGroups() {
return m_useGroups;
}
public UserManager getUserManager() {
return m_userManager;
}
public void setUserManager(final UserManager mgr) {
m_userManager = mgr;
}
public GroupManager getGroupManager() {
return m_groupManager;
}
public void setGroupManager(final GroupManager mgr) {
m_groupManager = mgr;
}
/**
* <p>afterPropertiesSet</p>
*/
@Override
public void afterPropertiesSet() {
Assert.state(m_usersConfigurationFile != null, "usersConfigurationFile parameter must be set to the location of the users.xml configuration file");
Assert.state(!m_useGroups || m_groupsConfigurationFile != null, "groupsConfigurationFile parameter must be set to the location of the groups.xml configuration file");
Assert.state(m_magicUsersConfigurationFile != null, "magicUsersConfigurationFile parameter must be set to the location of the magic-users.properties configuration file");
Assert.notNull(m_userManager);
Assert.notNull(m_groupManager);
}
}