/// <summary> /// Overridden to assign the IsSensitive property values /// </summary> /// <param name="content"></param> /// <param name="properties"></param> /// <param name="context"></param> /// <returns></returns> protected override List <ContentPropertyDisplay> MapProperties(IContentBase content, List <IProperty> properties, MapperContext context) { List <ContentPropertyDisplay> result = base.MapProperties(content, properties, context); var member = (IMember)content; IMemberType?memberType = _memberTypeService.Get(member.ContentTypeId); // now update the IsSensitive value foreach (ContentPropertyDisplay prop in result) { // check if this property is flagged as sensitive var isSensitiveProperty = memberType?.IsSensitiveProperty(prop.Alias) ?? false; // check permissions for viewing sensitive data if (isSensitiveProperty && _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasAccessToSensitiveData() == false) { // mark this property as sensitive prop.IsSensitive = true; // mark this property as readonly so that it does not post any data prop.Readonly = true; // replace this editor with a sensitive value prop.View = "sensitivevalue"; // clear the value prop.Value = null; } } return(result); }
// no MapAll - take care private void Map(IMemberType source, MemberTypeDisplay target, MapperContext context) { MapTypeToDisplayBase <MemberTypeDisplay, MemberPropertyTypeDisplay>(source, target); //map the MemberCanEditProperty,MemberCanViewProperty,IsSensitiveData foreach (var propertyType in source.PropertyTypes) { var localCopy = propertyType; var displayProp = target.Groups.SelectMany(dest => dest.Properties).SingleOrDefault(dest => dest.Alias.InvariantEquals(localCopy.Alias)); if (displayProp == null) { continue; } displayProp.MemberCanEditProperty = source.MemberCanEditProperty(localCopy.Alias); displayProp.MemberCanViewProperty = source.MemberCanViewProperty(localCopy.Alias); displayProp.IsSensitiveData = source.IsSensitiveProperty(localCopy.Alias); } }
/// <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) { contentItem.PersistedContent.WriterId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id; // 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()) { IMemberType memberType = _memberTypeService.Get(contentItem.PersistedContent.ContentTypeId); var sensitiveProperties = memberType .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias)) .ToList(); 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; } } } // 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 = _memberService.GetById(contentItem.PersistedContent.Id); } return(true); }