diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/PeopleImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/PeopleImpl.java index d789a58d4e..b689625797 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/PeopleImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/PeopleImpl.java @@ -1,996 +1,990 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2017 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * 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 . - * #L% - */ -package org.alfresco.rest.api.impl; - -import org.alfresco.model.ContentModel; -import org.alfresco.query.PagingRequest; -import org.alfresco.query.PagingResults; -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.authentication.ResetPasswordService; -import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordDetails; -import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordWorkflowException; -import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordWorkflowInvalidUserException; -import org.alfresco.repo.security.sync.UserRegistrySynchronizer; -import org.alfresco.rest.api.Nodes; -import org.alfresco.rest.api.People; -import org.alfresco.rest.api.Renditions; -import org.alfresco.rest.api.Sites; -import org.alfresco.rest.api.model.Node; -import org.alfresco.rest.api.model.PasswordReset; -import org.alfresco.rest.api.model.Person; -import org.alfresco.rest.api.model.Rendition; -import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; -import org.alfresco.rest.framework.core.exceptions.DisabledServiceException; -import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; -import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; -import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; -import org.alfresco.rest.framework.resource.content.BasicContentInfo; -import org.alfresco.rest.framework.resource.content.BinaryResource; -import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; -import org.alfresco.rest.framework.resource.parameters.Paging; -import org.alfresco.rest.framework.resource.parameters.Parameters; -import org.alfresco.rest.framework.resource.parameters.SortColumn; -import org.alfresco.service.cmr.repository.AssociationRef; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.ContentData; -import org.alfresco.service.cmr.repository.ContentReader; -import org.alfresco.service.cmr.repository.ContentService; -import org.alfresco.service.cmr.repository.ContentWriter; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.security.AuthenticationService; -import org.alfresco.service.cmr.security.AuthorityService; -import org.alfresco.service.cmr.security.MutableAuthenticationService; -import org.alfresco.service.cmr.security.NoSuchPersonException; -import org.alfresco.service.cmr.security.PermissionService; -import org.alfresco.service.cmr.security.PersonService; -import org.alfresco.service.cmr.site.SiteService; -import org.alfresco.service.cmr.thumbnail.ThumbnailService; -import org.alfresco.service.cmr.usage.ContentUsageService; -import org.alfresco.service.namespace.NamespaceService; -import org.alfresco.service.namespace.QName; -import org.alfresco.util.Pair; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import java.io.InputStream; -import java.io.Serializable; -import java.util.AbstractList; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Centralises access to people services and maps between representations. - * - * @author steveglover - * @since publicapi1.0 - */ -public class PeopleImpl implements People -{ - private static final Log LOGGER = LogFactory.getLog(PeopleImpl.class); - - private static final List EXCLUDED_NS = Arrays.asList( - NamespaceService.SYSTEM_MODEL_1_0_URI, - "http://www.alfresco.org/model/user/1.0", - NamespaceService.CONTENT_MODEL_1_0_URI); - private static final List EXCLUDED_ASPECTS = Arrays.asList(); - private static final List EXCLUDED_PROPS = Arrays.asList(); - private static final int USERNAME_MAXLENGTH = 100; - private static final String[] RESERVED_AUTHORITY_PREFIXES = - { - PermissionService.GROUP_PREFIX, - PermissionService.ROLE_PREFIX - }; - - protected Nodes nodes; - protected Sites sites; - protected SiteService siteService; - protected NodeService nodeService; - protected PersonService personService; - protected AuthenticationService authenticationService; - protected AuthorityService authorityService; - protected ContentUsageService contentUsageService; - protected ContentService contentService; - protected ThumbnailService thumbnailService; - protected ResetPasswordService resetPasswordService; - protected UserRegistrySynchronizer userRegistrySynchronizer; - protected Renditions renditions; - private Boolean allowImmutableEnabledUpdate; - - private final static Map sort_params_to_qnames; - static - { - Map aMap = new HashMap<>(3); - aMap.put(PARAM_FIRST_NAME, ContentModel.PROP_FIRSTNAME); - aMap.put(PARAM_LAST_NAME, ContentModel.PROP_LASTNAME); - aMap.put(PARAM_ID, ContentModel.PROP_USERNAME); - sort_params_to_qnames = Collections.unmodifiableMap(aMap); - } - - public void setSites(Sites sites) - { - this.sites = sites; - } - - public void setSiteService(SiteService siteService) - { - this.siteService = siteService; - } - - public void setNodes(Nodes nodes) - { - this.nodes = nodes; - } - - public void setNodeService(NodeService nodeService) - { - this.nodeService = nodeService; - } - - public void setPersonService(PersonService personService) - { - this.personService = personService; - } - - public void setAuthenticationService(AuthenticationService authenticationService) - { - this.authenticationService = authenticationService; - } - - public void setAuthorityService(AuthorityService authorityService) - { - this.authorityService = authorityService; - } - - public void setContentUsageService(ContentUsageService contentUsageService) - { - this.contentUsageService = contentUsageService; - } - - public void setContentService(ContentService contentService) - { - this.contentService = contentService; - } - - public void setThumbnailService(ThumbnailService thumbnailService) - { - this.thumbnailService = thumbnailService; - } - - public void setResetPasswordService(ResetPasswordService resetPasswordService) - { - this.resetPasswordService = resetPasswordService; - } - - public void setRenditions(Renditions renditions) - { - this.renditions = renditions; - } - - public void setUserRegistrySynchronizer(UserRegistrySynchronizer userRegistrySynchronizer) - { - this.userRegistrySynchronizer = userRegistrySynchronizer; - } - - public void setAllowImmutableEnabledUpdate(Boolean allowImmutableEnabledUpdate) - { - this.allowImmutableEnabledUpdate = allowImmutableEnabledUpdate; - } - - /** - * Validate, perform -me- substitution and canonicalize the person ID. - * - * @param personId - * @return The validated and processed ID. - */ - @Override - public String validatePerson(String personId) - { - return validatePerson(personId, false); - } - - @Override - public String validatePerson(final String requestedPersonId, boolean validateIsCurrentUser) - { - String personId = requestedPersonId; - if(personId == null) - { - throw new InvalidArgumentException("personId is null."); - } - - if(personId.equalsIgnoreCase(DEFAULT_USER)) - { - personId = AuthenticationUtil.getFullyAuthenticatedUser(); - } - - personId = personService.getUserIdentifier(personId); - if(personId == null) - { - // Could not find canonical user ID by case-sensitive ID. - throw new EntityNotFoundException(requestedPersonId); - } - - if(validateIsCurrentUser) - { - String currentUserId = AuthenticationUtil.getFullyAuthenticatedUser(); - if(!currentUserId.equalsIgnoreCase(personId)) - { - throw new EntityNotFoundException(personId); - } - } - - return personId; - } - - protected void processPersonProperties(String userName, final Map nodeProps) - { - if(!contentUsageService.getEnabled()) - { - // quota used will always be 0 in this case so remove from the person properties - nodeProps.remove(ContentModel.PROP_SIZE_QUOTA); - nodeProps.remove(ContentModel.PROP_SIZE_CURRENT); - } - - // The person description is located in a separate content file located at cm:persondescription - // "Inline" this data, by removing the cm:persondescription property and adding a temporary property - // (Person.PROP_PERSON_DESCRIPTION) containing the actual content as a string. - final ContentData personDescription = (ContentData)nodeProps.get(ContentModel.PROP_PERSONDESC); - if(personDescription != null) - { - nodeProps.remove(ContentModel.PROP_PERSONDESC); - - AuthenticationUtil.runAsSystem(new RunAsWork() - { - @Override - public Void doWork() throws Exception - { - ContentReader reader = contentService.getRawReader(personDescription.getContentUrl()); - if(reader != null && reader.exists()) - { - String description = reader.getContentString(); - nodeProps.put(Person.PROP_PERSON_DESCRIPTION, description); - } - - return null; - } - }); - } - } - - public boolean hasAvatar(NodeRef personNodeRef) - { - return (getAvatarOriginal(personNodeRef) != null); - } - - @Override - public NodeRef getAvatar(String personId) - { - NodeRef avatar = null; - personId = validatePerson(personId); - NodeRef personNode = personService.getPerson(personId); - if(personNode != null) - { - NodeRef avatarOrig = getAvatarOriginal(personNode); - avatar = thumbnailService.getThumbnailByName(avatarOrig, ContentModel.PROP_CONTENT, "avatar"); - } - - if (avatar == null) - { - throw new EntityNotFoundException(personId); - } - - return avatar; - } - - private NodeRef getAvatarOriginal(NodeRef personNode) - { - NodeRef avatarOrigNodeRef = null; - List avatarChildAssocs = nodeService.getChildAssocs(personNode, Collections.singleton(ContentModel.ASSOC_PREFERENCE_IMAGE)); - if (avatarChildAssocs.size() > 0) - { - ChildAssociationRef ref = avatarChildAssocs.get(0); - avatarOrigNodeRef = ref.getChildRef(); - } - else - { - // TODO do we still need this ? - backward compatible with JSF web-client avatar - List avatorAssocs = nodeService.getTargetAssocs(personNode, ContentModel.ASSOC_AVATAR); - if (avatorAssocs.size() > 0) - { - AssociationRef ref = avatorAssocs.get(0); - avatarOrigNodeRef = ref.getTargetRef(); - } - } - return avatarOrigNodeRef; - } - - @Override - public BinaryResource downloadAvatarContent(String personId, Parameters parameters) - { - personId = validatePerson(personId); - NodeRef personNode = personService.getPerson(personId); - NodeRef avatarNodeRef = getAvatarOriginal(personNode); - - return renditions.getContentNoValidation(avatarNodeRef, "avatar", parameters); - } - - @Override - public Person uploadAvatarContent(String personId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters) - { - if (!thumbnailService.getThumbnailsEnabled()) - { - throw new DisabledServiceException("Thumbnail generation has been disabled."); - } - - personId = validatePerson(personId); - checkCurrentUserOrAdmin(personId); - - NodeRef personNode = personService.getPerson(personId); - NodeRef avatarOrigNodeRef = getAvatarOriginal(personNode); - - if (avatarOrigNodeRef != null) - { - deleteAvatar(avatarOrigNodeRef); - } - - QName origAvatarQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "origAvatar"); - nodeService.addAspect(personNode, ContentModel.ASPECT_PREFERENCES, null); - ChildAssociationRef assoc = nodeService.createNode(personNode, ContentModel.ASSOC_PREFERENCE_IMAGE, origAvatarQName, - ContentModel.TYPE_CONTENT); - NodeRef avatar = assoc.getChildRef(); - String avatarOriginalNodeId = avatar.getId(); - - // TODO do we still need this ? - backward compatible with JSF web-client avatar - nodeService.createAssociation(personNode, avatar, ContentModel.ASSOC_AVATAR); - - Node n = nodes.updateContent(avatarOriginalNodeId, contentInfo, stream, parameters); - String mimeType = n.getContent().getMimeType(); - - if (mimeType.indexOf("image/") != 0) - { - throw new InvalidArgumentException( - "Uploaded content must be an image (content type determined to be '"+mimeType+"')"); - } - - // create thumbnail synchronously - Rendition avatarR = new Rendition(); - avatarR.setId("avatar"); - renditions.createRendition(avatar, avatarR, false, parameters); - - List include = Arrays.asList( - PARAM_INCLUDE_ASPECTNAMES, - PARAM_INCLUDE_PROPERTIES); - - return getPersonWithProperties(personId, include); - } - - @Override - public void deleteAvatarContent(String personId) - { - personId = validatePerson(personId); - checkCurrentUserOrAdmin(personId); - - NodeRef personNode = personService.getPerson(personId); - NodeRef avatarOrigNodeRef = getAvatarOriginal(personNode); - if (avatarOrigNodeRef != null) - { - deleteAvatar(avatarOrigNodeRef); - } - } - - private void deleteAvatar(NodeRef avatarOrigNodeRef) - { - // Set as temporary to permanently delete node (instead of archiving) - nodeService.addAspect(avatarOrigNodeRef, ContentModel.ASPECT_TEMPORARY, null); - - nodeService.deleteNode(avatarOrigNodeRef); - } - - - /** - * Get a full representation of a person. - * - * @throws NoSuchPersonException - * if personId does not exist - */ - @Override - public Person getPerson(String personId) - { - personId = validatePerson(personId); - List include = Arrays.asList( - PARAM_INCLUDE_ASPECTNAMES, - PARAM_INCLUDE_PROPERTIES, - PARAM_INCLUDE_CAPABILITIES); - Person person = getPersonWithProperties(personId, include); - - return person; - } - - public Person getPerson(String personId, List include) - { - personId = validatePerson(personId); - Person person = getPersonWithProperties(personId, include); - - return person; - } - - @Override - public CollectionWithPagingInfo getPeople(final Parameters parameters) - { - Paging paging = parameters.getPaging(); - PagingRequest pagingRequest = Util.getPagingRequest(paging); - - List> sortProps = getSortProps(parameters); - - // For now the results are not filtered - // please see REPO-555 - final PagingResults pagingResult = personService.getPeople(null, null, sortProps, pagingRequest); - - final List page = pagingResult.getPage(); - int totalItems = pagingResult.getTotalResultCount().getFirst(); - final String personId = AuthenticationUtil.getFullyAuthenticatedUser(); - List people = new AbstractList() - { - @Override - public Person get(int index) - { - PersonService.PersonInfo personInfo = page.get(index); - Person person = getPersonWithProperties(personInfo.getUserName(), parameters.getInclude()); - return person; - } - - @Override - public int size() - { - return page.size(); - } - }; - - return CollectionWithPagingInfo.asPaged(paging, people, pagingResult.hasMoreItems(), totalItems); - } - - private List> getSortProps(Parameters parameters) - { - List> sortProps = new ArrayList<>(); - List sortCols = parameters.getSorting(); - if ((sortCols != null) && (sortCols.size() > 0)) - { - for (SortColumn sortCol : sortCols) - { - QName sortPropQName = sort_params_to_qnames.get(sortCol.column); - if (sortPropQName == null) - { - throw new InvalidArgumentException("Invalid sort field: " + sortCol.column); - } - sortProps.add(new Pair<>(sortPropQName, (sortCol.asc ? Boolean.TRUE : Boolean.FALSE))); - } - } - else - { - // default sort order - sortProps.add(new Pair<>(ContentModel.PROP_USERNAME, Boolean.TRUE)); - } - return sortProps; - } - - private Person getPersonWithProperties(String personId, List include) - { - Person person = null; - NodeRef personNode = personService.getPerson(personId, false); - if (personNode != null) - { - Map nodeProps = nodeService.getProperties(personNode); - processPersonProperties(personId, nodeProps); - // TODO this needs to be run as admin but should we do this here? - final String pId = personId; - Boolean enabled = AuthenticationUtil.runAsSystem(new RunAsWork() - { - public Boolean doWork() throws Exception - { - return authenticationService.getAuthenticationEnabled(pId); - } - }); - person = new Person(personNode, nodeProps, enabled); - - // Remove the temporary property used to help inline the person description content property. - // It may be accessed from the person object (person.getDescription()). - nodeProps.remove(Person.PROP_PERSON_DESCRIPTION); - - // Expose properties - if (include.contains(PARAM_INCLUDE_PROPERTIES)) - { - // Note that custProps may be null. - Map custProps = nodes.mapFromNodeProperties(nodeProps, new ArrayList<>(), new HashMap<>(), EXCLUDED_NS, EXCLUDED_PROPS); - person.setProperties(custProps); - } - if (include.contains(PARAM_INCLUDE_ASPECTNAMES)) - { - // Expose aspect names - Set aspects = nodeService.getAspects(personNode); - person.setAspectNames(nodes.mapFromNodeAspects(aspects, EXCLUDED_NS, EXCLUDED_ASPECTS)); - } - if (include.contains(PARAM_INCLUDE_CAPABILITIES)) - { - // Expose capabilities - Map capabilities = new HashMap<>(3); - capabilities.put("isAdmin", isAdminAuthority(personId)); - capabilities.put("isGuest", isGuestAuthority(personId)); - capabilities.put("isMutable", isMutableAuthority(personId)); - person.setCapabilities(capabilities); - } - - // get avatar information - if (hasAvatar(personNode)) - { - try - { - NodeRef avatar = getAvatar(personId); - person.setAvatarId(avatar); - } - catch (EntityNotFoundException e) - { - // shouldn't happen, but ok - } - } - } - else - { - throw new EntityNotFoundException(personId); - } - - return person; - } - - @Override - public Person create(Person person) - { - validateCreatePersonData(person); - - if (! isAdminAuthority()) - { - // note: do an explict check for admin here (since personExists does not throw 403 unlike createPerson, - // hence next block would cause 409 to be returned) - throw new PermissionDeniedException(); - } - - // Unfortunately PersonService.createPerson(...) only throws an AlfrescoRuntimeException - // rather than a more specific exception and does not use a message ID either, so there's - // no sensible way to know that it was thrown due to the user already existing - hence this check here. - if (personService.personExists(person.getUserName())) - { - throw new ConstraintViolatedException("Person '" + person.getUserName() + "' already exists."); - } - - // set enabled default value true - if (person.isEnabled() == null) - { - person.setEnabled(true); - } - - Map props = person.toProperties(); - - MutableAuthenticationService mas = (MutableAuthenticationService) authenticationService; - mas.createAuthentication(person.getUserName(), person.getPassword().toCharArray()); - mas.setAuthenticationEnabled(person.getUserName(), person.isEnabled()); - - // Add custom properties - if (person.getProperties() != null) - { - Map customProps = person.getProperties(); - props.putAll(nodes.mapToNodeProperties(customProps)); - } - - NodeRef nodeRef = personService.createPerson(props); - - // Add custom aspects - nodes.addCustomAspects(nodeRef, person.getAspectNames(), EXCLUDED_ASPECTS); - - // Write the contents of PersonUpdate.getDescription() text to a content file - // and store the content URL in ContentModel.PROP_PERSONDESC - if (person.getDescription() != null) - { - savePersonDescription(person.getDescription(), nodeRef); - } - - // Return a fresh retrieval - return getPerson(person.getUserName()); - } - - /** - * Write the description to a content file and store the content URL in - * ContentModel.PROP_PERSONDESC - * - * @param description - * @param nodeRef - */ - private void savePersonDescription(final String description, final NodeRef nodeRef) - { - AuthenticationUtil.runAsSystem(new RunAsWork() - { - @Override - public Void doWork() throws Exception - { - if (description != null) - { - ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_PERSONDESC, true); - writer.putContent(description); - } - else - { - nodeService.setProperty(nodeRef, ContentModel.PROP_PERSONDESC, null); - } - return null; - } - }); - } - - private void validateCreatePersonData(Person person) - { - // Mandatory field checks first - checkRequiredField("id", person.getUserName()); - checkRequiredField("firstName", person.getFirstName()); - checkRequiredField("email", person.getEmail()); - checkRequiredField("password", person.getPassword()); - - validateUsername(person.getUserName()); - nodes.validateAspects(person.getAspectNames(), EXCLUDED_NS, EXCLUDED_ASPECTS); - nodes.validateProperties(person.getProperties(), EXCLUDED_NS, EXCLUDED_PROPS); - } - - private void validateUsername(String username) - { - if (username.length() > 100) - { - throw new InvalidArgumentException("Username exceeds max length of " + USERNAME_MAXLENGTH + " characters."); - } - - for (String prefix : RESERVED_AUTHORITY_PREFIXES) - { - if (username.toUpperCase().startsWith(prefix)) - { - throw new IllegalArgumentException("Username cannot start with the reserved prefix '"+prefix+"'."); - } - } - } - - private void checkRequiredField(String fieldName, Object fieldValue) - { - if (fieldValue == null) - { - throw new InvalidArgumentException("Field '"+fieldName+"' is null, but is required."); - } - - // belts-and-braces - note: should not see empty string (since converted to null via custom json deserializer) - if ((fieldValue instanceof String) && ((String)fieldValue).isEmpty()) - { - throw new InvalidArgumentException("Field '"+fieldName+"' is empty, but is required."); - } - } - - @Override - public Person update(String personId, final Person person) - { - // Validate, perform -me- substitution and canonicalize the person ID. - personId = validatePerson(personId); - validateUpdatePersonData(person); - - // Check if user updating *their own* details or is an admin - boolean isAdmin = checkCurrentUserOrAdmin(personId); - - final String personIdToUpdate = validatePerson(personId); - final Map properties = person.toProperties(); - - // if requested, update password - updatePassword(isAdmin, personIdToUpdate, person); - - Set immutableProperties = userRegistrySynchronizer.getPersonMappedProperties(personIdToUpdate); - - Boolean isEnabled = person.isEnabled(); - if (isEnabled != null) - { - if (isAdminAuthority(personIdToUpdate)) - { - throw new PermissionDeniedException("Admin authority cannot be disabled."); - } - - if (allowImmutableEnabledStatusUpdate(personIdToUpdate, isAdmin, immutableProperties)) - { - LOGGER.info("User " + personIdToUpdate + " is immutable but enabled status will be set to: " + isEnabled); - } - else - { - // note: if current user is not an admin then permission denied exception is thrown - MutableAuthenticationService mutableAuthenticationService = (MutableAuthenticationService) authenticationService; - mutableAuthenticationService.setAuthenticationEnabled(personIdToUpdate, person.isEnabled()); - } - } - - NodeRef personNodeRef = personService.getPerson(personIdToUpdate, false); - if (person.wasSet(Person.PROP_PERSON_DESCRIPTION)) - { - // Remove person description from saved properties - properties.remove(ContentModel.PROP_PERSONDESC); - - // Custom save for person description. - savePersonDescription(person.getDescription(), personNodeRef); - } - - // Update custom aspects - do this *before* adding custom properties. The - // addition of custom properties may result in the auto-addition of aspects - // and we don't want to remove them during the update of explicitly specified aspects. - nodes.updateCustomAspects(personNodeRef, person.getAspectNames(), EXCLUDED_ASPECTS); - - // Add custom properties - if (person.getProperties() != null) - { - Map customProps = person.getProperties(); - properties.putAll(nodes.mapToNodeProperties(customProps)); - } - - // MNT-21150 LDAP synced attributes can't be changed using REST API - immutableProperties.forEach(immutableProperty -> { - if (properties.containsKey(immutableProperty)) - { - properties.remove(immutableProperty); - } - }); - - // The person service only allows admin users to set the properties by default. - if(!properties.isEmpty()) - { - AuthenticationUtil.runAsSystem(new RunAsWork() - { - @Override public Void doWork() throws Exception - { - personService.setPersonProperties(personIdToUpdate, properties, false); - return null; - } - }); - } - - return getPerson(personId); - } - - private boolean allowImmutableEnabledStatusUpdate(String userId, boolean isAdmin, Set immutableProperties) - { - if (allowImmutableEnabledUpdate) - { - boolean containLdapUserAccountStatus = false; - QName propertyNameToCheck = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "userAccountStatusProperty"); - - for (QName immutableProperty : immutableProperties) - { - if (immutableProperty.equals(propertyNameToCheck)) - { - containLdapUserAccountStatus = true; - break; - } - } - - return isAdmin && !containLdapUserAccountStatus && !isMutableAuthority(userId); - } - - return false; - } - - private boolean checkCurrentUserOrAdmin(String personId) - { - boolean isAdmin = isAdminAuthority(); - - String currentUserId = AuthenticationUtil.getFullyAuthenticatedUser(); - if (!isAdmin && !currentUserId.equalsIgnoreCase(personId)) - { - throw new PermissionDeniedException(); - } - - return isAdmin; - } - - private void validateUpdatePersonData(Person person) - { - nodes.validateAspects(person.getAspectNames(), EXCLUDED_NS, EXCLUDED_ASPECTS); - nodes.validateProperties(person.getProperties(), EXCLUDED_NS, EXCLUDED_PROPS); - - if (person.wasSet(ContentModel.PROP_FIRSTNAME)) - { - checkRequiredField("firstName", person.getFirstName()); - } - - if (person.wasSet(ContentModel.PROP_EMAIL)) - { - checkRequiredField("email", person.getEmail()); - } - - if (person.wasSet(ContentModel.PROP_ENABLED) && (person.isEnabled() == null)) - { - throw new IllegalArgumentException("'enabled' field cannot be empty."); - } - - if (person.wasSet(ContentModel.PROP_EMAIL_FEED_DISABLED) && (person.isEmailNotificationsEnabled() == null)) - { - throw new IllegalArgumentException("'emailNotificationsEnabled' field cannot be empty."); - } - } - - private void updatePassword(boolean isAdmin, String personIdToUpdate, Person person) - { - MutableAuthenticationService mutableAuthenticationService = (MutableAuthenticationService) authenticationService; - - boolean isOldPassword = person.wasSet(Person.PROP_PERSON_OLDPASSWORD); - boolean isPassword = person.wasSet(Person.PROP_PERSON_PASSWORD); - - if (isPassword || isOldPassword) - { - if (isOldPassword && ((person.getOldPassword() == null) || (person.getOldPassword().isEmpty()))) - { - throw new IllegalArgumentException("'oldPassword' field cannot be empty."); - } - - if (!isPassword || (person.getPassword() == null) || (person.getPassword().isEmpty())) - { - throw new IllegalArgumentException("password' field cannot be empty."); - } - - char[] newPassword = person.getPassword().toCharArray(); - - if (!isAdmin) - { - // Non-admin users can update their own password, but must provide their current password. - if (!isOldPassword) - { - throw new IllegalArgumentException("To change password, both 'oldPassword' and 'password' fields are required."); - } - - char[] oldPassword = person.getOldPassword().toCharArray(); - try - { - mutableAuthenticationService.updateAuthentication(personIdToUpdate, oldPassword, newPassword); - } - catch (AuthenticationException e) - { - throw new PermissionDeniedException("Incorrect password."); - } - } - else - { - // An admin user can update without knowing the original pass - but must know their own! - // note: is it reasonable to silently ignore oldPassword if supplied ? - - mutableAuthenticationService.setAuthentication(personIdToUpdate, newPassword); - } - } - } - - private boolean isAdminAuthority() - { - return authorityService.hasAdminAuthority(); - } - - private boolean isAdminAuthority(String authorityName) - { - return authorityService.isAdminAuthority(authorityName); - } - - @Deprecated - @Override - public void requestPasswordReset(String userId, String client) - { - // Validate the userId and the client - checkRequiredField("userId", userId); - checkRequiredField("client", client); - - // This is an un-authenticated API call so we wrap it to run as System - AuthenticationUtil.runAsSystem(() -> { - try - { - resetPasswordService.requestReset(userId, client); - } - catch (ResetPasswordWorkflowInvalidUserException ex) - { - // we don't throw an exception. - // For security reason (prevent the attackers to determine that userId exists in the system or not), - // the endpoint returns a 202 response if the userId does not exist or - // if the user is disabled by an Administrator. - if (LOGGER.isDebugEnabled()) - { - LOGGER.debug("Invalid user. " + ex.getMessage()); - } - } - - return null; - }); - } - - @Deprecated - @Override - public void resetPassword(String personId, final PasswordReset passwordReset) - { - checkResetPasswordData(passwordReset); - checkRequiredField("personId", personId); - - ResetPasswordDetails resetDetails = new ResetPasswordDetails() - .setUserId(personId) - .setPassword(passwordReset.getPassword()) - .setWorkflowId(passwordReset.getId()) - .setWorkflowKey(passwordReset.getKey()); - try - { - // This is an un-authenticated API call so we wrap it to run as System - AuthenticationUtil.runAsSystem(() -> { - resetPasswordService.initiateResetPassword(resetDetails); - - return null; - }); - - } - catch (ResetPasswordWorkflowException ex) - { - // we don't throw an exception. - // For security reason, the endpoint returns a 202 response - // See APPSREPO-35 acceptance criteria - if (LOGGER.isWarnEnabled()) - { - LOGGER.warn(ex.getMessage()); - } - } - } - - private void checkResetPasswordData(PasswordReset data) - { - checkRequiredField("password", data.getPassword()); - checkRequiredField("id", data.getId()); - checkRequiredField("key", data.getKey()); - } - - private boolean isGuestAuthority(String authorityName) - { - return authorityService.isGuestAuthority(authorityName); - } - - private boolean isMutableAuthority(String authorityName) - { - MutableAuthenticationService mutableAuthenticationService = (MutableAuthenticationService) authenticationService; - // Check whether the account is mutable according to the authentication service - if (!mutableAuthenticationService.isAuthenticationMutable(authorityName)) - { - return false; - } - // Only allow non-admin users to mutate their own accounts - String currentUser = mutableAuthenticationService.getCurrentUserName(); - if (currentUser.equals(authorityName) || authorityService.isAdminAuthority(currentUser)) - { - return true; - } - return false; - } -} +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.io.InputStream; +import java.io.Serializable; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +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.authentication.ResetPasswordService; +import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordDetails; +import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordWorkflowException; +import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordWorkflowInvalidUserException; +import org.alfresco.repo.security.sync.UserRegistrySynchronizer; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.Renditions; +import org.alfresco.rest.api.Sites; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.PasswordReset; +import org.alfresco.rest.api.model.Person; +import org.alfresco.rest.api.model.Rendition; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.DisabledServiceException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.resource.content.BasicContentInfo; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.NoSuchPersonException; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.thumbnail.ThumbnailService; +import org.alfresco.service.cmr.usage.ContentUsageService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; + +/** + * Centralises access to people services and maps between representations. + * + * @author steveglover + * @since publicapi1.0 + */ +public class PeopleImpl implements People +{ + private static final Log LOGGER = LogFactory.getLog(PeopleImpl.class); + + private static final List EXCLUDED_NS = Arrays.asList( + NamespaceService.SYSTEM_MODEL_1_0_URI, + "http://www.alfresco.org/model/user/1.0", + NamespaceService.CONTENT_MODEL_1_0_URI); + private static final List EXCLUDED_ASPECTS = Arrays.asList(); + private static final List EXCLUDED_PROPS = Arrays.asList(); + private static final int USERNAME_MAXLENGTH = 100; + private static final String[] RESERVED_AUTHORITY_PREFIXES = { + PermissionService.GROUP_PREFIX, + PermissionService.ROLE_PREFIX + }; + + protected Nodes nodes; + protected Sites sites; + protected SiteService siteService; + protected NodeService nodeService; + protected PersonService personService; + protected AuthenticationService authenticationService; + protected AuthorityService authorityService; + protected ContentUsageService contentUsageService; + protected ContentService contentService; + protected ThumbnailService thumbnailService; + protected ResetPasswordService resetPasswordService; + protected UserRegistrySynchronizer userRegistrySynchronizer; + protected Renditions renditions; + private Boolean allowImmutableEnabledUpdate; + + private final static Map sort_params_to_qnames; + static + { + Map aMap = new HashMap<>(3); + aMap.put(PARAM_FIRST_NAME, ContentModel.PROP_FIRSTNAME); + aMap.put(PARAM_LAST_NAME, ContentModel.PROP_LASTNAME); + aMap.put(PARAM_ID, ContentModel.PROP_USERNAME); + sort_params_to_qnames = Collections.unmodifiableMap(aMap); + } + + public void setSites(Sites sites) + { + this.sites = sites; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public void setContentUsageService(ContentUsageService contentUsageService) + { + this.contentUsageService = contentUsageService; + } + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + public void setThumbnailService(ThumbnailService thumbnailService) + { + this.thumbnailService = thumbnailService; + } + + public void setResetPasswordService(ResetPasswordService resetPasswordService) + { + this.resetPasswordService = resetPasswordService; + } + + public void setRenditions(Renditions renditions) + { + this.renditions = renditions; + } + + public void setUserRegistrySynchronizer(UserRegistrySynchronizer userRegistrySynchronizer) + { + this.userRegistrySynchronizer = userRegistrySynchronizer; + } + + public void setAllowImmutableEnabledUpdate(Boolean allowImmutableEnabledUpdate) + { + this.allowImmutableEnabledUpdate = allowImmutableEnabledUpdate; + } + + /** + * Validate, perform -me- substitution and canonicalize the person ID. + * + * @param personId + * @return The validated and processed ID. + */ + @Override + public String validatePerson(String personId) + { + return validatePerson(personId, false); + } + + @Override + public String validatePerson(final String requestedPersonId, boolean validateIsCurrentUser) + { + String personId = requestedPersonId; + if (personId == null) + { + throw new InvalidArgumentException("personId is null."); + } + + if (personId.equalsIgnoreCase(DEFAULT_USER)) + { + personId = AuthenticationUtil.getFullyAuthenticatedUser(); + } + + personId = personService.getUserIdentifier(personId); + if (personId == null) + { + // Could not find canonical user ID by case-sensitive ID. + throw new EntityNotFoundException(requestedPersonId); + } + + if (validateIsCurrentUser) + { + String currentUserId = AuthenticationUtil.getFullyAuthenticatedUser(); + if (!currentUserId.equalsIgnoreCase(personId)) + { + throw new EntityNotFoundException(personId); + } + } + + return personId; + } + + protected void processPersonProperties(String userName, final Map nodeProps) + { + if (!contentUsageService.getEnabled()) + { + // quota used will always be 0 in this case so remove from the person properties + nodeProps.remove(ContentModel.PROP_SIZE_QUOTA); + nodeProps.remove(ContentModel.PROP_SIZE_CURRENT); + } + + // The person description is located in a separate content file located at cm:persondescription + // "Inline" this data, by removing the cm:persondescription property and adding a temporary property + // (Person.PROP_PERSON_DESCRIPTION) containing the actual content as a string. + final ContentData personDescription = (ContentData) nodeProps.get(ContentModel.PROP_PERSONDESC); + if (personDescription != null) + { + nodeProps.remove(ContentModel.PROP_PERSONDESC); + + AuthenticationUtil.runAsSystem(new RunAsWork() { + @Override + public Void doWork() throws Exception + { + ContentReader reader = contentService.getRawReader(personDescription.getContentUrl()); + if (reader != null && reader.exists()) + { + String description = reader.getContentString(); + nodeProps.put(Person.PROP_PERSON_DESCRIPTION, description); + } + + return null; + } + }); + } + } + + public boolean hasAvatar(NodeRef personNodeRef) + { + return (getAvatarOriginal(personNodeRef) != null); + } + + @Override + public NodeRef getAvatar(String personId) + { + NodeRef avatar = null; + personId = validatePerson(personId); + NodeRef personNode = personService.getPerson(personId); + if (personNode != null) + { + NodeRef avatarOrig = getAvatarOriginal(personNode); + avatar = thumbnailService.getThumbnailByName(avatarOrig, ContentModel.PROP_CONTENT, "avatar"); + } + + if (avatar == null) + { + throw new EntityNotFoundException(personId); + } + + return avatar; + } + + private NodeRef getAvatarOriginal(NodeRef personNode) + { + NodeRef avatarOrigNodeRef = null; + List avatarChildAssocs = nodeService.getChildAssocs(personNode, Collections.singleton(ContentModel.ASSOC_PREFERENCE_IMAGE)); + if (avatarChildAssocs.size() > 0) + { + ChildAssociationRef ref = avatarChildAssocs.get(0); + avatarOrigNodeRef = ref.getChildRef(); + } + else + { + // TODO do we still need this ? - backward compatible with JSF web-client avatar + List avatorAssocs = nodeService.getTargetAssocs(personNode, ContentModel.ASSOC_AVATAR); + if (avatorAssocs.size() > 0) + { + AssociationRef ref = avatorAssocs.get(0); + avatarOrigNodeRef = ref.getTargetRef(); + } + } + return avatarOrigNodeRef; + } + + @Override + public BinaryResource downloadAvatarContent(String personId, Parameters parameters) + { + personId = validatePerson(personId); + NodeRef personNode = personService.getPerson(personId); + NodeRef avatarNodeRef = getAvatarOriginal(personNode); + + return renditions.getContentNoValidation(avatarNodeRef, "avatar", parameters); + } + + @Override + public Person uploadAvatarContent(String personId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters) + { + if (!thumbnailService.getThumbnailsEnabled()) + { + throw new DisabledServiceException("Thumbnail generation has been disabled."); + } + + personId = validatePerson(personId); + checkCurrentUserOrAdmin(personId); + + NodeRef personNode = personService.getPerson(personId); + NodeRef avatarOrigNodeRef = getAvatarOriginal(personNode); + + if (avatarOrigNodeRef != null) + { + deleteAvatar(avatarOrigNodeRef); + } + + QName origAvatarQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "origAvatar"); + nodeService.addAspect(personNode, ContentModel.ASPECT_PREFERENCES, null); + ChildAssociationRef assoc = nodeService.createNode(personNode, ContentModel.ASSOC_PREFERENCE_IMAGE, origAvatarQName, + ContentModel.TYPE_CONTENT); + NodeRef avatar = assoc.getChildRef(); + String avatarOriginalNodeId = avatar.getId(); + + // TODO do we still need this ? - backward compatible with JSF web-client avatar + nodeService.createAssociation(personNode, avatar, ContentModel.ASSOC_AVATAR); + + Node n = nodes.updateContent(avatarOriginalNodeId, contentInfo, stream, parameters); + String mimeType = n.getContent().getMimeType(); + + if (mimeType.indexOf("image/") != 0) + { + throw new InvalidArgumentException( + "Uploaded content must be an image (content type determined to be '" + mimeType + "')"); + } + + // create thumbnail synchronously + Rendition avatarR = new Rendition(); + avatarR.setId("avatar"); + renditions.createRendition(avatar, avatarR, false, parameters); + + List include = Arrays.asList( + PARAM_INCLUDE_ASPECTNAMES, + PARAM_INCLUDE_PROPERTIES); + + return getPersonWithProperties(personId, include); + } + + @Override + public void deleteAvatarContent(String personId) + { + personId = validatePerson(personId); + checkCurrentUserOrAdmin(personId); + + NodeRef personNode = personService.getPerson(personId); + NodeRef avatarOrigNodeRef = getAvatarOriginal(personNode); + if (avatarOrigNodeRef != null) + { + deleteAvatar(avatarOrigNodeRef); + } + } + + private void deleteAvatar(NodeRef avatarOrigNodeRef) + { + // Set as temporary to permanently delete node (instead of archiving) + nodeService.addAspect(avatarOrigNodeRef, ContentModel.ASPECT_TEMPORARY, null); + + nodeService.deleteNode(avatarOrigNodeRef); + } + + /** + * Get a full representation of a person. + * + * @throws NoSuchPersonException + * if personId does not exist + */ + @Override + public Person getPerson(String personId) + { + personId = validatePerson(personId); + List include = Arrays.asList( + PARAM_INCLUDE_ASPECTNAMES, + PARAM_INCLUDE_PROPERTIES, + PARAM_INCLUDE_CAPABILITIES); + Person person = getPersonWithProperties(personId, include); + + return person; + } + + public Person getPerson(String personId, List include) + { + personId = validatePerson(personId); + Person person = getPersonWithProperties(personId, include); + + return person; + } + + @Override + public CollectionWithPagingInfo getPeople(final Parameters parameters) + { + Paging paging = parameters.getPaging(); + PagingRequest pagingRequest = Util.getPagingRequest(paging); + + List> sortProps = getSortProps(parameters); + + // For now the results are not filtered + // please see REPO-555 + final PagingResults pagingResult = personService.getPeople(null, null, sortProps, pagingRequest); + + final List page = pagingResult.getPage(); + int totalItems = pagingResult.getTotalResultCount().getFirst(); + final String personId = AuthenticationUtil.getFullyAuthenticatedUser(); + List people = new AbstractList() { + @Override + public Person get(int index) + { + PersonService.PersonInfo personInfo = page.get(index); + Person person = getPersonWithProperties(personInfo.getUserName(), parameters.getInclude()); + return person; + } + + @Override + public int size() + { + return page.size(); + } + }; + + return CollectionWithPagingInfo.asPaged(paging, people, pagingResult.hasMoreItems(), totalItems); + } + + private List> getSortProps(Parameters parameters) + { + List> sortProps = new ArrayList<>(); + List sortCols = parameters.getSorting(); + if ((sortCols != null) && (sortCols.size() > 0)) + { + for (SortColumn sortCol : sortCols) + { + QName sortPropQName = sort_params_to_qnames.get(sortCol.column); + if (sortPropQName == null) + { + throw new InvalidArgumentException("Invalid sort field: " + sortCol.column); + } + sortProps.add(new Pair<>(sortPropQName, (sortCol.asc ? Boolean.TRUE : Boolean.FALSE))); + } + } + else + { + // default sort order + sortProps.add(new Pair<>(ContentModel.PROP_USERNAME, Boolean.TRUE)); + } + return sortProps; + } + + private Person getPersonWithProperties(String personId, List include) + { + Person person = null; + NodeRef personNode = personService.getPerson(personId, false); + if (personNode != null) + { + Map nodeProps = nodeService.getProperties(personNode); + processPersonProperties(personId, nodeProps); + // TODO this needs to be run as admin but should we do this here? + final String pId = personId; + Boolean enabled = AuthenticationUtil.runAsSystem(new RunAsWork() { + public Boolean doWork() throws Exception + { + return authenticationService.getAuthenticationEnabled(pId); + } + }); + person = new Person(personNode, nodeProps, enabled); + + // Remove the temporary property used to help inline the person description content property. + // It may be accessed from the person object (person.getDescription()). + nodeProps.remove(Person.PROP_PERSON_DESCRIPTION); + + // Expose properties + if (include.contains(PARAM_INCLUDE_PROPERTIES)) + { + // Note that custProps may be null. + Map custProps = nodes.mapFromNodeProperties(nodeProps, new ArrayList<>(), new HashMap<>(), EXCLUDED_NS, EXCLUDED_PROPS); + person.setProperties(custProps); + } + if (include.contains(PARAM_INCLUDE_ASPECTNAMES)) + { + // Expose aspect names + Set aspects = nodeService.getAspects(personNode); + person.setAspectNames(nodes.mapFromNodeAspects(aspects, EXCLUDED_NS, EXCLUDED_ASPECTS)); + } + if (include.contains(PARAM_INCLUDE_CAPABILITIES)) + { + // Expose capabilities + Map capabilities = new HashMap<>(3); + capabilities.put("isAdmin", isAdminAuthority(personId)); + capabilities.put("isGuest", isGuestAuthority(personId)); + capabilities.put("isMutable", isMutableAuthority(personId)); + person.setCapabilities(capabilities); + } + + // get avatar information + if (hasAvatar(personNode)) + { + try + { + NodeRef avatar = getAvatar(personId); + person.setAvatarId(avatar); + } + catch (EntityNotFoundException e) + { + // shouldn't happen, but ok + } + } + } + else + { + throw new EntityNotFoundException(personId); + } + + return person; + } + + @Override + public Person create(Person person) + { + validateCreatePersonData(person); + + if (!isAdminAuthority()) + { + // note: do an explict check for admin here (since personExists does not throw 403 unlike createPerson, + // hence next block would cause 409 to be returned) + throw new PermissionDeniedException(); + } + + // Unfortunately PersonService.createPerson(...) only throws an AlfrescoRuntimeException + // rather than a more specific exception and does not use a message ID either, so there's + // no sensible way to know that it was thrown due to the user already existing - hence this check here. + if (personService.personExists(person.getUserName())) + { + throw new ConstraintViolatedException("Person '" + person.getUserName() + "' already exists."); + } + + // set enabled default value true + if (person.isEnabled() == null) + { + person.setEnabled(true); + } + + Map props = person.toProperties(); + + MutableAuthenticationService mas = (MutableAuthenticationService) authenticationService; + mas.createAuthentication(person.getUserName(), person.getPassword().toCharArray()); + mas.setAuthenticationEnabled(person.getUserName(), person.isEnabled()); + + // Add custom properties + if (person.getProperties() != null) + { + Map customProps = person.getProperties(); + props.putAll(nodes.mapToNodeProperties(customProps)); + } + + NodeRef nodeRef = personService.createPerson(props); + + // Add custom aspects + nodes.addCustomAspects(nodeRef, person.getAspectNames(), EXCLUDED_ASPECTS); + + // Write the contents of PersonUpdate.getDescription() text to a content file + // and store the content URL in ContentModel.PROP_PERSONDESC + if (person.getDescription() != null) + { + savePersonDescription(person.getDescription(), nodeRef); + } + + // Return a fresh retrieval + return getPerson(person.getUserName()); + } + + /** + * Write the description to a content file and store the content URL in ContentModel.PROP_PERSONDESC + * + * @param description + * @param nodeRef + */ + private void savePersonDescription(final String description, final NodeRef nodeRef) + { + AuthenticationUtil.runAsSystem(new RunAsWork() { + @Override + public Void doWork() throws Exception + { + if (description != null) + { + ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_PERSONDESC, true); + writer.putContent(description); + } + else + { + nodeService.setProperty(nodeRef, ContentModel.PROP_PERSONDESC, null); + } + return null; + } + }); + } + + private void validateCreatePersonData(Person person) + { + // Mandatory field checks first + checkRequiredField("id", person.getUserName()); + checkRequiredField("firstName", person.getFirstName()); + checkRequiredField("email", person.getEmail()); + checkRequiredField("password", person.getPassword()); + + validateUsername(person.getUserName()); + nodes.validateAspects(person.getAspectNames(), EXCLUDED_NS, EXCLUDED_ASPECTS); + nodes.validateProperties(person.getProperties(), EXCLUDED_NS, EXCLUDED_PROPS); + } + + private void validateUsername(String username) + { + if (username.length() > 100) + { + throw new InvalidArgumentException("Username exceeds max length of " + USERNAME_MAXLENGTH + " characters."); + } + + for (String prefix : RESERVED_AUTHORITY_PREFIXES) + { + if (username.toUpperCase().startsWith(prefix)) + { + throw new IllegalArgumentException("Username cannot start with the reserved prefix '" + prefix + "'."); + } + } + } + + private void checkRequiredField(String fieldName, Object fieldValue) + { + if (fieldValue == null) + { + throw new InvalidArgumentException("Field '" + fieldName + "' is null, but is required."); + } + + // belts-and-braces - note: should not see empty string (since converted to null via custom json deserializer) + if ((fieldValue instanceof String) && ((String) fieldValue).isEmpty()) + { + throw new InvalidArgumentException("Field '" + fieldName + "' is empty, but is required."); + } + } + + @Override + public Person update(String personId, final Person person) + { + // Validate, perform -me- substitution and canonicalize the person ID. + personId = validatePerson(personId); + validateUpdatePersonData(person); + + // Check if user updating *their own* details or is an admin + boolean isAdmin = checkCurrentUserOrAdmin(personId); + + final String personIdToUpdate = validatePerson(personId); + final Map properties = person.toProperties(); + + // if requested, update password + updatePassword(isAdmin, personIdToUpdate, person); + + Set immutableProperties = userRegistrySynchronizer.getPersonMappedProperties(personIdToUpdate); + + Boolean isEnabled = person.isEnabled(); + if (isEnabled != null) + { + if (!isEnabled && isAdminAuthority(personIdToUpdate)) + { + throw new PermissionDeniedException("Admin authority cannot be disabled."); + } + + if (allowImmutableEnabledStatusUpdate(personIdToUpdate, isAdmin, immutableProperties)) + { + LOGGER.info("User " + personIdToUpdate + " is immutable but enabled status will be set to: " + isEnabled); + } + else + { + // note: if current user is not an admin then permission denied exception is thrown + MutableAuthenticationService mutableAuthenticationService = (MutableAuthenticationService) authenticationService; + mutableAuthenticationService.setAuthenticationEnabled(personIdToUpdate, person.isEnabled()); + } + } + + NodeRef personNodeRef = personService.getPerson(personIdToUpdate, false); + if (person.wasSet(Person.PROP_PERSON_DESCRIPTION)) + { + // Remove person description from saved properties + properties.remove(ContentModel.PROP_PERSONDESC); + + // Custom save for person description. + savePersonDescription(person.getDescription(), personNodeRef); + } + + // Update custom aspects - do this *before* adding custom properties. The + // addition of custom properties may result in the auto-addition of aspects + // and we don't want to remove them during the update of explicitly specified aspects. + nodes.updateCustomAspects(personNodeRef, person.getAspectNames(), EXCLUDED_ASPECTS); + + // Add custom properties + if (person.getProperties() != null) + { + Map customProps = person.getProperties(); + properties.putAll(nodes.mapToNodeProperties(customProps)); + } + + // MNT-21150 LDAP synced attributes can't be changed using REST API + immutableProperties.forEach(immutableProperty -> { + if (properties.containsKey(immutableProperty)) + { + properties.remove(immutableProperty); + } + }); + + // The person service only allows admin users to set the properties by default. + if (!properties.isEmpty()) + { + AuthenticationUtil.runAsSystem(new RunAsWork() { + @Override + public Void doWork() throws Exception + { + personService.setPersonProperties(personIdToUpdate, properties, false); + return null; + } + }); + } + + return getPerson(personId); + } + + private boolean allowImmutableEnabledStatusUpdate(String userId, boolean isAdmin, Set immutableProperties) + { + if (allowImmutableEnabledUpdate) + { + boolean containLdapUserAccountStatus = false; + QName propertyNameToCheck = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "userAccountStatusProperty"); + + for (QName immutableProperty : immutableProperties) + { + if (immutableProperty.equals(propertyNameToCheck)) + { + containLdapUserAccountStatus = true; + break; + } + } + + return isAdmin && !containLdapUserAccountStatus && !isMutableAuthority(userId); + } + + return false; + } + + private boolean checkCurrentUserOrAdmin(String personId) + { + boolean isAdmin = isAdminAuthority(); + + String currentUserId = AuthenticationUtil.getFullyAuthenticatedUser(); + if (!isAdmin && !currentUserId.equalsIgnoreCase(personId)) + { + throw new PermissionDeniedException(); + } + + return isAdmin; + } + + private void validateUpdatePersonData(Person person) + { + nodes.validateAspects(person.getAspectNames(), EXCLUDED_NS, EXCLUDED_ASPECTS); + nodes.validateProperties(person.getProperties(), EXCLUDED_NS, EXCLUDED_PROPS); + + if (person.wasSet(ContentModel.PROP_FIRSTNAME)) + { + checkRequiredField("firstName", person.getFirstName()); + } + + if (person.wasSet(ContentModel.PROP_EMAIL)) + { + checkRequiredField("email", person.getEmail()); + } + + if (person.wasSet(ContentModel.PROP_ENABLED) && (person.isEnabled() == null)) + { + throw new IllegalArgumentException("'enabled' field cannot be empty."); + } + + if (person.wasSet(ContentModel.PROP_EMAIL_FEED_DISABLED) && (person.isEmailNotificationsEnabled() == null)) + { + throw new IllegalArgumentException("'emailNotificationsEnabled' field cannot be empty."); + } + } + + private void updatePassword(boolean isAdmin, String personIdToUpdate, Person person) + { + MutableAuthenticationService mutableAuthenticationService = (MutableAuthenticationService) authenticationService; + + boolean isOldPassword = person.wasSet(Person.PROP_PERSON_OLDPASSWORD); + boolean isPassword = person.wasSet(Person.PROP_PERSON_PASSWORD); + + if (isPassword || isOldPassword) + { + if (isOldPassword && ((person.getOldPassword() == null) || (person.getOldPassword().isEmpty()))) + { + throw new IllegalArgumentException("'oldPassword' field cannot be empty."); + } + + if (!isPassword || (person.getPassword() == null) || (person.getPassword().isEmpty())) + { + throw new IllegalArgumentException("password' field cannot be empty."); + } + + char[] newPassword = person.getPassword().toCharArray(); + + if (!isAdmin) + { + // Non-admin users can update their own password, but must provide their current password. + if (!isOldPassword) + { + throw new IllegalArgumentException("To change password, both 'oldPassword' and 'password' fields are required."); + } + + char[] oldPassword = person.getOldPassword().toCharArray(); + try + { + mutableAuthenticationService.updateAuthentication(personIdToUpdate, oldPassword, newPassword); + } + catch (AuthenticationException e) + { + throw new PermissionDeniedException("Incorrect password."); + } + } + else + { + // An admin user can update without knowing the original pass - but must know their own! + // note: is it reasonable to silently ignore oldPassword if supplied ? + + mutableAuthenticationService.setAuthentication(personIdToUpdate, newPassword); + } + } + } + + private boolean isAdminAuthority() + { + return authorityService.hasAdminAuthority(); + } + + private boolean isAdminAuthority(String authorityName) + { + return authorityService.isAdminAuthority(authorityName); + } + + @Deprecated + @Override + public void requestPasswordReset(String userId, String client) + { + // Validate the userId and the client + checkRequiredField("userId", userId); + checkRequiredField("client", client); + + // This is an un-authenticated API call so we wrap it to run as System + AuthenticationUtil.runAsSystem(() -> { + try + { + resetPasswordService.requestReset(userId, client); + } + catch (ResetPasswordWorkflowInvalidUserException ex) + { + // we don't throw an exception. + // For security reason (prevent the attackers to determine that userId exists in the system or not), + // the endpoint returns a 202 response if the userId does not exist or + // if the user is disabled by an Administrator. + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Invalid user. " + ex.getMessage()); + } + } + + return null; + }); + } + + @Deprecated + @Override + public void resetPassword(String personId, final PasswordReset passwordReset) + { + checkResetPasswordData(passwordReset); + checkRequiredField("personId", personId); + + ResetPasswordDetails resetDetails = new ResetPasswordDetails() + .setUserId(personId) + .setPassword(passwordReset.getPassword()) + .setWorkflowId(passwordReset.getId()) + .setWorkflowKey(passwordReset.getKey()); + try + { + // This is an un-authenticated API call so we wrap it to run as System + AuthenticationUtil.runAsSystem(() -> { + resetPasswordService.initiateResetPassword(resetDetails); + + return null; + }); + + } + catch (ResetPasswordWorkflowException ex) + { + // we don't throw an exception. + // For security reason, the endpoint returns a 202 response + // See APPSREPO-35 acceptance criteria + if (LOGGER.isWarnEnabled()) + { + LOGGER.warn(ex.getMessage()); + } + } + } + + private void checkResetPasswordData(PasswordReset data) + { + checkRequiredField("password", data.getPassword()); + checkRequiredField("id", data.getId()); + checkRequiredField("key", data.getKey()); + } + + private boolean isGuestAuthority(String authorityName) + { + return authorityService.isGuestAuthority(authorityName); + } + + private boolean isMutableAuthority(String authorityName) + { + MutableAuthenticationService mutableAuthenticationService = (MutableAuthenticationService) authenticationService; + // Check whether the account is mutable according to the authentication service + if (!mutableAuthenticationService.isAuthenticationMutable(authorityName)) + { + return false; + } + // Only allow non-admin users to mutate their own accounts + String currentUser = mutableAuthenticationService.getCurrentUserName(); + if (currentUser.equals(authorityName) || authorityService.isAdminAuthority(currentUser)) + { + return true; + } + return false; + } +}