Files
Umbraco-CMS/src/Umbraco.Web/Security/WebSecurity.cs

562 lines
25 KiB
C#

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
{
/// <summary>
/// A utility class used for dealing with security in Umbraco
/// </summary>
public class WebSecurity
{
/// <summary>
/// Returns true or false if the currently logged in member is authorized based on the parameters provided
/// </summary>
/// <param name="allowAll"></param>
/// <param name="allowTypes"></param>
/// <param name="allowGroups"></param>
/// <param name="allowMembers"></param>
/// <returns></returns>
public bool IsMemberAuthorized(
bool allowAll = false,
IEnumerable<string> allowTypes = null,
IEnumerable<string> allowGroups = null,
IEnumerable<int> allowMembers = null)
{
if (allowAll)
return true;
if (allowTypes == null)
allowTypes = Enumerable.Empty<string>();
if (allowGroups == null)
allowGroups = Enumerable.Empty<string>();
if (allowMembers == null)
allowMembers = Enumerable.Empty<int>();
// 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<string> ?? 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<string> ?? 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;
}
/// <summary>
/// Gets the SQL helper.
/// </summary>
/// <value>The SQL helper.</value>
private ISqlHelper SqlHelper
{
get { return Application.SqlHelper; }
}
private const long TicksPrMinute = 600000000;
private static readonly int UmbracoTimeOutInMinutes = GlobalSettings.TimeOutInMinutes;
private User _currentUser;
/// <summary>
/// Gets the current user.
/// </summary>
/// <value>The current user.</value>
/// <remarks>
/// 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.
/// </remarks>
internal User CurrentUser
{
get
{
//only load it once per instance!
return _currentUser ?? (_currentUser = User.GetCurrent());
}
}
/// <summary>
/// Logs a user in.
/// </summary>
/// <param name="userId">The user Id</param>
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<WebSecurity>("User Id: {0} logged in", () => userId);
}
/// <summary>
/// Clears the current login for the currently logged in user
/// </summary>
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<WebSecurity>(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));
}
/// <summary>
/// Validates credentials for a back office user
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <returns></returns>
internal bool ValidateBackOfficeCredentials(string username, string password)
{
var membershipProvider = Membership.Providers[UmbracoSettings.DefaultBackofficeProvider];
return membershipProvider != null && membershipProvider.ValidateUser(username, password);
}
/// <summary>
/// Changes password for a member/user given the membership provider and the password change model
/// </summary>
/// <param name="username"></param>
/// <param name="passwordModel"></param>
/// <param name="membershipProvider"></param>
/// <returns></returns>
/// <remarks>
/// YES! It is completely insane how many options you have to take into account based on the membership provider. yikes!
/// </remarks>
internal Attempt<PasswordChangedModel> 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<WebSecurity>("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<WebSecurity>("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<WebSecurity>("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<WebSecurity>("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<WebSecurity>("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" }) });
}
}
/// <summary>
/// Validates the user node tree permissions.
/// </summary>
/// <param name="umbracoUser"></param>
/// <param name="path">The path.</param>
/// <param name="action">The action.</param>
/// <returns></returns>
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<WebSecurity>("User {0} has insufficient permissions in UmbracoEnsuredPage: '{1}', '{2}', '{3}'", () => user.Name, () => path, () => permissions, () => action);
return false;
}
/// <summary>
/// Validates the current user to see if they have access to the specified app
/// </summary>
/// <param name="app"></param>
/// <returns></returns>
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<long>("select timeout from umbracoUserLogins where contextId=@contextId",
SqlHelper.CreateParameter("@contextId", new Guid(UmbracoUserContextId))
);
}
return GetTimeout(UmbracoUserContextId);
}
/// <summary>
/// Gets the user id.
/// </summary>
/// <param name="umbracoUserContextId">The umbraco user context ID.</param>
/// <returns></returns>
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<int?>(
"select userID from umbracoUserLogins where contextID = @contextId",
SqlHelper.CreateParameter("@contextId", guid)));
if (id == null)
return -1;
return id.Value;
}
/// <summary>
/// Validates the user context ID.
/// </summary>
/// <param name="currentUmbracoUserContextId">The umbraco user context ID.</param>
/// <returns></returns>
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;
}
/// <summary>
/// Validates the current user
/// </summary>
/// <param name="httpContext"></param>
/// <param name="throwExceptions">set to true if you want exceptions to be thrown if failed</param>
/// <returns></returns>
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;
}
/// <summary>
/// Authorizes the full request, checks for SSL and validates the current user
/// </summary>
/// <param name="httpContext"></param>
/// <param name="throwExceptions">set to true if you want exceptions to be thrown if failed</param>
/// <returns></returns>
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);
}
/// <summary>
/// Checks if the specified user as access to the app
/// </summary>
/// <param name="app"></param>
/// <param name="user"></param>
/// <returns></returns>
internal bool UserHasAppAccess(string app, User user)
{
return user.Applications.Any(uApp => uApp.alias == app);
}
/// <summary>
/// Checks if the specified user by username as access to the app
/// </summary>
/// <param name="app"></param>
/// <param name="username"></param>
/// <returns></returns>
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);
}
/// <summary>
/// Gets or sets the umbraco user context ID.
/// </summary>
/// <value>The umbraco user context ID.</value>
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
});
}
}
}
}
}
}