Java tutorial
/* * Copyright (C) 2009-2018 by the geOrchestra PSC * * This file is part of geOrchestra. * * geOrchestra 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. * * geOrchestra 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 * geOrchestra. If not, see <http://www.gnu.org/licenses/>. */ package org.georchestra.console.ds; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.georchestra.console.dao.AdminLogDao; import org.georchestra.console.dto.Account; import org.georchestra.console.dto.AccountFactory; import org.georchestra.console.dto.Role; import org.georchestra.console.dto.UserSchema; import org.georchestra.console.model.AdminLogEntry; import org.georchestra.console.model.AdminLogType; import org.georchestra.console.ws.newaccount.UidGenerator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ldap.NameNotFoundException; import org.springframework.ldap.core.AttributesMapper; import org.springframework.ldap.core.ContextMapper; import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.DistinguishedName; import org.springframework.ldap.core.LdapRdn; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.filter.AbstractFilter; import org.springframework.ldap.filter.AndFilter; import org.springframework.ldap.filter.EqualsFilter; import org.springframework.ldap.filter.Filter; import org.springframework.ldap.filter.PresentFilter; import org.springframework.security.authentication.encoding.LdapShaPasswordEncoder; import javax.naming.Name; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.SearchControls; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.SortedSet; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This class is responsible of maintaining the user accounts (CRUD operations). * * @author Mauricio Pazos */ public final class AccountDaoImpl implements AccountDao { private AccountContextMapper attributMapper; private LdapTemplate ldapTemplate; private RoleDao roleDao; private OrgsDao orgDao; private String uniqueNumberField = "employeeNumber"; private LdapRdn userSearchBaseDN; private AtomicInteger uniqueNumberCounter = new AtomicInteger(-1); @Autowired private AdminLogDao logDao; private static final Log LOG = LogFactory.getLog(AccountDaoImpl.class.getName()); private String basePath; private String orgSearchBaseDN; public String getBasePath() { return basePath; } public void setBasePath(String basePath) { this.basePath = basePath; } @Autowired public AccountDaoImpl(LdapTemplate ldapTemplate, RoleDao roleDao, OrgsDao orgDao) { this.ldapTemplate = ldapTemplate; this.roleDao = roleDao; this.orgDao = orgDao; } public void init() { this.attributMapper = new AccountContextMapper(this.getOrgSearchBaseDN() + "," + this.getBasePath()); } public LdapTemplate getLdapTemplate() { return ldapTemplate; } public void setLdapTemplate(LdapTemplate ldapTemplate) { this.ldapTemplate = ldapTemplate; } public RoleDao getRoleDao() { return roleDao; } public void setRoleDao(RoleDao roleDao) { this.roleDao = roleDao; } public void setUniqueNumberField(String uniqueNumberField) { this.uniqueNumberField = uniqueNumberField; } public void setUserSearchBaseDN(String userSearchBaseDN) { this.userSearchBaseDN = new LdapRdn(userSearchBaseDN); } public void setLogDao(AdminLogDao logDao) { this.logDao = logDao; } public void setOrgSearchBaseDN(String orgSearchBaseDN) { this.orgSearchBaseDN = orgSearchBaseDN; } public String getOrgSearchBaseDN() { return orgSearchBaseDN; } /** * @see {@link AccountDao#insert(Account, String, String)} */ @Override public synchronized void insert(final Account account, final String roleID, final String originLogin) throws DataServiceException, DuplicatedUidException, DuplicatedEmailException { assert account != null; checkMandatoryFields(account); // checks unique uid String uid = account.getUid().toLowerCase(); try { findByUID(uid); throw new DuplicatedUidException( "there is a user with this user identifier (uid): " + account.getUid()); } catch (NameNotFoundException e1) { // if no account with the given UID can be found, then the new // account can be added. LOG.debug("User with uid " + uid + " not found, account can be created"); } // checks unique email try { findByEmail(account.getEmail().trim()); throw new DuplicatedEmailException("there is a user with this email: " + account.getEmail()); } catch (NameNotFoundException e1) { // if no other accounts with the same e-mail exists yet, then the // new account can be added. LOG.debug("No account with the mail " + account.getEmail() + ", account can be created."); } // inserts the new user account try { Name dn = buildDn(uid); AndFilter filter = new AndFilter(); filter.and(new EqualsFilter("objectClass", "inetOrgPerson")); filter.and(new EqualsFilter("objectClass", "organizationalPerson")); filter.and(new EqualsFilter("objectClass", "person")); Integer uniqueNumber = findUniqueNumber(filter, uniqueNumberField, this.uniqueNumberCounter, ldapTemplate); DirContextAdapter context = new DirContextAdapter(dn); mapToContext(uniqueNumber, account, context); // Maps the password separately context.setAttributeValue(UserSchema.USER_PASSWORD_KEY, account.getPassword()); this.ldapTemplate.bind(dn, context, null); // Add user to the role this.roleDao.addUser(roleID, account.getUid(), originLogin); // Add user to the organization if (account.getOrg().length() > 0) this.orgDao.addUser(account.getOrg(), account.getUid()); } catch (NameNotFoundException e) { throw new DataServiceException(e); } } static Integer findUniqueNumber(AbstractFilter searchFilter, final String uniqueNumberField, AtomicInteger uniqueNumber, LdapTemplate ldapTemplate) { if (uniqueNumberField == null || uniqueNumberField.trim().isEmpty()) { return null; } if (uniqueNumber.get() < 0) { @SuppressWarnings("unchecked") final List<Integer> uniqueIds = ldapTemplate.search(DistinguishedName.EMPTY_PATH, searchFilter.encode(), new AttributesMapper() { @Override public Object mapFromAttributes(Attributes attributes) throws NamingException { final Attribute attribute = attributes.get(uniqueNumberField); if (attribute == null) { return 0; } final Object number = attribute.get(); if (number != null) { try { return Integer.valueOf(number.toString()); } catch (NumberFormatException e) { return 0; } } return 0; } }); for (Integer uniqueId : uniqueIds) { if (uniqueId != null && uniqueId > uniqueNumber.get()) { uniqueNumber.set(uniqueId); } } if (uniqueNumber.get() < 0) { uniqueNumber.set(0); } uniqueNumber.incrementAndGet(); } boolean isUnique = false; while (!isUnique) { AndFilter filter = new AndFilter(); filter.and(searchFilter); filter.and(new EqualsFilter(uniqueNumberField, uniqueNumber.get())); isUnique = ldapTemplate .search(DistinguishedName.EMPTY_PATH, filter.encode(), new AccountContextMapper("")).isEmpty(); uniqueNumber.incrementAndGet(); } return uniqueNumber.get(); } /** * @see {@link AccountDao#update(Account, String)} */ @Override public synchronized void update(final Account account, String originLogin) throws DataServiceException, DuplicatedEmailException { // checks mandatory fields if (account.getUid().length() == 0) { throw new IllegalArgumentException("uid is required"); } if (account.getSurname().length() == 0) { throw new IllegalArgumentException("surname is required"); } if (account.getCommonName().length() == 0) { throw new IllegalArgumentException("common name is required"); } if (account.getGivenName().length() == 0) { throw new IllegalArgumentException("given name is required"); } // checks unique email try { // if the email is found in other account different that this // account, the new email cannot be used. Account foundAccount = findByEmail(account.getEmail()); if (!foundAccount.getUid().equals(account.getUid())) { throw new DuplicatedEmailException( "There is already an existing user with this email: " + account.getEmail()); } } catch (NameNotFoundException e1) { // if it doesn't exist an account with this e-mail the it can be // part of the updated account. LOG.debug("Updated account with email " + account.getEmail() + " does not exist, update possible."); } // update the entry in the ldap tree Name dn = buildDn(account.getUid()); DirContextOperations context = ldapTemplate.lookupContext(dn); mapToContext(null /* don't update number */, account, context); ldapTemplate.modifyAttributes(context); // Add log entry for this modification if (originLogin != null) { AdminLogEntry log = new AdminLogEntry(originLogin, account.getUid(), AdminLogType.LDAP_ATTRIBUTE_CHANGE, new Date()); this.logDao.save(log); } } /** * @see {@link AccountDao#update(Account, Account, String)} */ @Override public synchronized void update(Account account, Account modified, String originLogin) throws DataServiceException, DuplicatedEmailException, NameNotFoundException { if (!account.getUid().equals(modified.getUid())) { ldapTemplate.rename(buildDn(account.getUid()), buildDn(modified.getUid())); for (Role g : roleDao.findAllForUser(account.getUid())) { roleDao.modifyUser(g.getName(), account.getUid(), modified.getUid()); } } update(modified, originLogin); } /** * Removes the user account and the reference included in the role * * @param uid user to delete from LDAP * @param originLogin login of admin that request deletion * * @see {@link AccountDao#delete(String, String)} */ @Override public synchronized void delete(final String uid, final String originLogin) throws DataServiceException, NameNotFoundException { this.roleDao.deleteUser(uid, originLogin); this.ldapTemplate.unbind(buildDn(uid), true); } /** * @see {@link AccountDao#findAll()} */ @Override public List<Account> findAll() throws DataServiceException { SearchControls sc = new SearchControls(); sc.setReturningAttributes(UserSchema.ATTR_TO_RETRIEVE); sc.setSearchScope(SearchControls.SUBTREE_SCOPE); EqualsFilter filter = new EqualsFilter("objectClass", "person"); return ldapTemplate.search(DistinguishedName.EMPTY_PATH, filter.encode(), sc, attributMapper); } @Override public List<Account> find(final ProtectedUserFilter filterProtected, Filter f) { SearchControls sc = new SearchControls(); sc.setReturningAttributes(UserSchema.ATTR_TO_RETRIEVE); sc.setSearchScope(SearchControls.SUBTREE_SCOPE); AndFilter and = new AndFilter(); and.and(new EqualsFilter("objectClass", "person")); and.and(f); List<Account> l = ldapTemplate.search(DistinguishedName.EMPTY_PATH, and.encode(), sc, attributMapper); return filterProtected.filterUsersList(l); } @Override public List<Account> findFilterBy(final ProtectedUserFilter filterProtected) throws DataServiceException { List<Account> allUsers = findAll(); List<Account> list = filterProtected.filterUsersList(allUsers); return list; } /** * @see {@link AccountDao#findByUID(String)} */ @Override public Account findByUID(final String uid) throws NameNotFoundException { if (uid == null) throw new NameNotFoundException("Cannot find user with uid : " + uid + " in LDAP server"); Account a = (Account) ldapTemplate.lookup(buildDn(uid.toLowerCase()), UserSchema.ATTR_TO_RETRIEVE, attributMapper); if (a == null) throw new NameNotFoundException("Cannot find user with uid : " + uid + " in LDAP server"); else return a; } /** * @see {@link AccountDao#findByEmail(String)} */ @Override public Account findByEmail(final String email) throws DataServiceException, NameNotFoundException { SearchControls sc = new SearchControls(); sc.setReturningAttributes(UserSchema.ATTR_TO_RETRIEVE); sc.setSearchScope(SearchControls.SUBTREE_SCOPE); AndFilter filter = new AndFilter(); filter.and(new EqualsFilter("objectClass", "inetOrgPerson")); filter.and(new EqualsFilter("objectClass", "organizationalPerson")); filter.and(new EqualsFilter("objectClass", "person")); filter.and(new EqualsFilter("mail", email)); List<Account> accountList = ldapTemplate.search(DistinguishedName.EMPTY_PATH, filter.encode(), sc, attributMapper); if (accountList.isEmpty()) { throw new NameNotFoundException("There is no user with this email: " + email); } Account account = accountList.get(0); return account; } public boolean exist(final String uid) throws DataServiceException { try { DistinguishedName dn = buildDn(uid.toLowerCase()); ldapTemplate.lookup(dn); return true; } catch (NameNotFoundException ex) { return false; } } /** * Create an ldap entry for the user * * @param uid * user id * @return */ private DistinguishedName buildDn(String uid) { DistinguishedName dn = new DistinguishedName(); dn.add(userSearchBaseDN); dn.add("uid", uid); return dn; } /** * Checks that mandatory fields are present in the {@link Account} */ private void checkMandatoryFields(Account a) throws IllegalArgumentException { // required by the account entry if (a.getUid().length() <= 0) { throw new IllegalArgumentException("uid is required"); } // required field in Person object if (a.getGivenName().length() <= 0) { throw new IllegalArgumentException("Given name (cn) is required"); } if (a.getSurname().length() <= 0) { throw new IllegalArgumentException("surname name (sn) is required"); } if (a.getEmail().length() <= 0) { throw new IllegalArgumentException("email is required"); } } /** * Maps the following the account object to the following LDAP entry schema: * * @param uniqueNumber * @param account * @param context */ private void mapToContext(Integer uniqueNumber, Account account, DirContextOperations context) { context.setAttributeValues("objectclass", new String[] { "top", "person", "organizationalPerson", "inetOrgPerson", "shadowAccount" }); // person attributes if (uniqueNumber != null) { setAccountField(context, uniqueNumberField, uniqueNumber.toString()); } setAccountField(context, UserSchema.SURNAME_KEY, account.getSurname()); setAccountField(context, UserSchema.COMMON_NAME_KEY, account.getCommonName()); setAccountField(context, UserSchema.DESCRIPTION_KEY, account.getDescription()); setAccountField(context, UserSchema.TELEPHONE_KEY, account.getPhone()); setAccountField(context, UserSchema.MOBILE_KEY, account.getMobile()); // organizationalPerson attributes setAccountField(context, UserSchema.TITLE_KEY, account.getTitle()); setAccountField(context, UserSchema.STREET_KEY, account.getStreet()); setAccountField(context, UserSchema.LOCALITY_KEY, account.getLocality()); setAccountField(context, UserSchema.FACSIMILE_KEY, account.getFacsimile()); setAccountField(context, UserSchema.ROOM_NUMBER_KEY, account.getRoomNumber()); // inetOrgPerson attributes setAccountField(context, UserSchema.GIVEN_NAME_KEY, account.getGivenName()); setAccountField(context, UserSchema.UID_KEY, account.getUid().toLowerCase()); setAccountField(context, UserSchema.MAIL_KEY, account.getEmail()); setAccountField(context, UserSchema.POSTAL_ADDRESS_KEY, account.getPostalAddress()); setAccountField(context, UserSchema.POSTAL_CODE_KEY, account.getPostalCode()); setAccountField(context, UserSchema.REGISTERED_ADDRESS_KEY, account.getRegisteredAddress()); setAccountField(context, UserSchema.POST_OFFICE_BOX_KEY, account.getPostOfficeBox()); setAccountField(context, UserSchema.PHYSICAL_DELIVERY_OFFICE_NAME_KEY, account.getPhysicalDeliveryOfficeName()); setAccountField(context, UserSchema.STATE_OR_PROVINCE_KEY, account.getStateOrProvince()); setAccountField(context, UserSchema.HOME_POSTAL_ADDRESS_KEY, account.getHomePostalAddress()); if (account.getManager() != null) setAccountField(context, UserSchema.MANAGER_KEY, "uid=" + account.getManager() + "," + this.userSearchBaseDN.toString() + "," + this.getBasePath()); else setAccountField(context, UserSchema.MANAGER_KEY, null); // Return shawdow Expire field as yyyy-mm-dd if (account.getShadowExpire() != null) setAccountField(context, UserSchema.SHADOW_EXPIRE_KEY, String.valueOf(account.getShadowExpire().getTime() / 1000)); else setAccountField(context, UserSchema.SHADOW_EXPIRE_KEY, null); setAccountField(context, UserSchema.CONTEXT_KEY, account.getContext()); } private void setAccountField(DirContextOperations context, String fieldName, Object value) { if (!isNullValue(value)) { context.setAttributeValue(fieldName, value); } else { Object[] values = context.getObjectAttributes(fieldName); if (values != null) { if (values.length == 1) { LOG.info("Removing attribue " + fieldName); context.removeAttributeValue(fieldName, values[0]); } else { LOG.error("Multiple values encountered for field " + fieldName + ", expected a single value"); } } } } public static class AccountContextMapper implements ContextMapper { private final String orgBasePath; private final Pattern pattern; public AccountContextMapper(String orgBasePath) { this.orgBasePath = orgBasePath; this.pattern = Pattern.compile("([^=,]+)=([^=,]+)," + this.orgBasePath + "$"); } @Override public Object mapFromContext(Object ctx) { DirContextAdapter context = (DirContextAdapter) ctx; Account account = AccountFactory.createFull(context.getStringAttribute(UserSchema.UID_KEY), context.getStringAttribute(UserSchema.COMMON_NAME_KEY), context.getStringAttribute(UserSchema.SURNAME_KEY), context.getStringAttribute(UserSchema.GIVEN_NAME_KEY), context.getStringAttribute(UserSchema.MAIL_KEY), context.getStringAttribute(UserSchema.TITLE_KEY), context.getStringAttribute(UserSchema.TELEPHONE_KEY), context.getStringAttribute(UserSchema.DESCRIPTION_KEY), context.getStringAttribute(UserSchema.POSTAL_ADDRESS_KEY), context.getStringAttribute(UserSchema.POSTAL_CODE_KEY), context.getStringAttribute(UserSchema.REGISTERED_ADDRESS_KEY), context.getStringAttribute(UserSchema.POST_OFFICE_BOX_KEY), context.getStringAttribute(UserSchema.PHYSICAL_DELIVERY_OFFICE_NAME_KEY), context.getStringAttribute(UserSchema.STREET_KEY), context.getStringAttribute(UserSchema.LOCALITY_KEY), context.getStringAttribute(UserSchema.FACSIMILE_KEY), context.getStringAttribute(UserSchema.HOME_POSTAL_ADDRESS_KEY), context.getStringAttribute(UserSchema.MOBILE_KEY), context.getStringAttribute(UserSchema.ROOM_NUMBER_KEY), context.getStringAttribute(UserSchema.STATE_OR_PROVINCE_KEY), context.getStringAttribute(UserSchema.MANAGER_KEY), context.getStringAttribute(UserSchema.CONTEXT_KEY), null); // Org will filled later String rawShadowExpire = context.getStringAttribute(UserSchema.SHADOW_EXPIRE_KEY); if (rawShadowExpire != null) { Long shadowExpire = Long.parseLong(rawShadowExpire); shadowExpire *= 1000; // Convert to milliseconds account.setShadowExpire(new Date(shadowExpire)); } // Set Organization String org = null; SortedSet<String> roles = context.getAttributeSortedStringSet("memberOf"); if (roles != null) { Iterator<String> it = roles.iterator(); while (it.hasNext()) { String role = it.next(); Matcher m = this.pattern.matcher(role); // Skip roles if (!m.matches()) continue; // Check organization cardinality if (org != null) throw new RuntimeException("More than one org per user on " + account.getCommonName()); org = m.group(2); } if (org != null) account.setOrg(org); } return account; } } private boolean isNullValue(Object value) { if (value == null) return true; if (value instanceof String && (StringUtils.isEmpty(value.toString()))) { return true; } return false; } @Override public void changePassword(final String uid, final String password) throws DataServiceException { if (StringUtils.isEmpty(uid)) { throw new IllegalArgumentException("uid is required"); } if (StringUtils.isEmpty(password)) { throw new IllegalArgumentException("password is required"); } // update the entry in the ldap tree Name dn = buildDn(uid); DirContextOperations context = ldapTemplate.lookupContext(dn); // the following action removes the old password. It there are two // passwords (old and new password) they will // be replaced by a single user password LdapShaPasswordEncoder lspe = new LdapShaPasswordEncoder(); String encrypted = lspe.encodePassword(password, String.valueOf(System.currentTimeMillis()).getBytes()); context.setAttributeValue("userPassword", encrypted); ldapTemplate.modifyAttributes(context); } /** * Adds the new password in the user password array. The new password is * maintained in array with two userPassword attributes. * * <pre> * Format: * userPassword[0] : old password * userPassword[1] : new password * </pre> * * @see {@link AccountDao#addNewPassword(String, String)} */ @Override public void addNewPassword(String uid, String newPassword) { if (uid.length() == 0) { throw new IllegalArgumentException("uid is required"); } if (newPassword.length() == 0) { throw new IllegalArgumentException("new password is required"); } LdapShaPasswordEncoder lspe = new LdapShaPasswordEncoder(); String encrypted = lspe.encodePassword(newPassword, String.valueOf(System.currentTimeMillis()).getBytes()); // update the entry in the LDAP tree Name dn = buildDn(uid); DirContextOperations context = ldapTemplate.lookupContext(dn); final String pwd = "userPassword"; Object[] pwdValues = context.getObjectAttributes(pwd); if (pwdValues.length < 2) { // adds the new password context.addAttributeValue(pwd, encrypted, false); } else { // update the last password with the new password pwdValues[1] = newPassword; context.setAttributeValues(pwd, pwdValues); } ldapTemplate.modifyAttributes(context); } /** * Generate a new uid based on the provided uid * * @param * * @return the proposed uid */ @Override public String generateUid(String uid) throws DataServiceException { String newUid = UidGenerator.next(uid); while (exist(newUid)) { newUid = UidGenerator.next(newUid); } return newUid; } @Override public List<Account> findByShadowExpire() { SearchControls sc = new SearchControls(); sc.setReturningAttributes(UserSchema.ATTR_TO_RETRIEVE); sc.setSearchScope(SearchControls.SUBTREE_SCOPE); AndFilter filter = new AndFilter(); filter.and(new EqualsFilter("objectClass", "shadowAccount")); filter.and(new EqualsFilter("objectClass", "inetOrgPerson")); filter.and(new EqualsFilter("objectClass", "organizationalPerson")); filter.and(new EqualsFilter("objectClass", "person")); filter.and(new PresentFilter("shadowExpire")); return ldapTemplate.search(DistinguishedName.EMPTY_PATH, filter.encode(), sc, attributMapper); } }