using System.Data.Common; using System.Globalization; using System.Linq.Expressions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; /// /// Represents the UserService, which is an easy access to operations involving , /// and eventually Backoffice Users. /// internal class UserService : RepositoryService, IUserService { private readonly GlobalSettings _globalSettings; private readonly ILogger _logger; private readonly IRuntimeState _runtimeState; private readonly IUserGroupRepository _userGroupRepository; private readonly IRequestCache _requestCache; private readonly IUserRepository _userRepository; [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")] public UserService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IRuntimeState runtimeState, IUserRepository userRepository, IUserGroupRepository userGroupRepository, IOptions globalSettings) : this(provider, loggerFactory, eventMessagesFactory, runtimeState, userRepository, userGroupRepository, globalSettings, StaticServiceProvider.Instance.GetRequiredService()) { } public UserService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IRuntimeState runtimeState, IUserRepository userRepository, IUserGroupRepository userGroupRepository, IOptions globalSettings, IRequestCache requestCache) : base(provider, loggerFactory, eventMessagesFactory) { _runtimeState = runtimeState; _userRepository = userRepository; _userGroupRepository = userGroupRepository; _requestCache = requestCache; _globalSettings = globalSettings.Value; _logger = loggerFactory.CreateLogger(); } private bool IsUpgrading => _runtimeState.Level == RuntimeLevel.Install || _runtimeState.Level == RuntimeLevel.Upgrade; /// /// 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)) { EntityPermission 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 (EntityPermission permission in permissions.Where(x => x.EntityId == nodeId).Skip(1)) { AddAdditionalPermissions(assignedPermissionsArray, permission.AssignedPermissions); } assignedPermissions = string.Join(string.Empty, assignedPermissionsArray); return true; } assignedPermissions = string.Empty; return false; } #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 (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _userRepository.ExistsByUserName(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) => 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) => 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) => CreateUserWithIdentity(username, email, passwordValue, isApproved); /// /// Gets a User by its integer id /// /// Id /// /// /// public IUser? GetById(int id) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _userRepository.Get(id); } } /// /// 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 (username == null) { throw new ArgumentNullException(nameof(username)); } if (string.IsNullOrWhiteSpace(username)) { throw new ArgumentException( "Value can't be empty or consist only of white-space characters.", nameof(username)); } EventMessages evtMsgs = EventMessagesFactory.Get(); // TODO: PUT lock here!! User user; using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { var loginExists = _userRepository.ExistsByLogin(username); if (loginExists) { throw new ArgumentException("Login already exists"); // causes rollback } user = new User(_globalSettings) { Email = email, Language = _globalSettings.DefaultUILanguage, Name = username, RawPasswordValue = passwordValue, Username = username, IsLockedOut = false, IsApproved = isApproved, }; var savingNotification = new UserSavingNotification(user, evtMsgs); if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return user; } _userRepository.Save(user); scope.Notifications.Publish(new UserSavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); scope.Complete(); } return user; } /// /// Gets an by its provider key /// /// Id to use for retrieval /// /// /// public IUser? GetByProviderKey(object id) { Attempt 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 (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery 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) { if (username is null) { return null; } using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { try { return _userRepository.GetByUsername(username, true); } catch (DbException) { // TODO: 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, false); } throw; } } } /// /// Disables an /// /// to disable public void Delete(IUser membershipUser) { // disable membershipUser.IsApproved = false; Save(membershipUser); } /// /// 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 { EventMessages evtMsgs = EventMessagesFactory.Get(); using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { var deletingNotification = new UserDeletingNotification(user, evtMsgs); if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; } _userRepository.Delete(user); scope.Notifications.Publish( new UserDeletedNotification(user, evtMsgs).WithStateFrom(deletingNotification)); scope.Complete(); } } } /// /// Saves an /// /// to Save public void Save(IUser entity) { EventMessages evtMsgs = EventMessagesFactory.Get(); using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { var savingNotification = new UserSavingNotification(entity, evtMsgs); if (scope.Notifications.PublishCancelable(savingNotification)) { 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)); } try { _userRepository.Save(entity); scope.Notifications.Publish( new UserSavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); scope.Complete(); } catch (DbException ex) { // if we are upgrading and an exception occurs, log and swallow it if (IsUpgrading == false) { throw; } _logger.LogWarning( 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 public void Save(IEnumerable entities) { EventMessages evtMsgs = EventMessagesFactory.Get(); IUser[] entitiesA = entities.ToArray(); using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { var savingNotification = new UserSavingNotification(entitiesA, evtMsgs); if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; } foreach (IUser 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); } scope.Notifications.Publish( new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); // 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() => Constants.Security.WriterGroupAlias; /// /// 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 (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery 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 (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery 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 (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery? query; switch (countType) { case MemberCountType.All: query = Query(); break; 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 (ICoreScope scope = ScopeProvider.CreateCoreScope()) { Guid session = _userRepository.CreateLoginSession(userId, requestingIpAddress); scope.Complete(); return session; } } public int ClearLoginSessions(int userId) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { var count = _userRepository.ClearLoginSessions(userId); scope.Complete(); return count; } } public void ClearLoginSession(Guid sessionId) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { _userRepository.ClearLoginSession(sessionId); scope.Complete(); } } public bool ValidateLoginSession(int userId, Guid sessionId) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { var result = _userRepository.ValidateLoginSession(userId, sessionId); scope.Complete(); return result; } } public IDictionary GetUserStates() { using (ICoreScope scope = ScopeProvider.CreateCoreScope(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 != null && 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 (ICoreScope scope = ScopeProvider.CreateCoreScope(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 (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, member => member.Name); } } public IEnumerable GetNextUsers(int id, int count) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _userRepository.GetNextUsers(id, count); } } /// /// Gets a list of objects associated with a given group /// /// Id of group /// /// /// public IEnumerable GetAllInGroup(int? groupId) { if (groupId is null) { return Array.Empty(); } using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _userRepository.GetAllInGroup(groupId.Value); } } /// /// Gets a list of objects not associated with a given group /// /// Id of group /// /// /// public IEnumerable GetAllNotInGroup(int groupId) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { 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 IUser? 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 (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _userRepository.GetProfile(username); } } /// /// Gets a user by Id /// /// Id of the user to retrieve /// /// /// public IUser? GetUserById(int id) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { try { return _userRepository.Get(id); } catch (DbException) { // TODO: 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, false); } throw; } } } public IEnumerable GetUsersById(params int[]? ids) { if (ids?.Length <= 0) { return Enumerable.Empty(); } using (ICoreScope scope = ScopeProvider.CreateCoreScope(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; } EventMessages evtMsgs = EventMessagesFactory.Get(); using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { _userGroupRepository.ReplaceGroupPermissions(groupId, permissions, entityIds); scope.Complete(); var assigned = permissions?.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); if (assigned is not null) { EntityPermission[] entityPermissions = entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); } } } /// /// 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; } EventMessages evtMsgs = EventMessagesFactory.Get(); using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { _userGroupRepository.AssignGroupPermission(groupId, permission, entityIds); scope.Complete(); var assigned = new[] { permission.ToString(CultureInfo.InvariantCulture) }; EntityPermission[] entityPermissions = entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); } } /// /// 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 (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _userGroupRepository.GetMany(ids).OrderBy(x => x.Name); } } public IEnumerable GetUserGroupsByAlias(params string[] aliases) { if (aliases.Length == 0) { return Enumerable.Empty(); } using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery query = Query().Where(x => aliases.SqlIn(x.Alias)); IEnumerable contents = _userGroupRepository.Get(query); return contents?.WhereNotNull().ToArray() ?? Enumerable.Empty(); } } /// /// 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 (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery query = Query().Where(x => x.Alias == alias); IEnumerable 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 (ICoreScope scope = ScopeProvider.CreateCoreScope(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. public void Save(IUserGroup userGroup, int[]? userIds = null) { EventMessages evtMsgs = EventMessagesFactory.Get(); using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { // we need to figure out which users have been added / removed, for audit purposes var empty = new IUser[0]; IUser[] addedUsers = empty; IUser[] removedUsers = empty; if (userIds != null) { IUser[] 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(); IEnumerable addedUserIds = userIds.Except(groupIds); addedUsers = addedUserIds.Count() > 0 ? _userRepository.GetMany(addedUserIds.ToArray()).Where(x => x.Id != 0).ToArray() : new IUser[] { }; removedUsers = groupIds.Except(userIds).Select(x => xGroupUsers[x]).Where(x => x.Id != 0).ToArray(); } var userGroupWithUsers = new UserGroupWithUsers(userGroup, addedUsers, removedUsers); // this is the default/expected notification for the IUserGroup entity being saved var savingNotification = new UserGroupSavingNotification(userGroup, evtMsgs); if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; } // this is an additional notification for special auditing var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, evtMsgs); if (scope.Notifications.PublishCancelable(savingUserGroupWithUsersNotification)) { scope.Complete(); return; } _userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, userIds); scope.Notifications.Publish( new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); scope.Notifications.Publish( new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom( savingUserGroupWithUsersNotification)); scope.Complete(); } } /// /// Deletes a UserGroup /// /// UserGroup to delete public void DeleteUserGroup(IUserGroup userGroup) { EventMessages evtMsgs = EventMessagesFactory.Get(); using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { var deletingNotification = new UserGroupDeletingNotification(userGroup, evtMsgs); if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; } _userGroupRepository.Delete(userGroup); scope.Notifications.Publish( new UserGroupDeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); 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 (ICoreScope scope = ScopeProvider.CreateCoreScope()) { IEnumerable assignedGroups = _userGroupRepository.GetGroupsAssignedToSection(sectionAlias); foreach (IUserGroup 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 /// Specifying nothing will return all permissions for all nodes /// An enumerable list of public EntityPermissionCollection GetPermissions(IUser? user, params int[] nodeIds) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _userGroupRepository.GetPermissions(user?.Groups.ToArray(), true, 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 /// /// Specifying 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 (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _userGroupRepository.GetPermissions( groups.WhereNotNull().Select(x => x.ToReadOnlyGroup()).ToArray(), fallbackToDefaultPermissions, 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 /// /// Specifying 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 (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { return _userGroupRepository.GetPermissions(groups, 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 result = (EntityPermissionSet?)_requestCache.Get($"{nameof(GetPermissionsForPath)}|{path}|{user?.Id}", () => { var nodeIds = path?.GetIdsFromPathReversed(); if (nodeIds is null || nodeIds.Length == 0 || user is null) { return EntityPermissionSet.Empty(); } // collect all permissions structures for all nodes for all groups belonging to the user EntityPermission[] groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, true).ToArray(); return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); }); return result ?? EntityPermissionSet.Empty(); } /// /// 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 EntityPermission[] groupPermissions = GetPermissionsForPath(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), nodeIds, true).ToArray(); return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); } /// /// 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 (KeyValuePair> byGroup in permissionsByGroup) { var added = false; // iterate deepest to shallowest foreach (var pathId in pathIds) { if (byGroup.Value.TryGetValue(pathId, out EntityPermission[]? 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 (EntityPermission entityPermission in permissionsForNodeAndGroup) { if (entityPermission.IsDefaultPermissions == false) { // explicit permission 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; } 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 IEnumerable> permissions = GetPermissions(groups, fallbackToDefaultPermissions, pathIds) .GroupBy(x => x.UserGroupId); return new EntityPermissionCollection( permissions.Select(x => GetPermissionsForPathForGroup(x, pathIds, fallbackToDefaultPermissions)) .Where(x => x is not null)!); } /// /// 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) { if (permissionsByEntityId.TryGetValue(id, out EntityPermission? 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]]; } private static void AddAdditionalPermissions(List assignedPermissions, string[] additionalPermissions) { IEnumerable permissionsToAdd = additionalPermissions .Where(x => assignedPermissions.Contains(x) == false); assignedPermissions.AddRange(permissionsToAdd); } #endregion }