org.alfresco.rest.api.impl.PeopleImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.rest.api.impl.PeopleImpl.java

Source

/*
 * #%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 <http://www.gnu.org/licenses/>.
 * #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.rest.api.Nodes;
import org.alfresco.rest.api.People;
import org.alfresco.rest.api.Sites;
import org.alfresco.rest.api.model.PasswordReset;
import org.alfresco.rest.api.model.Person;
import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException;
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.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.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.*;
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.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<String> 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<QName> EXCLUDED_ASPECTS = Arrays.asList();
    private static final List<QName> 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;

    private final static Map<String, QName> sort_params_to_qnames;
    static {
        Map<String, QName> 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;
    }

    /**
     * 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<QName, Serializable> 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<Void>() {
                @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) {
        if (personNodeRef != null) {
            List<AssociationRef> avatorAssocs = nodeService.getTargetAssocs(personNodeRef,
                    ContentModel.ASSOC_AVATAR);
            return (avatorAssocs.size() > 0);
        } else {
            return false;
        }
    }

    @Override
    public NodeRef getAvatar(String personId) {
        NodeRef avatar = null;

        personId = validatePerson(personId);
        NodeRef personNode = personService.getPerson(personId);
        if (personNode != null) {
            List<AssociationRef> avatorAssocs = nodeService.getTargetAssocs(personNode, ContentModel.ASSOC_AVATAR);
            if (avatorAssocs.size() > 0) {
                AssociationRef ref = avatorAssocs.get(0);
                NodeRef thumbnailNodeRef = thumbnailService.getThumbnailByName(ref.getTargetRef(),
                        ContentModel.PROP_CONTENT, "avatar");
                if (thumbnailNodeRef != null) {
                    avatar = thumbnailNodeRef;
                } else {
                    throw new EntityNotFoundException("avatar");
                }
            } else {
                throw new EntityNotFoundException("avatar");
            }
        } else {
            throw new EntityNotFoundException(personId);
        }

        return avatar;
    }

    /**
     * Get a full representation of a person.
     * 
     * @throws NoSuchPersonException
     *             if personId does not exist
     */
    @Override
    public Person getPerson(String personId) {
        personId = validatePerson(personId);
        List<String> include = Arrays.asList(PARAM_INCLUDE_ASPECTNAMES, PARAM_INCLUDE_PROPERTIES);
        Person person = getPersonWithProperties(personId, include);

        return person;
    }

    public Person getPerson(String personId, List<String> include) {
        personId = validatePerson(personId);
        Person person = getPersonWithProperties(personId, include);

        return person;
    }

    @Override
    public CollectionWithPagingInfo<Person> getPeople(final Parameters parameters) {
        Paging paging = parameters.getPaging();
        PagingRequest pagingRequest = Util.getPagingRequest(paging);

        List<Pair<QName, Boolean>> sortProps = getSortProps(parameters);

        // For now the results are not filtered
        // please see REPO-555
        final PagingResults<PersonService.PersonInfo> pagingResult = personService.getPeople(null, null, sortProps,
                pagingRequest);

        final List<PersonService.PersonInfo> page = pagingResult.getPage();
        int totalItems = pagingResult.getTotalResultCount().getFirst();
        final String personId = AuthenticationUtil.getFullyAuthenticatedUser();
        List<Person> people = new AbstractList<Person>() {
            @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<Pair<QName, Boolean>> getSortProps(Parameters parameters) {
        List<Pair<QName, Boolean>> sortProps = new ArrayList<>();
        List<SortColumn> 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<String> include) {
        Person person = null;
        NodeRef personNode = personService.getPerson(personId, false);
        if (personNode != null) {
            Map<QName, Serializable> 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<Boolean>() {
                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<String, Object> 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<QName> aspects = nodeService.getAspects(personNode);
                person.setAspectNames(nodes.mapFromNodeAspects(aspects, EXCLUDED_NS, EXCLUDED_ASPECTS));
            }

            // 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<QName, Serializable> 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<String, Object> 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<Void>() {
            @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());
        validateNamespaces(person.getAspectNames(), person.getProperties());
    }

    private void validateUsername(String username) {
        if (username.length() > 100) {
            throw new InvalidArgumentException(
                    "Username exceeds max length of " + USERNAME_MAXLENGTH + " characters.");
        }

        if (username.indexOf('/') != -1) {
            throw new IllegalArgumentException("Username contains characters that are not permitted.");
        }

        for (String prefix : RESERVED_AUTHORITY_PREFIXES) {
            if (username.toUpperCase().startsWith(prefix)) {
                throw new IllegalArgumentException(
                        "Username cannot start with the reserved prefix '" + prefix + "'.");
            }
        }
    }

    private void validateNamespaces(List<String> aspectNames, Map<String, Object> properties) {
        if (aspectNames != null) {
            Set<QName> aspects = nodes.mapToNodeAspects(aspectNames);
            aspects.forEach(aspect -> {
                if (EXCLUDED_NS.contains(aspect.getNamespaceURI())) {
                    throw new IllegalArgumentException(
                            "Namespace cannot be used by People API: " + aspect.toPrefixString());
                }
            });
        }

        if (properties != null) {
            Map<QName, Serializable> nodeProps = nodes.mapToNodeProperties(properties);
            nodeProps.keySet().forEach(qname -> {
                if (EXCLUDED_NS.contains(qname.getNamespaceURI())) {
                    throw new IllegalArgumentException(
                            "Namespace cannot be used by People API: " + qname.toPrefixString());
                }
            });
        }
    }

    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);

        boolean isAdmin = isAdminAuthority();

        String currentUserId = AuthenticationUtil.getFullyAuthenticatedUser();
        if (!isAdmin && !currentUserId.equalsIgnoreCase(personId)) {
            // The user is not an admin user and is not attempting to update *their own* details.
            throw new PermissionDeniedException();
        }

        final String personIdToUpdate = validatePerson(personId);
        final Map<QName, Serializable> properties = person.toProperties();

        // if requested, update password
        updatePassword(isAdmin, personIdToUpdate, person);

        if (person.isEnabled() != null) {
            if (isAdminAuthority(personIdToUpdate)) {
                throw new PermissionDeniedException("Admin authority cannot be disabled.");
            }

            // 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<String, Object> customProps = person.getProperties();
            properties.putAll(nodes.mapToNodeProperties(customProps));
        }

        // The person service only allows admin users to set the properties by default.
        AuthenticationUtil.runAsSystem(new RunAsWork<Void>() {
            @Override
            public Void doWork() throws Exception {
                personService.setPersonProperties(personIdToUpdate, properties, false);
                return null;
            }
        });

        return getPerson(personId);
    }

    private void validateUpdatePersonData(Person person) {
        validateNamespaces(person.getAspectNames(), person.getProperties());

        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);
    }

    @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;
        });
    }

    @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());
    }
}