private static void UpdateName(MemberSave memberSave) { //Don't update the name if it is empty if (memberSave.Name.IsNullOrWhiteSpace() == false) { memberSave.PersistedContent.Name = memberSave.Name; } }
/// <summary> /// Maps the property values to the persisted entity /// </summary> /// <param name="contentItem"></param> private void MapPropertyValues(MemberSave contentItem) { UpdateName(contentItem); //map the custom properties - this will already be set for new entities in our member binder contentItem.PersistedContent.Email = contentItem.Email; contentItem.PersistedContent.Username = contentItem.Username; //use the base method to map the rest of the properties base.MapPropertyValues(contentItem); }
internal bool ValidateUniqueEmail(MemberSave model, MembershipProvider membershipProvider) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (membershipProvider == null) { throw new ArgumentNullException(nameof(membershipProvider)); } if (membershipProvider.RequiresUniqueEmail == false) { return(true); } int totalRecs; var existingByEmail = membershipProvider.FindUsersByEmail(model.Email.Trim(), 0, int.MaxValue, out totalRecs); switch (model.Action) { case ContentSaveAction.Save: //ok, we're updating the member, we need to check if they are changing their email and if so, does it exist already ? if (model.PersistedContent.Email.InvariantEquals(model.Email.Trim()) == false) { //they are changing their email if (existingByEmail.Cast <MembershipUser>().Select(x => x.Email) .Any(x => x.InvariantEquals(model.Email.Trim()))) { //the user cannot use this email return(false); } } break; case ContentSaveAction.SaveNew: //check if the user's email already exists if (existingByEmail.Cast <MembershipUser>().Select(x => x.Email) .Any(x => x.InvariantEquals(model.Email.Trim()))) { //the user cannot use this email return(false); } break; default: //we don't support this for members throw new ArgumentOutOfRangeException(); } return(true); }
/// <summary> /// Gets an instance of IMember used when creating a member /// </summary> /// <param name="model"></param> /// <returns></returns> /// <remarks> /// Depending on whether a custom membership provider is configured this will return different results. /// </remarks> private IMember CreateNew(MemberSave model) { var contentType = _memberTypeService.Get(model.ContentTypeAlias); if (contentType == null) { throw new InvalidOperationException("No member type found with alias " + model.ContentTypeAlias); } //remove all membership properties, these values are set with the membership provider. FilterMembershipProviderProperties(contentType); //return the new member with the details filled in return(new Member(model.Name, model.Email, model.Username, model.Password.NewPassword, contentType)); }
/// <summary> /// Re-fetches the database data to map to the PersistedContent object and re-assigns the already mapped the posted properties so that the display object is up-to-date /// </summary> /// <param name="contentItem"></param> /// <param name="lookup"></param> /// <remarks> /// This is done during an update if the membership provider has changed some underlying data - we need to ensure that our model is consistent with that data /// </remarks> private void RefetchMemberData(MemberSave contentItem, LookupType lookup) { var currProps = contentItem.PersistedContent.Properties.ToArray(); switch (MembershipScenario) { case MembershipScenario.NativeUmbraco: switch (lookup) { case LookupType.ByKey: //Go and re-fetch the persisted item contentItem.PersistedContent = Services.MemberService.GetByKey(contentItem.Key); break; case LookupType.ByUserName: contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); break; } break; case MembershipScenario.CustomProviderWithUmbracoLink: case MembershipScenario.StandaloneCustomProvider: default: var membershipUser = _provider.GetUser(contentItem.Key, false); //Go and re-fetch the persisted item contentItem.PersistedContent = Mapper.Map <MembershipUser, IMember>(membershipUser); break; } UpdateName(contentItem); //re-assign the mapped values that are not part of the membership provider properties. var builtInAliases = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); foreach (var p in contentItem.PersistedContent.Properties) { var valueMapped = currProps.FirstOrDefault(x => x.Alias == p.Alias); if (builtInAliases.Contains(p.Alias) == false && valueMapped != null) { p.SetValue(valueMapped.GetValue()); // fixme - ok, I give up, at that point tags are dead here, until we figure it out //p.TagChanges.Behavior = valueMapped.TagChanges.Behavior; //p.TagChanges.Enable = valueMapped.TagChanges.Enable; //p.TagChanges.Tags = valueMapped.TagChanges.Tags; } } }
/// <summary> /// Returns an IMember instance used to bind values to and save (depending on the membership scenario) /// </summary> /// <param name="model"></param> /// <returns></returns> private IMember GetExisting(MemberSave model) { var scenario = _services.MemberService.GetMembershipScenario(); var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); switch (scenario) { case MembershipScenario.NativeUmbraco: return(GetExisting(model.Key)); case MembershipScenario.CustomProviderWithUmbracoLink: case MembershipScenario.StandaloneCustomProvider: default: var membershipUser = provider.GetUser(model.Key, false); if (membershipUser == null) { throw new InvalidOperationException("Could not find member with key " + model.Key); } // TODO: Support this scenario! //if (scenario == MembershipScenario.CustomProviderWithUmbracoLink) //{ // //if there's a 'Member' type then we should be able to just go get it from the db since it was created with a link // // to our data. // var memberType = ApplicationContext.Services.MemberTypeService.GetMemberType(Constants.Conventions.MemberTypes.Member); // if (memberType != null) // { // var existing = GetExisting(model.Key); // FilterContentTypeProperties(existing.ContentType, existing.ContentType.PropertyTypes.Select(x => x.Alias).ToArray()); // } //} //generate a member for a generic membership provider //NOTE: We don't care about the password here, so just generate something //var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N")); //var convertResult = membershipUser.ProviderUserKey.TryConvertTo<Guid>(); //if (convertResult.Success == false) //{ // throw new InvalidOperationException("Only membership providers that store a GUID as their ProviderUserKey are supported" + model.Key); //} //member.Key = convertResult.Result; var member = Current.Mapper.Map <MembershipUser, IMember>(membershipUser); return(member); } }
/// <summary> /// Maps the property values to the persisted entity /// </summary> /// <param name="contentItem"></param> private void MapPropertyValues(MemberSave contentItem) { UpdateName(contentItem); //map the custom properties - this will already be set for new entities in our member binder contentItem.PersistedContent.Email = contentItem.Email; contentItem.PersistedContent.Username = contentItem.Username; //use the base method to map the rest of the properties base.MapPropertyValuesForPersistence <IMember, MemberSave>( contentItem, contentItem.PropertyCollectionDto, (save, property) => property.GetValue(), //get prop val (save, property, v) => property.SetValue(v), //set prop val null); // member are all invariant }
private async Task <bool> ValidateMemberDataAsync(MemberSave contentItem) { if (contentItem.Name.IsNullOrWhiteSpace()) { ModelState.AddPropertyError( new ValidationResult("Invalid user name", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); return(false); } if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) { IdentityResult validPassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword); if (!validPassword.Succeeded) { ModelState.AddPropertyError( new ValidationResult("Invalid password: "******"value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password"); return(false); } } IMember?byUsername = _memberService.GetByUsername(contentItem.Username); if (byUsername != null && byUsername.Key != contentItem.Key) { ModelState.AddPropertyError( new ValidationResult("Username is already in use", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); return(false); } IMember?byEmail = _memberService.GetByEmail(contentItem.Email); if (byEmail != null && byEmail.Key != contentItem.Key) { ModelState.AddPropertyError( new ValidationResult("Email address is already in use", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); return(false); } return(true); }
internal bool ValidateUniqueLogin(MemberSave model) { if (model == null) { throw new ArgumentNullException(nameof(model)); } IMember?existingByName = _memberService.GetByUsername(model.Username.Trim()); switch (model.Action) { case ContentSaveAction.Save: //ok, we're updating the member, we need to check if they are changing their login and if so, does it exist already ? if (model.PersistedContent?.Username.InvariantEquals(model.Username.Trim()) == false) { //they are changing their login name if (existingByName != null && existingByName.Username == model.Username.Trim()) { //the user cannot use this login return(false); } } break; case ContentSaveAction.SaveNew: //check if the user's login already exists if (existingByName != null && existingByName.Username == model.Username.Trim()) { //the user cannot use this login return(false); } break; default: //we don't support this for members throw new ArgumentOutOfRangeException(); } return(true); }
internal bool ValidateUniqueEmail(MemberSave contentItem, MembershipProvider membershipProvider, HttpActionContext actionContext) { if (contentItem == null) throw new ArgumentNullException("contentItem"); if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); if (membershipProvider.RequiresUniqueEmail == false) { return true; } int totalRecs; var existingByEmail = membershipProvider.FindUsersByEmail(contentItem.Email.Trim(), 0, int.MaxValue, out totalRecs); switch (contentItem.Action) { case ContentSaveAction.Save: //ok, we're updating the member, we need to check if they are changing their email and if so, does it exist already ? if (contentItem.PersistedContent.Email.InvariantEquals(contentItem.Email.Trim()) == false) { //they are changing their email if (existingByEmail.Cast<MembershipUser>().Select(x => x.Email) .Any(x => x.InvariantEquals(contentItem.Email.Trim()))) { //the user cannot use this email return false; } } break; case ContentSaveAction.SaveNew: //check if the user's email already exists if (existingByEmail.Cast<MembershipUser>().Select(x => x.Email) .Any(x => x.InvariantEquals(contentItem.Email.Trim()))) { //the user cannot use this email return false; } break; default: //we don't support this for members throw new HttpResponseException(HttpStatusCode.NotFound); } return true; }
private static void Map(MemberSave source, IMember target, MapperContext context) { target.IsApproved = source.IsApproved; target.Name = source.Name; target.Email = source.Email; target.Key = source.Key; target.Username = source.Username; target.Comments = source.Comments; target.CreateDate = source.CreateDate; target.UpdateDate = source.UpdateDate; target.Email = source.Email; // TODO: ensure all properties are mapped as required // target.Id = source.Id; // target.ParentId = -1; // target.Path = "-1," + source.Id; // TODO: add groups as required }
/// <summary> /// Gets an instance of IMember used when creating a member /// </summary> /// <param name="model"></param> /// <returns></returns> /// <remarks> /// Depending on whether a custom membership provider is configured this will return different results. /// </remarks> private IMember CreateNew(MemberSave model) { var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); if (provider.IsUmbracoMembershipProvider()) { var contentType = _services.MemberTypeService.Get(model.ContentTypeAlias); if (contentType == null) { throw new InvalidOperationException("No member type found with alias " + model.ContentTypeAlias); } //remove all membership properties, these values are set with the membership provider. FilterMembershipProviderProperties(contentType); //return the new member with the details filled in return(new Member(model.Name, model.Email, model.Username, model.Password.NewPassword, contentType)); } else { //A custom membership provider is configured //NOTE: Below we are assigning the password to just a new GUID because we are not actually storing the password, however that // field is mandatory in the database so we need to put something there. //If the default Member type exists, we'll use that to create the IMember - that way we can associate the custom membership // provider to our data - eventually we can support editing custom properties with a custom provider. var memberType = _services.MemberTypeService.Get(Constants.Conventions.MemberTypes.DefaultAlias); if (memberType != null) { FilterContentTypeProperties(memberType, memberType.PropertyTypes.Select(x => x.Alias).ToArray()); return(new Member(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N"), memberType)); } //generate a member for a generic membership provider var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N")); //we'll just remove all properties here otherwise we'll end up with validation errors, we don't want to persist any property data anyways // in this case. memberType = _services.MemberTypeService.Get(member.ContentTypeId); FilterContentTypeProperties(memberType, memberType.PropertyTypes.Select(x => x.Alias).ToArray()); return(member); } }
/// <summary> /// Maps the property values to the persisted entity /// </summary> /// <param name="contentItem">The member content item to map properties from</param> private void MapPropertyValues(MemberSave contentItem) { // Don't update the name if it is empty if (contentItem.Name.IsNullOrWhiteSpace() == false) { contentItem.PersistedContent.Name = contentItem.Name; } // map the custom properties - this will already be set for new entities in our member binder contentItem.PersistedContent.IsApproved = contentItem.IsApproved; contentItem.PersistedContent.Email = contentItem.Email.Trim(); contentItem.PersistedContent.Username = contentItem.Username; // use the base method to map the rest of the properties MapPropertyValuesForPersistence <IMember, MemberSave>( contentItem, contentItem.PropertyCollectionDto, (save, property) => property.GetValue(), // get prop val (save, property, v) => property.SetValue(v), // set prop val null); // member are all invariant }
/// <summary> /// We need to manually validate a few things here like email and login to make sure they are valid and aren't duplicates /// </summary> /// <param name="model"></param> /// <param name="dto"></param> /// <param name="modelState"></param> /// <param name="modelWithProperties"></param> /// <returns></returns> public override bool ValidatePropertiesData(MemberSave model, IContentProperties <ContentPropertyBasic> modelWithProperties, ContentPropertyCollectionDto dto, ModelStateDictionary modelState) { if (model.Username.IsNullOrWhiteSpace()) { modelState.AddPropertyError( new ValidationResult("Invalid user name", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); } if (model.Email.IsNullOrWhiteSpace() || new EmailAddressAttribute().IsValid(model.Email) == false) { modelState.AddPropertyError( new ValidationResult("Invalid email", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); } //default provider! var membershipProvider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); var validEmail = ValidateUniqueEmail(model, membershipProvider); if (validEmail == false) { modelState.AddPropertyError( new ValidationResult("Email address is already in use", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); } var validLogin = ValidateUniqueLogin(model, membershipProvider); if (validLogin == false) { modelState.AddPropertyError( new ValidationResult("Username is already in use", new[] { "value" }), $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); } return(base.ValidatePropertiesData(model, modelWithProperties, dto, modelState)); }
/// <summary> /// This ensures that the internal membership property types are removed from validation before processing the validation /// since those properties are actually mapped to real properties of the IMember. /// This also validates any posted data for fields that are sensitive. /// </summary> /// <param name="model"></param> /// <param name="modelWithProperties"></param> /// <param name="actionContext"></param> /// <returns></returns> public override bool ValidateProperties(MemberSave model, IContentProperties <ContentPropertyBasic> modelWithProperties, HttpActionContext actionContext) { var propertiesToValidate = model.Properties.ToList(); var defaultProps = ConventionsHelper.GetStandardPropertyTypeStubs(); var exclude = defaultProps.Select(x => x.Value.Alias).ToArray(); foreach (var remove in exclude) { propertiesToValidate.RemoveAll(property => property.Alias == remove); } //if the user doesn't have access to sensitive values, then we need to validate the incoming properties to check //if a sensitive value is being submitted. if (UmbracoContextAccessor.UmbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false) { var contentType = _memberTypeService.Get(model.PersistedContent.ContentTypeId); var sensitiveProperties = contentType .PropertyTypes.Where(x => contentType.IsSensitiveProperty(x.Alias)) .ToList(); foreach (var sensitiveProperty in sensitiveProperties) { var prop = propertiesToValidate.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); if (prop != null) { //this should not happen, this means that there was data posted for a sensitive property that //the user doesn't have access to, which means that someone is trying to hack the values. var message = $"property with alias: {prop.Alias} cannot be posted"; actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, new InvalidOperationException(message)); return(false); } } } return(ValidateProperties(propertiesToValidate, model.PersistedContent.Properties.ToList(), actionContext)); }
/// <summary> /// Quick check to see if the 'normal' settable properties for the membership provider have changed /// </summary> /// <param name="membershipUser"></param> /// <param name="contentItem"></param> /// <returns></returns> /// <remarks> /// By 'normal' we mean that they can simply be set on the membership user and don't require method calls like ChangePassword or UnlockUser /// </remarks> private bool HasMembershipUserChanged(MembershipUser membershipUser, MemberSave contentItem) { if (contentItem.Email.Trim().InvariantEquals(membershipUser.Email) == false || contentItem.IsApproved != membershipUser.IsApproved || contentItem.Comments != membershipUser.Comment) { return true; } return false; }
/// <summary> /// This is going to create the user with the membership provider and check for validation /// </summary> /// <param name="contentItem"></param> /// <param name="status"></param> /// <returns></returns> /// <remarks> /// Depending on if the Umbraco membership provider is active or not, the process differs slightly: /// /// * If the umbraco membership provider is used - we create the membership user first with the membership provider, since /// it's the umbraco membership provider, this writes to the umbraco tables. When that is complete we re-fetch the IMember /// model data from the db. In this case we don't care what the provider user key is. /// * If we're using a non-umbraco membership provider - we check if there is a 'Member' member type - if so /// we create an empty IMember instance first (of type 'Member'), this gives us a unique ID (GUID) /// that we then use to create the member in the custom membership provider. This acts as the link between Umbraco data and /// the custom membership provider data. This gives us the ability to eventually have custom membership properties but still use /// a custom memberhip provider. If there is no 'Member' member type, then we will simply just create the membership provider member /// with no link to our data. /// /// If this is successful, it will go and re-fetch the IMember from the db because it will now have an ID because the Umbraco provider /// uses the umbraco data store - then of course we need to re-map it to the saved property values. /// </remarks> private MembershipUser CreateWithMembershipProvider(MemberSave contentItem, out MembershipCreateStatus status) { MembershipUser membershipUser; switch (MembershipScenario) { case MembershipScenario.NativeUmbraco: //We are using the umbraco membership provider, create the member using the membership provider first. var umbracoMembershipProvider = (global::umbraco.providers.members.UmbracoMembershipProvider)Membership.Provider; //TODO: We are not supporting q/a - passing in empty here membershipUser = umbracoMembershipProvider.CreateUser( contentItem.ContentTypeAlias, contentItem.Username, contentItem.Password.NewPassword, contentItem.Email, "", "", contentItem.IsApproved, Guid.NewGuid(), //since it's the umbraco provider, the user key here doesn't make any difference out status); break; case MembershipScenario.CustomProviderWithUmbracoLink: //We are using a custom membership provider, we'll create an empty IMember first to get the unique id to use // as the provider user key. //create it - this persisted item has already been set in the MemberBinder based on the 'Member' member type: Services.MemberService.Save(contentItem.PersistedContent); //TODO: We are not supporting q/a - passing in empty here membershipUser = Membership.CreateUser( contentItem.Username, contentItem.Password.NewPassword, contentItem.Email, "TEMP", //some membership provider's require something here even if q/a is disabled! "TEMP", //some membership provider's require something here even if q/a is disabled! contentItem.IsApproved, contentItem.PersistedContent.Key, //custom membership provider, we'll link that based on the IMember unique id (GUID) out status); break; case MembershipScenario.StandaloneCustomProvider: // we don't have a member type to use so we will just create the basic membership user with the provider with no // link back to the umbraco data var newKey = Guid.NewGuid(); //TODO: We are not supporting q/a - passing in empty here membershipUser = Membership.CreateUser( contentItem.Username, contentItem.Password.NewPassword, contentItem.Email, "TEMP", //some membership provider's require something here even if q/a is disabled! "TEMP", //some membership provider's require something here even if q/a is disabled! contentItem.IsApproved, newKey, out status); //we need to set the key back on the PersistedContent property so that the display model is returned correctly contentItem.PersistedContent.Key = newKey; break; default: throw new ArgumentOutOfRangeException(); } //TODO: Localize these! switch (status) { case MembershipCreateStatus.Success: //if the comments are there then we need to save them if (contentItem.Comments.IsNullOrWhiteSpace() == false) { membershipUser.Comment = contentItem.Comments; Membership.UpdateUser(membershipUser); } //if we're using the umbraco provider, we'll have to go re-fetch the IMember since the provider // has now updated it separately but we need to maintain all the correct bound data if (MembershipScenario == MembershipScenario.NativeUmbraco) { //Go and re-fetch the persisted item contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); //remap the values to save MapPropertyValues(contentItem); } break; case MembershipCreateStatus.InvalidUserName: ModelState.AddPropertyError( new ValidationResult("Invalid user name", new[] { "value" }), string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case MembershipCreateStatus.InvalidPassword: ModelState.AddPropertyError( new ValidationResult("Invalid password", new[] { "value" }), string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case MembershipCreateStatus.InvalidQuestion: case MembershipCreateStatus.InvalidAnswer: throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); case MembershipCreateStatus.InvalidEmail: ModelState.AddPropertyError( new ValidationResult("Invalid email", new[] { "value" }), string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case MembershipCreateStatus.DuplicateUserName: ModelState.AddPropertyError( new ValidationResult("Username is already in use", new[] { "value" }), string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case MembershipCreateStatus.DuplicateEmail: ModelState.AddPropertyError( new ValidationResult("Email address is already in use", new[] { "value" }), string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case MembershipCreateStatus.InvalidProviderUserKey: ModelState.AddPropertyError( //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("Invalid provider user key"), "default"); break; case MembershipCreateStatus.DuplicateProviderUserKey: ModelState.AddPropertyError( //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("Duplicate provider user key"), "default"); break; case MembershipCreateStatus.ProviderError: case MembershipCreateStatus.UserRejected: ModelState.AddPropertyError( //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("User could not be created (rejected by provider)"), "default"); break; default: throw new ArgumentOutOfRangeException(); } return membershipUser; }
/// <summary> /// Returns an IMember instance used to bind values to and save (depending on the membership scenario) /// </summary> /// <param name="model"></param> /// <returns></returns> private IMember GetExisting(MemberSave model) { return(GetExisting(model.Key)); }
/// <summary> /// Setup all standard member data for test /// </summary> private Member SetupMemberTestData( out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction contentAction) { // arrange var memberType = MemberTypeBuilder.CreateSimpleMemberType(); var member = MemberBuilder.CreateSimpleMember(memberType, "Test Member", "*****@*****.**", "123", "test"); var memberId = 123; member.Id = memberId; // TODO: replace with builder for MemberSave and MemberDisplay fakeMemberData = new MemberSave { Id = memberId, SortOrder = member.SortOrder, ContentTypeId = memberType.Id, Key = member.Key, Password = new ChangingPasswordModel { Id = 456, NewPassword = member.RawPasswordValue, OldPassword = null }, Name = member.Name, Email = member.Email, Username = member.Username, PersistedContent = member, PropertyCollectionDto = new ContentPropertyCollectionDto(), Groups = new List <string>(), // Alias = "fakeAlias", ContentTypeAlias = member.ContentTypeAlias, Action = contentAction, Icon = "icon-document", Path = member.Path, }; memberDisplay = new MemberDisplay { Id = memberId, SortOrder = member.SortOrder, ContentTypeId = memberType.Id, Key = member.Key, Name = member.Name, Email = member.Email, Username = member.Username, // Alias = "fakeAlias", ContentTypeAlias = member.ContentTypeAlias, ContentType = new ContentTypeBasic(), ContentTypeName = member.ContentType.Name, Icon = fakeMemberData.Icon, Path = member.Path, Tabs = new List <Tab <ContentPropertyDisplay> > { new() { Alias = "test", Id = 77, Properties = new List <ContentPropertyDisplay> { new() { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login" }, new() { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email" }, new() { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password", }, new() { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup", }, new() { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}failedPasswordAttempts", }, new() { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}approved", }, new() { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lockedOut", }, new() { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLockoutDate", }, new() { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLoginDate", }, new() { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastPasswordChangeDate", }, }, }, }, }; return(member); }
/// <summary> /// Re-fetches the database data to map to the PersistedContent object and re-maps the posted properties so that the display object is up-to-date /// </summary> /// <param name="contentItem"></param> /// <remarks> /// This is done during an update if the membership provider has changed some underlying data - we need to ensure that our model is consistent with that data /// </remarks> private void RefetchMemberData(MemberSave contentItem) { switch (MembershipScenario) { case MembershipScenario.NativeUmbraco: //Go and re-fetch the persisted item contentItem.PersistedContent = Services.MemberService.GetByKey(contentItem.Key); //remap the values to save MapPropertyValues(contentItem); break; case MembershipScenario.CustomProviderWithUmbracoLink: case MembershipScenario.StandaloneCustomProvider: default: var membershipUser = Membership.GetUser(contentItem.Key, false); //Go and re-fetch the persisted item contentItem.PersistedContent = Mapper.Map<MembershipUser, IMember>(membershipUser); //remap the values to save MapPropertyValues(contentItem); break; } }
/// <summary> /// Following a refresh of member data called during an update if the membership provider has changed some underlying data, /// we don't want to lose the provided, and potentially changed, username /// </summary> /// <param name="contentItem"></param> /// <param name="providedUserName"></param> private static void RestoreProvidedUserName(MemberSave contentItem, string providedUserName) { contentItem.PersistedContent.Username = providedUserName; }
public async Task <ActionResult <MemberDisplay?> > PostSave([ModelBinder(typeof(MemberBinder))] MemberSave contentItem) { if (contentItem == null) { throw new ArgumentNullException("The member content item was null"); } // If we've reached here it means: // * Our model has been bound // * and validated // * any file attachments have been saved to their temporary location for us to use // * we have a reference to the DTO object and the persisted object // * Permissions are valid // map the properties to the persisted entity MapPropertyValues(contentItem); await ValidateMemberDataAsync(contentItem); // Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors if (ModelState.IsValid == false) { MemberDisplay?forDisplay = _umbracoMapper.Map <MemberDisplay>(contentItem.PersistedContent); return(ValidationProblem(forDisplay, ModelState)); } // Create a scope here which will wrap all child data operations in a single transaction. // We'll complete this at the end of this method if everything succeeeds, else // all data operations will roll back. using ICoreScope scope = _scopeProvider.CreateCoreScope(); // Depending on the action we need to first do a create or update using the membership manager // this ensures that passwords are formatted correctly and also performs the validation on the provider itself. switch (contentItem.Action) { case ContentSaveAction.Save: ActionResult <bool> updateSuccessful = await UpdateMemberAsync(contentItem); if (!(updateSuccessful.Result is null)) { return(updateSuccessful.Result); } break; case ContentSaveAction.SaveNew: ActionResult <bool> createSuccessful = await CreateMemberAsync(contentItem); if (!(createSuccessful.Result is null)) { return(createSuccessful.Result); } break; default: // we don't support anything else for members return(NotFound()); } // return the updated model MemberDisplay?display = _umbracoMapper.Map <MemberDisplay>(contentItem.PersistedContent); // lastly, if it is not valid, add the model state to the outgoing object and throw a 403 if (!ModelState.IsValid) { return(ValidationProblem(display, ModelState, StatusCodes.Status403Forbidden)); } // put the correct messages in switch (contentItem.Action) { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: display?.AddSuccessNotification( _localizedTextService.Localize("speechBubbles", "editMemberSaved"), _localizedTextService.Localize("speechBubbles", "editMemberSaved")); break; } // Mark transaction to commit all changes scope.Complete(); return(display); }
/// <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); }
/// <summary> /// Create a member from the supplied member content data /// /// All member password processing and creation is done via the identity manager /// </summary> /// <param name="contentItem">Member content data</param> /// <returns>The identity result of the created member</returns> private async Task <ActionResult <bool> > CreateMemberAsync(MemberSave contentItem) { IMemberType?memberType = _memberTypeService.Get(contentItem.ContentTypeAlias); if (memberType == null) { throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); } var identityMember = MemberIdentityUser.CreateNew( contentItem.Username, contentItem.Email, memberType.Alias, contentItem.IsApproved, contentItem.Name); IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password?.NewPassword); if (created.Succeeded == false) { MemberDisplay?forDisplay = _umbracoMapper.Map <MemberDisplay>(contentItem.PersistedContent); foreach (IdentityError error in created.Errors) { switch (error.Code) { case nameof(IdentityErrorDescriber.InvalidUserName): ModelState.AddPropertyError( new ValidationResult(error.Description, new[] { "value" }), string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case nameof(IdentityErrorDescriber.PasswordMismatch): case nameof(IdentityErrorDescriber.PasswordRequiresDigit): case nameof(IdentityErrorDescriber.PasswordRequiresLower): case nameof(IdentityErrorDescriber.PasswordRequiresNonAlphanumeric): case nameof(IdentityErrorDescriber.PasswordRequiresUniqueChars): case nameof(IdentityErrorDescriber.PasswordRequiresUpper): case nameof(IdentityErrorDescriber.PasswordTooShort): ModelState.AddPropertyError( new ValidationResult(error.Description, new[] { "value" }), string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case nameof(IdentityErrorDescriber.InvalidEmail): ModelState.AddPropertyError( new ValidationResult(error.Description, new[] { "value" }), string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case nameof(IdentityErrorDescriber.DuplicateUserName): ModelState.AddPropertyError( new ValidationResult(error.Description, new[] { "value" }), string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case nameof(IdentityErrorDescriber.DuplicateEmail): ModelState.AddPropertyError( new ValidationResult(error.Description, new[] { "value" }), string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; } } return(ValidationProblem(forDisplay, ModelState)); } // now re-look up the member, which will now exist IMember?member = _memberService.GetByEmail(contentItem.Email); if (member is null) { return(false); } // map the save info over onto the user member = _umbracoMapper.Map <MemberSave, IMember>(contentItem, member); int creatorId = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1; member.CreatorId = creatorId; // assign the mapped property values that are not part of the identity properties string[] builtInAliases = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Select(x => x.Key).ToArray(); foreach (ContentPropertyBasic property in contentItem.Properties) { if (builtInAliases.Contains(property.Alias) == false) { member.Properties[property.Alias]?.SetValue(property.Value); } } // now the member has been saved via identity, resave the member with mapped content properties _memberService.Save(member); contentItem.PersistedContent = member; ActionResult <bool> rolesChanged = await AddOrUpdateRoles(contentItem.Groups, identityMember); if (!rolesChanged.Value && rolesChanged.Result != null) { return(rolesChanged.Result); } return(true); }
public MemberDisplay PostSave( [ModelBinder(typeof(MemberBinder))] MemberSave contentItem) { //If we've reached here it means: // * Our model has been bound // * and validated // * any file attachments have been saved to their temporary location for us to use // * we have a reference to the DTO object and the persisted object // * Permissions are valid //This is a special case for when we're not using the umbraco membership provider - when this is the case // we will not have a ContentTypeAlias set which means the model state will be invalid but we don't care about that // so we'll remove that model state value if (MembershipScenario != MembershipScenario.NativeUmbraco) { ModelState.Remove("ContentTypeAlias"); // TODO: We're removing this because we are not displaying it but when we support the CustomProviderWithUmbracoLink scenario // we will be able to have a real name associated so do not remove this state once that is implemented! ModelState.Remove("Name"); } //map the properties to the persisted entity MapPropertyValues(contentItem); //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors if (ModelState.IsValid == false) { var forDisplay = Mapper.Map <MemberDisplay>(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } // TODO: WE need to support this! - requires UI updates, etc... if (_provider.RequiresQuestionAndAnswer) { throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); } //We're gonna look up the current roles now because the below code can cause // events to be raised and developers could be manually adding roles to members in // their handlers. If we don't look this up now there's a chance we'll just end up // removing the roles they've assigned. var currRoles = Roles.GetRolesForUser(contentItem.PersistedContent.Username); //find the ones to remove and remove them var rolesToRemove = currRoles.Except(contentItem.Groups).ToArray(); string generatedPassword = null; //Depending on the action we need to first do a create or update using the membership provider // this ensures that passwords are formatted correctly and also performs the validation on the provider itself. switch (contentItem.Action) { case ContentSaveAction.Save: generatedPassword = UpdateWithMembershipProvider(contentItem); break; case ContentSaveAction.SaveNew: MembershipCreateStatus status; CreateWithMembershipProvider(contentItem, out status); // save the ID of the creator contentItem.PersistedContent.CreatorId = Security.CurrentUser.Id; break; default: //we don't support anything else for members throw new HttpResponseException(HttpStatusCode.NotFound); } //If we've had problems creating/updating the user with the provider then return the error if (ModelState.IsValid == false) { var forDisplay = Mapper.Map <MemberDisplay>(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } //save the IMember - // TODO: When we support the CustomProviderWithUmbracoLink scenario, we'll need to save the custom properties for that here too if (MembershipScenario == MembershipScenario.NativeUmbraco) { //save the item //NOTE: We are setting the password to NULL - this indicates to the system to not actually save the password // so it will not get overwritten! contentItem.PersistedContent.RawPasswordValue = null; //create/save the IMember Services.MemberService.Save(contentItem.PersistedContent); } //Now let's do the role provider stuff - now that we've saved the content item (that is important since // if we are changing the username, it must be persisted before looking up the member roles). if (rolesToRemove.Any()) { Roles.RemoveUserFromRoles(contentItem.PersistedContent.Username, rolesToRemove); } //find the ones to add and add them var toAdd = contentItem.Groups.Except(currRoles).ToArray(); if (toAdd.Any()) { //add the ones submitted Roles.AddUserToRoles(contentItem.PersistedContent.Username, toAdd); } //set the generated password (if there was one) - in order to do this we'll chuck the generated password into the // additional data of the IUmbracoEntity of the persisted item - then we can retrieve this in the model mapper and set // the value to be given to the UI. Hooray for AdditionalData :) contentItem.PersistedContent.AdditionalData["GeneratedPassword"] = generatedPassword; //return the updated model var display = Mapper.Map <MemberDisplay>(contentItem.PersistedContent); //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 HandleInvalidModelState(display); var localizedTextService = Services.TextService; //put the correct messages in switch (contentItem.Action) { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); break; } return(display); }
/// <summary> /// Update the membership user using the membership provider (for things like email, etc...) /// If a password change is detected then we'll try that too. /// </summary> /// <param name="contentItem"></param> /// <returns> /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. /// </returns> private string UpdateWithMembershipProvider(MemberSave contentItem) { //Get the member from the provider var membershipUser = _provider.GetUser(contentItem.PersistedContent.Key, false); if (membershipUser == null) { //This should never happen! so we'll let it YSOD if it does. throw new InvalidOperationException("Could not get member from membership provider " + _provider.Name + " with key " + contentItem.PersistedContent.Key); } var shouldReFetchMember = false; var providedUserName = contentItem.PersistedContent.Username; //Update the membership user if it has changed try { var requiredUpdating = Members.UpdateMember(membershipUser, _provider, contentItem.Email.Trim(), contentItem.IsApproved, comment: contentItem.Comments); if (requiredUpdating.Success) { //re-map these values shouldReFetchMember = true; } } catch (Exception ex) { LogHelper.WarnWithException <MemberController>("Could not update member, the provider returned an error", ex); ModelState.AddPropertyError( //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("Could not update member, the provider returned an error: " + ex.Message + " (see log for full details)"), "default"); } //if they were locked but now they are trying to be unlocked if (membershipUser.IsLockedOut && contentItem.IsLockedOut == false) { try { var result = _provider.UnlockUser(membershipUser.UserName); if (result == false) { //it wasn't successful - but it won't really tell us why. ModelState.AddModelError("custom", "Could not unlock the user"); } else { shouldReFetchMember = true; } } catch (Exception ex) { ModelState.AddModelError("custom", ex); } } else if (membershipUser.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 un-approve them ModelState.AddModelError("custom", "An admin cannot lock a user"); } //password changes ? if (contentItem.Password == null) { //If the provider has changed some values, these values need to be reflected in the member object //that will get mapped to the display object if (shouldReFetchMember) { RefetchMemberData(contentItem, LookupType.ByKey); RestoreProvidedUserName(contentItem, providedUserName); } return(null); } var passwordChangeResult = Members.ChangePassword(membershipUser.UserName, contentItem.Password, _provider); if (passwordChangeResult.Success) { //If the provider has changed some values, these values need to be reflected in the member object //that will get mapped to the display object if (shouldReFetchMember) { RefetchMemberData(contentItem, LookupType.ByKey); RestoreProvidedUserName(contentItem, providedUserName); } //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 return(passwordChangeResult.Result.ResetPassword); } //it wasn't successful, so add the change error to the model state ModelState.AddPropertyError( passwordChangeResult.Result.ChangeError, string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); return(null); }
/// <summary> /// Update the membership user using the membership provider (for things like email, etc...) /// If a password change is detected then we'll try that too. /// </summary> /// <param name="contentItem"></param> /// <returns> /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. /// </returns> private string UpdateWithMembershipProvider(MemberSave contentItem) { //Get the member from the provider var membershipUser = _provider.GetUser(contentItem.PersistedContent.Key, false); if (membershipUser == null) { //This should never happen! so we'll let it YSOD if it does. throw new InvalidOperationException("Could not get member from membership provider " + _provider.Name + " with key " + contentItem.PersistedContent.Key); } var shouldReFetchMember = false; var providedUserName = contentItem.PersistedContent.Username; //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 if (Security.CurrentUser.HasAccessToSensitiveData() == false) { var memberType = Services.MemberTypeService.Get(contentItem.PersistedContent.ContentTypeId); var sensitiveProperties = memberType .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias)) .ToList(); foreach (var sensitiveProperty in sensitiveProperties) { //if found, change the value of the contentItem model to the persisted value so it remains unchanged switch (sensitiveProperty.Alias) { case Constants.Conventions.Member.Comments: contentItem.Comments = contentItem.PersistedContent.Comments; break; case Constants.Conventions.Member.IsApproved: contentItem.IsApproved = contentItem.PersistedContent.IsApproved; break; case Constants.Conventions.Member.IsLockedOut: contentItem.IsLockedOut = contentItem.PersistedContent.IsLockedOut; break; } } } //Update the membership user if it has changed try { var requiredUpdating = Members.UpdateMember(membershipUser, _provider, contentItem.Email.Trim(), contentItem.IsApproved, comment: contentItem.Comments); if (requiredUpdating.Success) { //re-map these values shouldReFetchMember = true; } } catch (Exception ex) { Logger.Warn <MemberController>(ex, "Could not update member, the provider returned an error"); ModelState.AddPropertyError( //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("Could not update member, the provider returned an error: " + ex.Message + " (see log for full details)"), "default"); } //if they were locked but now they are trying to be unlocked if (membershipUser.IsLockedOut && contentItem.IsLockedOut == false) { try { var result = _provider.UnlockUser(membershipUser.UserName); if (result == false) { //it wasn't successful - but it won't really tell us why. ModelState.AddModelError("custom", "Could not unlock the user"); } else { shouldReFetchMember = true; } } catch (Exception ex) { ModelState.AddModelError("custom", ex); } } else if (membershipUser.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 un-approve them ModelState.AddModelError("custom", "An admin cannot lock a user"); } //password changes ? if (contentItem.Password == null) { //If the provider has changed some values, these values need to be reflected in the member object //that will get mapped to the display object if (shouldReFetchMember) { RefetchMemberData(contentItem, LookupType.ByKey); RestoreProvidedUserName(contentItem, providedUserName); } return(null); } var passwordChangeResult = Members.ChangePassword(membershipUser.UserName, contentItem.Password, _provider); if (passwordChangeResult.Success) { //If the provider has changed some values, these values need to be reflected in the member object //that will get mapped to the display object if (shouldReFetchMember) { RefetchMemberData(contentItem, LookupType.ByKey); RestoreProvidedUserName(contentItem, providedUserName); } //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 return(passwordChangeResult.Result.ResetPassword); } //it wasn't successful, so add the change error to the model state ModelState.AddPropertyError( passwordChangeResult.Result.ChangeError, string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); return(null); }
/// <summary> /// This is going to create the user with the membership provider and check for validation /// </summary> /// <param name="contentItem"></param> /// <param name="status"></param> /// <returns></returns> /// <remarks> /// Depending on if the Umbraco membership provider is active or not, the process differs slightly: /// /// * If the umbraco membership provider is used - we create the membership user first with the membership provider, since /// it's the umbraco membership provider, this writes to the umbraco tables. When that is complete we re-fetch the IMember /// model data from the db. In this case we don't care what the provider user key is. /// * If we're using a non-umbraco membership provider - we check if there is a 'Member' member type - if so /// we create an empty IMember instance first (of type 'Member'), this gives us a unique ID (GUID) /// that we then use to create the member in the custom membership provider. This acts as the link between Umbraco data and /// the custom membership provider data. This gives us the ability to eventually have custom membership properties but still use /// a custom membership provider. If there is no 'Member' member type, then we will simply just create the membership provider member /// with no link to our data. /// /// If this is successful, it will go and re-fetch the IMember from the db because it will now have an ID because the Umbraco provider /// uses the umbraco data store - then of course we need to re-map it to the saved property values. /// </remarks> private MembershipUser CreateWithMembershipProvider(MemberSave contentItem, out MembershipCreateStatus status) { MembershipUser membershipUser; switch (MembershipScenario) { case MembershipScenario.NativeUmbraco: //We are using the umbraco membership provider, create the member using the membership provider first. var umbracoMembershipProvider = (UmbracoMembershipProviderBase)_provider; // TODO: We are not supporting q/a - passing in empty here membershipUser = umbracoMembershipProvider.CreateUser( contentItem.ContentTypeAlias, contentItem.Username, contentItem.Password.NewPassword, contentItem.Email, "", "", contentItem.IsApproved, Guid.NewGuid(), //since it's the umbraco provider, the user key here doesn't make any difference out status); break; case MembershipScenario.CustomProviderWithUmbracoLink: //We are using a custom membership provider, we'll create an empty IMember first to get the unique id to use // as the provider user key. //create it - this persisted item has already been set in the MemberBinder based on the 'Member' member type: Services.MemberService.Save(contentItem.PersistedContent); // TODO: We are not supporting q/a - passing in empty here membershipUser = _provider.CreateUser( contentItem.Username, contentItem.Password.NewPassword, contentItem.Email, "TEMP", //some membership provider's require something here even if q/a is disabled! "TEMP", //some membership provider's require something here even if q/a is disabled! contentItem.IsApproved, contentItem.PersistedContent.Key, //custom membership provider, we'll link that based on the IMember unique id (GUID) out status); break; case MembershipScenario.StandaloneCustomProvider: // we don't have a member type to use so we will just create the basic membership user with the provider with no // link back to the umbraco data var newKey = Guid.NewGuid(); // TODO: We are not supporting q/a - passing in empty here membershipUser = _provider.CreateUser( contentItem.Username, contentItem.Password.NewPassword, contentItem.Email, "TEMP", //some membership provider's require something here even if q/a is disabled! "TEMP", //some membership provider's require something here even if q/a is disabled! contentItem.IsApproved, newKey, out status); break; default: throw new ArgumentOutOfRangeException(); } // TODO: Localize these! switch (status) { case MembershipCreateStatus.Success: //map the key back contentItem.Key = membershipUser.ProviderUserKey.TryConvertTo <Guid>().Result; contentItem.PersistedContent.Key = contentItem.Key; //if the comments are there then we need to save them if (contentItem.Comments.IsNullOrWhiteSpace() == false) { membershipUser.Comment = contentItem.Comments; _provider.UpdateUser(membershipUser); } RefetchMemberData(contentItem, LookupType.ByUserName); break; case MembershipCreateStatus.InvalidUserName: ModelState.AddPropertyError( new ValidationResult("Invalid user name", new[] { "value" }), string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case MembershipCreateStatus.InvalidPassword: ModelState.AddPropertyError( new ValidationResult("Invalid password", new[] { "value" }), string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case MembershipCreateStatus.InvalidQuestion: case MembershipCreateStatus.InvalidAnswer: throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); case MembershipCreateStatus.InvalidEmail: ModelState.AddPropertyError( new ValidationResult("Invalid email", new[] { "value" }), string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case MembershipCreateStatus.DuplicateUserName: ModelState.AddPropertyError( new ValidationResult("Username is already in use", new[] { "value" }), string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case MembershipCreateStatus.DuplicateEmail: ModelState.AddPropertyError( new ValidationResult("Email address is already in use", new[] { "value" }), string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); break; case MembershipCreateStatus.InvalidProviderUserKey: ModelState.AddPropertyError( //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("Invalid provider user key"), "default"); break; case MembershipCreateStatus.DuplicateProviderUserKey: ModelState.AddPropertyError( //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("Duplicate provider user key"), "default"); break; case MembershipCreateStatus.ProviderError: case MembershipCreateStatus.UserRejected: ModelState.AddPropertyError( //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("User could not be created (rejected by provider)"), "default"); break; default: throw new ArgumentOutOfRangeException(); } return(membershipUser); }
/// <summary> /// Maps the property values to the persisted entity /// </summary> /// <param name="contentItem"></param> private void MapPropertyValues(MemberSave contentItem) { UpdateName(contentItem); //map the custom properties - this will already be set for new entities in our member binder contentItem.PersistedContent.Email = contentItem.Email; contentItem.PersistedContent.Username = contentItem.Username; //use the base method to map the rest of the properties base.MapPropertyValues(contentItem); }
/// <summary> /// Update the membership user using the membership provider (for things like email, etc...) /// If a password change is detected then we'll try that too. /// </summary> /// <param name="contentItem"></param> /// <returns> /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. /// </returns> private string UpdateWithMembershipProvider(MemberSave contentItem) { //Get the member from the provider var membershipUser = Membership.Provider.GetUser(contentItem.PersistedContent.Key, false); if (membershipUser == null) { //This should never happen! so we'll let it YSOD if it does. throw new InvalidOperationException("Could not get member from membership provider " + Membership.Provider.Name + " with key " + contentItem.PersistedContent.Key); } var shouldReFetchMember = false; //Update the membership user if it has changed if (HasMembershipUserChanged(membershipUser, contentItem)) { membershipUser.Email = contentItem.Email.Trim(); membershipUser.IsApproved = contentItem.IsApproved; membershipUser.Comment = contentItem.Comments; try { Membership.Provider.UpdateUser(membershipUser); //re-map these values shouldReFetchMember = true; } catch (Exception ex) { LogHelper.WarnWithException<MemberController>("Could not update member, the provider returned an error", ex); ModelState.AddPropertyError( //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("Could not update member, the provider returned an error: " + ex.Message + " (see log for full details)"), "default"); } } //if they were locked but now they are trying to be unlocked if (membershipUser.IsLockedOut && contentItem.IsLockedOut == false) { try { var result = Membership.Provider.UnlockUser(membershipUser.UserName); if (result == false) { //it wasn't successful - but it won't really tell us why. ModelState.AddModelError("custom", "Could not unlock the user"); } else { shouldReFetchMember = true; } } catch (Exception ex) { ModelState.AddModelError("custom", ex); } } else if (membershipUser.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 un-approve them ModelState.AddModelError("custom", "An admin cannot lock a user"); } //password changes ? if (contentItem.Password == null) { //If the provider has changed some values, these values need to be reflected in the member object //that will get mapped to the display object if (shouldReFetchMember) { RefetchMemberData(contentItem); } return null; } var passwordChangeResult = Security.ChangePassword(membershipUser.UserName, contentItem.Password, Membership.Provider); if (passwordChangeResult.Success) { //If the provider has changed some values, these values need to be reflected in the member object //that will get mapped to the display object if (shouldReFetchMember) { RefetchMemberData(contentItem); } //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 return passwordChangeResult.Result.ResetPassword; } //it wasn't successful, so add the change error to the model state ModelState.AddPropertyError( passwordChangeResult.Result.ChangeError, string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); return null; }
/// <summary> /// Setup all standard member data for test /// </summary> private Member SetupMemberTestData( out MemberSave fakeMemberData, out MemberDisplay memberDisplay, ContentSaveAction contentAction) { // arrange MemberType memberType = MemberTypeBuilder.CreateSimpleMemberType(); Member member = MemberBuilder.CreateSimpleMember(memberType, "Test Member", "*****@*****.**", "123", "test"); var memberId = 123; member.Id = memberId; // TODO: replace with builder for MemberSave and MemberDisplay fakeMemberData = new MemberSave() { Id = memberId, SortOrder = member.SortOrder, ContentTypeId = memberType.Id, Key = member.Key, Password = new ChangingPasswordModel() { Id = 456, NewPassword = member.RawPasswordValue, OldPassword = null }, Name = member.Name, Email = member.Email, Username = member.Username, PersistedContent = member, PropertyCollectionDto = new ContentPropertyCollectionDto() { }, Groups = new List <string>(), //Alias = "fakeAlias", ContentTypeAlias = member.ContentTypeAlias, Action = contentAction, Icon = "icon-document", Path = member.Path }; memberDisplay = new MemberDisplay() { Id = memberId, SortOrder = member.SortOrder, ContentTypeId = memberType.Id, Key = member.Key, Name = member.Name, Email = member.Email, Username = member.Username, //Alias = "fakeAlias", ContentTypeAlias = member.ContentTypeAlias, ContentType = new ContentTypeBasic(), ContentTypeName = member.ContentType.Name, Icon = fakeMemberData.Icon, Path = member.Path, Tabs = new List <Tab <ContentPropertyDisplay> >() { new Tab <ContentPropertyDisplay>() { Alias = "test", Id = 77, Properties = new List <ContentPropertyDisplay>() { new ContentPropertyDisplay() { Alias = "_umb_id", View = "idwithguid", Value = new [] { "123", "guid" } }, new ContentPropertyDisplay() { Alias = "_umb_doctype" }, new ContentPropertyDisplay() { Alias = "_umb_login" }, new ContentPropertyDisplay() { Alias = "_umb_email" }, new ContentPropertyDisplay() { Alias = "_umb_password" }, new ContentPropertyDisplay() { Alias = "_umb_membergroup" } } } } }; return(member); }
/// <summary> /// Returns an IMember instance used to bind values to and save (depending on the membership scenario) /// </summary> /// <param name="model"></param> /// <returns></returns> private IMember GetExisting(MemberSave model) => GetExisting(model.Key);