using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
using System.Web.Security;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
using umbraco;
using umbraco.DataLayer;
using umbraco.businesslogic.Exceptions;
using umbraco.providers;
using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings;
using Member = umbraco.cms.businesslogic.member.Member;
using User = umbraco.BusinessLogic.User;
namespace Umbraco.Web.Security
{
///
/// A utility class used for dealing with security in Umbraco
///
public class WebSecurity : DisposableObject
{
private HttpContextBase _httpContext;
private ApplicationContext _applicationContext;
public WebSecurity(HttpContextBase httpContext, ApplicationContext applicationContext)
{
_httpContext = httpContext;
_applicationContext = applicationContext;
//This ensures the dispose method is called when the request terminates, though
// we also ensure this happens in the Umbraco module because the UmbracoContext is added to the
// http context items.
_httpContext.DisposeOnPipelineCompleted(this);
}
///
/// 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;
}
private IUser _currentUser;
///
/// Gets the current user.
///
/// The current user.
internal IUser CurrentUser
{
get
{
//only load it once per instance!
if (_currentUser == null)
{
var id = GetUserId();
if (id == -1)
{
return null;
}
_currentUser = _applicationContext.Services.UserService.GetUserById(id);
}
return _currentUser;
}
}
///
/// Logs a user in.
///
/// The user Id
/// returns the number of seconds until their session times out
public double PerformLogin(int userId)
{
var user = _applicationContext.Services.UserService.GetUserById(userId);
return PerformLogin(user).GetRemainingAuthSeconds();
}
///
/// Logs the user in
///
///
/// returns the number of seconds until their session times out
internal FormsAuthenticationTicket PerformLogin(IUser user)
{
var ticket = _httpContext.CreateUmbracoAuthTicket(new UserData(Guid.NewGuid().ToString("N"))
{
Id = user.Id,
AllowedApplications = user.AllowedSections.ToArray(),
RealName = user.Name,
//currently we only have one user type!
Roles = new[] { user.UserType.Alias },
StartContentNode = user.StartContentId,
StartMediaNode = user.StartMediaId,
Username = user.Username,
Culture = ui.Culture(user.Language)
});
LogHelper.Info("User Id: {0} logged in", () => user.Id);
return ticket;
}
///
/// Clears the current login for the currently logged in user
///
public void ClearCurrentLogin()
{
_httpContext.UmbracoLogout();
}
///
/// Renews the user's login ticket
///
public void RenewLoginTimeout()
{
_httpContext.RenewUmbracoAuthTicket();
}
///
/// Validates credentials for a back office user
///
///
///
///
internal bool ValidateBackOfficeCredentials(string username, string password)
{
var membershipProvider = Membership.Providers[UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider];
return membershipProvider != null && membershipProvider.ValidateUser(username, password);
}
///
/// Returns the MembershipUser from the back office membership provider
///
///
///
///
internal MembershipUser GetBackOfficeMembershipUser(string username, bool setOnline)
{
var membershipProvider = Membership.Providers[UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider];
return membershipProvider != null ? membershipProvider.GetUser(username, setOnline) : null;
}
///
/// Returns the back office IUser instance for the username specified
///
///
///
///
/// This will return an Iuser instance no matter what membership provider is installed for the back office, it will automatically
/// create any missing Iuser accounts if one is not found and a custom membership provider is being used.
///
internal IUser GetBackOfficeUser(string username)
{
//get the membership user (set user to be 'online' in the provider too)
var membershipUser = GetBackOfficeMembershipUser(username, true);
if (membershipUser == null)
{
throw new InvalidOperationException(
"The username & password validated but the membership provider '" +
Membership.Providers[UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider].Name +
"' did not return a MembershipUser with the username supplied");
}
//regarldess of the membership provider used, see if this user object already exists in the umbraco data
var user = _applicationContext.Services.UserService.GetUserByUserName(membershipUser.UserName);
//we're using the built-in membership provider so the user will already be available
if (Membership.Providers[UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider] is UsersMembershipProvider)
{
if (user == null)
{
//this should never happen
throw new InvalidOperationException("The user '" + username + "' could not be found in the Umbraco database");
}
return user;
}
//we are using a custom membership provider for the back office, in this case we need to create user accounts for the logged in member.
//if we already have a user object in Umbraco we don't need to do anything, otherwise we need to create a mapped Umbraco account.
if (user != null) return user;
//we need to create an Umbraco IUser of a 'writer' type with access to only content - this was how v6 operates.
var writer = _applicationContext.Services.UserService.GetUserTypeByAlias("writer");
var email = membershipUser.Email;
if (email.IsNullOrWhiteSpace())
{
//in some cases if there is no email we have to generate one since it is required!
email = Guid.NewGuid().ToString("N") + "@example.com";
}
user = new Core.Models.Membership.User(writer)
{
Email = email,
Language = GlobalSettings.DefaultUILanguage,
Name = membershipUser.UserName,
Password = Guid.NewGuid().ToString("N"), //Need to set this to something - will not be used though
DefaultPermissions = writer.Permissions,
Username = membershipUser.UserName,
StartContentId = -1,
StartMediaId = -1,
NoConsole = false,
IsApproved = true
};
user.AddAllowedSection("content");
_applicationContext.Services.UserService.SaveUser(user);
return user;
}
///
/// 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;
}
var userApps = _applicationContext.Services.UserService.GetUserSections(CurrentUser);
return userApps.Any(uApp => uApp.InvariantEquals(app));
}
///
/// Gets the user id.
///
/// This is not used
///
[Obsolete("This method is no longer used, use the GetUserId() method without parameters instead")]
public int GetUserId(string umbracoUserContextId)
{
return GetUserId();
}
///
/// Gets the currnet user's id.
///
///
public int GetUserId()
{
var identity = _httpContext.GetCurrentIdentity(true);
if (identity == null)
return -1;
return Convert.ToInt32(identity.Id);
}
///
/// Returns the current user's unique session id - used to mitigate csrf attacks or any other reason to validate a request
///
///
public string GetSessionId()
{
var identity = _httpContext.GetCurrentIdentity(true);
if (identity == null)
return null;
return identity.SessionId;
}
///
/// Validates the user context ID.
///
/// This doesn't do anything
///
[Obsolete("This method is no longer used, use the ValidateCurrentUser() method instead")]
public bool ValidateUserContextId(string currentUmbracoUserContextId)
{
return ValidateCurrentUser();
}
///
/// Validates the currently logged in user and ensures they are not timed out
///
///
public bool ValidateCurrentUser()
{
var result = ValidateCurrentUser(false);
return result == ValidateRequestAttempt.Success;
}
///
/// Validates the current user
///
/// set to true if you want exceptions to be thrown if failed
///
internal ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions)
{
var ticket = _httpContext.GetUmbracoAuthTicket();
if (ticket != null)
{
if (ticket.Expired == false)
{
var user = CurrentUser;
// Check for console access
if (user.IsLockedOut || (user.NoConsole && GlobalSettings.RequestIsInUmbracoApplication(_httpContext)))
{
if (throwExceptions) throw new ArgumentException("You have no priviledges to the umbraco console. Please contact your administrator");
return ValidateRequestAttempt.FailedNoPrivileges;
}
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(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(throwExceptions);
}
///
/// Checks if the specified user as access to the app
///
///
///
///
internal bool UserHasAppAccess(string app, IUser user)
{
var apps = _applicationContext.Services.UserService.GetUserSections(user);
return apps.Any(uApp => uApp.InvariantEquals(app));
}
[Obsolete("Do not use this method if you don't have to, use the overload with IUser instead")]
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 user = _applicationContext.Services.UserService.GetUserByUserName(username);
if (user == null)
{
return false;
}
return UserHasAppAccess(app, user);
}
[Obsolete("Returns the current user's unique umbraco sesion id - this cannot be set and isn't intended to be used in your code")]
public string UmbracoUserContextId
{
get
{
return _httpContext.GetUmbracoAuthTicket() == null ? "" : GetSessionId();
}
set
{
}
}
protected override void DisposeResources()
{
_httpContext = null;
_applicationContext = null;
}
}
}