using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Web; using System.Web.Security; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Security; using Umbraco.Web.Models; using umbraco; using umbraco.BusinessLogic; using umbraco.DataLayer; using umbraco.businesslogic.Exceptions; using umbraco.cms.businesslogic.member; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using UmbracoSettings = Umbraco.Core.Configuration.UmbracoSettings; namespace Umbraco.Web.Security { /// /// A utility class used for dealing with security in Umbraco /// public class WebSecurity { /// /// Returns true or false if the currently logged in member is authorized based on the parameters provided /// /// /// /// /// /// public 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; // Get member details var member = Member.GetCurrentMember(); if (member == null) { // If not logged on, not allowed allowAction = false; } else { // 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 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 = Roles.GetRolesForUser(member.LoginName); allowAction = allowGroupsList.Select(s => s.ToLowerInvariant()).Intersect(groups.Select(myGroup => myGroup.ToLowerInvariant())).Any(); } // 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); } } return allowAction; } /// /// Gets the SQL helper. /// /// The SQL helper. private ISqlHelper SqlHelper { get { return Application.SqlHelper; } } private const long TicksPrMinute = 600000000; private static readonly int UmbracoTimeOutInMinutes = GlobalSettings.TimeOutInMinutes; private User _currentUser; /// /// Gets the current user. /// /// The current user. /// /// This is internal because we don't want to expose the legacy User object on this class, instead we'll wait until IUser /// is public. If people want to reference the current user, they can reference it from the UmbracoContext. /// internal User CurrentUser { get { //only load it once per instance! return _currentUser ?? (_currentUser = User.GetCurrent()); } } /// /// Logs a user in. /// /// The user Id public void PerformLogin(int userId) { var retVal = Guid.NewGuid(); SqlHelper.ExecuteNonQuery( "insert into umbracoUserLogins (contextID, userID, timeout) values (@contextId,'" + userId + "','" + (DateTime.Now.Ticks + (TicksPrMinute * UmbracoTimeOutInMinutes)) + "') ", SqlHelper.CreateParameter("@contextId", retVal)); UmbracoUserContextId = retVal.ToString(); LogHelper.Info("User Id: {0} logged in", () => userId); } /// /// Clears the current login for the currently logged in user /// public void ClearCurrentLogin() { // Added try-catch in case login doesn't exist in the database // Either due to old cookie or running multiple sessions on localhost with different port number try { SqlHelper.ExecuteNonQuery( "DELETE FROM umbracoUserLogins WHERE contextId = @contextId", SqlHelper.CreateParameter("@contextId", UmbracoUserContextId)); } catch (Exception ex) { LogHelper.Error(string.Format("Login with contextId {0} didn't exist in the database", UmbracoUserContextId), ex); } //this clears the cookie UmbracoUserContextId = ""; } public void RenewLoginTimeout() { // only call update if more than 1/10 of the timeout has passed SqlHelper.ExecuteNonQuery( "UPDATE umbracoUserLogins SET timeout = @timeout WHERE contextId = @contextId", SqlHelper.CreateParameter("@timeout", DateTime.Now.Ticks + (TicksPrMinute * UmbracoTimeOutInMinutes)), SqlHelper.CreateParameter("@contextId", UmbracoUserContextId)); } /// /// Validates credentials for a back office user /// /// /// /// internal bool ValidateBackOfficeCredentials(string username, string password) { var membershipProvider = Membership.Providers[UmbracoSettings.DefaultBackofficeProvider]; return membershipProvider != null && membershipProvider.ValidateUser(username, password); } /// /// Changes password for a member/user given the membership provider and the password change model /// /// /// /// /// /// /// YES! It is completely insane how many options you have to take into account based on the membership provider. yikes! /// internal Attempt ChangePassword(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider) { if (passwordModel == null) throw new ArgumentNullException("passwordModel"); if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); //Are we resetting the password?? if (passwordModel.Reset.HasValue && passwordModel.Reset.Value) { if (membershipProvider.EnablePasswordReset == false) { return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not enabled", new[] { "resetPassword" }) }); } if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace()) { return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset requires a password answer", new[] { "resetPassword" }) }); } //ok, we should be able to reset it try { var newPass = membershipProvider.ResetPassword( username, membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null); //return the generated pword return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass }); } catch (Exception ex) { LogHelper.WarnWithException("Could not reset member password", ex); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, error: " + ex.Message + " (see log for full details)", new[] { "resetPassword" }) }); } } //we're not resetting it so we need to try to change it. if (passwordModel.NewPassword.IsNullOrWhiteSpace()) { return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) }); } //This is an edge case and is only necessary for backwards compatibility: var umbracoBaseProvider = membershipProvider as MembershipProviderBase; if (umbracoBaseProvider != null && umbracoBaseProvider.AllowManuallyChangingPassword) { //this provider allows manually changing the password without the old password, so we can just do it try { var result = umbracoBaseProvider.ChangePassword(username, "", passwordModel.NewPassword); return result == false ? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "value" }) }) : Attempt.Succeed(new PasswordChangedModel()); } catch (Exception ex) { LogHelper.WarnWithException("Could not change member password", ex); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) }); } } //The provider does not support manually chaning the password but no old password supplied - need to return an error if (passwordModel.OldPassword.IsNullOrWhiteSpace() && membershipProvider.EnablePasswordRetrieval == false) { //if password retrieval is not enabled but there is no old password we cannot continue return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "value" }) }); } if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false) { //if an old password is suplied try to change it try { var result = membershipProvider.ChangePassword(username, passwordModel.OldPassword, passwordModel.NewPassword); return result == false ? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "value" }) }) : Attempt.Succeed(new PasswordChangedModel()); } catch (Exception ex) { LogHelper.WarnWithException("Could not change member password", ex); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) }); } } if (membershipProvider.EnablePasswordRetrieval == false) { //we cannot continue if we cannot get the current password return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "value" }) }); } if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace()) { //if the question answer is required but there isn't one, we cannot continue return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the password answer", new[] { "value" }) }); } //lets try to get the old one so we can change it try { var oldPassword = membershipProvider.GetPassword( username, membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null); try { var result = membershipProvider.ChangePassword(username, oldPassword, passwordModel.NewPassword); return result == false ? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password", new[] { "value" }) }) : Attempt.Succeed(new PasswordChangedModel()); } catch (Exception ex1) { LogHelper.WarnWithException("Could not change member password", ex1); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex1.Message + " (see log for full details)", new[] { "value" }) }); } } catch (Exception ex2) { LogHelper.WarnWithException("Could not retrieve member password", ex2); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex2.Message + " (see log for full details)", new[] { "value" }) }); } } /// /// Validates the user node tree permissions. /// /// /// The path. /// The action. /// internal bool ValidateUserNodeTreePermissions(User umbracoUser, string path, string action) { var permissions = umbracoUser.GetPermissions(path); if (permissions.IndexOf(action, StringComparison.Ordinal) > -1 && (path.Contains("-20") || ("," + path + ",").Contains("," + umbracoUser.StartNodeId + ","))) return true; var user = umbracoUser; LogHelper.Info("User {0} has insufficient permissions in UmbracoEnsuredPage: '{1}', '{2}', '{3}'", () => user.Name, () => path, () => permissions, () => action); return false; } /// /// Validates the current user to see if they have access to the specified app /// /// /// internal bool ValidateUserApp(string app) { //if it is empty, don't validate if (app.IsNullOrWhiteSpace()) { return true; } return CurrentUser.Applications.Any(uApp => uApp.alias == app); } internal void UpdateLogin(long timeout) { // only call update if more than 1/10 of the timeout has passed if (timeout - (((TicksPrMinute * UmbracoTimeOutInMinutes) * 0.8)) < DateTime.Now.Ticks) SqlHelper.ExecuteNonQuery( "UPDATE umbracoUserLogins SET timeout = @timeout WHERE contextId = @contextId", SqlHelper.CreateParameter("@timeout", DateTime.Now.Ticks + (TicksPrMinute * UmbracoTimeOutInMinutes)), SqlHelper.CreateParameter("@contextId", UmbracoUserContextId)); } internal long GetTimeout(string umbracoUserContextId) { return ApplicationContext.Current.ApplicationCache.GetCacheItem( CacheKeys.UserContextTimeoutCacheKey + umbracoUserContextId, new TimeSpan(0, UmbracoTimeOutInMinutes / 10, 0), () => GetTimeout(true)); } internal long GetTimeout(bool byPassCache) { if (UmbracoSettings.KeepUserLoggedIn) RenewLoginTimeout(); if (byPassCache) { return SqlHelper.ExecuteScalar("select timeout from umbracoUserLogins where contextId=@contextId", SqlHelper.CreateParameter("@contextId", new Guid(UmbracoUserContextId)) ); } return GetTimeout(UmbracoUserContextId); } /// /// Gets the user id. /// /// The umbraco user context ID. /// public int GetUserId(string umbracoUserContextId) { //need to parse to guid Guid guid; if (Guid.TryParse(umbracoUserContextId, out guid) == false) { return -1; } var id = ApplicationContext.Current.ApplicationCache.GetCacheItem( CacheKeys.UserContextCacheKey + umbracoUserContextId, new TimeSpan(0, UmbracoTimeOutInMinutes / 10, 0), () => SqlHelper.ExecuteScalar( "select userID from umbracoUserLogins where contextID = @contextId", SqlHelper.CreateParameter("@contextId", guid))); if (id == null) return -1; return id.Value; } /// /// Validates the user context ID. /// /// The umbraco user context ID. /// public bool ValidateUserContextId(string currentUmbracoUserContextId) { if ((currentUmbracoUserContextId != "")) { int uid = GetUserId(currentUmbracoUserContextId); long timeout = GetTimeout(currentUmbracoUserContextId); if (timeout > DateTime.Now.Ticks) { return true; } var user = User.GetUser(uid); LogHelper.Info(typeof(WebSecurity), "User {0} (Id:{1}) logged out", () => user.Name, () => user.Id); } return false; } /// /// Validates the current user /// /// /// set to true if you want exceptions to be thrown if failed /// internal ValidateRequestAttempt ValidateCurrentUser(HttpContextBase httpContext, bool throwExceptions = false) { if (UmbracoUserContextId != "") { var uid = GetUserId(UmbracoUserContextId); var timeout = GetTimeout(UmbracoUserContextId); if (timeout > DateTime.Now.Ticks) { var user = User.GetUser(uid); // Check for console access if (user.Disabled || (user.NoConsole && GlobalSettings.RequestIsInUmbracoApplication(httpContext) && GlobalSettings.RequestIsLiveEditRedirector(httpContext) == false)) { if (throwExceptions) throw new ArgumentException("You have no priviledges to the umbraco console. Please contact your administrator"); return ValidateRequestAttempt.FailedNoPrivileges; } UpdateLogin(timeout); return ValidateRequestAttempt.Success; } if (throwExceptions) throw new ArgumentException("User has timed out!!"); return ValidateRequestAttempt.FailedTimedOut; } if (throwExceptions) throw new InvalidOperationException("The user has no umbraco contextid - try logging in"); return ValidateRequestAttempt.FailedNoContextId; } /// /// Authorizes the full request, checks for SSL and validates the current user /// /// /// set to true if you want exceptions to be thrown if failed /// internal ValidateRequestAttempt AuthorizeRequest(HttpContextBase httpContext, bool throwExceptions = false) { // check for secure connection if (GlobalSettings.UseSSL && httpContext.Request.IsSecureConnection == false) { if (throwExceptions) throw new UserAuthorizationException("This installation requires a secure connection (via SSL). Please update the URL to include https://"); return ValidateRequestAttempt.FailedNoSsl; } return ValidateCurrentUser(httpContext, throwExceptions); } /// /// Checks if the specified user as access to the app /// /// /// /// internal bool UserHasAppAccess(string app, User user) { return user.Applications.Any(uApp => uApp.alias == app); } /// /// Checks if the specified user by username as access to the app /// /// /// /// internal bool UserHasAppAccess(string app, string username) { var uid = User.getUserId(username); if (uid < 0) return false; var usr = User.GetUser(uid); if (usr == null) return false; return UserHasAppAccess(app, usr); } /// /// Gets or sets the umbraco user context ID. /// /// The umbraco user context ID. public string UmbracoUserContextId { get { var authTicket = HttpContext.Current.GetUmbracoAuthTicket(); if (authTicket == null) { return ""; } var identity = authTicket.CreateUmbracoIdentity(); if (identity == null) { HttpContext.Current.UmbracoLogout(); return ""; } return identity.UserContextId; } set { if (value.IsNullOrWhiteSpace()) { HttpContext.Current.UmbracoLogout(); } else { var uid = GetUserId(value); if (uid == -1) { HttpContext.Current.UmbracoLogout(); } else { var user = User.GetUser(uid); HttpContext.Current.CreateUmbracoAuthTicket( new UserData { Id = uid, AllowedApplications = user.Applications.Select(x => x.alias).ToArray(), Culture = ui.Culture(user), RealName = user.Name, Roles = new string[] { user.UserType.Alias }, StartContentNode = user.StartNodeId, StartMediaNode = user.StartMediaId, UserContextId = value, Username = user.LoginName }); } } } } } }