/// <summary>
    ///     Changes the users password
    /// </summary>
    /// <param name="changingPasswordModel">The changing password model</param>
    /// <returns>
    ///     If the password is being reset it will return the newly reset password, otherwise will return an empty value
    /// </returns>
    public async Task <ActionResult <ModelWithNotifications <string?> >?> PostChangePassword(
        ChangingPasswordModel changingPasswordModel)
    {
        IUser?currentUser = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;

        if (currentUser is null)
        {
            return(null);
        }

        changingPasswordModel.Id = currentUser.Id;

        // all current users have access to reset/manually change their password

        Attempt <PasswordChangedModel?> passwordChangeResult =
            await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _backOfficeUserManager);

        if (passwordChangeResult.Success)
        {
            // even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword
            var result = new ModelWithNotifications <string?>(passwordChangeResult.Result?.ResetPassword);
            result.AddSuccessNotification(_localizedTextService.Localize("user", "password"), _localizedTextService.Localize("user", "passwordChanged"));
            return(result);
        }

        if (passwordChangeResult.Result?.ChangeError?.MemberNames is not null)
        {
            foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames)
            {
                ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage ?? string.Empty);
            }
        }

        return(ValidationProblem(ModelState));
    }
Esempio n. 2
0
    /// <summary>
    /// </summary>
    /// <param name="changingPasswordModel"></param>
    /// <returns></returns>
    public async Task<ActionResult<ModelWithNotifications<string?>>> PostChangePassword(
        ChangingPasswordModel changingPasswordModel)
    {
        changingPasswordModel = changingPasswordModel ?? throw new ArgumentNullException(nameof(changingPasswordModel));

        if (ModelState.IsValid == false)
        {
            return ValidationProblem(ModelState);
        }

        IUser? found = _userService.GetUserById(changingPasswordModel.Id);
        if (found == null)
        {
            return NotFound();
        }

        IUser? currentUser = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;

        // if it's the current user, the current user cannot reset their own password without providing their old password
        if (currentUser?.Username == found.Username && string.IsNullOrEmpty(changingPasswordModel.OldPassword))
        {
            return ValidationProblem("Password reset is not allowed without providing old password");
        }

        if ((!currentUser?.IsAdmin() ?? false) && found.IsAdmin())
        {
            return ValidationProblem("The current user cannot change the password for the specified user");
        }

        Attempt<PasswordChangedModel?> passwordChangeResult =
            await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _userManager);

        if (passwordChangeResult.Success)
        {
            var result = new ModelWithNotifications<string?>(passwordChangeResult.Result?.ResetPassword);
            result.AddSuccessNotification(_localizedTextService.Localize("general", "success"),
                _localizedTextService.Localize("user", "passwordChangedGeneric"));
            return result;
        }

        if (passwordChangeResult.Result?.ChangeError is not null)
        {
            foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames)
            {
                ModelState.AddModelError(memberName,
                    passwordChangeResult.Result.ChangeError.ErrorMessage ?? string.Empty);
            }
        }

        return ValidationProblem(ModelState);
    }
        /// <summary>
        /// Update existing member data
        /// </summary>
        /// <param name="contentItem">The member to save</param>
        /// <remarks>
        /// We need to use both IMemberService and ASP.NET Identity to do our updates because Identity is responsible for passwords/security.
        /// When this method is called, the IMember will already have updated/mapped values from the http POST.
        /// So then we do this in order:
        /// 1. Deal with sensitive property values on IMember
        /// 2. Use IMemberService to persist all changes
        /// 3. Use ASP.NET and MemberUserManager to deal with lockouts
        /// 4. Use ASP.NET, MemberUserManager and password changer to deal with passwords
        /// 5. Deal with groups/roles
        /// </remarks>
        private async Task <ActionResult <bool> > UpdateMemberAsync(MemberSave contentItem)
        {
            if (contentItem.PersistedContent is not null)
            {
                contentItem.PersistedContent.WriterId = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1;
            }

            // If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types
            // have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted.
            // There's only 3 special ones we need to deal with that are part of the MemberSave instance: Comments, IsApproved, IsLockedOut
            // but we will take care of this in a generic way below so that it works for all props.
            if (!_backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasAccessToSensitiveData() ?? true)
            {
                IMemberType?memberType          = contentItem.PersistedContent is null ? null : _memberTypeService.Get(contentItem.PersistedContent.ContentTypeId);
                var         sensitiveProperties = memberType?
                                                  .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias))
                                                  .ToList();

                if (sensitiveProperties is not null)
                {
                    foreach (IPropertyType sensitiveProperty in sensitiveProperties)
                    {
                        // TODO: This logic seems to deviate from the logic that is in v8 where we are explitly checking
                        // against 3 properties: Comments, IsApproved, IsLockedOut, is the v8 version incorrect?

                        ContentPropertyBasic?destProp = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
                        if (destProp != null)
                        {
                            // if found, change the value of the contentItem model to the persisted value so it remains unchanged
                            object?origValue = contentItem.PersistedContent?.GetValue(sensitiveProperty.Alias);
                            destProp.Value = origValue;
                        }
                    }
                }
            }

            if (contentItem.PersistedContent is not null)
            {
                // First save the IMember with mapped values before we start updating data with aspnet identity
                _memberService.Save(contentItem.PersistedContent);
            }

            bool needsResync = false;

            MemberIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id?.ToString());

            if (identityMember == null)
            {
                return(ValidationProblem("Member was not found"));
            }

            // Handle unlocking with the member manager (takes care of other nuances)
            if (identityMember.IsLockedOut && contentItem.IsLockedOut == false)
            {
                IdentityResult unlockResult = await _memberManager.SetLockoutEndDateAsync(identityMember, DateTimeOffset.Now.AddMinutes(-1));

                if (unlockResult.Succeeded == false)
                {
                    return(ValidationProblem(
                               $"Could not unlock for member {contentItem.Id} - error {unlockResult.Errors.ToErrorMessage()}"));
                }
                needsResync = true;
            }
            else if (identityMember.IsLockedOut == false && contentItem.IsLockedOut)
            {
                // NOTE: This should not ever happen unless someone is mucking around with the request data.
                // An admin cannot simply lock a user, they get locked out by password attempts, but an admin can unlock them
                return(ValidationProblem("An admin cannot lock a member"));
            }

            // If we're changing the password...
            // Handle changing with the member manager & password changer (takes care of other nuances)
            if (contentItem.Password != null)
            {
                IdentityResult validatePassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword);

                if (validatePassword.Succeeded == false)
                {
                    return(ValidationProblem(validatePassword.Errors.ToErrorMessage()));
                }

                if (!int.TryParse(identityMember.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId))
                {
                    return(ValidationProblem("Member ID was not valid"));
                }

                var changingPasswordModel = new ChangingPasswordModel
                {
                    Id          = intId,
                    OldPassword = contentItem.Password.OldPassword,
                    NewPassword = contentItem.Password.NewPassword,
                };

                // Change and persist the password
                Attempt <PasswordChangedModel?> passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _memberManager);

                if (!passwordChangeResult.Success)
                {
                    foreach (string memberName in passwordChangeResult.Result?.ChangeError?.MemberNames ?? Enumerable.Empty <string>())
                    {
                        ModelState.AddModelError(memberName, passwordChangeResult.Result?.ChangeError?.ErrorMessage ?? string.Empty);
                    }
                    return(ValidationProblem(ModelState));
                }

                needsResync = true;
            }

            // Update the roles and check for changes
            ActionResult <bool> rolesChanged = await AddOrUpdateRoles(contentItem.Groups, identityMember);

            if (!rolesChanged.Value && rolesChanged.Result != null)
            {
                return(rolesChanged.Result);
            }
            else
            {
                needsResync = true;
            }

            // If there have been underlying changes made by ASP.NET Identity, then we need to resync the
            // IMember on the PersistedContent with what is stored since it will be mapped to display.
            if (needsResync && contentItem.PersistedContent is not null)
            {
                contentItem.PersistedContent = _memberService.GetById(contentItem.PersistedContent.Id) !;
            }

            return(true);
        }