using System; using System.Collections.Generic; using System.ComponentModel; using System.Data.Common; using System.Data.SqlClient; using System.Data.SqlServerCe; using System.Globalization; using System.Linq; using System.Linq.Expressions; using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Core.Security; namespace Umbraco.Core.Services.Implement { /// /// Represents the UserService, which is an easy access to operations involving , and eventually Backoffice Users. /// public class UserService : ScopeRepositoryService, IUserService { private readonly IUserRepository _userRepository; private readonly IUserGroupRepository _userGroupRepository; private readonly IGlobalSettings _globalSettings; private readonly bool _isUpgrading; public UserService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IRuntimeState runtimeState, IUserRepository userRepository, IUserGroupRepository userGroupRepository, IGlobalSettings globalSettings) : base(provider, logger, eventMessagesFactory) { _userRepository = userRepository; _userGroupRepository = userGroupRepository; _globalSettings = globalSettings; _isUpgrading = runtimeState.Level == RuntimeLevel.Install || runtimeState.Level == RuntimeLevel.Upgrade; } #region Implementation of IMembershipUserService /// /// Checks if a User with the username exists /// /// Username to check /// True if the User exists otherwise False public bool Exists(string username) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userRepository.Exists(username); } } /// /// Creates a new User /// /// The user will be saved in the database and returned with an Id /// Username of the user to create /// Email of the user to create /// public IUser CreateUserWithIdentity(string username, string email) { return CreateUserWithIdentity(username, email, string.Empty); } /// /// Creates and persists a new /// /// Username of the to create /// Email of the to create /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database /// Not used for users /// IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) { return CreateUserWithIdentity(username, email, passwordValue); } /// /// Creates and persists a new /// /// Username of the to create /// Email of the to create /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database /// Alias of the Type /// Is the member approved /// IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved) { return CreateUserWithIdentity(username, email, passwordValue, isApproved); } /// /// Creates and persists a Member /// /// Using this method will persist the Member object before its returned /// meaning that it will have an Id available (unlike the CreateMember method) /// Username of the Member to create /// Email of the Member to create /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database /// Is the user approved /// private IUser CreateUserWithIdentity(string username, string email, string passwordValue, bool isApproved = true) { if (string.IsNullOrWhiteSpace(username)) throw new ArgumentNullOrEmptyException(nameof(username)); //TODO: PUT lock here!! User user; using (var scope = ScopeProvider.CreateScope()) { var loginExists = scope.Database.ExecuteScalar("SELECT COUNT(id) FROM umbracoUser WHERE userLogin = @Login", new { Login = username }) != 0; if (loginExists) throw new ArgumentException("Login already exists"); // causes rollback // causes rollback user = new User { DefaultToLiveEditing = false, Email = email, Language = _globalSettings.DefaultUILanguage, Name = username, RawPasswordValue = passwordValue, Username = username, IsLockedOut = false, IsApproved = isApproved }; var saveEventArgs = new SaveEventArgs(user); if (scope.Events.DispatchCancelable(SavingUser, this, saveEventArgs)) { scope.Complete(); return user; } _userRepository.Save(user); saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedUser, this, saveEventArgs); scope.Complete(); } return user; } /// /// Gets a User by its integer id /// /// Id /// public IUser GetById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userRepository.Get(id); } } /// /// Gets an by its provider key /// /// Id to use for retrieval /// public IUser GetByProviderKey(object id) { var asInt = id.TryConvertTo(); return asInt.Success ? GetById(asInt.Result) : null; } /// /// Get an by email /// /// Email to use for retrieval /// public IUser GetByEmail(string email) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.Email.Equals(email)); return _userRepository.Get(query).FirstOrDefault(); } } /// /// Get an by username /// /// Username to use for retrieval /// public IUser GetByUsername(string username) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { try { return _userRepository.GetByUsername(username, includeSecurityData: true); } catch (DbException) { // fixme - refactor users/upgrade // currently kinda accepting anything on upgrade, but that won't deal with all cases // so we need to do it differently, see the custom UmbracoPocoDataBuilder which should // be better BUT requires that the app restarts after the upgrade! if (_isUpgrading) { //NOTE: this will not be cached return _userRepository.GetByUsername(username, includeSecurityData: false); } throw; } } } /// /// Disables an /// /// to disable public void Delete(IUser membershipUser) { //disable membershipUser.IsApproved = false; Save(membershipUser); } [Obsolete("ASP.NET Identity APIs like the BackOfficeUserManager should be used to manage passwords, this will not work with correct security practices because you would need the existing password")] [EditorBrowsable(EditorBrowsableState.Never)] public void SavePassword(IUser user, string password) { if (user == null) throw new ArgumentNullException(nameof(user)); var provider = MembershipProviderExtensions.GetUsersMembershipProvider(); if (provider.IsUmbracoMembershipProvider() == false) throw new NotSupportedException("When using a non-Umbraco membership provider you must change the user password by using the MembershipProvider.ChangePassword method"); provider.ChangePassword(user.Username, "", password); //go re-fetch the member and update the properties that may have changed var result = GetByUsername(user.Username); if (result != null) { //should never be null but it could have been deleted by another thread. user.RawPasswordValue = result.RawPasswordValue; user.LastPasswordChangeDate = result.LastPasswordChangeDate; user.UpdateDate = result.UpdateDate; } } /// /// Deletes or disables a User /// /// to delete /// True to permanently delete the user, False to disable the user public void Delete(IUser user, bool deletePermanently) { if (deletePermanently == false) { Delete(user); } else { using (var scope = ScopeProvider.CreateScope()) { var deleteEventArgs = new DeleteEventArgs(user); if (scope.Events.DispatchCancelable(DeletingUser, this, deleteEventArgs)) { scope.Complete(); return; } _userRepository.Delete(user); deleteEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedUser, this, deleteEventArgs); scope.Complete(); } } } /// /// Saves an /// /// to Save /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events public void Save(IUser entity, bool raiseEvents = true) { using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(entity); if (raiseEvents && scope.Events.DispatchCancelable(SavingUser, this, saveEventArgs)) { scope.Complete(); return; } if (string.IsNullOrWhiteSpace(entity.Username)) throw new ArgumentException("Empty username.", nameof(entity)); if (string.IsNullOrWhiteSpace(entity.Name)) throw new ArgumentException("Empty name.", nameof(entity)); //Now we have to check for backwards compat hacks, we'll need to process any groups //to save first before we update the user since these groups might be new groups. var explicitUser = entity as User; if (explicitUser != null && explicitUser.GroupsToSave.Count > 0) { foreach (var userGroup in explicitUser.GroupsToSave) { _userGroupRepository.Save(userGroup); } } try { _userRepository.Save(entity); if (raiseEvents) { saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedUser, this, saveEventArgs); } scope.Complete(); } catch (DbException ex) { // if we are upgrading and an exception occurs, log and swallow it if (_isUpgrading == false) throw; Logger.Warn(ex, "An error occurred attempting to save a user instance during upgrade, normally this warning can be ignored"); // we don't want the uow to rollback its scope! scope.Complete(); } } } /// /// Saves a list of objects /// /// to save /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events public void Save(IEnumerable entities, bool raiseEvents = true) { var entitiesA = entities.ToArray(); using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(entitiesA); if (raiseEvents && scope.Events.DispatchCancelable(SavingUser, this, saveEventArgs)) { scope.Complete(); return; } foreach (var user in entitiesA) { if (string.IsNullOrWhiteSpace(user.Username)) throw new ArgumentException("Empty username.", nameof(entities)); if (string.IsNullOrWhiteSpace(user.Name)) throw new ArgumentException("Empty name.", nameof(entities)); _userRepository.Save(user); //Now we have to check for backwards compat hacks //Now we have to check for backwards compat hacks var explicitUser = user as User; if (explicitUser != null && explicitUser.GroupsToSave.Count > 0) { foreach (var userGroup in explicitUser.GroupsToSave) { _userGroupRepository.Save(userGroup); } } } if (raiseEvents) { saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedUser, this, saveEventArgs); } //commit the whole lot in one go //commit the whole lot in one go scope.Complete(); } } /// /// This is just the default user group that the membership provider will use /// /// public string GetDefaultMemberType() { return "writer"; } /// /// Finds a list of objects by a partial email string /// /// Partial email string to match /// Current page index /// Size of the page /// Total number of records found (out) /// The type of match to make as . Default is /// public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query(); switch (matchType) { case StringPropertyMatchType.Exact: query.Where(member => member.Email.Equals(emailStringToMatch)); break; case StringPropertyMatchType.Contains: query.Where(member => member.Email.Contains(emailStringToMatch)); break; case StringPropertyMatchType.StartsWith: query.Where(member => member.Email.StartsWith(emailStringToMatch)); break; case StringPropertyMatchType.EndsWith: query.Where(member => member.Email.EndsWith(emailStringToMatch)); break; case StringPropertyMatchType.Wildcard: query.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar)); break; default: throw new ArgumentOutOfRangeException(nameof(matchType)); } return _userRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Email); } } /// /// Finds a list of objects by a partial username /// /// Partial username to match /// Current page index /// Size of the page /// Total number of records found (out) /// The type of match to make as . Default is /// public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query(); switch (matchType) { case StringPropertyMatchType.Exact: query.Where(member => member.Username.Equals(login)); break; case StringPropertyMatchType.Contains: query.Where(member => member.Username.Contains(login)); break; case StringPropertyMatchType.StartsWith: query.Where(member => member.Username.StartsWith(login)); break; case StringPropertyMatchType.EndsWith: query.Where(member => member.Username.EndsWith(login)); break; case StringPropertyMatchType.Wildcard: query.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar)); break; default: throw new ArgumentOutOfRangeException(nameof(matchType)); } return _userRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Username); } } /// /// Gets the total number of Users based on the count type /// /// /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science /// but that is how MS have made theirs so we'll follow that principal. /// /// to count by /// with number of Users for passed in type public int GetCount(MemberCountType countType) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { IQuery query; switch (countType) { case MemberCountType.All: query = Query(); break; case MemberCountType.Online: throw new NotImplementedException(); //var fromDate = DateTime.Now.AddMinutes(-Membership.UserIsOnlineTimeWindow); //query = // Query.Builder.Where( // x => // ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.LastLoginDate && // ((Member)x).DateTimePropertyValue > fromDate); //return repository.GetCountByQuery(query); //var fromDate = DateTime.Now.AddMinutes(-Membership.UserIsOnlineTimeWindow); //query = // Query.Builder.Where( // x => // ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.LastLoginDate && // ((Member)x).DateTimePropertyValue > fromDate); //return repository.GetCountByQuery(query); case MemberCountType.LockedOut: query = Query().Where(x => x.IsLockedOut); break; case MemberCountType.Approved: query = Query().Where(x => x.IsApproved); break; default: throw new ArgumentOutOfRangeException(nameof(countType)); } return _userRepository.GetCountByQuery(query); } } public Guid CreateLoginSession(int userId, string requestingIpAddress) { using (var scope = ScopeProvider.CreateScope()) { var session = _userRepository.CreateLoginSession(userId, requestingIpAddress); scope.Complete(); return session; } } public int ClearLoginSessions(int userId) { using (var scope = ScopeProvider.CreateScope()) { var count = _userRepository.ClearLoginSessions(userId); scope.Complete(); return count; } } public void ClearLoginSession(Guid sessionId) { using (var scope = ScopeProvider.CreateScope()) { _userRepository.ClearLoginSession(sessionId); scope.Complete(); } } public bool ValidateLoginSession(int userId, Guid sessionId) { using (ScopeProvider.CreateScope(autoComplete: true)) { return _userRepository.ValidateLoginSession(userId, sessionId); } } public IDictionary GetUserStates() { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userRepository.GetUserStates(); } } public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState[] userState = null, string[] userGroups = null, string filter = null) { IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) { filterQuery = Query().Where(x => x.Name.Contains(filter) || x.Username.Contains(filter)); } return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, userState, userGroups, null, filterQuery); } public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState[] userState = null, string[] includeUserGroups = null, string[] excludeUserGroups = null, IQuery filter = null) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { Expression> sort; switch (orderBy.ToUpperInvariant()) { case "USERNAME": sort = member => member.Username; break; case "LANGUAGE": sort = member => member.Language; break; case "NAME": sort = member => member.Name; break; case "EMAIL": sort = member => member.Email; break; case "ID": sort = member => member.Id; break; case "CREATEDATE": sort = member => member.CreateDate; break; case "UPDATEDATE": sort = member => member.UpdateDate; break; case "ISAPPROVED": sort = member => member.IsApproved; break; case "ISLOCKEDOUT": sort = member => member.IsLockedOut; break; case "LASTLOGINDATE": sort = member => member.LastLoginDate; break; default: throw new IndexOutOfRangeException("The orderBy parameter " + orderBy + " is not valid"); } return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, sort, orderDirection, includeUserGroups, excludeUserGroups, userState, filter); } } /// /// Gets a list of paged objects /// /// Current page index /// Size of the page /// Total number of records found (out) /// public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, member => member.Username); } } internal IEnumerable GetNextUsers(int id, int count) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return ((UserRepository) _userRepository).GetNextUsers(id, count); } } /// /// Gets a list of objects associated with a given group /// /// Id of group /// public IEnumerable GetAllInGroup(int groupId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userRepository.GetAllInGroup(groupId); } } /// /// Gets a list of objects not associated with a given group /// /// Id of group /// public IEnumerable GetAllNotInGroup(int groupId) { using (var scope = ScopeProvider.CreateScope()) { return _userRepository.GetAllNotInGroup(groupId); } } #endregion #region Implementation of IUserService /// /// Gets an IProfile by User Id. /// /// Id of the User to retrieve /// public IProfile GetProfileById(int id) { //This is called a TON. Go get the full user from cache which should already be IProfile var fullUser = GetUserById(id); if (fullUser == null) return null; var asProfile = fullUser as IProfile; return asProfile ?? new UserProfile(fullUser.Id, fullUser.Name); } /// /// Gets a profile by username /// /// Username /// public IProfile GetProfileByUserName(string username) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userRepository.GetProfile(username); } } /// /// Gets a user by Id /// /// Id of the user to retrieve /// public IUser GetUserById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { try { return _userRepository.Get(id); } catch (DbException) { // fixme - refactor users/upgrade // currently kinda accepting anything on upgrade, but that won't deal with all cases // so we need to do it differently, see the custom UmbracoPocoDataBuilder which should // be better BUT requires that the app restarts after the upgrade! if (_isUpgrading) { //NOTE: this will not be cached return _userRepository.Get(id, includeSecurityData: false); } throw; } } } public IEnumerable GetUsersById(params int[] ids) { if (ids.Length <= 0) return Enumerable.Empty(); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userRepository.GetMany(ids); } } /// /// Replaces the same permission set for a single group to any number of entities /// /// If no 'entityIds' are specified all permissions will be removed for the specified group. /// Id of the group /// Permissions as enumerable list of If nothing is specified all permissions are removed. /// Specify the nodes to replace permissions for. public void ReplaceUserGroupPermissions(int groupId, IEnumerable permissions, params int[] entityIds) { if (entityIds.Length == 0) return; using (var scope = ScopeProvider.CreateScope()) { _userGroupRepository.ReplaceGroupPermissions(groupId, permissions, entityIds); scope.Complete(); var assigned = permissions.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); scope.Events.Dispatch(UserGroupPermissionsAssigned, this, new SaveEventArgs(entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(), false)); } } /// /// Assigns the same permission set for a single user group to any number of entities /// /// Id of the user group /// /// Specify the nodes to replace permissions for public void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds) { if (entityIds.Length == 0) return; using (var scope = ScopeProvider.CreateScope()) { _userGroupRepository.AssignGroupPermission(groupId, permission, entityIds); scope.Complete(); var assigned = new[] { permission.ToString(CultureInfo.InvariantCulture) }; scope.Events.Dispatch(UserGroupPermissionsAssigned, this, new SaveEventArgs(entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(), false)); } } /// /// Gets all UserGroups or those specified as parameters /// /// Optional Ids of UserGroups to retrieve /// An enumerable list of public IEnumerable GetAllUserGroups(params int[] ids) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userGroupRepository.GetMany(ids).OrderBy(x => x.Name); } } public IEnumerable GetUserGroupsByAlias(params string[] aliases) { if (aliases.Length == 0) return Enumerable.Empty(); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => aliases.SqlIn(x.Alias)); var contents = _userGroupRepository.Get(query); return contents.WhereNotNull().ToArray(); } } /// /// Gets a UserGroup by its Alias /// /// Alias of the UserGroup to retrieve /// public IUserGroup GetUserGroupByAlias(string alias) { if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value cannot be null or whitespace.", "alias"); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.Alias == alias); var contents = _userGroupRepository.Get(query); return contents.FirstOrDefault(); } } /// /// Gets a UserGroup by its Id /// /// Id of the UserGroup to retrieve /// public IUserGroup GetUserGroupById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userGroupRepository.Get(id); } } /// /// Saves a UserGroup /// /// UserGroup to save /// /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in /// than all users will be removed from this group and only these users will be added /// /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events public void Save(IUserGroup userGroup, int[] userIds = null, bool raiseEvents = true) { using (var scope = ScopeProvider.CreateScope()) { // we need to figure out which users have been added / removed, for audit purposes var empty = new IUser[0]; var addedUsers = empty; var removedUsers = empty; if (userIds != null) { var groupUsers = userGroup.HasIdentity ? _userRepository.GetAllInGroup(userGroup.Id).ToArray() : empty; var xGroupUsers = groupUsers.ToDictionary(x => x.Id, x => x); var groupIds = groupUsers.Select(x => x.Id).ToArray(); addedUsers = _userRepository.GetMany(userIds.Except(groupIds).ToArray()).Where(x => x.Id != 0).ToArray(); removedUsers = groupIds.Except(userIds).Select(x => xGroupUsers[x]).Where(x => x.Id != 0).ToArray(); } var saveEventArgs = new SaveEventArgs(new UserGroupWithUsers(userGroup, addedUsers, removedUsers)); if (raiseEvents && scope.Events.DispatchCancelable(SavingUserGroup, this, saveEventArgs)) { scope.Complete(); return; } _userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, userIds); if (raiseEvents) { saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedUserGroup, this, saveEventArgs); } scope.Complete(); } } /// /// Deletes a UserGroup /// /// UserGroup to delete public void DeleteUserGroup(IUserGroup userGroup) { using (var scope = ScopeProvider.CreateScope()) { var deleteEventArgs = new DeleteEventArgs(userGroup); if (scope.Events.DispatchCancelable(DeletingUserGroup, this, deleteEventArgs)) { scope.Complete(); return; } _userGroupRepository.Delete(userGroup); deleteEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedUserGroup, this, deleteEventArgs); scope.Complete(); } } /// /// Removes a specific section from all users /// /// This is useful when an entire section is removed from config /// Alias of the section to remove public void DeleteSectionFromAllUserGroups(string sectionAlias) { using (var scope = ScopeProvider.CreateScope()) { var assignedGroups = _userGroupRepository.GetGroupsAssignedToSection(sectionAlias); foreach (var group in assignedGroups) { //now remove the section for each user and commit //now remove the section for each user and commit group.RemoveAllowedSection(sectionAlias); _userGroupRepository.Save(group); } scope.Complete(); } } /// /// Get explicitly assigned permissions for a user and optional node ids /// /// User to retrieve permissions for /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of public EntityPermissionCollection GetPermissions(IUser user, params int[] nodeIds) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userGroupRepository.GetPermissions(user.Groups.ToArray(), true, nodeIds); } } /// /// Get explicitly assigned permissions for a group and optional node Ids /// /// Groups to retrieve permissions for /// /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of private IEnumerable GetPermissions(IReadOnlyUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) { if (groups == null) throw new ArgumentNullException(nameof(groups)); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userGroupRepository.GetPermissions(groups, fallbackToDefaultPermissions, nodeIds); } } /// /// Get explicitly assigned permissions for a group and optional node Ids /// /// /// /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// /// Specifiying nothing will return all permissions for all nodes /// An enumerable list of public EntityPermissionCollection GetPermissions(IUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) { if (groups == null) throw new ArgumentNullException(nameof(groups)); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return _userGroupRepository.GetPermissions(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), fallbackToDefaultPermissions, nodeIds); } } /// /// Gets the implicit/inherited permissions for the user for the given path /// /// User to check permissions for /// Path to check permissions for public EntityPermissionSet GetPermissionsForPath(IUser user, string path) { var nodeIds = path.GetIdsFromPathReversed(); if (nodeIds.Length == 0) return EntityPermissionSet.Empty(); //collect all permissions structures for all nodes for all groups belonging to the user var groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); } /// /// Gets the permissions for the provided group and path /// /// /// Path to check permissions for /// /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// /// String indicating permissions for provided user and path public EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false) { var nodeIds = path.GetIdsFromPathReversed(); if (nodeIds.Length == 0) return EntityPermissionSet.Empty(); //collect all permissions structures for all nodes for all groups var groupPermissions = GetPermissionsForPath(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); } private EntityPermissionCollection GetPermissionsForPath(IReadOnlyUserGroup[] groups, int[] pathIds, bool fallbackToDefaultPermissions = false) { if (pathIds.Length == 0) return new EntityPermissionCollection(Enumerable.Empty()); //get permissions for all nodes in the path by group var permissions = GetPermissions(groups, fallbackToDefaultPermissions, pathIds) .GroupBy(x => x.UserGroupId); return new EntityPermissionCollection( permissions.Select(x => GetPermissionsForPathForGroup(x, pathIds, fallbackToDefaultPermissions))); } /// /// This performs the calculations for inherited nodes based on this http://issues.umbraco.org/issue/U4-10075#comment=67-40085 /// /// /// /// internal static EntityPermissionSet CalculatePermissionsForPathForUser( EntityPermission[] groupPermissions, int[] pathIds) { // not sure this will ever happen, it shouldn't since this should return defaults, but maybe those are empty? if (groupPermissions.Length == 0 || pathIds.Length == 0) return EntityPermissionSet.Empty(); //The actual entity id being looked at (deepest part of the path) var entityId = pathIds[0]; var resultPermissions = new EntityPermissionCollection(); //create a grouped by dictionary of another grouped by dictionary var permissionsByGroup = groupPermissions .GroupBy(x => x.UserGroupId) .ToDictionary( x => x.Key, x => x.GroupBy(a => a.EntityId).ToDictionary(a => a.Key, a => a.ToArray())); //iterate through each group foreach (var byGroup in permissionsByGroup) { var added = false; //iterate deepest to shallowest foreach (var pathId in pathIds) { EntityPermission[] permissionsForNodeAndGroup; if (byGroup.Value.TryGetValue(pathId, out permissionsForNodeAndGroup) == false) continue; //In theory there will only be one EntityPermission in this group // but there's nothing stopping the logic of this method // from having more so we deal with it here foreach (var entityPermission in permissionsForNodeAndGroup) { if (entityPermission.IsDefaultPermissions == false) { //explicit permision found so we'll append it and move on, the collection is a hashset anyways //so only supports adding one element per groupid/contentid resultPermissions.Add(entityPermission); added = true; break; } } //if the permission has been added for this group and this branch then we can exit this loop if (added) break; } if (added == false && byGroup.Value.Count > 0) { //if there was no explicit permissions assigned in this branch for this group, then we will //add the group's default permissions resultPermissions.Add(byGroup.Value[entityId][0]); } } var permissionSet = new EntityPermissionSet(entityId, resultPermissions); return permissionSet; } /// /// Returns the resulting permission set for a group for the path based on all permissions provided for the branch /// /// /// The collective set of permissions provided to calculate the resulting permissions set for the path /// based on a single group /// /// Must be ordered deepest to shallowest (right to left) /// /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set /// /// internal static EntityPermission GetPermissionsForPathForGroup( IEnumerable pathPermissions, int[] pathIds, bool fallbackToDefaultPermissions = false) { //get permissions for all nodes in the path var permissionsByEntityId = pathPermissions.ToDictionary(x => x.EntityId, x => x); //then the permissions assigned to the path will be the 'deepest' node found that has permissions foreach (var id in pathIds) { EntityPermission permission; if (permissionsByEntityId.TryGetValue(id, out permission)) { //don't return the default permissions if that is the one assigned here (we'll do that below if nothing was found) if (permission.IsDefaultPermissions == false) return permission; } } //if we've made it here it means that no implicit/inherited permissions were found so we return the defaults if that is specified if (fallbackToDefaultPermissions == false) return null; return permissionsByEntityId[pathIds[0]]; } /// /// Checks in a set of permissions associated with a user for those related to a given nodeId /// /// The set of permissions /// The node Id /// The permissions to return /// True if permissions for the given path are found public static bool TryGetAssignedPermissionsForNode(IList permissions, int nodeId, out string assignedPermissions) { if (permissions.Any(x => x.EntityId == nodeId)) { var found = permissions.First(x => x.EntityId == nodeId); var assignedPermissionsArray = found.AssignedPermissions.ToList(); // Working with permissions assigned directly to a user AND to their groups, so maybe several per node // and we need to get the most permissive set foreach (var permission in permissions.Where(x => x.EntityId == nodeId).Skip(1)) { AddAdditionalPermissions(assignedPermissionsArray, permission.AssignedPermissions); } assignedPermissions = string.Join("", assignedPermissionsArray); return true; } assignedPermissions = string.Empty; return false; } private static void AddAdditionalPermissions(List assignedPermissions, string[] additionalPermissions) { var permissionsToAdd = additionalPermissions .Where(x => assignedPermissions.Contains(x) == false); assignedPermissions.AddRange(permissionsToAdd); } #endregion /// /// Occurs before Save /// public static event TypedEventHandler> SavingUser; /// /// Occurs after Save /// public static event TypedEventHandler> SavedUser; /// /// Occurs before Delete /// public static event TypedEventHandler> DeletingUser; /// /// Occurs after Delete /// public static event TypedEventHandler> DeletedUser; /// /// Occurs before Save /// internal static event TypedEventHandler> SavingUserGroup; /// /// Occurs after Save /// internal static event TypedEventHandler> SavedUserGroup; /// /// Occurs before Delete /// public static event TypedEventHandler> DeletingUserGroup; /// /// Occurs after Delete /// public static event TypedEventHandler> DeletedUserGroup; //TODO: still don't know if we need this yet unless we start caching permissions, but that also means we'll need another // event on the ContentService since there's a method there to modify node permissions too, or we can proxy events if needed. internal static event TypedEventHandler> UserGroupPermissionsAssigned; } }