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
}