using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Web; using System.Web.Security; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Security; using Umbraco.Web.Models; using Umbraco.Web.PublishedCache; using Umbraco.Core.Cache; using Umbraco.Web.Editors; using Umbraco.Web.Security.Providers; using Umbraco.Core.Services; using MPE = global::Umbraco.Core.Security.MembershipProviderExtensions; namespace Umbraco.Web.Security { /// /// A helper class for handling Members /// public class MembershipHelper { private readonly MembershipProvider _membershipProvider; private readonly RoleProvider _roleProvider; private readonly ApplicationContext _applicationContext; private readonly HttpContextBase _httpContext; private readonly UmbracoContext _umbracoContext; #region Constructors [Obsolete("Use the constructor specifying an UmbracoContext")] [EditorBrowsable(EditorBrowsableState.Never)] public MembershipHelper(ApplicationContext applicationContext, HttpContextBase httpContext) : this(applicationContext, httpContext, MPE.GetMembersMembershipProvider(), Roles.Enabled ? Roles.Provider : new MembersRoleProvider(applicationContext.Services.MemberService)) { } [Obsolete("Use the constructor specifying an UmbracoContext")] [EditorBrowsable(EditorBrowsableState.Never)] public MembershipHelper(ApplicationContext applicationContext, HttpContextBase httpContext, MembershipProvider membershipProvider, RoleProvider roleProvider) { if (applicationContext == null) throw new ArgumentNullException("applicationContext"); if (httpContext == null) throw new ArgumentNullException("httpContext"); if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); if (roleProvider == null) throw new ArgumentNullException("roleProvider"); _applicationContext = applicationContext; _httpContext = httpContext; _membershipProvider = membershipProvider; _roleProvider = roleProvider; } public MembershipHelper(UmbracoContext umbracoContext) : this(umbracoContext, MPE.GetMembersMembershipProvider(), Roles.Enabled ? Roles.Provider : new MembersRoleProvider(umbracoContext.Application.Services.MemberService)) { } public MembershipHelper(UmbracoContext umbracoContext, MembershipProvider membershipProvider, RoleProvider roleProvider) { if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); if (roleProvider == null) throw new ArgumentNullException("roleProvider"); _httpContext = umbracoContext.HttpContext; _applicationContext = umbracoContext.Application; _membershipProvider = membershipProvider; _roleProvider = roleProvider; _umbracoContext = umbracoContext; } #endregion /// /// Check if a document object is protected by the "Protect Pages" functionality in umbraco /// /// The full path of the document object to check /// True if the document object is protected public virtual bool IsProtected(string path) { //this is a cached call return _applicationContext.Services.PublicAccessService.IsProtected(path); } /// /// Check if the current user has access to a document /// /// The full path of the document object to check /// True if the current user has access or if the current document isn't protected public virtual bool MemberHasAccess(string path) { //cache this in the request cache return _applicationContext.ApplicationCache.RequestCache.GetCacheItem(string.Format("{0}.{1}-{2}", typeof(MembershipHelper), "MemberHasAccess", path), () => { if (IsProtected(path)) { return IsLoggedIn() && HasAccess(path, Roles.Provider); } return true; }); } /// /// This will check if the member has access to this path /// /// /// /// /// /// This is essentially the same as the PublicAccessServiceExtensions.HasAccess however this will use the PCR cache /// of the already looked up roles for the member so this doesn't need to happen more than once. /// This does a safety check in case of things like unit tests where there is no PCR and if that is the case it will use /// lookup the roles directly. /// private bool HasAccess(string path, RoleProvider roleProvider) { return _umbracoContext.PublishedContentRequest == null ? _applicationContext.Services.PublicAccessService.HasAccess(path, CurrentUserName, roleProvider.GetRolesForUser) : _applicationContext.Services.PublicAccessService.HasAccess(path, CurrentUserName, _umbracoContext.PublishedContentRequest.GetRolesForLogin); } /// /// Returns true if the current membership provider is the Umbraco built-in one. /// /// public bool IsUmbracoMembershipProviderActive() { var provider = _membershipProvider; return provider.IsUmbracoMembershipProvider(); } /// /// Updates the currently logged in members profile /// /// /// /// The updated MembershipUser object /// public virtual Attempt UpdateMemberProfile(ProfileModel model) { if (IsLoggedIn() == false) { throw new NotSupportedException("No member is currently logged in"); } //get the current membership user var provider = _membershipProvider; var membershipUser = provider.GetCurrentUser(); //NOTE: This should never happen since they are logged in if (membershipUser == null) throw new InvalidOperationException("Could not find member with username " + _httpContext.User.Identity.Name); try { //check if the email needs to change if (model.Email.InvariantEquals(membershipUser.Email) == false) { //Use the membership provider to change the email since that is configured to do the checks to check for unique emails if that is configured. var requiresUpdating = UpdateMember(membershipUser, provider, model.Email); membershipUser = requiresUpdating.Result; } } catch (Exception ex) { //This will occur if an email already exists! return Attempt.Fail(ex); } var member = GetCurrentPersistedMember(); //NOTE: If changing the username is a requirement, than that needs to be done via the IMember directly since MembershipProvider's natively do // not support changing a username! if (model.Name != null && member.Name != model.Name) { member.Name = model.Name; } if (model.MemberProperties != null) { foreach (var property in model.MemberProperties //ensure the property they are posting exists .Where(p => member.ContentType.PropertyTypeExists(p.Alias)) .Where(property => member.Properties.Contains(property.Alias)) //needs to be editable .Where(p => member.ContentType.MemberCanEditProperty(p.Alias))) { member.Properties[property.Alias].Value = property.Value; } } _applicationContext.Services.MemberService.Save(member); //reset the FormsAuth cookie since the username might have changed FormsAuthentication.SetAuthCookie(member.Username, true); return Attempt.Succeed(membershipUser); } /// /// Registers a new member /// /// /// /// /// true to log the member in upon successful registration /// /// public virtual MembershipUser RegisterMember(RegisterModel model, out MembershipCreateStatus status, bool logMemberIn = true) { model.Username = (model.UsernameIsEmail || model.Username == null) ? model.Email : model.Username; MembershipUser membershipUser; var provider = _membershipProvider; //update their real name if (provider.IsUmbracoMembershipProvider()) { membershipUser = ((UmbracoMembershipProviderBase)provider).CreateUser( model.MemberTypeAlias, model.Username, model.Password, model.Email, //TODO: Support q/a http://issues.umbraco.org/issue/U4-3213 null, null, true, null, out status); if (status != MembershipCreateStatus.Success) return null; var member = _applicationContext.Services.MemberService.GetByUsername(membershipUser.UserName); member.Name = model.Name; if (model.MemberProperties != null) { foreach (var property in model.MemberProperties.Where(p => p.Value != null) .Where(property => member.Properties.Contains(property.Alias))) { member.Properties[property.Alias].Value = property.Value; } } _applicationContext.Services.MemberService.Save(member); } else { membershipUser = provider.CreateUser(model.Username, model.Password, model.Email, //TODO: Support q/a http://issues.umbraco.org/issue/U4-3213 null, null, true, null, out status); if (status != MembershipCreateStatus.Success) return null; } if (logMemberIn) { //Set member online provider.GetUser(model.Username, true); //Log them in FormsAuthentication.SetAuthCookie(membershipUser.UserName, model.CreatePersistentLoginCookie); } return membershipUser; } /// /// A helper method to perform the validation and logging in of a member - this is simply wrapping standard membership provider and asp.net forms auth logic. /// /// /// /// public virtual bool Login(string username, string password) { var provider = _membershipProvider; //Validate credentials if (provider.ValidateUser(username, password) == false) { return false; } //Set member online var member = provider.GetUser(username, true); if (member == null) { //this should not happen LogHelper.Warn("The member validated but then no member was returned with the username " + username); return false; } //Log them in FormsAuthentication.SetAuthCookie(member.UserName, true); return true; } /// /// Logs out the current member /// public virtual void Logout() { FormsAuthentication.SignOut(); } #region Querying for front-end public virtual IPublishedContent GetByProviderKey(object key) { return _applicationContext.ApplicationCache.RequestCache.GetCacheItem( GetCacheKey("GetByProviderKey", key), () => { var provider = _membershipProvider; if (provider.IsUmbracoMembershipProvider() == false) { throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); } var result = _applicationContext.Services.MemberService.GetByProviderKey(key); return result == null ? null : new MemberPublishedContent(result).CreateModel(); }); } public virtual IPublishedContent GetById(int memberId) { return _applicationContext.ApplicationCache.RequestCache.GetCacheItem( GetCacheKey("GetById", memberId), () => { var provider = _membershipProvider; if (provider.IsUmbracoMembershipProvider() == false) { throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); } var result = _applicationContext.Services.MemberService.GetById(memberId); return result == null ? null : new MemberPublishedContent(result).CreateModel(); }); } public virtual IPublishedContent GetByUsername(string username) { return _applicationContext.ApplicationCache.RequestCache.GetCacheItem( GetCacheKey("GetByUsername", username), () => { var provider = _membershipProvider; if (provider.IsUmbracoMembershipProvider() == false) { throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); } var result = _applicationContext.Services.MemberService.GetByUsername(username); return result == null ? null : new MemberPublishedContent(result).CreateModel(); }); } public virtual IPublishedContent GetByEmail(string email) { return _applicationContext.ApplicationCache.RequestCache.GetCacheItem( GetCacheKey("GetByEmail", email), () => { var provider = _membershipProvider; if (provider.IsUmbracoMembershipProvider() == false) { throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active"); } var result = _applicationContext.Services.MemberService.GetByEmail(email); return result == null ? null : new MemberPublishedContent(result).CreateModel(); }); } /// /// Returns the currently logged in member as IPublishedContent /// /// public virtual IPublishedContent GetCurrentMember() { if (IsLoggedIn() == false) { return null; } var result = GetCurrentPersistedMember(); return result == null ? null : new MemberPublishedContent(result).CreateModel(); } /// /// Returns the currently logged in member id, -1 if they are not logged in /// /// public int GetCurrentMemberId() { if (IsLoggedIn() == false) { return -1; } var result = GetCurrentMember(); return result == null ? -1 : result.Id; } #endregion #region Model Creation methods for member data editing on the front-end /// /// Creates a new profile model filled in with the current members details if they are logged in which allows for editing /// profile properties /// /// public virtual ProfileModel GetCurrentMemberProfileModel() { if (IsLoggedIn() == false) { return null; } var provider = _membershipProvider; if (provider.IsUmbracoMembershipProvider()) { var membershipUser = provider.GetCurrentUserOnline(); var member = GetCurrentPersistedMember(); //this shouldn't happen but will if the member is deleted in the back office while the member is trying // to use the front-end! if (member == null) { //log them out since they've been removed FormsAuthentication.SignOut(); return null; } var model = ProfileModel.CreateModel(); model.Name = member.Name; model.MemberTypeAlias = member.ContentTypeAlias; model.Email = membershipUser.Email; model.UserName = membershipUser.UserName; model.PasswordQuestion = membershipUser.PasswordQuestion; model.Comment = membershipUser.Comment; model.IsApproved = membershipUser.IsApproved; model.IsLockedOut = membershipUser.IsLockedOut; model.LastLockoutDate = membershipUser.LastLockoutDate; model.CreationDate = membershipUser.CreationDate; model.LastLoginDate = membershipUser.LastLoginDate; model.LastActivityDate = membershipUser.LastActivityDate; model.LastPasswordChangedDate = membershipUser.LastPasswordChangedDate; var memberType = member.ContentType; var builtIns = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); model.MemberProperties = GetMemberPropertiesViewModel(memberType, builtIns, member).ToList(); return model; } //we can try to look up an associated member by the provider user key //TODO: Support this at some point! throw new NotSupportedException("Currently a member profile cannot be edited unless using the built-in Umbraco membership providers"); } /// /// Creates a model to use for registering new members with custom member properties /// /// /// public virtual RegisterModel CreateRegistrationModel(string memberTypeAlias = null) { var provider = _membershipProvider; if (provider.IsUmbracoMembershipProvider()) { memberTypeAlias = memberTypeAlias ?? Constants.Conventions.MemberTypes.DefaultAlias; var memberType = _applicationContext.Services.MemberTypeService.Get(memberTypeAlias); if (memberType == null) throw new InvalidOperationException("Could not find a member type with alias " + memberTypeAlias); var builtIns = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); var model = RegisterModel.CreateModel(); model.MemberTypeAlias = memberTypeAlias; model.MemberProperties = GetMemberPropertiesViewModel(memberType, builtIns).ToList(); return model; } else { var model = RegisterModel.CreateModel(); model.MemberTypeAlias = string.Empty; return model; } } private IEnumerable GetMemberPropertiesViewModel(IMemberType memberType, IEnumerable builtIns, IMember member = null) { var viewProperties = new List(); foreach (var prop in memberType.PropertyTypes .Where(x => builtIns.Contains(x.Alias) == false && memberType.MemberCanEditProperty(x.Alias)) .OrderBy(p => p.SortOrder)) { var value = string.Empty; if (member != null) { var propValue = member.Properties[prop.Alias]; if (propValue != null && propValue.Value != null) { value = propValue.Value.ToString(); } } var viewProperty = new UmbracoProperty { Alias = prop.Alias, Name = prop.Name, Value = value }; //TODO: Perhaps one day we'll ship with our own EditorTempates but for now developers // can just render their own. ////This is a rudimentary check to see what data template we should render //// if developers want to change the template they can do so dynamically in their views or controllers //// for a given property. ////These are the default built-in MVC template types: “Boolean”, “Decimal”, “EmailAddress”, “HiddenInput”, “Html”, “Object”, “String”, “Text”, and “Url” //// by default we'll render a text box since we've defined that metadata on the UmbracoProperty.Value property directly. //if (prop.DataTypeId == new Guid(Constants.PropertyEditors.TrueFalse)) //{ // viewProperty.EditorTemplate = "UmbracoBoolean"; //} //else //{ // switch (prop.DataTypeDatabaseType) // { // case DataTypeDatabaseType.Integer: // viewProperty.EditorTemplate = "Decimal"; // break; // case DataTypeDatabaseType.Ntext: // viewProperty.EditorTemplate = "Text"; // break; // case DataTypeDatabaseType.Date: // case DataTypeDatabaseType.Nvarchar: // break; // } //} viewProperties.Add(viewProperty); } return viewProperties; } #endregion /// /// Returns the login status model of the currently logged in member, if no member is logged in it returns null; /// /// public virtual LoginStatusModel GetCurrentLoginStatus() { var model = LoginStatusModel.CreateModel(); if (IsLoggedIn() == false) { model.IsLoggedIn = false; return model; } var provider = _membershipProvider; if (provider.IsUmbracoMembershipProvider()) { var member = GetCurrentPersistedMember(); //this shouldn't happen but will if the member is deleted in the back office while the member is trying // to use the front-end! if (member == null) { //log them out since they've been removed FormsAuthentication.SignOut(); model.IsLoggedIn = false; return model; } model.Name = member.Name; model.Username = member.Username; model.Email = member.Email; } else { var member = provider.GetCurrentUserOnline(); //this shouldn't happen but will if the member is deleted in the back office while the member is trying // to use the front-end! if (member == null) { //log them out since they've been removed FormsAuthentication.SignOut(); model.IsLoggedIn = false; return model; } model.Name = member.UserName; model.Username = member.UserName; model.Email = member.Email; } model.IsLoggedIn = true; return model; } /// /// Check if a member is logged in /// /// public bool IsLoggedIn() { return _httpContext.User != null && _httpContext.User.Identity.IsAuthenticated; } /// /// Returns the currently logged in username /// public string CurrentUserName { get { return _httpContext.User.Identity.Name; } } /// /// Returns true or false if the currently logged in member is authorized based on the parameters provided /// /// /// /// /// /// public virtual bool IsMemberAuthorized( bool allowAll = false, IEnumerable allowTypes = null, IEnumerable allowGroups = null, IEnumerable allowMembers = null) { if (allowAll) return true; if (allowTypes == null) allowTypes = Enumerable.Empty(); if (allowGroups == null) allowGroups = Enumerable.Empty(); if (allowMembers == null) allowMembers = Enumerable.Empty(); // Allow by default var allowAction = true; if (IsLoggedIn() == false) { // If not logged on, not allowed allowAction = false; } else { var provider = _membershipProvider; string username; if (provider.IsUmbracoMembershipProvider()) { var member = GetCurrentPersistedMember(); // If a member could not be resolved from the provider, we are clearly not authorized and can break right here if (member == null) return false; username = member.Username; // If types defined, check member is of one of those types var allowTypesList = allowTypes as IList ?? allowTypes.ToList(); if (allowTypesList.Any(allowType => allowType != string.Empty)) { // Allow only if member's type is in list allowAction = allowTypesList.Select(x => x.ToLowerInvariant()).Contains(member.ContentType.Alias.ToLowerInvariant()); } // If specific members defined, check member is of one of those if (allowAction && allowMembers.Any()) { // Allow only if member's Id is in the list allowAction = allowMembers.Contains(member.Id); } } else { var member = provider.GetCurrentUser(); // If a member could not be resolved from the provider, we are clearly not authorized and can break right here if (member == null) return false; username = member.UserName; } // If groups defined, check member is of one of those groups var allowGroupsList = allowGroups as IList ?? allowGroups.ToList(); if (allowAction && allowGroupsList.Any(allowGroup => allowGroup != string.Empty)) { // Allow only if member is assigned to a group in the list var groups = _roleProvider.GetRolesForUser(username); allowAction = allowGroupsList.Select(s => s.ToLowerInvariant()).Intersect(groups.Select(myGroup => myGroup.ToLowerInvariant())).Any(); } } return allowAction; } /// /// Changes password for a member/user given the membership provider name and the password change model /// /// /// /// /// public virtual Attempt ChangePassword(string username, ChangingPasswordModel passwordModel, string membershipProviderName) { var provider = Membership.Providers[membershipProviderName]; if (provider == null) { throw new InvalidOperationException("Could not find provider with name " + membershipProviderName); } return ChangePassword(username, passwordModel, provider); } /// /// Changes password for a member/user given the membership provider and the password change model /// /// /// /// /// public virtual Attempt ChangePassword(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider) { var passwordChanger = new PasswordChanger(_applicationContext.ProfilingLogger.Logger, _applicationContext.Services.UserService, UmbracoContext.Current.HttpContext); return passwordChanger.ChangePasswordWithMembershipProvider(username, passwordModel, membershipProvider); } /// /// Updates a membership user with all of it's writable properties /// /// /// /// /// /// /// /// /// /// Returns successful if the membershipuser required updating, otherwise returns failed if it didn't require updating. /// internal Attempt UpdateMember(MembershipUser member, MembershipProvider provider, string email = null, bool? isApproved = null, DateTime? lastLoginDate = null, DateTime? lastActivityDate = null, string comment = null) { var needsUpdating = new List(); //set the writable properties if (email != null) { needsUpdating.Add(member.Email != email); member.Email = email; } if (isApproved.HasValue) { needsUpdating.Add(member.IsApproved != isApproved.Value); member.IsApproved = isApproved.Value; } if (lastLoginDate.HasValue) { needsUpdating.Add(member.LastLoginDate != lastLoginDate.Value); member.LastLoginDate = lastLoginDate.Value; } if (lastActivityDate.HasValue) { needsUpdating.Add(member.LastActivityDate != lastActivityDate.Value); member.LastActivityDate = lastActivityDate.Value; } if (comment != null) { needsUpdating.Add(member.Comment != comment); member.Comment = comment; } //Don't persist anything if nothing has changed if (needsUpdating.Any(x => x == true)) { provider.UpdateUser(member); return Attempt.Succeed(member); } return Attempt.Fail(member); } /// /// Returns the currently logged in IMember object - this should never be exposed to the front-end since it's returning a business logic entity! /// /// private IMember GetCurrentPersistedMember() { return _applicationContext.ApplicationCache.RequestCache.GetCacheItem( GetCacheKey("GetCurrentPersistedMember"), () => { var provider = _membershipProvider; if (provider.IsUmbracoMembershipProvider() == false) { throw new NotSupportedException("An IMember model can only be retreived when using the built-in Umbraco membership providers"); } var username = provider.GetCurrentUserName(); var member = _applicationContext.Services.MemberService.GetByUsername(username); return member; }); } private string GetCacheKey(string key, params object[] additional) { var sb = new StringBuilder(string.Format("{0}-{1}", typeof(MembershipHelper).Name, key)); foreach (var s in additional) { sb.Append("-"); sb.Append(s); } return sb.ToString(); } } }