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