/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/user/impl/BaseUserDirectoryService.java $ * $Id: BaseUserDirectoryService.java 130609 2013-10-18 13:50:55Z azeckoski@unicon.net $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.user.impl; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.TreeSet; import java.util.Vector; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.authz.api.AuthzGroupService; import org.sakaiproject.authz.api.AuthzPermissionException; import org.sakaiproject.authz.api.FunctionManager; import org.sakaiproject.authz.api.GroupNotDefinedException; import org.sakaiproject.authz.api.SecurityService; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.entity.api.Entity; import org.sakaiproject.entity.api.EntityManager; import org.sakaiproject.entity.api.HttpAccess; import org.sakaiproject.entity.api.Reference; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.entity.api.ResourcePropertiesEdit; import org.sakaiproject.event.api.EventTrackingService; import org.sakaiproject.id.api.IdManager; import org.sakaiproject.memory.api.Cache; import org.sakaiproject.memory.api.MemoryService; import org.sakaiproject.thread_local.api.ThreadLocalManager; import org.sakaiproject.time.api.Time; import org.sakaiproject.time.api.TimeService; import org.sakaiproject.tool.api.SessionBindingEvent; import org.sakaiproject.tool.api.SessionBindingListener; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.user.api.AuthenticatedUserProvider; import org.sakaiproject.user.api.AuthenticationManager; import org.sakaiproject.user.api.ContextualUserDisplayService; import org.sakaiproject.user.api.DisplayAdvisorUDP; import org.sakaiproject.user.api.DisplaySortAdvisorUPD; import org.sakaiproject.user.api.ExternalUserSearchUDP; import org.sakaiproject.user.api.PasswordPolicyProvider; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserAlreadyDefinedException; import org.sakaiproject.user.api.UserDirectoryProvider; import org.sakaiproject.user.api.UserDirectoryService; import org.sakaiproject.user.api.UserEdit; import org.sakaiproject.user.api.UserFactory; import org.sakaiproject.user.api.UserIdInvalidException; import org.sakaiproject.user.api.UserLockedException; import org.sakaiproject.user.api.UserNotDefinedException; import org.sakaiproject.user.api.UserPermissionException; import org.sakaiproject.user.api.UsersShareEmailUDP; import org.sakaiproject.user.api.UserDirectoryService.PasswordRating; import org.sakaiproject.util.BaseResourceProperties; import org.sakaiproject.util.BaseResourcePropertiesEdit; import org.sakaiproject.util.StringUtil; import org.sakaiproject.util.Validator; import org.sakaiproject.util.api.FormattedText; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * <p> * BaseUserDirectoryService is a Sakai user directory services implementation. * </p> * <p> * User records can be kept locally, in Sakai, externally, by a UserDirectoryProvider, or a mixture of both. * </p> * <p> * Each User that ever goes through Sakai is allocated a Sakai unique UUID. Even if we don't keep the User record in Sakai, we keep a map of this id to the external eid. * </p> */ public abstract class BaseUserDirectoryService implements UserDirectoryService, UserFactory { /** Our log (commons). */ private static Log M_log = LogFactory.getLog(BaseUserDirectoryService.class); /** Storage manager for this service. */ protected Storage m_storage = null; /** The initial portion of a relative access point URL. */ protected String m_relativeAccessPoint = null; /** An anon. user. */ protected User m_anon = null; /** A user directory provider. */ protected UserDirectoryProvider m_provider = null; /** Component ID used to find the provider if it's not directly injected. */ protected String m_providerName = null; /** Key for current service caching of current user */ protected final String M_curUserKey = getClass().getName() + ".currentUser"; /** A cache of users */ protected Cache m_callCache = null; /** Optional service to provide site-specific aliases for a user's display ID and display name. */ protected ContextualUserDisplayService m_contextualUserDisplayService = null; /** Collaborator for doing passwords. */ protected PasswordService m_pwdService = null; /** For validating passwords */ protected PasswordPolicyProvider m_passwordPolicyProvider = null; /** Component ID used to find the password policy provider */ protected String m_passwordPolicyProviderName = PasswordPolicyProvider.class.getName(); /********************************************************************************************************************************************************************************************************************************************************** * Abstractions, etc. *********************************************************************************************************************************************************************************************************************************************************/ /** * Construct storage for this service. */ protected abstract Storage newStorage(); /* (non-Javadoc) * @see org.sakaiproject.user.api.UserDirectoryService#validatePassword(java.lang.String, org.sakaiproject.user.api.User) */ public PasswordRating validatePassword(String password, User user) { // NOTE: all passwords are valid by default PasswordRating rating = PasswordRating.PASSED_DEFAULT; PasswordPolicyProvider ppp = getPasswordPolicy(); if (ppp != null) { if (user == null) { user = getCurrentUser(); if (user == m_anon) { user = null; // no user available } } rating = ppp.validatePassword(password, user); } return rating; } /** * @return the current password policy provider * OR null if there is not one OR null if the password policy is disabled */ public PasswordPolicyProvider getPasswordPolicy() { // https://jira.sakaiproject.org/browse/KNL-1123 // If the password policy object is not null, return it to the caller if ( m_passwordPolicyProvider == null ) { // Otherwise, try to get the (default) password policy object before returning it // Try getting it by the configured name if ( m_passwordPolicyProviderName != null ) { m_passwordPolicyProvider = (PasswordPolicyProvider) ComponentManager.get( m_passwordPolicyProviderName ); } // Try getting the default impl via ComponentManager if ( m_passwordPolicyProvider == null ) { m_passwordPolicyProvider = (PasswordPolicyProvider) ComponentManager.get(PasswordPolicyProvider.class); } // If all else failed, manually instantiate default implementation if ( m_passwordPolicyProvider == null ) { m_passwordPolicyProvider = new PasswordPolicyProviderDefaultImpl(serverConfigurationService()); } } PasswordPolicyProvider ppp = m_passwordPolicyProvider; if (!serverConfigurationService().getBoolean("user.password.policy", false)) { ppp = null; // don't send back a policy if disabled } return ppp; } /** * Access the partial URL that forms the root of resource URLs. * * @param relative * if true, form within the access path only (i.e. starting with /content) * @return the partial URL that forms the root of resource URLs. */ protected String getAccessPoint(boolean relative) { return (relative ? "" : serverConfigurationService().getAccessUrl()) + m_relativeAccessPoint; } /** * Access the internal reference which can be used to access the resource from within the system. * * @param id * The user id string. * @return The the internal reference which can be used to access the resource from within the system. */ public String userReference(String id) { // clean up the id id = cleanId(id); return getAccessPoint(true) + Entity.SEPARATOR + ((id == null) ? "" : id); } /** * Access the user id extracted from a user reference. * * @param ref * The user reference string. * @return The the user id extracted from a user reference. */ protected String userId(String ref) { String start = getAccessPoint(true) + Entity.SEPARATOR; int i = ref.indexOf(start); if (i == -1) return ref; String id = ref.substring(i + start.length()); return id; } /** * Check security permission. * * @param lock * The lock id string. * @param resource * The resource reference string, or null if no resource is involved. * @return true if allowd, false if not */ protected boolean unlockCheck(String lock, String resource) { if (!securityService().unlock(lock, resource)) { return false; } return true; } /** * Check security permission. * * @param lock * A list of lock strings to consider. * @param resource * The resource reference string, or null if no resource is involved. * @return true if any of these locks are allowed, false if not */ protected boolean unlockCheck(List<String> locks, String resource) { Iterator<String> locksIterator = locks.iterator(); while(locksIterator.hasNext()) { if(securityService().unlock((String) locksIterator.next(), resource)) return true; } return false; } /** * Check security permission. * * @param lock1 * The lock id string. * @param lock2 * The lock id string. * @param resource * The resource reference string, or null if no resource is involved. * @return true if either allowed, false if not */ protected boolean unlockCheck2(String lock1, String lock2, String resource) { if (!securityService().unlock(lock1, resource)) { if (!securityService().unlock(lock2, resource)) { return false; } } return true; } /** * Check security permission. * * @param lock * The lock id string. * @param resource * The resource reference string, or null if no resource is involved. * @exception UserPermissionException * Thrown if the user does not have access * @return The lock id string that succeeded */ protected String unlock(String lock, String resource) throws UserPermissionException { if (!unlockCheck(lock, resource)) { throw new UserPermissionException(sessionManager().getCurrentSessionUserId(), lock, resource); } return lock; } /** * Check security permission. * * @param lock1 * The lock id string. * @param lock2 * The lock id string. * @param resource * The resource reference string, or null if no resource is involved. * @exception UserPermissionException * Thrown if the user does not have access to either. */ protected void unlock2(String lock1, String lock2, String resource) throws UserPermissionException { if (!unlockCheck2(lock1, lock2, resource)) { throw new UserPermissionException(sessionManager().getCurrentSessionUserId(), lock1 + "/" + lock2, resource); } } /** * Check security permission. * * * @param locks * The list of lock strings. * @param resource * The resource reference string, or null if no resource is involved. * @exception UserPermissionException * Thrown if the user does not have access to either. * @return A list of the lock strings that the user has access to. */ protected List<String> unlock(List<String> locks, String resource) throws UserPermissionException { List<String> locksSucceeded = new ArrayList<String>(); Iterator<String> locksIterator = locks.iterator(); StringBuilder locksFailedSb = new StringBuilder(); while (locksIterator.hasNext()) { String lock = (String) locksIterator.next(); if (unlockCheck(lock, resource)) { locksSucceeded.add(lock); } else { locksFailedSb.append(lock + " "); } } if (locksSucceeded.size() < 1) { throw new UserPermissionException(sessionManager().getCurrentSessionUserId(), locksFailedSb.toString(), resource); } return locksSucceeded; } /** * Make sure we have a good uuid for a user record. If id is specified, use that. Otherwise get one from the provider or allocate a uuid. * * @param id * The proposed id. * @param eid * The proposed eid. * @return The id to use as the User's uuid. */ protected String assureUuid(String id, String eid) { // if we are not using separate id and eid, return the eid if (!m_separateIdEid) return eid; if (id != null) return id; // TODO: let the provider have a chance to set this? -ggolden // allocate a uuid id = idManager().createUuid(); return id; } /********************************************************************************************************************************************************************************************************************************************************** * Configuration *********************************************************************************************************************************************************************************************************************************************************/ /** * Configuration: set the user directory provider helper service. * * @param provider * the user directory provider helper service. */ public void setProvider(UserDirectoryProvider provider) { m_provider = provider; } public void setProviderName(String userDirectoryProviderName) { m_providerName = StringUtils.trimToNull(userDirectoryProviderName); } public void setContextualUserDisplayService(ContextualUserDisplayService contextualUserDisplayService) { m_contextualUserDisplayService = contextualUserDisplayService; } public void setPasswordPolicyProvider( PasswordPolicyProvider passwordPolicyProvider ) { m_passwordPolicyProvider = passwordPolicyProvider; } public void setPasswordPolicyProviderName( String passwordPolicyProviderName ) { m_passwordPolicyProviderName = StringUtils.trimToNull( passwordPolicyProviderName ); } /** The # seconds to cache gets. 0 disables the cache. */ protected int m_cacheSeconds = 0; /** * Set the # minutes to cache a get. * * @param time * The # minutes to cache a get (as an integer string). */ public void setCacheMinutes(String time) { m_cacheSeconds = Integer.parseInt(time) * 60; } /** The # seconds to cache gets. 0 disables the cache. */ protected int m_cacheCleanerSeconds = 0; /** * Set the # minutes between cache cleanings. * * @param time * The # minutes between cache cleanings. (as an integer string). */ public void setCacheCleanerMinutes(String time) { m_cacheCleanerSeconds = Integer.parseInt(time) * 60; } /** Configuration: case sensitive user eid. */ protected boolean m_caseSensitiveEid = false; /** * Configuration: case sensitive user eid * * @param value * The case sensitive user eid. */ public void setCaseSensitiveId(String value) { m_caseSensitiveEid = Boolean.valueOf(value).booleanValue(); } /** Configuration: use a different id and eid for each record (otherwise make them the same value). */ protected boolean m_separateIdEid = false; /** * Configuration: to use a separate value for id and eid for each user record, or not. * * @param value * The separateIdEid setting. */ public void setSeparateIdEid(String value) { m_separateIdEid = Boolean.valueOf(value).booleanValue(); } /** * Configuration: set the password service to use. * */ public void setPasswordService(PasswordService pwdService) { m_pwdService = pwdService; } /********************************************************************************************************************************************************************************************************************************************************** * Dependencies *********************************************************************************************************************************************************************************************************************************************************/ /** * @return the ServerConfigurationService collaborator. */ protected abstract ServerConfigurationService serverConfigurationService(); /** * @return the EntityManager collaborator. */ protected abstract EntityManager entityManager(); /** * @return the SecurityService collaborator. */ protected abstract SecurityService securityService(); /** * @return the FunctionManager collaborator. */ protected abstract FunctionManager functionManager(); /** * @return the SessionManager collaborator. */ protected abstract SessionManager sessionManager(); /** * @return the MemoryService collaborator. */ protected abstract MemoryService memoryService(); /** * @return the EventTrackingService collaborator. */ protected abstract EventTrackingService eventTrackingService(); /** * @return the ThreadLocalManager collaborator. */ protected abstract ThreadLocalManager threadLocalManager(); /** * @return the AuthzGroupService collaborator. */ protected abstract AuthzGroupService authzGroupService(); /** * @return the TimeService collaborator. */ protected abstract TimeService timeService(); /** * @return the IdManager collaborator. */ protected abstract IdManager idManager(); /** * @return the FormattedTextProcessor collaborator */ protected abstract FormattedText formattedText(); /********************************************************************************************************************************************************************************************************************************************************** * Init and Destroy *********************************************************************************************************************************************************************************************************************************************************/ /** * Final initialization, once all dependencies are set. */ public void init() { try { m_relativeAccessPoint = REFERENCE_ROOT; // construct storage and read m_storage = newStorage(); m_storage.open(); // make an anon. user m_anon = new BaseUserEdit(""); // <= 0 indicates no caching desired if (m_cacheSeconds > 0) { M_log.warn("cacheSeconds@org.sakaiproject.user.api.UserDirectoryService is no longer supported"); } if (m_cacheCleanerSeconds > 0) { M_log.warn("cacheCleanerSeconds@org.sakaiproject.user.api.UserDirectoryService is no longer supported"); } m_callCache = memoryService().newCache( "org.sakaiproject.user.api.UserDirectoryService.callCache", userReference("")); // register as an entity producer entityManager().registerEntityProducer(this, REFERENCE_ROOT); // register functions functionManager().registerFunction(SECURE_ADD_USER); functionManager().registerFunction(SECURE_REMOVE_USER); functionManager().registerFunction(SECURE_UPDATE_USER_OWN); functionManager().registerFunction(SECURE_UPDATE_USER_OWN_NAME); functionManager().registerFunction(SECURE_UPDATE_USER_OWN_EMAIL); functionManager().registerFunction(SECURE_UPDATE_USER_OWN_PASSWORD); functionManager().registerFunction(SECURE_UPDATE_USER_OWN_TYPE); functionManager().registerFunction(SECURE_UPDATE_USER_ANY); // if no provider was set, see if we can find one if ((m_provider == null) && (m_providerName != null)) { m_provider = (UserDirectoryProvider) ComponentManager.get(m_providerName); } // Check for optional contextual user display service. if (m_contextualUserDisplayService == null) { m_contextualUserDisplayService = (ContextualUserDisplayService) ComponentManager.get(ContextualUserDisplayService.class); } // Fallback to the default password service. if (m_pwdService == null) { m_pwdService = new PasswordService(); } m_passwordPolicyProviderName = serverConfigurationService().getString(PasswordPolicyProvider.SAK_PROP_PROVIDER_NAME, PasswordPolicyProvider.class.getName()); if (StringUtils.isEmpty(m_passwordPolicyProviderName)) { m_passwordPolicyProviderName = PasswordPolicyProvider.class.getName(); M_log.warn("init(): Empty name for passwordPolicyProvider: Using the default name instead: "+m_passwordPolicyProviderName); } if (m_passwordPolicyProvider == null) { m_passwordPolicyProvider = getPasswordPolicy(); // this will load the PasswordPolicy provider bean or instantiate the default } M_log.info("init(): PasswordPolicyProvider ("+m_passwordPolicyProviderName+"): " + ((m_passwordPolicyProvider == null) ? "none" : m_passwordPolicyProvider.getClass().getName())); M_log.info("init(): provider: " + ((m_provider == null) ? "none" : m_provider.getClass().getName()) + " separateIdEid: " + m_separateIdEid); } catch (Exception t) { M_log.warn("init(): ", t); } } /** * Returns to uninitialized state. You can use this method to release resources thet your Service allocated when Turbine shuts down. */ public void destroy() { m_storage.close(); m_storage = null; m_provider = null; m_anon = null; m_passwordPolicyProvider = null; M_log.info("destroy()"); } /********************************************************************************************************************************************************************************************************************************************************** * UserDirectoryService implementation *********************************************************************************************************************************************************************************************************************************************************/ /** * {@inheritDoc} */ public String getUserEid(String id) throws UserNotDefinedException { id = cleanId(id); // first, check our map String eid = m_storage.checkMapForEid(id); if (eid != null) return eid; throw new UserNotDefinedException(id); } /** * {@inheritDoc} */ public String getUserId(String eid) throws UserNotDefinedException { eid = cleanEid(eid); // first, check our map String id = m_storage.checkMapForId(eid); if (id != null) return id; // Try the provider. UserEdit user = getProvidedUserByEid(null, eid); if (user != null) { id = user.getId(); putCachedUser(userReference(id), user); return id; } // not found throw new UserNotDefinedException(eid); } protected UserEdit getProvidedUserByEid(String id, String eid) { if (m_provider != null) { if (eid == null) { //theres no point in asking a provider if we have no eid return null; } // make a new edit to hold the provider's info, hoping it will be filled in // Since the provider may actually want to fill in the user ID itself, // there's no point in us allocating a new user ID until after it returns. BaseUserEdit user = new BaseUserEdit(id, eid); // check with the provider if (m_provider.getUser(user)) { user.setEid(cleanEid(user.getEid())); ensureMappedIdForProvidedUser(user); return user; } else { return null; } } return null; } protected void ensureMappedIdForProvidedUser(UserEdit user) { if (user.getId() == null) { user.setEid(cleanEid(user.getEid())); String eid = user.getEid(); String id = assureUuid(null, eid); m_storage.putMap(id, eid); user.setId(id); } } protected void checkAndEnsureMappedIdForProvidedUser(UserEdit user) { if (user.getId() == null) { user.setEid(cleanEid(user.getEid())); user.setId(m_storage.checkMapForId(user.getEid())); ensureMappedIdForProvidedUser(user); } } /** * @inheritDoc */ public User getUser(String id) throws UserNotDefinedException { // clean up the id id = cleanId(id); if (id == null) throw new UserNotDefinedException("null"); // see if we've done this already in this thread String ref = userReference(id); UserEdit user = getCachedUser(ref); if (user == null) { // find our user record, and use it if we have it user = m_storage.getById(id); // let the provider provide if needed if ((user == null) && (m_provider != null)) { // we need the eid for the provider - if we can't find an eid, we throw UserNotDefinedException String eid = m_storage.checkMapForEid(id); if (eid != null) { // TODO Should we distinguish an obsolete user ID from an incorrect user ID? // An obsolete ID will have an associated EID but that EID won't be known to // the provider any longer. // An incorrect ID is not found at all. user = getProvidedUserByEid(id, eid); } } if (user != null) { putCachedUser(ref, user); } } // if not found if (user == null) { throw new UserNotDefinedException(id); } return user; } /** * @inheritDoc */ public User getUserByEid(String eid) throws UserNotDefinedException { UserEdit user = null; // clean up the eid eid = cleanEid(eid); if (eid == null) throw new UserNotDefinedException("null"); String id = m_storage.checkMapForId(eid); if (id != null) { user = getCachedUser(userReference(id)); if (user != null) { return user; } user = m_storage.getById(id); } if (user == null) { user = getProvidedUserByEid(id, eid); if (user == null) throw new UserNotDefinedException(eid); } putCachedUser(userReference(user.getId()), user); return user; } /** * @inheritDoc */ public List getUsers(Collection<String> ids) { // Clean IDs to match the by-user case. Set<String> searchIds = new HashSet<String>(); for (Iterator<String> idIter = ids.iterator(); idIter.hasNext(); ) { String id = (String)idIter.next(); id = cleanEid(id); if (id != null) searchIds.add(id); } if (m_separateIdEid) { return m_storage.getUsersByIds(searchIds); } // Fall back to the old logic if this is a legacy system where // "ID == EID", since that setting makes it difficult // to optimize while maintaining backwards compatibility: the user // record may be in the Sakai user table or not, and may be in the // EID-mapping table or not. // User objects to return List<UserEdit> rv = new Vector<UserEdit>(); // a list of User (edits) setup to check with the provider Collection<UserEdit> fromProvider = new Vector<UserEdit>(); // for each requested id for (String id : searchIds) { // see if we've done this already in this thread String ref = userReference(id); UserEdit user = getCachedUser(ref); if (user == null) { // find our user record user = m_storage.getById(id); if (user != null) { putCachedUser(ref, user); } else if (m_provider != null) { // get the eid for this user so we can ask the provider String eid = m_storage.checkMapForEid(id); if (eid != null) { // make a new edit to hold the provider's info; the provider will either fill this in, if known, or remove it from the collection fromProvider.add(new BaseUserEdit(id, eid)); } else { // this user is not internally defined, and we can't find an eid for it, so we skip it M_log.warn("getUsers: cannot find eid for user id: " + id); } } } // add to return if (user != null) rv.add(user); } // check the provider, all at once if (!fromProvider.isEmpty()) { m_provider.getUsers(fromProvider); // for each User in the collection that was filled in (and not removed) by the provider, cache and return it for (Iterator i = fromProvider.iterator(); i.hasNext();) { UserEdit user = (UserEdit) i.next(); putCachedUser(user.getReference(), user); // add to return rv.add(user); } } return rv; } /** * @see org.sakaiproject.user.api.UserDirectoryService#getUsersByEids(java.util.Collection) */ public List<User> getUsersByEids(Collection<String> eids) { if (!m_separateIdEid) { return getUsers(eids); } // Clean EIDs to match the by-user case. Set<String> searchEids = new HashSet<String>(); for (String eid : eids) { eid = cleanEid(eid); if (eid != null) searchEids.add(eid); } return m_storage.getUsersByEids(searchEids); } /** * @inheritDoc */ public User getCurrentUser() { String id = sessionManager().getCurrentSessionUserId(); // check current service caching - discard if the session user is different User rv = (User) threadLocalManager().get(M_curUserKey); if ((rv != null) && (rv.getId().equals(id))) return rv; try { rv = getUser(id); } catch (UserNotDefinedException e) { rv = getAnonymousUser(); } // cache in the current service threadLocalManager().set(M_curUserKey, rv); return rv; } /** * @inheritDoc */ public boolean allowUpdateUser(String id) { // clean up the id id = cleanId(id); if (id == null) return false; // is this the user's own? if (id.equals(sessionManager().getCurrentSessionUserId())) { ArrayList<String> locks = new ArrayList<String>(); locks.add(SECURE_UPDATE_USER_OWN); locks.add(SECURE_UPDATE_USER_ANY); locks.add(SECURE_UPDATE_USER_OWN_NAME); locks.add(SECURE_UPDATE_USER_OWN_EMAIL); locks.add(SECURE_UPDATE_USER_OWN_PASSWORD); locks.add(SECURE_UPDATE_USER_OWN_TYPE); // own or any return unlockCheck(locks, userReference(id)); } else { // just any return unlockCheck(SECURE_UPDATE_USER_ANY, userReference(id)); } } /** * @inheritDoc */ public boolean allowUpdateUserName(String id) { // clean up the id id = cleanId(id); if (id == null) return false; // is this the user's own? if (id.equals(sessionManager().getCurrentSessionUserId())) { ArrayList<String> locks = new ArrayList<String>(); locks.add(SECURE_UPDATE_USER_OWN); locks.add(SECURE_UPDATE_USER_ANY); locks.add(SECURE_UPDATE_USER_OWN_NAME); // own or any return unlockCheck(locks, userReference(id)); } else { // just any return unlockCheck(SECURE_UPDATE_USER_ANY, userReference(id)); } } /** * @inheritDoc */ public boolean allowUpdateUserEmail(String id) { // clean up the id id = cleanId(id); if (id == null) return false; // is this the user's own? if (id.equals(sessionManager().getCurrentSessionUserId())) { ArrayList<String> locks = new ArrayList<String>(); locks.add(SECURE_UPDATE_USER_OWN); locks.add(SECURE_UPDATE_USER_ANY); locks.add(SECURE_UPDATE_USER_OWN_EMAIL); // own or any return unlockCheck(locks, userReference(id)); } else { // just any return unlockCheck(SECURE_UPDATE_USER_ANY, userReference(id)); } } /** * @inheritDoc */ public boolean allowUpdateUserPassword(String id) { // clean up the id id = cleanId(id); if (id == null) return false; // is this the user's own? if (id.equals(sessionManager().getCurrentSessionUserId())) { ArrayList<String> locks = new ArrayList<String>(); locks.add(SECURE_UPDATE_USER_OWN); locks.add(SECURE_UPDATE_USER_ANY); locks.add(SECURE_UPDATE_USER_OWN_PASSWORD); // own or any return unlockCheck(locks, userReference(id)); } else { // just any return unlockCheck(SECURE_UPDATE_USER_ANY, userReference(id)); } } /** * @inheritDoc */ public boolean allowUpdateUserType(String id) { // clean up the id id = cleanId(id); if (id == null) return false; // is this the user's own? if (id.equals(sessionManager().getCurrentSessionUserId())) { ArrayList<String> locks = new ArrayList<String>(); locks.add(SECURE_UPDATE_USER_OWN); locks.add(SECURE_UPDATE_USER_ANY); locks.add(SECURE_UPDATE_USER_OWN_TYPE); // own or any return unlockCheck(locks, userReference(id)); } else { // just any return unlockCheck(SECURE_UPDATE_USER_ANY, userReference(id)); } } /** * @inheritDoc */ public UserEdit editUser(String id) throws UserNotDefinedException, UserPermissionException, UserLockedException { // clean up the id id = cleanId(id); if (id == null) throw new UserNotDefinedException("null"); // is this the user's own? List<String> locksSucceeded = new ArrayList<String>(); String function = null; if (id.equals(sessionManager().getCurrentSessionUserId())) { // own or any List<String> locks = new ArrayList<String>(); locks.add(SECURE_UPDATE_USER_OWN); locks.add(SECURE_UPDATE_USER_OWN_NAME); locks.add(SECURE_UPDATE_USER_OWN_EMAIL); locks.add(SECURE_UPDATE_USER_OWN_PASSWORD); locks.add(SECURE_UPDATE_USER_OWN_TYPE); locks.add(SECURE_UPDATE_USER_ANY); locksSucceeded = unlock(locks, userReference(id)); function = SECURE_UPDATE_USER_OWN; } else { // just any locksSucceeded.add(unlock(SECURE_UPDATE_USER_ANY, userReference(id))); function = SECURE_UPDATE_USER_ANY; } // ignore the cache - get the user with a lock from the info store UserEdit user = m_storage.edit(id); if (user == null) { // Figure out which exception to throw. if (!m_storage.check(id)) { throw new UserNotDefinedException(id); } else { throw new UserLockedException(id); } } if(!locksSucceeded.contains(SECURE_UPDATE_USER_ANY) && !locksSucceeded.contains(SECURE_UPDATE_USER_OWN)) { // current session does not have permission to edit all properties for this user // lock the properties the user does not have access to edit if(!locksSucceeded.contains(SECURE_UPDATE_USER_OWN_NAME)) { user.restrictEditFirstName(); user.restrictEditLastName(); } if(!locksSucceeded.contains(SECURE_UPDATE_USER_OWN_EMAIL)) { user.restrictEditEmail(); } if(!locksSucceeded.contains(SECURE_UPDATE_USER_OWN_PASSWORD)) { user.restrictEditPassword(); } if(!locksSucceeded.contains(SECURE_UPDATE_USER_OWN_TYPE)) { user.restrictEditType(); } } //only a super user should ever be able to edit the EID if (!securityService().isSuperUser()) { user.restrictEditEid(); } ((BaseUserEdit) user).setEvent(function); return user; } /** * @inheritDoc */ public void commitEdit(UserEdit user) throws UserAlreadyDefinedException { // check for closed edit if (!user.isActiveEdit()) { M_log.warn("commitEdit(): closed UserEdit", new Exception()); return; } // update the properties addLiveUpdateProperties((BaseUserEdit) user); // complete the edit if (!m_storage.commit(user)) { m_storage.cancel(user); ((BaseUserEdit) user).closeEdit(); throw new UserAlreadyDefinedException(user.getEid()); } String ref = user.getReference(); // track it eventTrackingService().post(eventTrackingService().newEvent(((BaseUserEdit) user).getEvent(), ref, true)); // close the edit object ((BaseUserEdit) user).closeEdit(); // Update the caches to match any changed data. putCachedUser(ref, user); // update in the threadLocal cache if this is the current user if (user.getId().equals(sessionManager().getCurrentSessionUserId())) { threadLocalManager().set(M_curUserKey, user); } } /** * @inheritDoc */ public void cancelEdit(UserEdit user) { // check for closed edit if (!user.isActiveEdit()) { try { throw new Exception(); } catch (Exception e) { M_log.warn("cancelEdit(): closed UserEdit", e); } return; } // release the edit lock m_storage.cancel(user); // close the edit object ((BaseUserEdit) user).closeEdit(); } /** * @inheritDoc */ public List<User> getUsers() { List<User> users = m_storage.getAll(); return users; } /** * @inheritDoc */ public List<User> getUsers(int first, int last) { List<User> all = m_storage.getAll(first, last); return all; } /** * @inheritDoc */ public int countUsers() { return m_storage.count(); } /** * @inheritDoc */ public List<User> searchUsers(String criteria, int first, int last) { //KNL-691 split term on whitespace and perform multiple searches, no duplicates will be returned Set<User> users = new TreeSet<User>(); List<String> terms = Arrays.asList(StringUtils.split(criteria)); for(String term:terms){ users.addAll(m_storage.search(term, first, last)); } List<User> userList = new ArrayList<User>(users); //sort on sortName, default. Collections.sort(userList); return userList; } /** * @inheritDoc */ public int countSearchUsers(String criteria) { //KNL-691 because we need to perform multiple searches and aggregate the results, but without duplicates, //we just call the above method, which takes care of this, and then count the results //return m_storage.countSearch(criteria); return searchUsers(criteria, 1, Integer.MAX_VALUE).size(); } /** * @inheritDoc */ public List<User> searchExternalUsers(String criteria, int first, int last){ List<User> users = new ArrayList<User>(); List<UserEdit> providedUserRecords = null; if (m_provider instanceof ExternalUserSearchUDP) { providedUserRecords = ((ExternalUserSearchUDP) m_provider).searchExternalUsers(criteria, first, last, this); } else { M_log.debug("searchExternalUsers capability is not supported by your provider"); } if (providedUserRecords != null){ for (UserEdit user : providedUserRecords){ // KNL-741 these useredit objects should already have the eid-id mapping // But just incase the provider hasn't mapped them. checkAndEnsureMappedIdForProvidedUser(user); users.add(user); } } return users; } /** * @inheritDoc */ @SuppressWarnings("unchecked") public Collection findUsersByEmail(String email) { // check internal users Collection users = m_storage.findUsersByEmail(email); // add in provider users if (m_provider != null) { Collection<BaseUserEdit> providedUserRecords = null; // support UDP that has multiple users per email if (m_provider instanceof UsersShareEmailUDP) { providedUserRecords = ((UsersShareEmailUDP) m_provider).findUsersByEmail(email, this); } else { // make a new edit to hold the provider's info BaseUserEdit edit = new BaseUserEdit(); if (m_provider.findUserByEmail(edit, email)) { providedUserRecords = Arrays.asList(new BaseUserEdit[] {edit}); } } if (providedUserRecords != null) { for (BaseUserEdit user : providedUserRecords) { checkAndEnsureMappedIdForProvidedUser(user); users.add(user); } } } return users; } /** * @inheritDoc */ public User getAnonymousUser() { return m_anon; } /** * @inheritDoc */ public boolean allowAddUser() { return unlockCheck(SECURE_ADD_USER, userReference("")); } /** * @inheritDoc */ public UserEdit addUser(String id, String eid) throws UserIdInvalidException, UserAlreadyDefinedException, UserPermissionException { // clean the ids id = cleanId(id); eid = cleanEid(eid); // make sure we have an id id = assureUuid(id, eid); //eid can't be longer than 255 chars if (eid.length() > 255) { throw new UserIdInvalidException("Eid is too long"); } // check security (throws if not permitted) unlock(SECURE_ADD_USER, userReference(id)); // reserve a user with this id from the info store - if it's in use, this will return null UserEdit user = m_storage.put(id, eid); if (user == null) { throw new UserAlreadyDefinedException(id + " -" + eid); } ((BaseUserEdit) user).setEvent(SECURE_ADD_USER); return user; } /** * @inheritDoc */ public User addUser(String id, String eid, String firstName, String lastName, String email, String pw, String type, ResourceProperties properties) throws UserIdInvalidException, UserAlreadyDefinedException, UserPermissionException { // get it added UserEdit edit = addUser(id, eid); // fill in the fields edit.setLastName(lastName); edit.setFirstName(firstName); edit.setEmail(email); edit.setPassword(pw); edit.setType(type); ResourcePropertiesEdit props = edit.getPropertiesEdit(); if (properties != null) { props.addAll(properties); } // no live props! // get it committed - no further security check if (!m_storage.commit(edit)) { m_storage.cancel(edit); ((BaseUserEdit) edit).closeEdit(); throw new UserAlreadyDefinedException(edit.getEid()); } // track it eventTrackingService().post(eventTrackingService().newEvent(((BaseUserEdit) edit).getEvent(), edit.getReference(), true)); // close the edit object ((BaseUserEdit) edit).closeEdit(); return edit; } /** * @inheritDoc */ public UserEdit mergeUser(Element el) throws UserIdInvalidException, UserAlreadyDefinedException, UserPermissionException { // construct from the XML User userFromXml = new BaseUserEdit(el); // check for a valid eid Validator.checkResourceId(userFromXml.getEid()); // check security (throws if not permitted) unlock(SECURE_ADD_USER, userFromXml.getReference()); // Check if this user is a provided one: if (getProvidedUserByEid(userFromXml.getId(), userFromXml.getEid()) != null) { // This doesn't mean we have a mapping from ID to EID mapping if (m_storage.checkMapForId(userFromXml.getEid()) == null) { m_storage.putMap(userFromXml.getId(), userFromXml.getEid()); } throw new UserAlreadyDefinedException("Provided user: "+ userFromXml.getId() + " - " + userFromXml.getEid()); } // reserve a user with this id from the info store - if it's in use, this will return null UserEdit user = m_storage.put(userFromXml.getId(), userFromXml.getEid()); if (user == null) { throw new UserAlreadyDefinedException(userFromXml.getId() + " - " + userFromXml.getEid()); } // transfer from the XML read user object to the UserEdit ((BaseUserEdit) user).set(userFromXml); ((BaseUserEdit) user).setEvent(SECURE_ADD_USER); return user; } /** * @inheritDoc */ public boolean allowRemoveUser(String id) { // clean up the id id = cleanId(id); if (id == null) return false; return unlockCheck(SECURE_REMOVE_USER, userReference(id)); } /** * @inheritDoc */ public void removeUser(UserEdit user) throws UserPermissionException { String ref = user.getReference(); // check for closed edit if (!user.isActiveEdit()) { M_log.warn("removeUser(): closed UserEdit", new Exception()); return; } // check security (throws if not permitted) unlock(SECURE_REMOVE_USER, ref); // complete the edit m_storage.remove(user); // track it eventTrackingService().post(eventTrackingService().newEvent(SECURE_REMOVE_USER, ref, true)); // close the edit object ((BaseUserEdit) user).closeEdit(); // remove any realm defined for this resource try { authzGroupService().removeAuthzGroup(authzGroupService().getAuthzGroup(ref)); } catch (AuthzPermissionException e) { M_log.warn("removeUser: removing realm for : " + ref + " : " + e); } catch (GroupNotDefinedException ignore) { } // Remove from cache. removeCachedUser(ref); } /** * {@inheritDoc} * * <b>WARNING:</b> Do not call this method directly! Use {@link AuthenticationManager#authenticate(org.sakaiproject.user.api.Evidence)} */ public User authenticate(String loginId, String password) { loginId = StringUtils.trimToNull(loginId); if (loginId == null) return null; UserEdit user = null; boolean authenticateWithProviderFirst = (m_provider != null) && m_provider.authenticateWithProviderFirst(loginId); if (authenticateWithProviderFirst) { user = getProviderAuthenticatedUser(loginId, password); if (user != null) return user; } user = getInternallyAuthenticatedUser(loginId, password); if (user != null) return user; if ((m_provider != null) && !authenticateWithProviderFirst) { return getProviderAuthenticatedUser(loginId, password); } return null; } protected UserEdit getInternallyAuthenticatedUser(String eid, String password) { try { UserEdit user = (UserEdit)getUserByEid(eid); return user.checkPassword(password) ? user : null; } catch (UserNotDefinedException e) { // Give up and possibly pass along to another authentication service. return null; } } protected UserEdit getProviderAuthenticatedUser(String loginId, String password) { UserEdit user = null; if (m_provider instanceof AuthenticatedUserProvider) { // Since the login ID might differ from the EID, the provider is in charge // of filling in user data as well as authenticating the user. user = ((AuthenticatedUserProvider)m_provider).getAuthenticatedUser(loginId, password); } else { // The pre-2.5 authenticateUser method was ambiguous due to the lack of a // distinct "AuthenticationProvider" and the inability to indicate "emptiness" // in a UserEdit record. Here are the revised options: // // 1) If the provider is basically authentication-only, then this logic will find // the locally stored user record and pass it on. // // 2) If the provider handles both data provision and authentication, then // this logic will find the provided user data and pass it on. // // 3) If the provider needs to authenticate a user using a login ID // that does not match the EID of an already locally stored user record or a // provided user, then the provider should be changed to implement the // AuthenticatedUserProvider interface. // // Note that this legacy interface does not allow EIDs and login IDs to differ, // and this logic will therefore not attempt to authenticate a user unless // "getUserByEid(loginId)" returns success. This preserves backwards // compatibility for legacy providers that perform authentication only for // locally stored users. try { user = (UserEdit)getUserByEid(loginId); } catch (UserNotDefinedException e) { return null; } boolean authenticated = m_provider.authenticateUser(loginId, user, password); if (!authenticated) user = null; } if (user != null) { checkAndEnsureMappedIdForProvidedUser(user); putCachedUser(user.getReference(), user); return user; } return null; } /** * @inheritDoc */ public void destroyAuthentication() { } /** * Create the live properties for the user. */ protected void addLiveProperties(BaseUserEdit edit) { String current = sessionManager().getCurrentSessionUserId(); edit.m_createdUserId = current; edit.m_lastModifiedUserId = current; Time now = timeService().newTime(); edit.m_createdTime = now; edit.m_lastModifiedTime = (Time) now.clone(); } /** * Update the live properties for a user for when modified. */ protected void addLiveUpdateProperties(BaseUserEdit edit) { String current = sessionManager().getCurrentSessionUserId(); edit.m_lastModifiedUserId = current; edit.m_lastModifiedTime = timeService().newTime(); } /** * Adjust the id - trim it to null. Note: eid case insensitive option does NOT apply to id. * * @param id * The id to clean up. * @return A cleaned up id. */ protected String cleanId(String id) { // if we are not doing separate id and eid, use the eid rules if (!m_separateIdEid) { id = cleanEid(id); } id = StringUtils.trimToNull(id); // max length for an id is 99 chars id = StringUtils.abbreviate(id, 99); return id; } /** * Adjust the eid - trim it to null, and lower case IF we are case insensitive. * * @param eid * The eid to clean up. * @return A cleaned up eid. */ protected String cleanEid(String eid) { if (!m_caseSensitiveEid) { eid = StringUtils.lowerCase(eid); } eid = StringUtils.trimToNull(eid); if (eid != null) { // remove all instances of these chars <>,;:\" eid = StringUtils.replaceChars(eid, "<>,;:\\/", ""); } // NOTE: length check is handled later on return eid; } protected UserEdit getCachedUser(String ref) { UserEdit user = (UserEdit)threadLocalManager().get(ref); if ((user == null) && (m_callCache != null)) { user = (UserEdit)m_callCache.get(ref); } return user; } protected void putCachedUser(String ref, UserEdit user) { threadLocalManager().set(ref, user); if (m_callCache != null) m_callCache.put(ref, user); } protected void removeCachedUser(String ref) { threadLocalManager().set(ref, null); if (m_callCache != null) m_callCache.remove(ref); } /********************************************************************************************************************************************************************************************************************************************************** * EntityProducer implementation *********************************************************************************************************************************************************************************************************************************************************/ /** * @inheritDoc */ public String getLabel() { return "user"; } /** * @inheritDoc */ public boolean willArchiveMerge() { return false; } /** * @inheritDoc */ public HttpAccess getHttpAccess() { return null; } /** * @inheritDoc */ public boolean parseEntityReference(String reference, Reference ref) { // for user access if (reference.startsWith(REFERENCE_ROOT)) { String id = null; // we will get null, service, userId String[] parts = StringUtil.split(reference, Entity.SEPARATOR); if (parts.length > 2) { id = parts[2]; } ref.set(APPLICATION_ID, null, id, null, null); return true; } return false; } /** * @inheritDoc */ public String getEntityDescription(Reference ref) { // double check that it's mine if (!APPLICATION_ID.equals(ref.getType())) return null; String rv = "User: " + ref.getReference(); try { User user = getUser(ref.getId()); rv = "User: " + user.getDisplayName(); } catch (UserNotDefinedException e) { } catch (NullPointerException e) { } return rv; } /** * @inheritDoc */ public ResourceProperties getEntityResourceProperties(Reference ref) { return null; } /** * @inheritDoc */ public Entity getEntity(Reference ref) { return null; } /** * @inheritDoc */ public Collection getEntityAuthzGroups(Reference ref, String userId) { // double check that it's mine if (!APPLICATION_ID.equals(ref.getType())) return null; Collection rv = new Vector(); // for user access: user and template realms try { rv.add(userReference(ref.getId())); ref.addUserTemplateAuthzGroup(rv, userId); } catch (NullPointerException e) { M_log.warn("getEntityRealms(): " + e); } return rv; } /** * @inheritDoc */ public String getEntityUrl(Reference ref) { return null; } /** * @inheritDoc */ public String archive(String siteId, Document doc, Stack stack, String archivePath, List attachments) { return ""; } /** * @inheritDoc */ public String merge(String siteId, Element root, String archivePath, String fromSiteId, Map attachmentNames, Map userIdTrans, Set userListAllowImport) { return ""; } /********************************************************************************************************************************************************************************************************************************************************** * UserFactory implementation *********************************************************************************************************************************************************************************************************************************************************/ /** * @inheritDoc */ public UserEdit newUser() { return new BaseUserEdit(); } /** * @inheritDoc */ public UserEdit newUser(String eid) { UserEdit u = new BaseUserEdit(); u.setEid(eid); checkAndEnsureMappedIdForProvidedUser(u); return u; } /********************************************************************************************************************************************************************************************************************************************************** * UserEdit implementation *********************************************************************************************************************************************************************************************************************************************************/ /** * <p> * BaseUserEdit is an implementation of the UserEdit object. * </p> */ public class BaseUserEdit implements UserEdit, SessionBindingListener { /** The event code for this edit. */ protected String m_event = null; /** Active flag. */ protected boolean m_active = false; /** The user id. */ protected String m_id = null; /** The user eid. */ protected String m_eid = null; /** The user first name. */ protected String m_firstName = null; /** The user last name. */ protected String m_lastName = null; /** The user email address. */ protected String m_email = null; /** The user password. */ protected String m_pw = null; /** The properties. */ protected ResourcePropertiesEdit m_properties = null; /** The user type. */ protected String m_type = null; /** The created user id. */ protected String m_createdUserId = null; /** The last modified user id. */ protected String m_lastModifiedUserId = null; /** The time created. */ protected Time m_createdTime = null; /** The time last modified. */ protected Time m_lastModifiedTime = null; /** If editing the first name is restricted **/ protected boolean m_restrictedFirstName = false; /** If editing the last name is restricted **/ protected boolean m_restrictedLastName = false; /** If editing the email is restricted **/ protected boolean m_restrictedEmail = false; /** If editing the password is restricted **/ protected boolean m_restrictedPassword = false; /** If editing the type is restricted **/ protected boolean m_restrictedType = false; /** if editing the eid is restricted **/ protected boolean m_restrictedEid = false; // in object cache of the sort name. private transient String m_sortName; /** * Construct. * * @param id * The user id. */ public BaseUserEdit(String id, String eid) { m_id = id; m_eid = eid; // setup for properties BaseResourcePropertiesEdit props = new BaseResourcePropertiesEdit(); m_properties = props; // if the id is not null (a new user, rather than a reconstruction) // and not the anon (id == "") user, // add the automatic (live) properties if ((m_id != null) && (m_id.length() > 0)) addLiveProperties(this); //KNL-567 lazy set the properties to be lazy so they get loaded props.setLazy(true); } public BaseUserEdit(String id) { this(id, null); } public BaseUserEdit() { this(null, null); } /** * Construct from another User object. * * @param user * The user object to use for values. */ public BaseUserEdit(User user) { setAll(user); } /** * Construct from information in XML. * * @param el * The XML DOM Element definining the user. */ public BaseUserEdit(Element el) { // setup for properties m_properties = new BaseResourcePropertiesEdit(); m_id = cleanId(el.getAttribute("id")); m_eid = cleanEid(el.getAttribute("eid")); m_firstName = StringUtils.trimToNull(el.getAttribute("first-name")); m_lastName = StringUtils.trimToNull(el.getAttribute("last-name")); setEmail(StringUtils.trimToNull(el.getAttribute("email"))); m_pw = el.getAttribute("pw"); m_type = StringUtils.trimToNull(el.getAttribute("type")); m_createdUserId = StringUtils.trimToNull(el.getAttribute("created-id")); m_lastModifiedUserId = StringUtils.trimToNull(el.getAttribute("modified-id")); String time = StringUtils.trimToNull(el.getAttribute("created-time")); if (time != null) { m_createdTime = timeService().newTimeGmt(time); } time = StringUtils.trimToNull(el.getAttribute("modified-time")); if (time != null) { m_lastModifiedTime = timeService().newTimeGmt(time); } // the children (roles, properties) NodeList children = el.getChildNodes(); final int length = children.getLength(); for (int i = 0; i < length; i++) { Node child = children.item(i); if (child.getNodeType() != Node.ELEMENT_NODE) continue; Element element = (Element) child; // look for properties if (element.getTagName().equals("properties")) { // re-create properties m_properties = new BaseResourcePropertiesEdit(element); // pull out some properties into fields to convert old (pre 1.38) versions if (m_createdUserId == null) { m_createdUserId = m_properties.getProperty("CHEF:creator"); } if (m_lastModifiedUserId == null) { m_lastModifiedUserId = m_properties.getProperty("CHEF:modifiedby"); } if (m_createdTime == null) { try { m_createdTime = m_properties.getTimeProperty("DAV:creationdate"); } catch (Exception ignore) { } } if (m_lastModifiedTime == null) { try { m_lastModifiedTime = m_properties.getTimeProperty("DAV:getlastmodified"); } catch (Exception ignore) { } } m_properties.removeProperty("CHEF:creator"); m_properties.removeProperty("CHEF:modifiedby"); m_properties.removeProperty("DAV:creationdate"); m_properties.removeProperty("DAV:getlastmodified"); } } } /** * ReConstruct. * * @param id * The id. * @param eid * The eid. * @param email * The email. * @param firstName * The first name. * @param lastName * The last name. * @param type * The type. * @param pw * The password. * @param createdBy * The createdBy property. * @param createdOn * The createdOn property. * @param modifiedBy * The modified by property. * @param modifiedOn * The modified on property. */ public BaseUserEdit(String id, String eid, String email, String firstName, String lastName, String type, String pw, String createdBy, Time createdOn, String modifiedBy, Time modifiedOn) { m_id = id; m_eid = eid; m_firstName = firstName; m_lastName = lastName; m_type = type; setEmail(email); m_pw = pw; m_createdUserId = createdBy; m_lastModifiedUserId = modifiedBy; m_createdTime = createdOn; m_lastModifiedTime = modifiedOn; // setup for properties, but mark them lazy since we have not yet established them from data BaseResourcePropertiesEdit props = new BaseResourcePropertiesEdit(); props.setLazy(true); m_properties = props; } /** * Take all values from this object. * * @param user * The user object to take values from. */ protected void setAll(User user) { m_id = user.getId(); m_eid = user.getEid(); m_firstName = user.getFirstName(); m_lastName = user.getLastName(); m_type = user.getType(); setEmail(user.getEmail()); m_pw = ((BaseUserEdit) user).m_pw; m_createdUserId = ((BaseUserEdit) user).m_createdUserId; m_lastModifiedUserId = ((BaseUserEdit) user).m_lastModifiedUserId; if (((BaseUserEdit) user).m_createdTime != null) m_createdTime = (Time) ((BaseUserEdit) user).m_createdTime.clone(); if (((BaseUserEdit) user).m_lastModifiedTime != null) m_lastModifiedTime = (Time) ((BaseUserEdit) user).m_lastModifiedTime.clone(); m_properties = new BaseResourcePropertiesEdit(); m_properties.addAll(user.getProperties()); ((BaseResourcePropertiesEdit) m_properties).setLazy(((BaseResourceProperties) user.getProperties()).isLazy()); } /** * @inheritDoc */ public Element toXml(Document doc, Stack stack) { Element user = doc.createElement("user"); if (stack.isEmpty()) { doc.appendChild(user); } else { ((Element) stack.peek()).appendChild(user); } stack.push(user); user.setAttribute("id", getId()); user.setAttribute("eid", getEid()); if (m_firstName != null) user.setAttribute("first-name", m_firstName); if (m_lastName != null) user.setAttribute("last-name", m_lastName); if (m_type != null) user.setAttribute("type", m_type); user.setAttribute("email", getEmail()); user.setAttribute("created-id", m_createdUserId); user.setAttribute("modified-id", m_lastModifiedUserId); if (m_createdTime != null) { user.setAttribute("created-time", m_createdTime.toString()); } if (m_lastModifiedTime != null) { user.setAttribute("modified-time", m_lastModifiedTime.toString()); } // properties getProperties().toXml(doc, stack); stack.pop(); return user; } /** * @inheritDoc */ public String getId() { return m_id; } /** * @inheritDoc */ public String getEid() { return m_eid; } /** * @inheritDoc */ public String getUrl() { return getAccessPoint(false) + m_id; } /** * @inheritDoc */ public String getReference() { return userReference(m_id); } /** * @inheritDoc */ public String getReference(String rootProperty) { return getReference(); } /** * @inheritDoc */ public String getUrl(String rootProperty) { return getUrl(); } /** * @inheritDoc */ public ResourceProperties getProperties() { // if lazy, resolve if (((BaseResourceProperties) m_properties).isLazy()) { ((BaseResourcePropertiesEdit) m_properties).setLazy(false); m_storage.readProperties(this, m_properties); } return m_properties; } /** * @inheritDoc */ public User getCreatedBy() { try { return getUser(m_createdUserId); } catch (Exception e) { return getAnonymousUser(); } } /** * @inheritDoc */ public User getModifiedBy() { try { return getUser(m_lastModifiedUserId); } catch (Exception e) { return getAnonymousUser(); } } /** * @inheritDoc */ public Time getCreatedTime() { return m_createdTime; } /** * @inheritDoc */ public Date getCreatedDate() { return new Date(m_createdTime.getTime()); } /** * @inheritDoc */ public Time getModifiedTime() { return m_lastModifiedTime; } /** * @inheritDoc */ public Date getModifiedDate() { return new Date(m_lastModifiedTime.getTime()); } /** * @inheritDoc */ public String getDisplayName() { String rv = null; // If a contextual aliasing service exists, let it have the first try. if (m_contextualUserDisplayService != null) { rv = m_contextualUserDisplayService.getUserDisplayName(this); if (rv != null) { return rv; } } // let the provider handle it, if we have that sort of provider, and it wants to handle this if ((m_provider != null) && (m_provider instanceof DisplayAdvisorUDP)) { rv = ((DisplayAdvisorUDP) m_provider).getDisplayName(this); } if (rv == null) { // or do it this way StringBuilder buf = new StringBuilder(128); if (m_firstName != null) buf.append(m_firstName); if (m_lastName != null) { if (buf.length() > 0) buf.append(" "); buf.append(m_lastName); } if (buf.length() == 0) { rv = getEid(); } else { rv = buf.toString(); } } return rv; } /** * @inheritDoc */ public String getDisplayId() { String rv = null; // If a contextual aliasing service exists, let it have the first try. if (m_contextualUserDisplayService != null) { rv = m_contextualUserDisplayService.getUserDisplayId(this); if (rv != null) { return rv; } } // let the provider handle it, if we have that sort of provider, and it wants to handle this if ((m_provider != null) && (m_provider instanceof DisplayAdvisorUDP)) { rv = ((DisplayAdvisorUDP) m_provider).getDisplayId(this); } // use eid if not if (rv == null) { rv = getEid(); } return rv; } /** * @inheritDoc */ public String getFirstName() { if (m_firstName == null) return ""; return m_firstName; } /** * @inheritDoc */ public String getLastName() { if (m_lastName == null) return ""; return m_lastName; } /** * @inheritDoc */ public String getSortName() { if (m_sortName == null) { if (m_provider != null && m_provider instanceof DisplaySortAdvisorUPD) { String rv = ((DisplaySortAdvisorUPD) m_provider).getSortName(this); if (rv != null) { m_sortName = rv; return rv; } } // Cache this locally in the object as otherwise when sorting users we generate lots of objects. StringBuilder buf = new StringBuilder(128); if (m_lastName != null) buf.append(m_lastName); if (m_firstName != null) { //KNL-524 no comma if the last name is null if (m_lastName != null) { buf.append(", "); } buf.append(m_firstName); } m_sortName = (buf.length() == 0)?getEid():buf.toString(); } return m_sortName; } /** * @inheritDoc */ public String getEmail() { if (m_email == null) return ""; return m_email; } /** * @inheritDoc */ public String getType() { return m_type; } /** * @inheritDoc */ public boolean checkPassword(String pw) { pw = StringUtils.trimToNull(pw); return m_pwdService.check(pw, m_pw); } /** * @inheritDoc */ public boolean equals(Object obj) { if (!(obj instanceof User)) return false; //its possible that the user object has no id set if (((User) obj).getId() == null) { return false; } return ((User) obj).getId().equals(getId()); } /** * @inheritDoc */ public int hashCode() { String id = getId(); if (id == null) { // Maintains consistency with Sakai 2.4.x behavior. id = ""; } return id.hashCode(); } /** * @inheritDoc */ public int compareTo(Object obj) { if (!(obj instanceof User)) throw new ClassCastException(); // if the object are the same, say so if (obj == this) return 0; // start the compare by comparing their sort names int compare = getSortName().compareTo(((User) obj).getSortName()); // if these are the same if (compare == 0) { // sort based on (unique) eid compare = getEid().compareTo(((User) obj).getEid()); } return compare; } /** * Clean up. */ protected void finalize() { // catch the case where an edit was made but never resolved if (m_active) { cancelEdit(this); } } /** * @inheritDoc */ public void setId(String id) { // set once only! if (m_id == null) { m_id = id; } else throw new UnsupportedOperationException("Tried to change user ID from " + m_id + " to " + id); } /** * @inheritDoc */ public void setEid(String eid) { if (!m_restrictedEid) { m_eid = eid; m_sortName = null; } } /** * @inheritDoc */ public void setFirstName(String name) { if(!m_restrictedFirstName) { // https://jira.sakaiproject.org/browse/SAK-20226 - removed html from name m_firstName = formattedText().convertFormattedTextToPlaintext(name); m_sortName = null; } } /** * @inheritDoc */ public void setLastName(String name) { if(!m_restrictedLastName) { // https://jira.sakaiproject.org/browse/SAK-20226 - removed html from name m_lastName = formattedText().convertFormattedTextToPlaintext(name); m_sortName = null; } } /** * @inheritDoc */ public void setEmail(String email) { if(!m_restrictedEmail) { m_email = email; } } /** * @inheritDoc */ public void setPassword(String pw) { if(!m_restrictedPassword) { // to clear it if (pw == null) { m_pw = null; } // else encode the new one else { // encode this password String encoded = m_pwdService.encrypt(pw); m_pw = encoded; } } } /** * @inheritDoc */ public void setType(String type) { if(!m_restrictedType) { m_type = type; } } public void restrictEditFirstName() { m_restrictedFirstName = true; } public void restrictEditLastName() { m_restrictedLastName = true; } public void restrictEditEmail() { m_restrictedEmail = true; } public void restrictEditPassword() { m_restrictedPassword = true; } public void restrictEditEid() { m_restrictedEid = true; } public void restrictEditType() { m_restrictedType = true; } /** * Take all values from this object. * * @param user * The user object to take values from. */ protected void set(User user) { setAll(user); } /** * Access the event code for this edit. * * @return The event code for this edit. */ protected String getEvent() { return m_event; } /** * Set the event code for this edit. * * @param event * The event code for this edit. */ protected void setEvent(String event) { m_event = event; } /** * @inheritDoc */ public ResourcePropertiesEdit getPropertiesEdit() { // if lazy, resolve if (((BaseResourceProperties) m_properties).isLazy()) { ((BaseResourcePropertiesEdit) m_properties).setLazy(false); m_storage.readProperties(this, m_properties); } return m_properties; } /** * Enable editing. */ protected void activate() { m_active = true; } /** * @inheritDoc */ public boolean isActiveEdit() { return m_active; } /** * Close the edit object - it cannot be used after this. */ protected void closeEdit() { m_active = false; } /** * Check this User object to see if it is selected by the criteria. * * @param criteria * The critera. * @return True if the User object is selected by the criteria, false if not. */ protected boolean selectedBy(String criteria) { if (StringUtil.containsIgnoreCase(getSortName(), criteria) || StringUtil.containsIgnoreCase(getDisplayName(), criteria) || StringUtil.containsIgnoreCase(getEid(), criteria) || StringUtil.containsIgnoreCase(getEmail(), criteria)) { return true; } return false; } /****************************************************************************************************************************************************************************************************************************************************** * SessionBindingListener implementation *****************************************************************************************************************************************************************************************************************************************************/ /** * @inheritDoc */ public void valueBound(SessionBindingEvent event) { } /** * @inheritDoc */ public void valueUnbound(SessionBindingEvent event) { if (M_log.isDebugEnabled()) M_log.debug("valueUnbound()"); // catch the case where an edit was made but never resolved if (m_active) { cancelEdit(this); } } } /********************************************************************************************************************************************************************************************************************************************************** * Storage *********************************************************************************************************************************************************************************************************************************************************/ protected interface Storage { /** * Open. */ public void open(); /** * Close. */ public void close(); /** * Check if a user by this id exists. * * @param id * The user id. * @return true if a user by this id exists, false if not. */ public boolean check(String id); /** * Get the user with this id, or null if not found. * * @param id * The user id. * @return The user with this id, or null if not found. */ public UserEdit getById(String id); /** * Get the users with this email, or return empty if none found. * * @param id * The user email. * @return The Collection (User) of users with this email, or an empty collection if none found. */ public Collection findUsersByEmail(String email); /** * Get all users. * * @return The List (UserEdit) of all users. */ public List getAll(); /** * Get all the users in record range. * * @param first * The first record position to return. * @param last * The last record position to return. * @return The List (BaseUserEdit) of all users. */ public List getAll(int first, int last); /** * Count all the users. * * @return The count of all users. */ public int count(); /** * Search for users with id or email, first or last name matching criteria, in range. * * @param criteria * The search criteria. * @param first * The first record position to return. * @param last * The last record position to return. * @return The List (BaseUserEdit) of all alias. */ public List search(String criteria, int first, int last); /** * Count all the users with id or email, first or last name matching criteria. * * @param criteria * The search criteria. * @return The count of all aliases with id or target matching criteria. */ public int countSearch(String criteria); /** * Add a new user with this id and eid. * * @param id * The user id. * @param eid * The user eid. * @return The locked User object with this id and eid, or null if the id is in use. */ public UserEdit put(String id, String eid); /** * Get a lock on the user with this id, or null if a lock cannot be gotten. * * @param id * The user id. * @return The locked User with this id, or null if this records cannot be locked. */ public UserEdit edit(String id); /** * Commit the changes and release the lock. * * @param user * The user to commit. * @return true if successful, false if not (eid may not be unique). */ public boolean commit(UserEdit user); /** * Cancel the changes and release the lock. * * @param user * The user to commit. */ public void cancel(UserEdit user); /** * Remove this user. * * @param user * The user to remove. */ public void remove(UserEdit user); /** * Read properties from storage into the edit's properties. * * @param edit * The user to read properties for. */ public void readProperties(UserEdit edit, ResourcePropertiesEdit props); /** * Create a mapping between the id and eid. * * @param id * The user id. * @param eid * The user eid. * @return true if successful, false if not (id or eid might be in use). */ public boolean putMap(String id, String eid); /** * Check the id -> eid mapping: lookup this id and return the eid if found * * @param id * The user id to lookup. * @return The eid mapped to this id, or null if none. */ public String checkMapForEid(String id); /** * Check the id -> eid mapping: lookup this eid and return the id if found * * @param eid * The user eid to lookup. * @return The id mapped to this eid, or null if none. */ public String checkMapForId(String eid); /** * Since optimizing this call requires access to SQL result sets and * internally-maintained caches, all the real work is performed by * the storage class. * * @param ids * @return any user records with matching IDs */ public List<User> getUsersByIds(Collection<String> ids); /** * Since optimizing this call requires access to SQL result sets and * internally-maintained caches, all the real work is performed by * the storage class. * * @param eids * @return any user records with matching EIDs */ public List<User> getUsersByEids(Collection<String> eids); } }