/*
* Copyright (C) 2005-2013 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see .
*/
package org.alfresco.repo.security.person;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.query.CannedQueryFactory;
import org.alfresco.query.CannedQueryResults;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.action.executer.MailActionExecuter;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.domain.permissions.AclDAO;
import org.alfresco.repo.node.NodeServicePolicies;
import org.alfresco.repo.node.NodeServicePolicies.BeforeCreateNodePolicy;
import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.search.SearcherException;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.permissions.PermissionServiceSPI;
import org.alfresco.repo.tenant.TenantDomainMismatchException;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.admin.RepoAdminService;
import org.alfresco.service.cmr.admin.RepoUsage.UsageType;
import org.alfresco.service.cmr.admin.RepoUsageStatus;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.invitation.InvitationException;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.TemplateService;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.search.LimitBy;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.AuthorityType;
import org.alfresco.service.cmr.security.MutableAuthenticationService;
import org.alfresco.service.cmr.security.NoSuchPersonException;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.NamespacePrefixResolver;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.GUID;
import org.alfresco.util.ModelUtil;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck;
import org.alfresco.util.registry.NamedObjectRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.util.I18NUtil;
public class PersonServiceImpl extends TransactionListenerAdapter implements PersonService,
NodeServicePolicies.BeforeCreateNodePolicy,
NodeServicePolicies.OnCreateNodePolicy,
NodeServicePolicies.BeforeDeleteNodePolicy,
NodeServicePolicies.OnUpdatePropertiesPolicy
{
private static Log logger = LogFactory.getLog(PersonServiceImpl.class);
private static String DELETING_PERSON_SET_RESOURCE = "DeletingPersonSetResource";
private static final String CANNED_QUERY_PEOPLE_LIST = "getPeopleCannedQueryFactory";
private static final String DELETE = "DELETE";
private static final String SPLIT = "SPLIT";
private static final String LEAVE = "LEAVE";
public static final String SYSTEM_FOLDER_SHORT_QNAME = "sys:system";
public static final String PEOPLE_FOLDER_SHORT_QNAME = "sys:people";
private static final String SYSTEM_USAGE_WARN_LIMIT_USERS_EXCEEDED_VERBOSE = "system.usage.err.limit_users_exceeded_verbose";
private static final String KEY_POST_TXN_DUPLICATES = "PersonServiceImpl.KEY_POST_TXN_DUPLICATES";
public static final String KEY_ALLOW_UID_UPDATE = "PersonServiceImpl.KEY_ALLOW_UID_UPDATE";
private static final String KEY_USERS_CREATED = "PersonServiceImpl.KEY_USERS_CREATED";
private StoreRef storeRef;
private TransactionService transactionService;
private NodeService nodeService;
private TenantService tenantService;
private SearchService searchService;
private AuthorityService authorityService;
private MutableAuthenticationService authenticationService;
private DictionaryService dictionaryService;
private PermissionServiceSPI permissionServiceSPI;
private NamespacePrefixResolver namespacePrefixResolver;
private HomeFolderManager homeFolderManager;
private PolicyComponent policyComponent;
private AclDAO aclDao;
private PermissionsManager permissionsManager;
private RepoAdminService repoAdminService;
private ServiceRegistry serviceRegistry;
private boolean createMissingPeople;
private static Set mutableProperties;
private String defaultHomeFolderProvider;
private boolean processDuplicates = true;
private String duplicateMode = LEAVE;
private boolean lastIsBest = true;
private boolean includeAutoCreated = false;
private NamedObjectRegistry> cannedQueryRegistry;
/** a transactionally-safe cache to be injected */
private SimpleCache> personCache;
/** People Container ref cache (Tennant aware) */
private Map peopleContainerRefs = new ConcurrentHashMap(4);
private UserNameMatcher userNameMatcher;
private JavaBehaviour beforeCreateNodeValidationBehaviour;
private JavaBehaviour beforeDeleteNodeValidationBehaviour;
private boolean homeFolderCreationEager;
static
{
Set props = new HashSet();
props.add(ContentModel.PROP_HOMEFOLDER);
props.add(ContentModel.PROP_FIRSTNAME);
// Middle Name
props.add(ContentModel.PROP_LASTNAME);
props.add(ContentModel.PROP_EMAIL);
props.add(ContentModel.PROP_ORGID);
mutableProperties = Collections.unmodifiableSet(props);
}
@Override
public boolean equals(Object obj)
{
return this == obj;
}
@Override
public int hashCode()
{
return 1;
}
/**
* Spring bean init method
*/
public void init()
{
PropertyCheck.mandatory(this, "storeUrl", storeRef);
PropertyCheck.mandatory(this, "transactionService", transactionService);
PropertyCheck.mandatory(this, "nodeService", nodeService);
PropertyCheck.mandatory(this, "permissionServiceSPI", permissionServiceSPI);
PropertyCheck.mandatory(this, "authorityService", authorityService);
PropertyCheck.mandatory(this, "authenticationService", authenticationService);
PropertyCheck.mandatory(this, "namespacePrefixResolver", namespacePrefixResolver);
PropertyCheck.mandatory(this, "policyComponent", policyComponent);
PropertyCheck.mandatory(this, "personCache", personCache);
PropertyCheck.mandatory(this, "aclDao", aclDao);
PropertyCheck.mandatory(this, "homeFolderManager", homeFolderManager);
PropertyCheck.mandatory(this, "repoAdminService", repoAdminService);
beforeCreateNodeValidationBehaviour = new JavaBehaviour(this, "beforeCreateNodeValidation");
this.policyComponent.bindClassBehaviour(
BeforeCreateNodePolicy.QNAME,
ContentModel.TYPE_PERSON,
beforeCreateNodeValidationBehaviour);
beforeDeleteNodeValidationBehaviour = new JavaBehaviour(this, "beforeDeleteNodeValidation");
this.policyComponent.bindClassBehaviour(
BeforeDeleteNodePolicy.QNAME,
ContentModel.TYPE_PERSON,
beforeDeleteNodeValidationBehaviour);
this.policyComponent.bindClassBehaviour(
OnCreateNodePolicy.QNAME,
ContentModel.TYPE_PERSON,
new JavaBehaviour(this, "onCreateNode"));
this.policyComponent.bindClassBehaviour(
BeforeDeleteNodePolicy.QNAME,
ContentModel.TYPE_PERSON,
new JavaBehaviour(this, "beforeDeleteNode"));
this.policyComponent.bindClassBehaviour(
OnUpdatePropertiesPolicy.QNAME,
ContentModel.TYPE_PERSON,
new JavaBehaviour(this, "onUpdateProperties"));
this.policyComponent.bindClassBehaviour(
OnUpdatePropertiesPolicy.QNAME,
ContentModel.TYPE_USER,
new JavaBehaviour(this, "onUpdatePropertiesUser"));
}
/**
* {@inheritDoc}
*/
public void setCreateMissingPeople(boolean createMissingPeople)
{
this.createMissingPeople = createMissingPeople;
}
public void setNamespacePrefixResolver(NamespacePrefixResolver namespacePrefixResolver)
{
this.namespacePrefixResolver = namespacePrefixResolver;
}
public void setAuthorityService(AuthorityService authorityService)
{
this.authorityService = authorityService;
}
public void setAuthenticationService(MutableAuthenticationService authenticationService)
{
this.authenticationService = authenticationService;
}
public void setDictionaryService(DictionaryService dictionaryService)
{
this.dictionaryService = dictionaryService;
}
public void setPermissionServiceSPI(PermissionServiceSPI permissionServiceSPI)
{
this.permissionServiceSPI = permissionServiceSPI;
}
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
public void setServiceRegistry(ServiceRegistry serviceRegistry)
{
this.serviceRegistry = serviceRegistry;
}
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
public void setTenantService(TenantService tenantService)
{
this.tenantService = tenantService;
}
public void setSearchService(SearchService searchService)
{
this.searchService = searchService;
}
public void setRepoAdminService(RepoAdminService repoAdminService)
{
this.repoAdminService = repoAdminService;
}
public void setPolicyComponent(PolicyComponent policyComponent)
{
this.policyComponent = policyComponent;
}
public void setStoreUrl(String storeUrl)
{
this.storeRef = new StoreRef(storeUrl);
}
public void setUserNameMatcher(UserNameMatcher userNameMatcher)
{
this.userNameMatcher = userNameMatcher;
}
void setDefaultHomeFolderProvider(String defaultHomeFolderProvider)
{
this.defaultHomeFolderProvider = defaultHomeFolderProvider;
}
public void setDuplicateMode(String duplicateMode)
{
this.duplicateMode = duplicateMode;
}
public void setIncludeAutoCreated(boolean includeAutoCreated)
{
this.includeAutoCreated = includeAutoCreated;
}
public void setLastIsBest(boolean lastIsBest)
{
this.lastIsBest = lastIsBest;
}
public void setProcessDuplicates(boolean processDuplicates)
{
this.processDuplicates = processDuplicates;
}
public void setHomeFolderManager(HomeFolderManager homeFolderManager)
{
this.homeFolderManager = homeFolderManager;
}
/**
* Indicates if home folders should be created when the person
* is created or delayed until first accessed.
*/
public void setHomeFolderCreationEager(boolean homeFolderCreationEager)
{
this.homeFolderCreationEager = homeFolderCreationEager;
}
public void setAclDAO(AclDAO aclDao)
{
this.aclDao = aclDao;
}
public void setPermissionsManager(PermissionsManager permissionsManager)
{
this.permissionsManager = permissionsManager;
}
/**
* Set the registry of {@link CannedQueryFactory canned queries}
*/
public void setCannedQueryRegistry(NamedObjectRegistry> cannedQueryRegistry)
{
this.cannedQueryRegistry = cannedQueryRegistry;
}
/**
* Set the username to person cache.
*/
public void setPersonCache(SimpleCache> personCache)
{
this.personCache = personCache;
}
/**
* Avoid injection issues: Look it up from the Service Registry as required
*/
private FileFolderService getFileFolderService()
{
return serviceRegistry.getFileFolderService();
}
/**
* Avoid injection issues: Look it up from the Service Registry as required
*/
private NamespaceService getNamespaceService()
{
return serviceRegistry.getNamespaceService();
}
/**
* Avoid injection issues: Look it up from the Service Registry as required
*/
private ActionService getActionService()
{
return serviceRegistry.getActionService();
}
/**
* {@inheritDoc}
*/
public NodeRef getPerson(String userName)
{
return getPerson(userName, true);
}
/**
* {@inheritDoc}
*/
public NodeRef getPerson(final String userName, final boolean autoCreateHomeFolderAndMissingPersonIfAllowed)
{
return getPersonImpl(userName, autoCreateHomeFolderAndMissingPersonIfAllowed);
}
private NodeRef getPersonImpl(String userName, boolean autoCreateHomeFolderAndMissingPersonIfAllowed)
{
if(userName == null)
{
return null;
}
if(userName.length() == 0)
{
return null;
}
NodeRef personNode = getPersonOrNull(userName);
if (personNode == null)
{
TxnReadState txnReadState = AlfrescoTransactionSupport.getTransactionReadState();
if (autoCreateHomeFolderAndMissingPersonIfAllowed && createMissingPeople() &&
txnReadState == TxnReadState.TXN_READ_WRITE)
{
// We create missing people AND are in a read-write txn
return createMissingPerson(userName, true);
}
else
{
throw new NoSuchPersonException(userName);
}
}
else if (autoCreateHomeFolderAndMissingPersonIfAllowed)
{
makeHomeFolderIfRequired(personNode);
}
return personNode;
}
/**
* {@inheritDoc}
*/
public boolean personExists(String caseSensitiveUserName)
{
return getPersonOrNull(caseSensitiveUserName) != null;
}
private NodeRef getPersonOrNull(String searchUserName)
{
Set allRefs = getFromCache(searchUserName);
boolean addToCache = false;
if (allRefs == null)
{
List childRefs = nodeService.getChildAssocs(
getPeopleContainer(),
ContentModel.ASSOC_CHILDREN,
getChildNameLower(searchUserName),
false);
allRefs = new LinkedHashSet(childRefs.size() * 2);
for (ChildAssociationRef childRef : childRefs)
{
NodeRef nodeRef = childRef.getChildRef();
allRefs.add(nodeRef);
}
addToCache = true;
}
List refs = new ArrayList(allRefs.size());
Set nodesToRemoveFromCache = new HashSet();
for (NodeRef nodeRef : allRefs)
{
if (nodeService.exists(nodeRef))
{
Serializable value = nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME);
String realUserName = DefaultTypeConverter.INSTANCE.convert(String.class, value);
if (userNameMatcher.matches(searchUserName, realUserName))
{
refs.add(nodeRef);
}
}
else
{
nodesToRemoveFromCache.add(nodeRef);
}
}
if (!nodesToRemoveFromCache.isEmpty())
{
allRefs.removeAll(nodesToRemoveFromCache);
}
NodeRef returnRef = null;
if (refs.size() > 1)
{
returnRef = handleDuplicates(refs, searchUserName);
}
else if (refs.size() == 1)
{
returnRef = refs.get(0);
if (addToCache)
{
// Don't bother caching unless we get a result that doesn't need duplicate processing
putToCache(searchUserName, allRefs);
}
}
return returnRef;
}
private NodeRef handleDuplicates(List refs, String searchUserName)
{
if (processDuplicates)
{
NodeRef best = findBest(refs);
HashSet toHandle = new HashSet();
toHandle.addAll(refs);
toHandle.remove(best);
addDuplicateNodeRefsToHandle(toHandle);
return best;
}
else
{
String userNameSensitivity = " (user name is case-" + (userNameMatcher.getUserNamesAreCaseSensitive() ? "sensitive" : "insensitive") + ")";
String domainNameSensitivity = "";
if (!userNameMatcher.getDomainSeparator().equals(""))
{
domainNameSensitivity = " (domain name is case-" + (userNameMatcher.getDomainNamesAreCaseSensitive() ? "sensitive" : "insensitive") + ")";
}
throw new AlfrescoRuntimeException("Found more than one user for " + searchUserName + userNameSensitivity + domainNameSensitivity);
}
}
/**
* Get the txn-bound usernames that need cleaning up
*/
private Set getPostTxnDuplicates()
{
@SuppressWarnings("unchecked")
Set postTxnDuplicates = (Set) AlfrescoTransactionSupport.getResource(KEY_POST_TXN_DUPLICATES);
if (postTxnDuplicates == null)
{
postTxnDuplicates = new HashSet();
AlfrescoTransactionSupport.bindResource(KEY_POST_TXN_DUPLICATES, postTxnDuplicates);
}
return postTxnDuplicates;
}
/**
* Flag a username for cleanup after the transaction.
*/
private void addDuplicateNodeRefsToHandle(Set refs)
{
// Firstly, bind this service to the transaction
AlfrescoTransactionSupport.bindListener(this);
// Now get the post txn duplicate list
Set postTxnDuplicates = getPostTxnDuplicates();
postTxnDuplicates.addAll(refs);
}
/**
* Process clean up any duplicates that were flagged during the transaction.
*/
@Override
public void afterCommit()
{
// Get the duplicates in a form that can be read by the transaction work anonymous instance
final Set postTxnDuplicates = getPostTxnDuplicates();
if (postTxnDuplicates.size() == 0)
{
// Nothing to do
return;
}
RetryingTransactionCallback