/////////////////////////////////////////////////////////////////////////////
//
// 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.Date;
import java.util.LinkedList;
import java.util.List;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.projectforge.common.NumberHelper;
import org.projectforge.core.ConfigXml;
import arlut.csd.crypto.SmbEncrypt;
/**
* @author Kai Reinhard (k.reinhard@micromata.de)
*/
public class LdapUserDao extends LdapDao<String, LdapUser>
{
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LdapUserDao.class);
public static final String DEACTIVATED_SUB_CONTEXT = "deactivated";
private static final String DEACTIVATED_SUB_CONTEXT2 = "ou=" + DEACTIVATED_SUB_CONTEXT;
private static final String DEACTIVATED_SUB_CONTEXT3 = DEACTIVATED_SUB_CONTEXT2 + ",";
static final String DEACTIVATED_MAIL = "deactivated@localhost";
public static final String RESTRICTED_USER_SUB_CONTEXT = "restricted";
private static final String RESTRICTED_USER_SUB_CONTEXT2 = "ou=" + RESTRICTED_USER_SUB_CONTEXT;
private static final String RESTRICTED_USER_SUB_CONTEXT3 = RESTRICTED_USER_SUB_CONTEXT2 + ",";
private boolean useUidInDn = false;
private LdapPersonDao ldapPersonDao;
static String[] ALL_OBJECT_CLASSES;
static String[] ALL_OBJECT_CLASSES_WITH_POSIX_ACCOUNT;
static String[] ALL_OBJECT_CLASSES_WITH_SAMBA_ACCOUNT;
static String[] ALL_OBJECT_CLASSES_WITH_SAMBA_AND_POSIX_ACCOUNT;
private static String POSIX_OBJECT_CLASS = "posixAccount";
private static String SAMBA_OBJECT_CLASS = "sambaSamAccount";
public static boolean isDeactivated(final LdapUser user)
{
return user.isDeactivated()
|| user.getOrganizationalUnit() != null
&& LdapUtils.getOu(user.getOrganizationalUnit()).contains(DEACTIVATED_SUB_CONTEXT) == true;
}
public static boolean isRestrictedUser(final LdapUser user)
{
return user.isRestrictedUser()
|| user.getOrganizationalUnit() != null
&& LdapUtils.getOu(user.getOrganizationalUnit()).contains(RESTRICTED_USER_SUB_CONTEXT) == true;
}
public static boolean isPosixAccountsConfigured()
{
final LdapConfig ldapConfig = ConfigXml.getInstance().getLdapConfig();
if (ldapConfig == null) {
return false;
}
final LdapPosixAccountsConfig posixAccountsConfig = ldapConfig.getPosixAccountsConfig();
return posixAccountsConfig != null;
}
public static boolean isSambaAccountsConfigured()
{
final LdapConfig ldapConfig = ConfigXml.getInstance().getLdapConfig();
if (ldapConfig == null) {
return false;
}
final LdapSambaAccountsConfig sambaAccountsConfig = ldapConfig.getSambaAccountsConfig();
return sambaAccountsConfig != null && StringUtils.isNotBlank(sambaAccountsConfig.getSambaSIDPrefix()) == true;
}
public LdapUserDao()
{
useUidInDn = true;
}
/**
* @see org.projectforge.ldap.LdapDao#getObjectClass()
*/
@Override
protected String getObjectClass()
{
return ldapPersonDao.getObjectClass();
}
/**
* @see org.projectforge.ldap.LdapDao#getAdditionalObjectClasses()
*/
@Override
protected String[] getAdditionalObjectClasses()
{
throw new UnsupportedOperationException("Call getAdditionalObjectClasses(LdapUser) instead.");
}
void initializeObjectClasses()
{
if (ALL_OBJECT_CLASSES != null) {
// Already initialized.
return;
}
final List<String> additionalObjectClassesList = new LinkedList<String>();
for (final String additionalObjectClass : ldapPersonDao.getAdditionalObjectClasses()) {
additionalObjectClassesList.add(additionalObjectClass);
}
ALL_OBJECT_CLASSES = additionalObjectClassesList.toArray(new String[0]);
additionalObjectClassesList.add(POSIX_OBJECT_CLASS);
ALL_OBJECT_CLASSES_WITH_POSIX_ACCOUNT = additionalObjectClassesList.toArray(new String[0]);
additionalObjectClassesList.add(SAMBA_OBJECT_CLASS);
ALL_OBJECT_CLASSES_WITH_SAMBA_AND_POSIX_ACCOUNT = additionalObjectClassesList.toArray(new String[0]);
additionalObjectClassesList.clear();
for (final String additionalObjectClass : ldapPersonDao.getAdditionalObjectClasses()) {
additionalObjectClassesList.add(additionalObjectClass);
}
additionalObjectClassesList.add(SAMBA_OBJECT_CLASS);
ALL_OBJECT_CLASSES_WITH_SAMBA_ACCOUNT = additionalObjectClassesList.toArray(new String[0]);
}
/**
* @see org.projectforge.ldap.LdapDao#getAdditionalObjectClasses(org.projectforge.ldap.LdapObject)
*/
@Override
protected String[] getAdditionalObjectClasses(final LdapUser obj)
{
final boolean posixAccount = isPosixAccountsConfigured() == true && PFUserDOConverter.isPosixAccountValuesEmpty(obj) == false;
final boolean sambaAccount = isSambaAccountsConfigured() == true && PFUserDOConverter.isSambaAccountValuesEmpty(obj) == false;
if (ALL_OBJECT_CLASSES == null) {
initializeObjectClasses();
}
if (posixAccount == true) {
if (sambaAccount == true) {
return ALL_OBJECT_CLASSES_WITH_SAMBA_AND_POSIX_ACCOUNT;
}
return ALL_OBJECT_CLASSES_WITH_POSIX_ACCOUNT;
}
if (sambaAccount == true) {
return ALL_OBJECT_CLASSES_WITH_SAMBA_ACCOUNT;
}
return ALL_OBJECT_CLASSES;
}
/**
* @see org.projectforge.ldap.LdapDao#getIdAttrId()
*/
@Override
public String getIdAttrId()
{
return "employeeNumber";
}
/**
* @see org.projectforge.ldap.LdapDao#getId(org.projectforge.ldap.LdapUser)
*/
@Override
public String getId(final LdapUser obj)
{
return obj.getEmployeeNumber();
}
/**
* @see org.projectforge.ldap.LdapDao#mapToObject(java.lang.String, javax.naming.directory.Attributes)
*/
@Override
protected LdapUser mapToObject(final String dn, final Attributes attributes) throws NamingException
{
final LdapUser user = new LdapUser();
ldapPersonDao.mapToObject(dn, user, attributes);
final boolean posixAccountsConfigured = isPosixAccountsConfigured();
final boolean sambaAccountsConfigured = isSambaAccountsConfigured();
if (posixAccountsConfigured == true || sambaAccountsConfigured == true) {
final String no = LdapUtils.getAttributeStringValue(attributes, "uidNumber");
user.setUidNumber(NumberHelper.parseInteger(no));
}
if (posixAccountsConfigured == true) {
final String no = LdapUtils.getAttributeStringValue(attributes, "gidNumber");
user.setGidNumber(NumberHelper.parseInteger(no));
user.setHomeDirectory(LdapUtils.getAttributeStringValue(attributes, "homeDirectory"));
user.setLoginShell(LdapUtils.getAttributeStringValue(attributes, "loginShell"));
}
if (sambaAccountsConfigured == true) {
final String sambaSID = LdapUtils.getAttributeStringValue(attributes, "sambaSID");
final Integer sambaSIDNumber = ldapConfig.getSambaAccountsConfig().getSambaSIDNumber(sambaSID);
user.setSambaSIDNumber(sambaSIDNumber);
final String sambaPrimaryGroupSID = LdapUtils.getAttributeStringValue(attributes, "sambaPrimaryGroupSID");
final Integer sambaPrimaryGroupSIDNumber = ldapConfig.getSambaAccountsConfig().getSambaSIDNumber(sambaPrimaryGroupSID);
user.setSambaPrimaryGroupSIDNumber(sambaPrimaryGroupSIDNumber);
user.setSambaNTPassword(LdapUtils.getAttributeStringValue(attributes, "sambaNTPassword"));
final String sambaPwdLastSet = LdapUtils.getAttributeStringValue(attributes, "sambaPwdLastSet");
if (sambaPwdLastSet != null) {
final long value = NumberHelper.parseLong(sambaPwdLastSet) * 1000; // ms since 1970
user.setSambaPwdLastSet(new Date(value));
}
}
if (dn != null) {
if (dn.contains(DEACTIVATED_SUB_CONTEXT2) == true) {
user.setDeactivated(true);
}
if (dn.contains(RESTRICTED_USER_SUB_CONTEXT2) == true) {
user.setRestrictedUser(true);
}
}
final Object userPassword = LdapUtils.getAttributeValue(attributes, "userPassword");
if (userPassword != null) {
user.setPasswordGiven(true);
}
return user;
}
public void deactivateUser(final LdapUser user)
{
new LdapTemplate(ldapConnector) {
@Override
protected Object call() throws NameNotFoundException, Exception
{
deactivateUser(ctx, user);
return null;
}
}.excecute();
}
public void deactivateUser(final DirContext ctx, final LdapUser user) throws NamingException
{
log.info("Deactivate user: " + buildDn(null, user));
final List<ModificationItem> modificationItems = new ArrayList<ModificationItem>();
modificationItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("userPassword", null)));
modificationItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("mail", DEACTIVATED_MAIL)));
buildDn(null, user);
modify(ctx, user, modificationItems);
final String ou = user.getOrganizationalUnit();
if (ou.startsWith(DEACTIVATED_SUB_CONTEXT2) == false) {
// Move user to the sub-context "deactivated".
final String newOu = LdapUtils.getOu(DEACTIVATED_SUB_CONTEXT, getOuBase());
move(ctx, user, newOu);
user.setOrganizationalUnit(newOu);
}
}
@Override
public String getOuBase()
{
return ldapConfig.getUserBase();
}
/**
* Moves the user only from the "deactivated" sub-context to the parent context. If the user isn't in the context name "deactivated"
* nothing will be done.
* @param user
*/
public void reactivateUser(final LdapUser user)
{
new LdapTemplate(ldapConnector) {
@Override
protected Object call() throws NameNotFoundException, Exception
{
reactivateUser(ctx, user);
return null;
}
}.excecute();
}
public void reactivateUser(final DirContext ctx, final LdapUser user) throws NamingException
{
log.info("Reactivate deactivated user: " + buildDn(null, user));
final String ou = LdapUtils.getOu(user.getOrganizationalUnit());
if (ou.startsWith(DEACTIVATED_SUB_CONTEXT2) == false) {
log.info("Object isn't in a deactivated sub-context, nothing will be done: " + buildDn(null, user));
return;
}
String newPath;
if (ou.startsWith(DEACTIVATED_SUB_CONTEXT3) == true) {
newPath = ou.substring(DEACTIVATED_SUB_CONTEXT3.length());
} else {
newPath = ou.substring(DEACTIVATED_SUB_CONTEXT2.length());
}
move(ctx, user, newPath);
user.setOrganizationalUnit(newPath);
}
void updateActivatedStatus(final DirContext ctx, final LdapUser user) throws NamingException
{
final String ou = LdapUtils.getOu(user.getOrganizationalUnit());
if (user.isDeactivated() == true) {
if (ou.startsWith(DEACTIVATED_SUB_CONTEXT2) == true) {
// User is already stored in deactivated context. Nothing to be done.
return;
} else {
deactivateUser(ctx, user);
}
} else {
if (ou.startsWith(DEACTIVATED_SUB_CONTEXT2) == false) {
// User isn't stored in deactivated context. Nothing to be done.
return;
} else {
reactivateUser(ctx, user);
}
}
}
void updateRestrictedUserStatus(final DirContext ctx, final LdapUser user) throws NamingException
{
final String ou = LdapUtils.getOu(user.getOrganizationalUnit());
if (user.isDeactivated() == true) {
// User is deactivated, thus the restricted-user-status is ignored.
return;
}
if (user.isRestrictedUser() == true) {
if (ou.startsWith(RESTRICTED_USER_SUB_CONTEXT2) == true) {
// User is already stored in restricted context. Nothing to be done.
return;
} else {
setUserAsRestrictedUser(ctx, user);
}
} else {
if (ou.startsWith(RESTRICTED_USER_SUB_CONTEXT2) == false) {
// User isn't stored in restricted context. Nothing to be done.
return;
} else {
log.info("Move user from restricted sub context: " + buildDn(null, user));
String newPath;
if (ou.startsWith(RESTRICTED_USER_SUB_CONTEXT3) == true) {
newPath = ou.substring(RESTRICTED_USER_SUB_CONTEXT3.length());
} else {
newPath = ou.substring(RESTRICTED_USER_SUB_CONTEXT2.length());
}
move(ctx, user, newPath);
user.setOrganizationalUnit(newPath);
}
}
}
private void setUserAsRestrictedUser(final DirContext ctx, final LdapUser user) throws NamingException
{
log.info("Move user to restricted sub context: " + buildDn(null, user));
if (user.isDeactivated() == true) {
log.info("User is deactivated, thus the restricted-user-status is ignored: " + buildDn(null, user));
return;
}
final String ou = user.getOrganizationalUnit();
if (ou.startsWith(RESTRICTED_USER_SUB_CONTEXT2) == false) {
// Move user to the sub-context "restricted".
final String newOu = LdapUtils.getOu(RESTRICTED_USER_SUB_CONTEXT, user.getOrganizationalUnit());
move(ctx, user, newOu);
user.setOrganizationalUnit(newOu);
}
}
/**
* Calls super method and {@link #deactivateUser(DirContext, LdapUser)} if the given user is deactivated. If the given user is deleted,
* nothing will be done.
* @see org.projectforge.ldap.LdapDao#create(javax.naming.directory.DirContext, org.projectforge.ldap.LdapObject, java.lang.Object[])
*/
@Override
public void create(final DirContext ctx, final String ouBase, final LdapUser user, final Object... args) throws NamingException
{
if (user.isDeleted() == true) {
log.info("Given LDAP user is deleted, so the user will not be created in the LDAP system (nothing will be done).");
return;
}
super.create(ctx, ouBase, user, args);
if (user.isDeactivated() == true) {
deactivateUser(ctx, user);
} else if (user.isRestrictedUser() == true) {
// Deactivated users shouldn't be moved to restricted ou sub context.
setUserAsRestrictedUser(ctx, user);
}
}
/**
* @see org.projectforge.ldap.LdapDao#update(javax.naming.directory.DirContext, org.projectforge.ldap.LdapObject, java.lang.Object[])
*/
@Override
public void update(final DirContext ctx, final String ouBase, final LdapUser user, final Object... objs) throws NamingException
{
if (user.isDeleted() == true) {
log.info("Given LDAP user is deleted, so the user will be removed from the LDAP system.");
delete(ctx, user);
return;
}
super.update(ctx, ouBase, user, objs);
updateActivatedStatus(ctx, user);
updateRestrictedUserStatus(ctx, user);
}
public void changePassword(final LdapUser user, final String oldPassword, final String newPassword)
{
log.info("Change password for " + getObjectClass() + ": " + buildDn(null, user));
final List<ModificationItem> modificationItems = new ArrayList<ModificationItem>();
if (oldPassword != null) {
modificationItems.add(new ModificationItem(DirContext.REMOVE_ATTRIBUTE, new BasicAttribute("userPassword", oldPassword)));
modificationItems.add(new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute("userPassword", newPassword)));
} else {
modificationItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("userPassword", newPassword)));
}
if (isSambaAccountsConfigured() == true && user.getSambaSIDNumber() != null) {
final String sambaNTPassword = SmbEncrypt.NTUNICODEHash(newPassword);
modificationItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("sambaNTPassword", sambaNTPassword)));
}
// Perform the update
modify(user, modificationItems);
}
public LdapUser findByUsername(final Object username, final String... organizationalUnits)
{
return (LdapUser) new LdapTemplate(ldapConnector) {
@Override
protected Object call() throws NameNotFoundException, Exception
{
NamingEnumeration< ? > results = null;
final SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
final String searchBase = getSearchBase(organizationalUnits);
results = ctx.search(searchBase, "(&(objectClass=" + getObjectClass() + ")(uid=" + username + "))", controls);
if (results.hasMore() == false) {
return null;
}
final SearchResult searchResult = (SearchResult) results.next();
final String dn = searchResult.getName();
final Attributes attributes = searchResult.getAttributes();
if (results.hasMore() == true) {
log.error("Oups, found entries with multiple id's: " + getObjectClass() + "." + username);
}
return mapToObject(dn, searchBase, attributes);
}
}.excecute();
}
public LdapUser authenticate(final String username, final String userPassword, final String... organizationalUnits)
{
String dn;
LdapUser user = null;
final String searchBase = getSearchBase(organizationalUnits);
if (StringUtils.isNotBlank(ldapConfig.getManagerUser()) == true && StringUtils.isNotBlank(ldapConfig.getManagerPassword()) == true) {
user = findByUsername(username, searchBase);
if (user == null || StringUtils.equals(username, user.getId()) == false) {
log.info("User with id '" + username + "' not found.");
return null;
}
dn = user.getDn() + "," + ldapConnector.getBase();
} else {
dn = "uid=" + username + "," + searchBase + "," + ldapConnector.getBase();
}
try {
ldapConnector.createContext(dn, userPassword);
log.info("User '" + username + "' (" + dn + ") successfully authenticated.");
return user;
} catch (final Exception ex) {
log.error("User '" + username + "' (" + dn + ") with invalid credentials.");
return null;
}
}
/**
* @see org.projectforge.ldap.LdapDao#createAndAddModificationItems(java.util.List, java.lang.String, java.lang.String[])
*/
@Override
protected void createAndAddModificationItems(final List<ModificationItem> list, final String attrId, final String... attrValues)
{
if ("uid".equals(attrId) == true) {
// Don't change uid because it's part of the dn.
return;
}
super.createAndAddModificationItems(list, attrId, attrValues);
}
/**
* @see org.projectforge.ldap.LdapPDao#getModificationItems(java.util.List, org.projectforge.ldap.LdapUser)
*/
@Override
protected List<ModificationItem> getModificationItems(List<ModificationItem> list, final LdapUser user)
{
list = ldapPersonDao.getModificationItems(list, user);
createAndAddModificationItems(list, "cn", user.getCommonName());
final boolean modifyPosixAccount = isPosixAccountsConfigured() == true && PFUserDOConverter.isPosixAccountValuesEmpty(user) == false;
final boolean modifySambaAccount = isSambaAccountsConfigured() == true && PFUserDOConverter.isSambaAccountValuesEmpty(user) == false;
if (modifyPosixAccount == true || modifySambaAccount == true) {
if (user.getObjectClasses() != null) {
final List<String> missedObjectClasses = LdapUtils.getMissedObjectClasses(getAdditionalObjectClasses(user), getObjectClass(),
user.getObjectClasses());
if (CollectionUtils.isNotEmpty(missedObjectClasses) == true) {
for (final String missedObjectClass : missedObjectClasses) {
list.add(createModificationItem(DirContext.ADD_ATTRIBUTE, "objectClass", missedObjectClass));
}
}
}
}
if (modifyPosixAccount == true) {
createAndAddModificationItems(list, "uidNumber", String.valueOf(user.getUidNumber()));
createAndAddModificationItems(list, "gidNumber", String.valueOf(user.getGidNumber()));
createAndAddModificationItems(list, "homeDirectory", user.getHomeDirectory());
createAndAddModificationItems(list, "loginShell", user.getLoginShell());
}
if (modifySambaAccount == true) {
createAndAddModificationItems(list, "sambaSID", ldapConfig.getSambaAccountsConfig().getSambaSID(user.getSambaSIDNumber()));
createAndAddModificationItems(list, "sambaPrimaryGroupSID",
ldapConfig.getSambaAccountsConfig().getSambaPrimaryGroupSID(user.getSambaPrimaryGroupSIDNumber()));
createAndAddModificationItems(list, "sambaAcctFlags", "U ");
createAndAddModificationItems(list, "sambaPasswordHistory", "0000000000000000000000000000000000000000000000000000000000000000");
createAndAddModificationItems(list, "sambaPwdLastSet", String.valueOf(user.getSambaPwdLastSetAsUnixEpochSeconds()));
}
return list;
}
/**
* @see org.projectforge.ldap.LdapDao#buildDnIdentifier(org.projectforge.ldap.LdapObject)
*/
@Override
protected String buildDnIdentifier(final LdapUser obj)
{
if (useUidInDn == true) {
return "uid=" + obj.getUid();
} else {
return "cn=" + LdapUtils.escapeCommonName(obj.getCommonName());
}
}
/**
* @see org.projectforge.ldap.LdapDao#buildId(java.lang.Object)
*/
@Override
protected String buildId(final Object id)
{
if (id == null) {
return null;
}
if (id instanceof String && ((String) id).startsWith(PFUserDOConverter.ID_PREFIX) == true) {
return String.valueOf(id);
}
return PFUserDOConverter.ID_PREFIX + id;
}
/**
* @param ldapPersonDao the ldapPersonDao to set
*/
public void setLdapPersonDao(final LdapPersonDao ldapPersonDao)
{
this.ldapPersonDao = ldapPersonDao;
}
}