///////////////////////////////////////////////////////////////////////////// // // Project ProjectForge Community Edition // www.projectforge.org // // Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de) // // ProjectForge is dual-licensed. // // This community edition 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; version 3 of the License. // // This community edition 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 this program; if not, see http://www.gnu.org/licenses/. // ///////////////////////////////////////////////////////////////////////////// package org.projectforge.ldap; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.naming.NameNotFoundException; import org.apache.commons.lang.StringUtils; import org.projectforge.registry.Registry; import org.projectforge.user.GroupDO; import org.projectforge.user.LoginDefaultHandler; import org.projectforge.user.LoginResult; import org.projectforge.user.LoginResultStatus; import org.projectforge.user.PFUserDO; import arlut.csd.crypto.SmbEncrypt; /** * TODO: nested groups.<br/> * This LDAP login handler has read-write access to the LDAP server and acts as master of the user and group data. All changes of * ProjectForge's users and groups will be written through. Any change of the LDAP server will be ignored and may be overwritten by * ProjectForge. <br/> * Use this login handler if you want to configure your LDAP users and LDAP groups via ProjectForge.<br/> * <h1>Passwords</h1> After each successful login-in at ProjectForge (via LoginForm) ProjectForges tries to authenticate the user with the * given username/password credentials at LDAP. If the LDAP authentication fails ProjectForge changes the password with the actual password * of the user (given in the LoginForm). <h1>Deactivated users</h1> Deactivated users will be moved to an sub userbase called "deactivated". * The e-mail will be invalidated and the password will be deleted. Deleted and deactivated users are removed from any LDAP group. After * reactivating the user, the password has to be reset if the user logins the next time via LoginForm. <h1>Deleted Users</h1> Deleted users * will not be synchronized and removed in LDAP if exist. <h1>Stay-logged-in</h1> The stay-logged-in mechanism will be ignored if the LDAP * password of the user isn't set (is null). Any existing LDAP password doesn't interrupt the normal stay-logged-in mechanism. <h1>New users * </h1> New users (created with ProjectForge's UserEditPage) will be created first without password in the LDAP system directly. Such users * need to log-in first at ProjectForge, otherwise their LDAP passwords aren't set (no log-in at any other system connecting to the LDAP is * possible until the first log-in at ProjectForge). * @author Kai Reinhard (k.reinhard@micromata.de) * */ public class LdapMasterLoginHandler extends LdapLoginHandler { private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LdapMasterLoginHandler.class); /** * For users of this list, the stay-logged-in mechanism interrupts, the user has to re-login via LoginForm to update the correct password * in the LDAP system. */ private Set<Integer> usersWithoutLdapPasswords = new HashSet<Integer>(); // Caches all Samba NT password of the LDAP users by user id. private Map<Integer, String> sambaNTPasswords = new HashMap<Integer, String>(); private boolean refreshInProgress; /** * @see org.projectforge.ldap.LdapLoginHandler#initialize() */ @Override public void initialize() { super.initialize(); ldapOrganizationalUnitDao.createIfNotExist(userBase, "ProjectForge's user base."); ldapOrganizationalUnitDao.createIfNotExist(LdapUserDao.DEACTIVATED_SUB_CONTEXT, "ProjectForge's user base for deactivated users.", userBase); ldapOrganizationalUnitDao.createIfNotExist(LdapUserDao.RESTRICTED_USER_SUB_CONTEXT, "ProjectForge's user base for restricted users.", userBase); ldapOrganizationalUnitDao.createIfNotExist(groupBase, "ProjectForge's group base."); } /** * @see org.projectforge.user.LoginHandler#checkLogin(java.lang.String, java.lang.String, boolean) */ @Override public LoginResult checkLogin(final String username, final String password) { final LoginResult loginResult = loginDefaultHandler.checkLogin(username, password); if (loginResult.getLoginResultStatus() != LoginResultStatus.SUCCESS) { return loginResult; } try { // User is now logged-in successfully. final LdapUser authLdapUser = ldapUserDao.authenticate(username, password, userBase); final PFUserDO user = loginResult.getUser(); final LdapUser ldapUser = PFUserDOConverter.convert(user); ldapUser.setOrganizationalUnit(userBase); if (authLdapUser == null) { log.info("User's credentials in LDAP not up-to-date: " + username + ". Updating LDAP entry..."); ldapUserDao.createOrUpdate(userBase, ldapUser); ldapUserDao.changePassword(ldapUser, null, password); } else { final String sambaNTPassword = sambaNTPasswords.get(loginResult.getUser().getId()); if (sambaNTPassword != null) { if ("".equals(sambaNTPassword) == true) { // sambaNTPassword needed to be set (isn't yet set): ldapUserDao.changePassword(ldapUser, null, password); } else { if (sambaNTPassword.equals(SmbEncrypt.NTUNICODEHash(password)) == false) { // sambaNTPassword needed to be updated: ldapUserDao.changePassword(ldapUser, null, password); } } } } } catch (final Exception ex) { log.error("An exception occured while checking login against LDAP system (ignoring this error): " + ex.getMessage(), ex); } return loginResult; } /** * @see org.projectforge.user.LoginHandler#getAllGroups() */ @Override public List<GroupDO> getAllGroups() { final List<GroupDO> groups = loginDefaultHandler.getAllGroups(); return groups; } /** * @see org.projectforge.user.LoginHandler#getAllUsers() */ @Override public List<PFUserDO> getAllUsers() { final List<PFUserDO> users = loginDefaultHandler.getAllUsers(); return users; } /** * Refreshes the LDAP. * @see org.projectforge.user.LoginHandler#afterUserGroupCacheRefresh(java.util.List, java.util.List) */ @Override public void afterUserGroupCacheRefresh(final Collection<PFUserDO> users, final Collection<GroupDO> groups) { new Thread() { @Override public void run() { synchronized (LdapMasterLoginHandler.this) { try { refreshInProgress = true; updateLdap(users, groups); } finally { refreshInProgress = false; } } } }.start(); } /** * @return true if currently a cache refresh is running, otherwise false. */ public boolean isRefreshInProgress() { return refreshInProgress; } private void updateLdap(final Collection<PFUserDO> users, final Collection<GroupDO> groups) { new LdapTemplate(ldapConnector) { @Override protected Object call() throws NameNotFoundException, Exception { log.info("Updating LDAP..."); // First, get set of all ldap entries: final List<LdapUser> ldapUsers = getAllLdapUsers(ctx); final List<LdapUser> updatedLdapUsers = new ArrayList<LdapUser>(); int error = 0, unmodified = 0, created = 0, updated = 0, deleted = 0, renamed = 0; final Set<Integer> shadowUsersWithoutLdapPasswords = new HashSet<Integer>(); final Map<Integer, String> shadowSambaNTPasswords = new HashMap<Integer, String>(); final boolean sambaConfigured = ldapConfig.getSambaAccountsConfig() != null; for (final PFUserDO user : users) { final LdapUser updatedLdapUser = PFUserDOConverter.convert(user); try { final LdapUser ldapUser = getLdapUser(ldapUsers, user); if (ldapUser == null) { updatedLdapUser.setOrganizationalUnit(userBase); if (user.isDeleted() == false && user.isLocalUser() == false) { // Do not add deleted or local users. // TODO: if (ldapConfig.isSupportPosixAccounts() == true &&) { // updatedLdapUser.addObjectClass(LdapUserDao.OBJECT_CLASS_POSIX_ACCOUNT); // } ldapUserDao.create(ctx, userBase, updatedLdapUser); shadowUsersWithoutLdapPasswords.add(user.getId()); // User can't be valid for created users. created++; } } else { // Need to set organizational unit for detecting the change of deactivated flag. The updateLdapUser needs the organizational // unit of the original ldap object: updatedLdapUser.setOrganizationalUnit(ldapUser.getOrganizationalUnit()); // Otherwise the NT password will be deleted in copy function below: updatedLdapUser.setSambaNTPassword(ldapUser.getSambaNTPassword()); if (user.isDeleted() == true || user.isLocalUser() == true) { // Deleted and local users shouldn't be synchronized with LDAP: ldapUserDao.delete(ctx, updatedLdapUser); shadowUsersWithoutLdapPasswords.add(user.getId()); // Paranoia code, stay-logged-in shouldn't work with deleted users. deleted++; } else { final boolean modified = PFUserDOConverter.copyUserFields(updatedLdapUser, ldapUser); if (StringUtils.equals(updatedLdapUser.getUid(), ldapUser.getUid()) == false) { // uid (dn) changed. ldapUserDao.rename(ctx, updatedLdapUser, ldapUser); renamed++; } if (modified == true) { updatedLdapUser.setObjectClasses(ldapUser.getObjectClasses()); ldapUserDao.update(ctx, userBase, updatedLdapUser); updated++; } else { unmodified++; } boolean passwordsGiven = false; if (ldapUser.isPasswordGiven() == true) { // If the user has a Samba SID then the Samba NT password mustn't be blank: if (sambaConfigured == false || ldapUser.getSambaSIDNumber() == null || StringUtils.isNotBlank(ldapUser.getSambaNTPassword()) == true) { passwordsGiven = true; } } if (passwordsGiven == true) { if (updatedLdapUser.isDeactivated()) { log.warn("User password for deactivated user is set: " + ldapUser); ldapUserDao.deactivateUser(ctx, updatedLdapUser); shadowUsersWithoutLdapPasswords.add(user.getId()); // Paranoia code, stay-logged-in shouldn't work with deleted or // deactivated users. } else { shadowUsersWithoutLdapPasswords.remove(user.getId()); // Remove if exists because password is given. } } else { shadowUsersWithoutLdapPasswords.add(user.getId()); // Password isn't given for the current user. if (ldapUser.getSambaSIDNumber() != null) { final String sambaNTPassword = ldapUser.getSambaNTPassword(); if (StringUtils.isNotBlank(sambaNTPassword) == true) { shadowSambaNTPasswords.put(user.getId(), sambaNTPassword); } else { shadowSambaNTPasswords.put(user.getId(), ""); // Empty password } } } } } ldapUserDao.buildDn(userBase, updatedLdapUser); updatedLdapUsers.add(updatedLdapUser); } catch (final Exception ex) { ldapUserDao.buildDn(userBase, updatedLdapUser); updatedLdapUsers.add(updatedLdapUser); log.error("Error while proceeding user '" + user.getUsername() + "'. Continuing with next user.", ex); error++; } } usersWithoutLdapPasswords = shadowUsersWithoutLdapPasswords; sambaNTPasswords = shadowSambaNTPasswords; log.info("" + shadowUsersWithoutLdapPasswords.size() + " users without password in the LDAP system (login required for these users for updating the LDAP password)."); log.info("Update of LDAP users: " + (error > 0 ? "*** " + error + " errors ***, " : "") + unmodified + " unmodified, " + created + " created, " + updated + " updated, " + renamed + " renamed, " + deleted + " deleted."); // Now get all groups: final List<LdapGroup> ldapGroups = getAllLdapGroups(ctx); final Map<Integer, LdapUser> ldapUserMap = getUserMap(updatedLdapUsers); error = unmodified = created = updated = renamed = deleted = 0; for (final GroupDO group : groups) { try { final LdapGroup updatedLdapGroup = GroupDOConverter.convert(group, baseDN, ldapUserMap); final LdapGroup ldapGroup = getLdapGroup(ldapGroups, group); if (ldapGroup == null) { updatedLdapGroup.setOrganizationalUnit(groupBase); if (group.isDeleted() == false && group.isLocalGroup() == false) { // Do not add deleted or local groups. setMembers(updatedLdapGroup, group.getAssignedUsers(), ldapUserMap); ldapGroupDao.create(ctx, groupBase, updatedLdapGroup); created++; } } else { updatedLdapGroup.setOrganizationalUnit(ldapGroup.getOrganizationalUnit()); if (group.isDeleted() == true || group.isLocalGroup() == true) { // Deleted and local users shouldn't be synchronized with LDAP: ldapGroupDao.delete(ctx, updatedLdapGroup); deleted++; } else { final boolean modified = GroupDOConverter.copyGroupFields(updatedLdapGroup, ldapGroup); if (modified == true) { updatedLdapGroup.setObjectClasses(ldapGroup.getObjectClasses()); setMembers(updatedLdapGroup, group.getAssignedUsers(), ldapUserMap); ldapGroupDao.update(ctx, groupBase, updatedLdapGroup); updated++; } else { unmodified++; } if (StringUtils.equals(updatedLdapGroup.getCommonName(), ldapGroup.getCommonName()) == false) { // CommonName (cn) and therefor dn changed. ldapGroupDao.rename(ctx, updatedLdapGroup, ldapGroup); renamed++; } } } } catch (final Exception ex) { log.error("Error while proceeding group '" + group.getName() + "'. Continuing with next group.", ex); error++; } } log.info("Update of LDAP groups: " + (error > 0 ? "*** " + error + " errors ***, " : "") + unmodified + " unmodified, " + created + " created, " + updated + " updated, " + renamed + " renamed, " + deleted + " deleted."); log.info("LDAP update done."); return null; } }.excecute(); } /** * Calls {@link LoginDefaultHandler#checkStayLoggedIn(PFUserDO)}. * @see org.projectforge.user.LoginHandler#checkStayLoggedIn(org.projectforge.user.PFUserDO) */ @Override public boolean checkStayLoggedIn(final PFUserDO user) { final boolean result = loginDefaultHandler.checkStayLoggedIn(user); if (result == true && usersWithoutLdapPasswords.contains(user.getId()) == true) { log.info("User's stay-logged-in mechanism is temporarily disabled until the user re-logins via LoginForm to update his LDAP password (which isn't yet available): " + user.getUserDisplayname()); return false; } return result; } /** * @see org.projectforge.user.LoginHandler#passwordChanged(org.projectforge.user.PFUserDO, java.lang.String) */ @Override public void passwordChanged(final PFUserDO user, final String newPassword) { final LdapUser ldapUser = ldapUserDao.findById(user.getId()); if (user.isDeleted() == true || user.isLocalUser() == true) { // Don't change passwords of such users. return; } if (ldapUser != null) { ldapUserDao.changePassword(ldapUser, null, newPassword); final LdapUser authenticatedUser = ldapUserDao.authenticate(user.getUsername(), newPassword); log.info("Password changed successfully for : " + authenticatedUser); } else { log.error("Can't change LDAP password for user '" + user.getUsername() + "'! Not such user found in LDAP!."); } } /** * @return always true because the change of passwords is supported for every user. * @see org.projectforge.user.LoginHandler#isPasswordChangeSupported(org.projectforge.user.PFUserDO) */ @Override public boolean isPasswordChangeSupported(final PFUserDO user) { return true; } /** * @param updatedLdapGroup * @param assignedUsers * @param ldapUserMap */ private void setMembers(final LdapGroup updatedLdapGroup, final Set<PFUserDO> assignedUsers, final Map<Integer, LdapUser> ldapUserMap) { updatedLdapGroup.clearMembers(); if (assignedUsers == null) { // No user to assign. return; } for (final PFUserDO assignedUser : assignedUsers) { final LdapUser ldapUser = ldapUserMap.get(assignedUser.getId()); if (ldapUser == null) { final PFUserDO cachedUser = Registry.instance().getUserGroupCache().getUser(assignedUser.getId()); if (cachedUser == null || cachedUser.isDeleted() == false) { log.warn("Can't assign ldap user to group: " + updatedLdapGroup.getCommonName() + "! Ldap user with id '" + assignedUser.getId() + "' not found, skipping user."); } } else { if (assignedUser.hasSystemAccess() == true) { // Do not add deleted or deactivated users. updatedLdapGroup.addMember(ldapUser, baseDN); } } } } private Map<Integer, LdapUser> getUserMap(final Collection<LdapUser> users) { final Map<Integer, LdapUser> map = new HashMap<Integer, LdapUser>(); if (users == null) { return map; } for (final LdapUser user : users) { final Integer id = PFUserDOConverter.getId(user); if (id != null) { map.put(id, user); } else { log.warn("Given ldap user has no id (employee number), ignoring user for group assignments: " + user); } } return map; } private LdapUser getLdapUser(final List<LdapUser> ldapUsers, final PFUserDO user) { for (final LdapUser ldapUser : ldapUsers) { if (StringUtils.equals(ldapUser.getUid(), user.getUsername()) == true || StringUtils.equals(ldapUser.getEmployeeNumber(), PFUserDOConverter.buildEmployeeNumber(user)) == true) { return ldapUser; } } return null; } private LdapGroup getLdapGroup(final List<LdapGroup> ldapGroups, final GroupDO group) { for (final LdapGroup ldapGroup : ldapGroups) { if (StringUtils.equals(ldapGroup.getBusinessCategory(), GroupDOConverter.buildBusinessCategory(group)) == true) { return ldapGroup; } } return null; } }