* Prevents the removal of all user groups from a user. * Add additional user group when removing --------- Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2505 lines
97 KiB
C#
2505 lines
97 KiB
C#
using System.ComponentModel.DataAnnotations;
|
|
using System.Linq.Expressions;
|
|
using System.Security.Claims;
|
|
using System.Security.Cryptography;
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Umbraco.Cms.Core.Configuration.Models;
|
|
using Umbraco.Cms.Core.Editors;
|
|
using Umbraco.Cms.Core.Events;
|
|
using Umbraco.Cms.Core.Exceptions;
|
|
using Umbraco.Cms.Core.IO;
|
|
using Umbraco.Cms.Core.Models;
|
|
using Umbraco.Cms.Core.Models.Entities;
|
|
using Umbraco.Cms.Core.Models.Membership;
|
|
using Umbraco.Cms.Core.Models.TemporaryFile;
|
|
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.Cms.Core.Security;
|
|
using Umbraco.Cms.Core.Services.OperationStatus;
|
|
using Umbraco.Cms.Core.Strings;
|
|
using Umbraco.Extensions;
|
|
using Guid = System.Guid;
|
|
using UserProfile = Umbraco.Cms.Core.Models.Membership.UserProfile;
|
|
|
|
namespace Umbraco.Cms.Core.Services;
|
|
|
|
/// <summary>
|
|
/// Represents the UserService, which is an easy access to operations involving <see cref="IProfile" />,
|
|
/// <see cref="IMembershipUser" /> and eventually Backoffice Users.
|
|
/// </summary>
|
|
internal partial class UserService : RepositoryService, IUserService
|
|
{
|
|
private readonly GlobalSettings _globalSettings;
|
|
private readonly SecuritySettings _securitySettings;
|
|
private readonly IUserGroupRepository _userGroupRepository;
|
|
private readonly UserEditorAuthorizationHelper _userEditorAuthorizationHelper;
|
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
|
private readonly IEntityService _entityService;
|
|
private readonly ILocalLoginSettingProvider _localLoginSettingProvider;
|
|
private readonly IUserInviteSender _inviteSender;
|
|
private readonly IUserForgotPasswordSender _forgotPasswordSender;
|
|
private readonly MediaFileManager _mediaFileManager;
|
|
private readonly ITemporaryFileService _temporaryFileService;
|
|
private readonly IShortStringHelper _shortStringHelper;
|
|
private readonly IIsoCodeValidator _isoCodeValidator;
|
|
private readonly IUserRepository _userRepository;
|
|
private readonly ContentSettings _contentSettings;
|
|
private readonly IUserIdKeyResolver _userIdKeyResolver;
|
|
|
|
public UserService(
|
|
ICoreScopeProvider provider,
|
|
ILoggerFactory loggerFactory,
|
|
IEventMessagesFactory eventMessagesFactory,
|
|
IUserRepository userRepository,
|
|
IUserGroupRepository userGroupRepository,
|
|
IOptions<GlobalSettings> globalSettings,
|
|
IOptions<SecuritySettings> securitySettings,
|
|
UserEditorAuthorizationHelper userEditorAuthorizationHelper,
|
|
IServiceScopeFactory serviceScopeFactory,
|
|
IEntityService entityService,
|
|
ILocalLoginSettingProvider localLoginSettingProvider,
|
|
IUserInviteSender inviteSender,
|
|
MediaFileManager mediaFileManager,
|
|
ITemporaryFileService temporaryFileService,
|
|
IShortStringHelper shortStringHelper,
|
|
IOptions<ContentSettings> contentSettings,
|
|
IIsoCodeValidator isoCodeValidator,
|
|
IUserForgotPasswordSender forgotPasswordSender,
|
|
IUserIdKeyResolver userIdKeyResolver)
|
|
: base(provider, loggerFactory, eventMessagesFactory)
|
|
{
|
|
_userRepository = userRepository;
|
|
_userGroupRepository = userGroupRepository;
|
|
_userEditorAuthorizationHelper = userEditorAuthorizationHelper;
|
|
_serviceScopeFactory = serviceScopeFactory;
|
|
_entityService = entityService;
|
|
_localLoginSettingProvider = localLoginSettingProvider;
|
|
_inviteSender = inviteSender;
|
|
_mediaFileManager = mediaFileManager;
|
|
_temporaryFileService = temporaryFileService;
|
|
_shortStringHelper = shortStringHelper;
|
|
_isoCodeValidator = isoCodeValidator;
|
|
_forgotPasswordSender = forgotPasswordSender;
|
|
_userIdKeyResolver = userIdKeyResolver;
|
|
_globalSettings = globalSettings.Value;
|
|
_securitySettings = securitySettings.Value;
|
|
_contentSettings = contentSettings.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks in a set of permissions associated with a user for those related to a given nodeId
|
|
/// </summary>
|
|
/// <param name="permissions">The set of permissions</param>
|
|
/// <param name="nodeId">The node Id</param>
|
|
/// <param name="assignedPermissions">The permissions to return</param>
|
|
/// <returns>True if permissions for the given path are found</returns>
|
|
public static bool TryGetAssignedPermissionsForNode(
|
|
IList<EntityPermission> permissions,
|
|
int nodeId,
|
|
out string assignedPermissions)
|
|
{
|
|
if (permissions.Any(x => x.EntityId == nodeId))
|
|
{
|
|
EntityPermission found = permissions.First(x => x.EntityId == nodeId);
|
|
ISet<string> assignedPermissionsArray = found.AssignedPermissions;
|
|
|
|
// 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
|
|
|
|
/// <summary>
|
|
/// Checks if a User with the username exists
|
|
/// </summary>
|
|
/// <param name="username">Username to check</param>
|
|
/// <returns><c>True</c> if the User exists otherwise <c>False</c></returns>
|
|
public bool Exists(string username)
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
return _userRepository.ExistsByUserName(username);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new User
|
|
/// </summary>
|
|
/// <remarks>The user will be saved in the database and returned with an Id</remarks>
|
|
/// <param name="username">Username of the user to create</param>
|
|
/// <param name="email">Email of the user to create</param>
|
|
/// <returns>
|
|
/// <see cref="IUser" />
|
|
/// </returns>
|
|
public IUser CreateUserWithIdentity(string username, string email) =>
|
|
CreateUserWithIdentity(username, email, string.Empty);
|
|
|
|
/// <summary>
|
|
/// Creates and persists a new <see cref="IUser" />
|
|
/// </summary>
|
|
/// <param name="username">Username of the <see cref="IUser" /> to create</param>
|
|
/// <param name="email">Email of the <see cref="IUser" /> to create</param>
|
|
/// <param name="passwordValue">
|
|
/// This value should be the encoded/encrypted/hashed value for the password that will be
|
|
/// stored in the database
|
|
/// </param>
|
|
/// <param name="memberTypeAlias">Not used for users</param>
|
|
/// <returns>
|
|
/// <see cref="IUser" />
|
|
/// </returns>
|
|
IUser IMembershipMemberService<IUser>.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) => CreateUserWithIdentity(username, email, passwordValue);
|
|
|
|
/// <summary>
|
|
/// Creates and persists a new <see cref="IUser" />
|
|
/// </summary>
|
|
/// <param name="username">Username of the <see cref="IUser" /> to create</param>
|
|
/// <param name="email">Email of the <see cref="IUser" /> to create</param>
|
|
/// <param name="passwordValue">
|
|
/// This value should be the encoded/encrypted/hashed value for the password that will be
|
|
/// stored in the database
|
|
/// </param>
|
|
/// <param name="memberTypeAlias">Alias of the Type</param>
|
|
/// <param name="isApproved">Is the member approved</param>
|
|
/// <returns>
|
|
/// <see cref="IUser" />
|
|
/// </returns>
|
|
IUser IMembershipMemberService<IUser>.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved) => CreateUserWithIdentity(username, email, passwordValue, isApproved);
|
|
|
|
/// <summary>
|
|
/// Creates and persists a Member
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Using this method will persist the Member object before its returned
|
|
/// meaning that it will have an Id available (unlike the CreateMember method)
|
|
/// </remarks>
|
|
/// <param name="username">Username of the Member to create</param>
|
|
/// <param name="email">Email of the Member to create</param>
|
|
/// <param name="passwordValue">
|
|
/// This value should be the encoded/encrypted/hashed value for the password that will be
|
|
/// stored in the database
|
|
/// </param>
|
|
/// <param name="isApproved">Is the user approved</param>
|
|
/// <returns>
|
|
/// <see cref="IUser" />
|
|
/// </returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an <see cref="IUser" /> by its provider key
|
|
/// </summary>
|
|
/// <param name="id">Id to use for retrieval</param>
|
|
/// <returns>
|
|
/// <see cref="IUser" />
|
|
/// </returns>
|
|
public IUser? GetByProviderKey(object id)
|
|
{
|
|
Attempt<int> asInt = id.TryConvertTo<int>();
|
|
Guid? userKey = null;
|
|
if (asInt.Success)
|
|
{
|
|
userKey = _userIdKeyResolver.GetAsync(asInt.Result).GetAwaiter().GetResult();
|
|
}
|
|
else if (Guid.TryParse(id.ToString(), out Guid idAsGuid))
|
|
{
|
|
userKey = idAsGuid;
|
|
}
|
|
|
|
if (userKey.HasValue is false)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
return _userRepository.Get(userKey.Value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get an <see cref="IUser" /> by email
|
|
/// </summary>
|
|
/// <param name="email">Email to use for retrieval</param>
|
|
/// <returns>
|
|
/// <see cref="IUser" />
|
|
/// </returns>
|
|
public IUser? GetByEmail(string email)
|
|
{
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
return backOfficeUserStore.GetByEmailAsync(email).GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get an <see cref="IUser" /> by username
|
|
/// </summary>
|
|
/// <param name="username">Username to use for retrieval</param>
|
|
/// <returns>
|
|
/// <see cref="IUser" />
|
|
/// </returns>
|
|
public IUser? GetByUsername(string? username)
|
|
{
|
|
if (username is null)
|
|
{
|
|
return null;
|
|
}
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
return backOfficeUserStore.GetByUserNameAsync(username).GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disables an <see cref="IUser" />
|
|
/// </summary>
|
|
/// <param name="membershipUser"><see cref="IUser" /> to disable</param>
|
|
public void Delete(IUser membershipUser)
|
|
{
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
backOfficeUserStore.DisableAsync(membershipUser).GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes or disables a User
|
|
/// </summary>
|
|
/// <param name="user"><see cref="IUser" /> to delete</param>
|
|
/// <param name="deletePermanently"><c>True</c> to permanently delete the user, <c>False</c> to disable the user</param>
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves an <see cref="IUser" />
|
|
/// </summary>
|
|
/// <param name="entity"><see cref="IUser" /> to Save</param>
|
|
public void Save(IUser entity)
|
|
{
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
backOfficeUserStore.SaveAsync(entity).GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves an <see cref="IUser" />
|
|
/// </summary>
|
|
/// <param name="entity"><see cref="IUser" /> to Save</param>
|
|
public async Task<UserOperationStatus> SaveAsync(IUser entity)
|
|
{
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
return await backOfficeUserStore.SaveAsync(entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a list of <see cref="IUser" /> objects
|
|
/// </summary>
|
|
/// <param name="entities"><see cref="IEnumerable{IUser}" /> to save</param>
|
|
public void Save(IEnumerable<IUser> 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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds a list of <see cref="IUser" /> objects by a partial email string
|
|
/// </summary>
|
|
/// <param name="emailStringToMatch">Partial email string to match</param>
|
|
/// <param name="pageIndex">Current page index</param>
|
|
/// <param name="pageSize">Size of the page</param>
|
|
/// <param name="totalRecords">Total number of records found (out)</param>
|
|
/// <param name="matchType">
|
|
/// The type of match to make as <see cref="StringPropertyMatchType" />. Default is
|
|
/// <see cref="StringPropertyMatchType.StartsWith" />
|
|
/// </param>
|
|
/// <returns>
|
|
/// <see cref="IEnumerable{IUser}" />
|
|
/// </returns>
|
|
public IEnumerable<IUser> FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
IQuery<IUser> query = Query<IUser>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds a list of <see cref="IUser" /> objects by a partial username
|
|
/// </summary>
|
|
/// <param name="login">Partial username to match</param>
|
|
/// <param name="pageIndex">Current page index</param>
|
|
/// <param name="pageSize">Size of the page</param>
|
|
/// <param name="totalRecords">Total number of records found (out)</param>
|
|
/// <param name="matchType">
|
|
/// The type of match to make as <see cref="StringPropertyMatchType" />. Default is
|
|
/// <see cref="StringPropertyMatchType.StartsWith" />
|
|
/// </param>
|
|
/// <returns>
|
|
/// <see cref="IEnumerable{IUser}" />
|
|
/// </returns>
|
|
public IEnumerable<IUser> FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
IQuery<IUser> query = Query<IUser>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total number of Users based on the count type
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
/// <param name="countType"><see cref="MemberCountType" /> to count by</param>
|
|
/// <returns><see cref="int" /> with number of Users for passed in type</returns>
|
|
public int GetCount(MemberCountType countType)
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
IQuery<IUser>? query;
|
|
|
|
switch (countType)
|
|
{
|
|
case MemberCountType.All:
|
|
query = Query<IUser>();
|
|
break;
|
|
case MemberCountType.LockedOut:
|
|
query = Query<IUser>()?.Where(x => x.IsLockedOut);
|
|
break;
|
|
case MemberCountType.Approved:
|
|
query = Query<IUser>()?.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<UserState, int> GetUserStates()
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
return _userRepository.GetUserStates();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<Attempt<UserCreationResult, UserOperationStatus>> CreateAsync(Guid performingUserKey, UserCreateModel model, bool approveUser = false)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
using IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
|
|
IUser? performingUser = await GetAsync(performingUserKey);
|
|
|
|
if (performingUser is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new UserCreationResult());
|
|
}
|
|
|
|
IUserGroup[] userGroups = _userGroupRepository.GetMany().Where(x=>model.UserGroupKeys.Contains(x.Key)).ToArray();
|
|
|
|
if (userGroups.Length != model.UserGroupKeys.Count)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MissingUserGroup, new UserCreationResult());
|
|
}
|
|
|
|
UserOperationStatus result = await ValidateUserCreateModel(model);
|
|
if (result != UserOperationStatus.Success)
|
|
{
|
|
return Attempt.FailWithStatus(result, new UserCreationResult());
|
|
}
|
|
|
|
Attempt<string?> authorizationAttempt = _userEditorAuthorizationHelper.IsAuthorized(
|
|
performingUser,
|
|
null,
|
|
null,
|
|
null,
|
|
userGroups.Select(x => x.Alias));
|
|
|
|
if (authorizationAttempt.Success is false)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.Unauthorized, new UserCreationResult());
|
|
}
|
|
|
|
ICoreBackOfficeUserManager backOfficeUserManager = serviceScope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
|
|
|
|
IdentityCreationResult identityCreationResult = await backOfficeUserManager.CreateAsync(model);
|
|
|
|
if (identityCreationResult.Succeded is false)
|
|
{
|
|
// If we fail from something in Identity we can't know exactly why, so we have to resolve to returning an unknown failure.
|
|
// But there should be more information in the message.
|
|
return Attempt.FailWithStatus(
|
|
UserOperationStatus.UnknownFailure,
|
|
new UserCreationResult { Error = new ValidationResult(identityCreationResult.ErrorMessage) });
|
|
}
|
|
|
|
// The user is now created, so we can fetch it to map it to a result model with our generated password.
|
|
// and set it to being approved
|
|
IBackOfficeUserStore backOfficeUserStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
IUser? createdUser = await backOfficeUserStore.GetByEmailAsync(model.Email);
|
|
|
|
if (createdUser is null)
|
|
{
|
|
// This really shouldn't happen, we literally just created the user
|
|
throw new PanicException("Was unable to get user after creating it");
|
|
}
|
|
|
|
createdUser.IsApproved = approveUser;
|
|
|
|
foreach (IUserGroup userGroup in userGroups)
|
|
{
|
|
createdUser.AddGroup(userGroup.ToReadOnlyGroup());
|
|
}
|
|
|
|
await backOfficeUserStore.SaveAsync(createdUser);
|
|
|
|
scope.Complete();
|
|
|
|
var creationResult = new UserCreationResult
|
|
{
|
|
CreatedUser = createdUser,
|
|
InitialPassword = identityCreationResult.InitialPassword
|
|
};
|
|
|
|
return Attempt.SucceedWithStatus(UserOperationStatus.Success, creationResult);
|
|
}
|
|
|
|
public async Task<Attempt<UserOperationStatus>> SendResetPasswordEmailAsync(string userEmail)
|
|
{
|
|
if (_forgotPasswordSender.CanSend() is false)
|
|
{
|
|
return Attempt.Fail(UserOperationStatus.CannotPasswordReset);
|
|
}
|
|
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
using IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
|
|
ICoreBackOfficeUserManager userManager = serviceScope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
|
|
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
IUser? user = await userStore.GetByEmailAsync(userEmail);
|
|
|
|
if (user is null)
|
|
{
|
|
return Attempt.Fail(UserOperationStatus.UserNotFound);
|
|
}
|
|
|
|
IForgotPasswordUriProvider uriProvider = serviceScope.ServiceProvider.GetRequiredService<IForgotPasswordUriProvider>();
|
|
Attempt<Uri, UserOperationStatus> uriAttempt = await uriProvider.CreateForgotPasswordUriAsync(user);
|
|
if (uriAttempt.Success is false)
|
|
{
|
|
return Attempt.Fail(uriAttempt.Status);
|
|
}
|
|
|
|
var message = new UserForgotPasswordMessage
|
|
{
|
|
ForgotPasswordUri = uriAttempt.Result,
|
|
Recipient = user,
|
|
};
|
|
await _forgotPasswordSender.SendForgotPassword(message);
|
|
|
|
userManager.NotifyForgotPasswordRequested(new ClaimsPrincipal(), user.Id.ToString()); //A bit of a hack, but since this method will be used without a signed in user, there is no real principal anyway.
|
|
|
|
scope.Complete();
|
|
|
|
return Attempt.Succeed(UserOperationStatus.Success);
|
|
}
|
|
public async Task<Attempt<UserInvitationResult, UserOperationStatus>> InviteAsync(Guid performingUserKey, UserInviteModel model)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
using IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
|
|
IUser? performingUser = await GetAsync(performingUserKey);
|
|
|
|
if (performingUser is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new UserInvitationResult());
|
|
}
|
|
|
|
IUserGroup[] userGroups = _userGroupRepository.GetMany().Where(x => model.UserGroupKeys.Contains(x.Key)).ToArray();
|
|
|
|
if (userGroups.Length != model.UserGroupKeys.Count)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MissingUserGroup, new UserInvitationResult());
|
|
}
|
|
|
|
UserOperationStatus validationResult = await ValidateUserCreateModel(model);
|
|
|
|
if (validationResult is not UserOperationStatus.Success)
|
|
{
|
|
return Attempt.FailWithStatus(validationResult, new UserInvitationResult());
|
|
}
|
|
|
|
Attempt<string?> authorizationAttempt = _userEditorAuthorizationHelper.IsAuthorized(
|
|
performingUser,
|
|
null,
|
|
null,
|
|
null,
|
|
userGroups.Select(x => x.Alias));
|
|
|
|
if (authorizationAttempt.Success is false)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.Unauthorized, new UserInvitationResult());
|
|
}
|
|
|
|
if (_inviteSender.CanSendInvites() is false)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.CannotInvite, new UserInvitationResult());
|
|
}
|
|
|
|
ICoreBackOfficeUserManager userManager = serviceScope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
|
|
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
IdentityCreationResult creationResult = await userManager.CreateForInvite(model);
|
|
if (creationResult.Succeded is false)
|
|
{
|
|
// If we fail from something in Identity we can't know exactly why, so we have to resolve to returning an unknown failure.
|
|
// But there should be more information in the message.
|
|
return Attempt.FailWithStatus(
|
|
UserOperationStatus.UnknownFailure,
|
|
new UserInvitationResult { Error = new ValidationResult(creationResult.ErrorMessage) });
|
|
}
|
|
|
|
IUser? invitedUser = await userStore.GetByEmailAsync(model.Email);
|
|
|
|
if (invitedUser is null)
|
|
{
|
|
// This really shouldn't happen, we literally just created the user
|
|
throw new PanicException("Was unable to get user after creating it");
|
|
}
|
|
|
|
invitedUser.InvitedDate = DateTime.Now;
|
|
invitedUser.ClearGroups();
|
|
foreach(IUserGroup userGroup in userGroups)
|
|
{
|
|
invitedUser.AddGroup(userGroup.ToReadOnlyGroup());
|
|
}
|
|
|
|
await userStore.SaveAsync(invitedUser);
|
|
|
|
Attempt<UserInvitationResult, UserOperationStatus> invitationAttempt = await SendInvitationAsync(performingUser, serviceScope, invitedUser, model.Message);
|
|
|
|
scope.Complete();
|
|
|
|
return invitationAttempt;
|
|
}
|
|
|
|
public async Task<Attempt<UserInvitationResult, UserOperationStatus>> ResendInvitationAsync(Guid performingUserKey, UserResendInviteModel model)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
using IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
|
|
IUser? performingUser = await GetAsync(performingUserKey);
|
|
if (performingUser == null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new UserInvitationResult());
|
|
}
|
|
|
|
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
IUser? invitedUser = await userStore.GetAsync(model.InvitedUserKey);
|
|
if (invitedUser == null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, new UserInvitationResult());
|
|
}
|
|
|
|
if (invitedUser.UserState != UserState.Invited)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.NotInInviteState, new UserInvitationResult());
|
|
}
|
|
|
|
// re-inviting so update invite date
|
|
invitedUser.InvitedDate = DateTime.Now;
|
|
await userStore.SaveAsync(invitedUser);
|
|
|
|
Attempt<UserInvitationResult, UserOperationStatus> invitationAttempt = await SendInvitationAsync(performingUser, serviceScope, invitedUser, model.Message);
|
|
scope.Complete();
|
|
|
|
return invitationAttempt;
|
|
}
|
|
|
|
private async Task<Attempt<UserInvitationResult, UserOperationStatus>> SendInvitationAsync(IUser performingUser, IServiceScope serviceScope, IUser invitedUser, string? message)
|
|
{
|
|
IInviteUriProvider inviteUriProvider = serviceScope.ServiceProvider.GetRequiredService<IInviteUriProvider>();
|
|
Attempt<Uri, UserOperationStatus> inviteUriAttempt = await inviteUriProvider.CreateInviteUriAsync(invitedUser);
|
|
if (inviteUriAttempt.Success is false)
|
|
{
|
|
return Attempt.FailWithStatus(inviteUriAttempt.Status, new UserInvitationResult());
|
|
}
|
|
|
|
var invitation = new UserInvitationMessage
|
|
{
|
|
InviteUri = inviteUriAttempt.Result,
|
|
Message = message ?? string.Empty,
|
|
Recipient = invitedUser,
|
|
Sender = performingUser,
|
|
};
|
|
await _inviteSender.InviteUser(invitation);
|
|
|
|
return Attempt.SucceedWithStatus(UserOperationStatus.Success, new UserInvitationResult { InvitedUser = invitedUser });
|
|
}
|
|
|
|
private async Task<UserOperationStatus> ValidateUserCreateModel(UserCreateModel model)
|
|
{
|
|
if (_securitySettings.UsernameIsEmail && model.UserName != model.Email)
|
|
{
|
|
return UserOperationStatus.UserNameIsNotEmail;
|
|
}
|
|
if (model.Email.IsEmail() is false)
|
|
{
|
|
return UserOperationStatus.InvalidEmail;
|
|
}
|
|
|
|
if (model.Id is not null && await GetAsync(model.Id.Value) is not null)
|
|
{
|
|
return UserOperationStatus.DuplicateId;
|
|
}
|
|
|
|
if (GetByEmail(model.Email) is not null)
|
|
{
|
|
return UserOperationStatus.DuplicateEmail;
|
|
}
|
|
|
|
if (GetByUsername(model.UserName) is not null)
|
|
{
|
|
return UserOperationStatus.DuplicateUserName;
|
|
}
|
|
|
|
if (model.UserGroupKeys.Count == 0)
|
|
{
|
|
return UserOperationStatus.NoUserGroup;
|
|
}
|
|
|
|
return UserOperationStatus.Success;
|
|
}
|
|
|
|
public async Task<Attempt<IUser?, UserOperationStatus>> UpdateAsync(Guid performingUserKey, UserUpdateModel model)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
using IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
IUser? existingUser = await userStore.GetAsync(model.ExistingUserKey);
|
|
|
|
if (existingUser is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, existingUser);
|
|
}
|
|
|
|
IUser? performingUser = await userStore.GetAsync(performingUserKey);
|
|
|
|
if (performingUser is null)
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus<IUser?, UserOperationStatus>(UserOperationStatus.MissingUser, existingUser);
|
|
}
|
|
|
|
// A user must remain assigned to at least one group.
|
|
if (model.UserGroupKeys.Count == 0)
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus<IUser?, UserOperationStatus>(UserOperationStatus.NoUserGroup, existingUser);
|
|
}
|
|
|
|
// User names can only contain the configured allowed characters. This is validated by ASP.NET Identity on create
|
|
// as the setting is applied to the BackOfficeIdentityOptions, but we need to check ourselves for updates.
|
|
var allowedUserNameCharacters = _securitySettings.AllowedUserNameCharacters;
|
|
if (model.UserName.Any(c => allowedUserNameCharacters.Contains(c) == false))
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus<IUser?, UserOperationStatus>(UserOperationStatus.InvalidUserName, existingUser);
|
|
}
|
|
|
|
IEnumerable<IUserGroup> allUserGroups = _userGroupRepository.GetMany().ToArray();
|
|
var userGroups = allUserGroups.Where(x => model.UserGroupKeys.Contains(x.Key)).ToHashSet();
|
|
|
|
if (userGroups.Count != model.UserGroupKeys.Count)
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus<IUser?, UserOperationStatus>(UserOperationStatus.MissingUserGroup, existingUser);
|
|
}
|
|
|
|
// We're de-admining a user, we need to ensure that this would not leave the admin group empty.
|
|
if (existingUser.IsAdmin() && model.UserGroupKeys.Contains(Constants.Security.AdminGroupKey) is false)
|
|
{
|
|
IUserGroup? adminGroup = allUserGroups.FirstOrDefault(x => x.Key == Constants.Security.AdminGroupKey);
|
|
if (adminGroup?.UserCount == 1)
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus<IUser?, UserOperationStatus>(UserOperationStatus.AdminUserGroupMustNotBeEmpty, existingUser);
|
|
}
|
|
}
|
|
|
|
// We have to resolve the keys to ids to be compatible with the repository, this could be done in the factory,
|
|
// but I'd rather keep the ids out of the service API as much as possible.
|
|
List<int>? startContentIds = GetIdsFromKeys(model.ContentStartNodeKeys, UmbracoObjectTypes.Document);
|
|
|
|
if (startContentIds is null || startContentIds.Count != model.ContentStartNodeKeys.Count)
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus<IUser?, UserOperationStatus>(UserOperationStatus.ContentStartNodeNotFound, existingUser);
|
|
}
|
|
|
|
List<int>? startMediaIds = GetIdsFromKeys(model.MediaStartNodeKeys, UmbracoObjectTypes.Media);
|
|
|
|
if (startMediaIds is null || startMediaIds.Count != model.MediaStartNodeKeys.Count)
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus<IUser?, UserOperationStatus>(UserOperationStatus.MediaStartNodeNotFound, existingUser);
|
|
}
|
|
|
|
if (model.HasContentRootAccess)
|
|
{
|
|
startContentIds.Add(Constants.System.Root);
|
|
}
|
|
|
|
if (model.HasMediaRootAccess)
|
|
{
|
|
startMediaIds.Add(Constants.System.Root);
|
|
}
|
|
|
|
Attempt<string?> isAuthorized = _userEditorAuthorizationHelper.IsAuthorized(
|
|
performingUser,
|
|
existingUser,
|
|
startContentIds,
|
|
startMediaIds,
|
|
userGroups.Select(x => x.Alias));
|
|
|
|
if (isAuthorized.Success is false)
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus<IUser?, UserOperationStatus>(UserOperationStatus.Unauthorized, existingUser);
|
|
}
|
|
|
|
UserOperationStatus validationStatus = ValidateUserUpdateModel(existingUser, model);
|
|
if (validationStatus is not UserOperationStatus.Success)
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus<IUser?, UserOperationStatus>(validationStatus, existingUser);
|
|
}
|
|
|
|
// Now that we're all authorized and validated we can actually map over changes and update the user
|
|
// TODO: This probably shouldn't live here, once we have user content start nodes as keys this can be moved to a mapper
|
|
// Alternatively it should be a map definition, but then we need to use entity service to resolve the IDs
|
|
// TODO: Add auditing
|
|
IUser updated = MapUserUpdate(model, userGroups, existingUser, startContentIds, startMediaIds);
|
|
UserOperationStatus saveStatus = await userStore.SaveAsync(updated);
|
|
|
|
if (saveStatus is not UserOperationStatus.Success)
|
|
{
|
|
return Attempt.FailWithStatus<IUser?, UserOperationStatus>(saveStatus, existingUser);
|
|
}
|
|
|
|
scope.Complete();
|
|
return Attempt.SucceedWithStatus<IUser?, UserOperationStatus>(UserOperationStatus.Success, updated);
|
|
|
|
}
|
|
|
|
public async Task<UserOperationStatus> SetAvatarAsync(Guid userKey, Guid temporaryFileKey)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
|
|
IUser? user = await GetAsync(userKey);
|
|
if (user is null)
|
|
{
|
|
return UserOperationStatus.UserNotFound;
|
|
}
|
|
|
|
TemporaryFileModel? avatarTemporaryFile = await _temporaryFileService.GetAsync(temporaryFileKey);
|
|
_temporaryFileService.EnlistDeleteIfScopeCompletes(temporaryFileKey, ScopeProvider);
|
|
|
|
if (avatarTemporaryFile is null)
|
|
{
|
|
return UserOperationStatus.AvatarFileNotFound;
|
|
}
|
|
|
|
const string allowedAvatarFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp";
|
|
|
|
// This shouldn't really be necessary since we're just gonna use it to generate a hash, but that's how it was.
|
|
var avatarFileName = avatarTemporaryFile.FileName.ToSafeFileName(_shortStringHelper);
|
|
var extension = Path.GetExtension(avatarFileName)[1..];
|
|
if(allowedAvatarFileTypes.Contains(extension) is false || _contentSettings.DisallowedUploadedFileExtensions.Contains(extension))
|
|
{
|
|
return UserOperationStatus.InvalidAvatar;
|
|
}
|
|
|
|
// Generate a path from known data, we don't want this to be guessable
|
|
var avatarHash = $"{user.Key}{avatarFileName}".GenerateHash<SHA1>();
|
|
var avatarPath = $"UserAvatars/{avatarHash}.{extension}";
|
|
|
|
await using (Stream fileStream = avatarTemporaryFile.OpenReadStream())
|
|
{
|
|
_mediaFileManager.FileSystem.AddFile(avatarPath, fileStream, true);
|
|
}
|
|
|
|
user.Avatar = avatarPath;
|
|
await SaveAsync(user);
|
|
|
|
scope.Complete();
|
|
return UserOperationStatus.Success;
|
|
}
|
|
|
|
private IUser MapUserUpdate(
|
|
UserUpdateModel source,
|
|
ISet<IUserGroup> sourceUserGroups,
|
|
IUser target,
|
|
List<int> startContentIds,
|
|
List<int> startMediaIds)
|
|
{
|
|
target.Name = source.Name;
|
|
target.Language = source.LanguageIsoCode;
|
|
target.Email = source.Email;
|
|
target.Username = source.UserName;
|
|
target.StartContentIds = startContentIds.ToArray();
|
|
target.StartMediaIds = startMediaIds.ToArray();
|
|
|
|
target.ClearGroups();
|
|
foreach (IUserGroup group in sourceUserGroups)
|
|
{
|
|
target.AddGroup(group.ToReadOnlyGroup());
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
private UserOperationStatus ValidateUserUpdateModel(IUser existingUser, UserUpdateModel model)
|
|
{
|
|
if (_isoCodeValidator.IsValid(model.LanguageIsoCode) is false)
|
|
{
|
|
return UserOperationStatus.InvalidIsoCode;
|
|
}
|
|
|
|
// We need to check if there's any Deny Local login providers present, if so we need to ensure that the user's email address cannot be changed.
|
|
if (_localLoginSettingProvider.HasDenyLocalLogin() && model.Email != existingUser.Email)
|
|
{
|
|
return UserOperationStatus.EmailCannotBeChanged;
|
|
}
|
|
|
|
if (_securitySettings.UsernameIsEmail && model.UserName != model.Email)
|
|
{
|
|
return UserOperationStatus.UserNameIsNotEmail;
|
|
}
|
|
|
|
if (model.Email.IsEmail() is false)
|
|
{
|
|
return UserOperationStatus.InvalidEmail;
|
|
}
|
|
|
|
IUser? existing = GetByEmail(model.Email);
|
|
if (existing is not null && existing.Key != existingUser.Key)
|
|
{
|
|
return UserOperationStatus.DuplicateEmail;
|
|
}
|
|
|
|
// In case the user has updated their username to be a different email, but not their actually email
|
|
// we have to try and get the user by email using their username, and ensure we don't get any collisions.
|
|
existing = GetByEmail(model.UserName);
|
|
if (existing is not null && existing.Key != existingUser.Key)
|
|
{
|
|
return UserOperationStatus.DuplicateUserName;
|
|
}
|
|
|
|
existing = GetByUsername(model.UserName);
|
|
if (existing is not null && existing.Key != existingUser.Key)
|
|
{
|
|
return UserOperationStatus.DuplicateUserName;
|
|
}
|
|
|
|
return UserOperationStatus.Success;
|
|
}
|
|
|
|
private List<int>? GetIdsFromKeys(IEnumerable<Guid>? guids, UmbracoObjectTypes type)
|
|
{
|
|
var keys = guids?
|
|
.Select(x => _entityService.GetId(x, type))
|
|
.Where(x => x.Success)
|
|
.Select(x => x.Result)
|
|
.ToList();
|
|
|
|
return keys;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<Attempt<PasswordChangedModel, UserOperationStatus>> ChangePasswordAsync(Guid performingUserKey, ChangeUserPasswordModel model)
|
|
{
|
|
IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
IUser? performingUser = await userStore.GetAsync(performingUserKey);
|
|
if (performingUser is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new PasswordChangedModel());
|
|
}
|
|
|
|
return await ChangePasswordAsync(performingUser, model);
|
|
}
|
|
|
|
private async Task<Attempt<PasswordChangedModel, UserOperationStatus>> ChangePasswordAsync(IUser performingUser, ChangeUserPasswordModel model)
|
|
{
|
|
IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
IUser? user = await userStore.GetAsync(model.UserKey);
|
|
if (user is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, new PasswordChangedModel());
|
|
}
|
|
|
|
if (user.Kind != UserKind.Default)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.InvalidUserType, new PasswordChangedModel());
|
|
}
|
|
|
|
// require old password for self change when outside of invite or resetByToken flows
|
|
if (performingUser.UserState != UserState.Invited && performingUser.Username == user.Username && string.IsNullOrEmpty(model.OldPassword) && string.IsNullOrEmpty(model.ResetPasswordToken))
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.SelfOldPasswordRequired, new PasswordChangedModel());
|
|
}
|
|
|
|
if (performingUser.IsAdmin() is false && user.IsAdmin())
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.Forbidden, new PasswordChangedModel());
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(model.ResetPasswordToken) is false)
|
|
{
|
|
Attempt<UserOperationStatus> verifyPasswordResetAsync = await VerifyPasswordResetAsync(model.UserKey, model.ResetPasswordToken);
|
|
if (verifyPasswordResetAsync.Result != UserOperationStatus.Success)
|
|
{
|
|
return Attempt.FailWithStatus(verifyPasswordResetAsync.Result, new PasswordChangedModel());
|
|
}
|
|
}
|
|
|
|
IBackOfficePasswordChanger passwordChanger = serviceScope.ServiceProvider.GetRequiredService<IBackOfficePasswordChanger>();
|
|
Attempt<PasswordChangedModel?> result = await passwordChanger.ChangeBackOfficePassword(
|
|
new ChangeBackOfficeUserPasswordModel
|
|
{
|
|
NewPassword = model.NewPassword,
|
|
OldPassword = model.OldPassword,
|
|
User = user,
|
|
ResetPasswordToken = model.ResetPasswordToken,
|
|
},
|
|
performingUser);
|
|
|
|
if (result.Success is false)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.UnknownFailure, result.Result ?? new PasswordChangedModel());
|
|
}
|
|
|
|
scope.Complete();
|
|
return Attempt.SucceedWithStatus(UserOperationStatus.Success, result.Result ?? new PasswordChangedModel());
|
|
}
|
|
|
|
public async Task<Attempt<PagedModel<IUser>?, UserOperationStatus>> GetAllAsync(Guid performingUserKey, int skip, int take)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
|
|
IUser? requestingUser = await GetAsync(performingUserKey);
|
|
|
|
if (requestingUser is null)
|
|
{
|
|
return Attempt.FailWithStatus<PagedModel<IUser>?, UserOperationStatus>(UserOperationStatus.MissingUser, null);
|
|
}
|
|
|
|
UserFilter baseFilter = CreateBaseUserFilter(requestingUser, out IQuery<IUser> query);
|
|
|
|
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out long pageNumber, out int pageSize);
|
|
|
|
HashSet<string> excludeUserGroupAliases = new();
|
|
if (baseFilter.ExcludeUserGroups is not null)
|
|
{
|
|
Attempt<IEnumerable<string>, UserOperationStatus> userGroupKeyConversionAttempt =
|
|
GetUserGroupAliasesFromKeys(baseFilter.ExcludeUserGroups);
|
|
|
|
|
|
if (userGroupKeyConversionAttempt.Success is false)
|
|
{
|
|
return Attempt.FailWithStatus<PagedModel<IUser>?, UserOperationStatus>(UserOperationStatus.MissingUserGroup, null);
|
|
}
|
|
|
|
excludeUserGroupAliases = new HashSet<string>(userGroupKeyConversionAttempt.Result);
|
|
}
|
|
|
|
IEnumerable<IUser> result = _userRepository.GetPagedResultsByQuery(
|
|
null,
|
|
pageNumber,
|
|
pageSize,
|
|
out long totalRecords,
|
|
x => x.Username,
|
|
excludeUserGroups: excludeUserGroupAliases.ToArray(),
|
|
filter: query,
|
|
userState: baseFilter.IncludeUserStates?.ToArray());
|
|
|
|
var pagedResult = new PagedModel<IUser> { Items = result, Total = totalRecords };
|
|
|
|
scope.Complete();
|
|
return Attempt.SucceedWithStatus<PagedModel<IUser>?, UserOperationStatus>(UserOperationStatus.Success, pagedResult);
|
|
}
|
|
|
|
public async Task<Attempt<PagedModel<IUser>, UserOperationStatus>> FilterAsync(
|
|
Guid userKey,
|
|
UserFilter filter,
|
|
int skip = 0,
|
|
int take = 100,
|
|
UserOrder orderBy = UserOrder.UserName,
|
|
Direction orderDirection = Direction.Ascending)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
|
|
IUser? requestingUser = await GetAsync(userKey);
|
|
|
|
if (requestingUser is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new PagedModel<IUser>());
|
|
}
|
|
|
|
UserFilter baseFilter = CreateBaseUserFilter(requestingUser, out IQuery<IUser> baseQuery);
|
|
|
|
UserFilter mergedFilter = filter.Merge(baseFilter);
|
|
|
|
// TODO: We should have a repository method that accepts keys so we don't have to do this conversion
|
|
HashSet<string>? excludedUserGroupAliases = null;
|
|
if (mergedFilter.ExcludeUserGroups is not null)
|
|
{
|
|
Attempt<IEnumerable<string>, UserOperationStatus> userGroupKeyConversionAttempt =
|
|
GetUserGroupAliasesFromKeys(mergedFilter.ExcludeUserGroups);
|
|
|
|
|
|
if (userGroupKeyConversionAttempt.Success is false)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MissingUserGroup, new PagedModel<IUser>());
|
|
}
|
|
|
|
excludedUserGroupAliases = new HashSet<string>(userGroupKeyConversionAttempt.Result);
|
|
}
|
|
|
|
string[]? includedUserGroupAliases = null;
|
|
if (mergedFilter.IncludedUserGroups is not null)
|
|
{
|
|
Attempt<IEnumerable<string>, UserOperationStatus> userGroupKeyConversionAttempt = GetUserGroupAliasesFromKeys(mergedFilter.IncludedUserGroups);
|
|
|
|
if (userGroupKeyConversionAttempt.Success is false)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MissingUserGroup, new PagedModel<IUser>());
|
|
}
|
|
|
|
includedUserGroupAliases = userGroupKeyConversionAttempt.Result.ToArray();
|
|
}
|
|
|
|
if (mergedFilter.NameFilters is not null)
|
|
{
|
|
foreach (var nameFilter in mergedFilter.NameFilters)
|
|
{
|
|
baseQuery.Where(x => x.Name!.Contains(nameFilter) || x.Username.Contains(nameFilter));
|
|
}
|
|
}
|
|
|
|
ISet<UserState>? includeUserStates = null;
|
|
|
|
// The issue is that this is a limiting filter we have to ensure that it still follows our rules
|
|
// So I'm not allowed to ask for the disabled users if the setting has been flipped
|
|
if (baseFilter.IncludeUserStates is null || baseFilter.IncludeUserStates.Count == 0)
|
|
{
|
|
includeUserStates = filter.IncludeUserStates;
|
|
}
|
|
else
|
|
{
|
|
includeUserStates = new HashSet<UserState>(baseFilter.IncludeUserStates);
|
|
if (filter.IncludeUserStates is not null && filter.IncludeUserStates.Contains(UserState.All) is false)
|
|
{
|
|
includeUserStates.IntersectWith(filter.IncludeUserStates);
|
|
}
|
|
|
|
// This means that we've only chosen to include a user state that is not allowed, so we'll return an empty result
|
|
if (includeUserStates.Count == 0)
|
|
{
|
|
return Attempt.SucceedWithStatus(UserOperationStatus.Success, new PagedModel<IUser>());
|
|
}
|
|
}
|
|
|
|
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out long pageNumber, out int pageSize);
|
|
Expression<Func<IUser, object?>> orderByExpression = GetOrderByExpression(orderBy);
|
|
|
|
// TODO: We should create a Query method on the repo that allows to filter by aliases.
|
|
IEnumerable<IUser> result = _userRepository.GetPagedResultsByQuery(
|
|
null,
|
|
pageNumber,
|
|
pageSize,
|
|
out long totalRecords,
|
|
orderByExpression,
|
|
orderDirection,
|
|
includedUserGroupAliases?.ToArray(),
|
|
excludedUserGroupAliases?.ToArray(),
|
|
includeUserStates?.ToArray(),
|
|
baseQuery);
|
|
|
|
scope.Complete();
|
|
|
|
var model = new PagedModel<IUser> { Items = result, Total = totalRecords };
|
|
|
|
return Attempt.SucceedWithStatus(UserOperationStatus.Success, model);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a base user filter which ensures our rules are followed, I.E. Only admins can se other admins.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// We return the query as an out parameter instead of having it in the intermediate object because a two queries cannot be merged into one.
|
|
/// </remarks>
|
|
/// <returns></returns>
|
|
private UserFilter CreateBaseUserFilter(IUser performingUser, out IQuery<IUser> baseQuery)
|
|
{
|
|
var filter = new UserFilter();
|
|
baseQuery = Query<IUser>();
|
|
|
|
// Only super can see super
|
|
if (performingUser.IsSuper() is false)
|
|
{
|
|
baseQuery.Where(x => x.Key != Constants.Security.SuperUserKey);
|
|
}
|
|
|
|
// Only admins can see admins
|
|
if (performingUser.IsAdmin() is false)
|
|
{
|
|
filter.ExcludeUserGroups = new HashSet<Guid> { Constants.Security.AdminGroupKey };
|
|
}
|
|
|
|
if (_securitySettings.HideDisabledUsersInBackOffice)
|
|
{
|
|
filter.IncludeUserStates = new HashSet<UserState> { UserState.Active, UserState.Invited, UserState.LockedOut, UserState.Inactive };
|
|
}
|
|
|
|
return filter;
|
|
}
|
|
|
|
private Attempt<IEnumerable<string>, UserOperationStatus> GetUserGroupAliasesFromKeys(IEnumerable<Guid> userGroupKeys)
|
|
{
|
|
var aliases = new List<string>();
|
|
|
|
foreach (Guid key in userGroupKeys)
|
|
{
|
|
IUserGroup? group = _userGroupRepository.Get(Query<IUserGroup>().Where(x => x.Key == key)).FirstOrDefault();
|
|
if (group is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MissingUserGroup, Enumerable.Empty<string>());
|
|
}
|
|
|
|
aliases.Add(group.Alias);
|
|
}
|
|
|
|
return Attempt.SucceedWithStatus<IEnumerable<string>, UserOperationStatus>(UserOperationStatus.Success, aliases);
|
|
}
|
|
|
|
private Expression<Func<IUser, object?>> GetOrderByExpression(UserOrder orderBy)
|
|
{
|
|
return orderBy switch
|
|
{
|
|
UserOrder.UserName => x => x.Username,
|
|
UserOrder.Language => x => x.Language,
|
|
UserOrder.Name => x => x.Name,
|
|
UserOrder.Email => x => x.Email,
|
|
UserOrder.Id => x => x.Id,
|
|
UserOrder.CreateDate => x => x.CreateDate,
|
|
UserOrder.UpdateDate => x => x.UpdateDate,
|
|
UserOrder.IsApproved => x => x.IsApproved,
|
|
UserOrder.IsLockedOut => x => x.IsLockedOut,
|
|
UserOrder.LastLoginDate => x => x.LastLoginDate,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(orderBy), orderBy, null)
|
|
};
|
|
}
|
|
|
|
public async Task<UserOperationStatus> DeleteAsync(Guid performingUserKey, ISet<Guid> keys)
|
|
{
|
|
if(keys.Any() is false)
|
|
{
|
|
return UserOperationStatus.Success;
|
|
}
|
|
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
IUser? performingUser = await GetAsync(performingUserKey);
|
|
|
|
if (performingUser is null)
|
|
{
|
|
return UserOperationStatus.MissingUser;
|
|
}
|
|
|
|
if (keys.Contains(performingUser.Key))
|
|
{
|
|
return UserOperationStatus.CannotDeleteSelf;
|
|
}
|
|
|
|
IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
IUser[] usersToDisable = (await userStore.GetUsersAsync(keys.ToArray())).ToArray();
|
|
|
|
if (usersToDisable.Length != keys.Count)
|
|
{
|
|
return UserOperationStatus.UserNotFound;
|
|
}
|
|
|
|
foreach (IUser user in usersToDisable)
|
|
{
|
|
// Check user hasn't logged in. If they have they may have made content changes which will mean
|
|
// the Id is associated with audit trails, versions etc. and can't be removed.
|
|
if (user.LastLoginDate is not null && user.LastLoginDate != default(DateTime))
|
|
{
|
|
return UserOperationStatus.CannotDelete;
|
|
}
|
|
|
|
user.IsApproved = false;
|
|
user.InvitedDate = null;
|
|
|
|
Delete(user, true);
|
|
}
|
|
|
|
scope.Complete();
|
|
return UserOperationStatus.Success;
|
|
}
|
|
|
|
public async Task<UserOperationStatus> DisableAsync(Guid performingUserKey, ISet<Guid> keys)
|
|
{
|
|
if(keys.Any() is false)
|
|
{
|
|
return UserOperationStatus.Success;
|
|
}
|
|
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
IUser? performingUser = await GetAsync(performingUserKey);
|
|
|
|
if (performingUser is null)
|
|
{
|
|
return UserOperationStatus.MissingUser;
|
|
}
|
|
|
|
if (keys.Contains(performingUser.Key))
|
|
{
|
|
return UserOperationStatus.CannotDisableSelf;
|
|
}
|
|
|
|
IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
IUser[] usersToDisable = (await userStore.GetUsersAsync(keys.ToArray())).ToArray();
|
|
|
|
if (usersToDisable.Length != keys.Count)
|
|
{
|
|
return UserOperationStatus.UserNotFound;
|
|
}
|
|
|
|
foreach (IUser user in usersToDisable)
|
|
{
|
|
if (user.UserState is UserState.Invited)
|
|
{
|
|
return UserOperationStatus.CannotDisableInvitedUser;
|
|
}
|
|
|
|
user.IsApproved = false;
|
|
user.InvitedDate = null;
|
|
}
|
|
|
|
Save(usersToDisable);
|
|
|
|
scope.Complete();
|
|
return UserOperationStatus.Success;
|
|
}
|
|
|
|
public async Task<UserOperationStatus> EnableAsync(Guid performingUserKey, ISet<Guid> keys)
|
|
{
|
|
if(keys.Any() is false)
|
|
{
|
|
return UserOperationStatus.Success;
|
|
}
|
|
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
IUser? performingUser = await GetAsync(performingUserKey);
|
|
|
|
if (performingUser is null)
|
|
{
|
|
return UserOperationStatus.MissingUser;
|
|
}
|
|
|
|
IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
IUser[] usersToEnable = (await userStore.GetUsersAsync(keys.ToArray())).ToArray();
|
|
|
|
if (usersToEnable.Length != keys.Count)
|
|
{
|
|
return UserOperationStatus.UserNotFound;
|
|
}
|
|
|
|
foreach (IUser user in usersToEnable)
|
|
{
|
|
user.IsApproved = true;
|
|
}
|
|
|
|
Save(usersToEnable);
|
|
|
|
scope.Complete();
|
|
return UserOperationStatus.Success;
|
|
}
|
|
|
|
public async Task<UserOperationStatus> ClearAvatarAsync(Guid userKey)
|
|
{
|
|
IUser? user = await GetAsync(userKey);
|
|
|
|
if (user is null)
|
|
{
|
|
return UserOperationStatus.UserNotFound;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(user.Avatar))
|
|
{
|
|
// Nothing to do
|
|
return UserOperationStatus.Success;
|
|
}
|
|
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
string filePath = user.Avatar;
|
|
user.Avatar = null;
|
|
UserOperationStatus result = await backOfficeUserStore.SaveAsync(user);
|
|
|
|
if (result is not UserOperationStatus.Success)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
if (_mediaFileManager.FileSystem.FileExists(filePath))
|
|
{
|
|
_mediaFileManager.FileSystem.DeleteFile(filePath);
|
|
}
|
|
|
|
return UserOperationStatus.Success;
|
|
}
|
|
|
|
public async Task<Attempt<UserUnlockResult, UserOperationStatus>> UnlockAsync(Guid performingUserKey, params Guid[] keys)
|
|
{
|
|
if (keys.Length == 0)
|
|
{
|
|
return Attempt.SucceedWithStatus(UserOperationStatus.Success, new UserUnlockResult());
|
|
}
|
|
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
IUser? performingUser = await GetAsync(performingUserKey);
|
|
|
|
if (performingUser is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new UserUnlockResult());
|
|
}
|
|
|
|
IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
ICoreBackOfficeUserManager manager = serviceScope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
|
|
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
IEnumerable<IUser> usersToUnlock = await userStore.GetUsersAsync(keys);
|
|
|
|
foreach (IUser user in usersToUnlock)
|
|
{
|
|
Attempt<UserUnlockResult, UserOperationStatus> result = await manager.UnlockUser(user);
|
|
if (result.Success is false)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.UnknownFailure, result.Result);
|
|
}
|
|
}
|
|
|
|
scope.Complete();
|
|
return Attempt.SucceedWithStatus(UserOperationStatus.Success, new UserUnlockResult());
|
|
}
|
|
|
|
public IEnumerable<IUser> GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState[]? userState = null, string[]? userGroups = null, string? filter = null)
|
|
{
|
|
IQuery<IUser>? filterQuery = null;
|
|
if (filter.IsNullOrWhiteSpace() == false)
|
|
{
|
|
filterQuery = Query<IUser>()?.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<IUser> GetAll(
|
|
long pageIndex,
|
|
int pageSize,
|
|
out long totalRecords,
|
|
string orderBy,
|
|
Direction orderDirection,
|
|
UserState[]? userState = null,
|
|
string[]? includeUserGroups = null,
|
|
string[]? excludeUserGroups = null,
|
|
IQuery<IUser>? filter = null)
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
Expression<Func<IUser, object?>> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a list of paged <see cref="IUser" /> objects
|
|
/// </summary>
|
|
/// <param name="pageIndex">Current page index</param>
|
|
/// <param name="pageSize">Size of the page</param>
|
|
/// <param name="totalRecords">Total number of records found (out)</param>
|
|
/// <returns>
|
|
/// <see cref="IEnumerable{IMember}" />
|
|
/// </returns>
|
|
public IEnumerable<IUser> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a list of <see cref="IUser" /> objects associated with a given group
|
|
/// </summary>
|
|
/// <param name="groupId">Id of group</param>
|
|
/// <returns>
|
|
/// <see cref="IEnumerable{IUser}" />
|
|
/// </returns>
|
|
public IEnumerable<IUser> GetAllInGroup(int? groupId)
|
|
{
|
|
if (groupId is null)
|
|
{
|
|
return Array.Empty<IUser>();
|
|
}
|
|
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
return backOfficeUserStore.GetAllInGroupAsync(groupId.Value).GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a list of <see cref="IUser" /> objects not associated with a given group
|
|
/// </summary>
|
|
/// <param name="groupId">Id of group</param>
|
|
/// <returns>
|
|
/// <see cref="IEnumerable{IUser}" />
|
|
/// </returns>
|
|
public IEnumerable<IUser> GetAllNotInGroup(int groupId)
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
|
{
|
|
return _userRepository.GetAllNotInGroup(groupId);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Implementation of IUserService
|
|
|
|
/// <summary>
|
|
/// Gets an IProfile by User Id.
|
|
/// </summary>
|
|
/// <param name="id">Id of the User to retrieve</param>
|
|
/// <returns>
|
|
/// <see cref="IProfile" />
|
|
/// </returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a profile by username
|
|
/// </summary>
|
|
/// <param name="username">Username</param>
|
|
/// <returns>
|
|
/// <see cref="IProfile" />
|
|
/// </returns>
|
|
public IProfile? GetProfileByUserName(string username)
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
return _userRepository.GetProfile(username);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a user by Id
|
|
/// </summary>
|
|
/// <param name="id">Id of the user to retrieve.</param>
|
|
/// <returns>
|
|
/// <see cref="IUser" />
|
|
/// </returns>
|
|
public IUser? GetUserById(int id)
|
|
{
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
return backOfficeUserStore.GetAsync(id).GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a user by it's key.
|
|
/// </summary>
|
|
/// <param name="key">Key of the user to retrieve.</param>
|
|
/// <returns>Task resolving into an <see cref="IUser"/>.</returns>
|
|
public Task<IUser?> GetAsync(Guid key)
|
|
{
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
return backOfficeUserStore.GetAsync(key);
|
|
}
|
|
|
|
public Task<IEnumerable<IUser>> GetAsync(IEnumerable<Guid> keys)
|
|
{
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
return backOfficeUserStore.GetUsersAsync(keys.ToArray());
|
|
}
|
|
|
|
public async Task<Attempt<ICollection<IIdentityUserLogin>, UserOperationStatus>> GetLinkedLoginsAsync(Guid userKey)
|
|
{
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
IUser? user = await backOfficeUserStore.GetAsync(userKey);
|
|
if (user is null)
|
|
{
|
|
return Attempt.FailWithStatus<ICollection<IIdentityUserLogin>, UserOperationStatus>(UserOperationStatus.UserNotFound, Array.Empty<IIdentityUserLogin>());
|
|
}
|
|
|
|
ICoreBackOfficeUserManager manager = scope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
|
|
|
|
Attempt<ICollection<IIdentityUserLogin>, UserOperationStatus> loginsAttempt = await manager.GetLoginsAsync(user);
|
|
|
|
return loginsAttempt.Success is false
|
|
? Attempt.FailWithStatus<ICollection<IIdentityUserLogin>, UserOperationStatus>(loginsAttempt.Status, Array.Empty<IIdentityUserLogin>())
|
|
: Attempt.SucceedWithStatus(UserOperationStatus.Success, loginsAttempt.Result);
|
|
}
|
|
|
|
public IEnumerable<IUser> GetUsersById(params int[]? ids)
|
|
{
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
IBackOfficeUserStore backOfficeUserStore = scope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
return backOfficeUserStore.GetUsersAsync(ids).GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replaces the same permission set for a single group to any number of entities
|
|
/// </summary>
|
|
/// <remarks>If no 'entityIds' are specified all permissions will be removed for the specified group.</remarks>
|
|
/// <param name="groupId">Id of the group</param>
|
|
/// <param name="permissions">
|
|
/// Permissions as enumerable list of <see cref="char" /> If nothing is specified all permissions
|
|
/// are removed.
|
|
/// </param>
|
|
/// <param name="entityIds">Specify the nodes to replace permissions for. </param>
|
|
public void ReplaceUserGroupPermissions(int groupId, ISet<string> 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();
|
|
|
|
if (permissions is not null)
|
|
{
|
|
EntityPermission[] entityPermissions =
|
|
entityIds.Select(x => new EntityPermission(groupId, x, permissions)).ToArray();
|
|
scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns the same permission set for a single user group to any number of entities
|
|
/// </summary>
|
|
/// <param name="groupId">Id of the user group</param>
|
|
/// <param name="permission"></param>
|
|
/// <param name="entityIds">Specify the nodes to replace permissions for</param>
|
|
public void AssignUserGroupPermission(int groupId, string 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 HashSet<string>() { permission };
|
|
EntityPermission[] entityPermissions =
|
|
entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray();
|
|
scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs));
|
|
}
|
|
}
|
|
|
|
public async Task<Attempt<UserOperationStatus>> VerifyPasswordResetAsync(Guid userKey, string token)
|
|
{
|
|
var decoded = token.FromUrlBase64();
|
|
|
|
if (decoded is null)
|
|
{
|
|
return Attempt.Fail(UserOperationStatus.InvalidPasswordResetToken);
|
|
}
|
|
|
|
IUser? user = await GetAsync(userKey);
|
|
|
|
if (user is null)
|
|
{
|
|
return Attempt.Fail(UserOperationStatus.UserNotFound);
|
|
}
|
|
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
ICoreBackOfficeUserManager backOfficeUserManager = scope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
|
|
|
|
var isValid = await backOfficeUserManager.IsResetPasswordTokenValidAsync(user, decoded);
|
|
|
|
return isValid
|
|
? Attempt.Succeed(UserOperationStatus.Success)
|
|
: Attempt.Fail(UserOperationStatus.InvalidPasswordResetToken);
|
|
}
|
|
|
|
public async Task<Attempt<UserOperationStatus>> VerifyInviteAsync(Guid userKey, string token)
|
|
{
|
|
var decoded = token.FromUrlBase64();
|
|
|
|
if (decoded is null)
|
|
{
|
|
return Attempt.Fail(UserOperationStatus.InvalidInviteToken);
|
|
}
|
|
|
|
IUser? user = await GetAsync(userKey);
|
|
|
|
if (user is null)
|
|
{
|
|
return Attempt.Fail(UserOperationStatus.UserNotFound);
|
|
}
|
|
|
|
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
|
ICoreBackOfficeUserManager backOfficeUserManager = scope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
|
|
|
|
var isValid = await backOfficeUserManager.IsEmailConfirmationTokenValidAsync(user, decoded);
|
|
|
|
return isValid
|
|
? Attempt.Succeed(UserOperationStatus.Success)
|
|
: Attempt.Fail(UserOperationStatus.InvalidInviteToken);
|
|
}
|
|
|
|
public async Task<Attempt<PasswordChangedModel, UserOperationStatus>> CreateInitialPasswordAsync(Guid userKey, string token, string password)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
|
|
Attempt<UserOperationStatus> verifyInviteAttempt = await VerifyInviteAsync(userKey, token);
|
|
if (verifyInviteAttempt.Result != UserOperationStatus.Success)
|
|
{
|
|
return Attempt.FailWithStatus(verifyInviteAttempt.Result, new PasswordChangedModel());
|
|
}
|
|
|
|
Attempt<PasswordChangedModel, UserOperationStatus> changePasswordAttempt = await ChangePasswordAsync(userKey, new ChangeUserPasswordModel() { NewPassword = password, UserKey = userKey });
|
|
|
|
Task<UserOperationStatus> enableAttempt = EnableAsync(userKey, new HashSet<Guid>() { userKey });
|
|
|
|
if (enableAttempt.Result != UserOperationStatus.Success)
|
|
{
|
|
return Attempt.FailWithStatus(enableAttempt.Result, new PasswordChangedModel());
|
|
}
|
|
|
|
scope.Complete();
|
|
return changePasswordAttempt;
|
|
}
|
|
|
|
public async Task<Attempt<PasswordChangedModel, UserOperationStatus>> ResetPasswordAsync(Guid userKey, string token, string password)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
|
|
EventMessages evtMsgs = EventMessagesFactory.Get();
|
|
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
|
|
|
IUser? user = await userStore.GetAsync(userKey);
|
|
if (user is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, new PasswordChangedModel());
|
|
}
|
|
|
|
var savingNotification = new UserPasswordResettingNotification(user, evtMsgs);
|
|
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus(UserOperationStatus.CancelledByNotification, new PasswordChangedModel());
|
|
}
|
|
|
|
Attempt<PasswordChangedModel, UserOperationStatus> changePasswordAttempt =
|
|
await ChangePasswordAsync(user, new ChangeUserPasswordModel
|
|
{
|
|
NewPassword = password,
|
|
UserKey = userKey,
|
|
ResetPasswordToken = token
|
|
});
|
|
|
|
scope.Complete();
|
|
return changePasswordAttempt;
|
|
}
|
|
|
|
public async Task<Attempt<PasswordChangedModel, UserOperationStatus>> ResetPasswordAsync(Guid performingUserKey, Guid userKey)
|
|
{
|
|
if (performingUserKey.Equals(userKey))
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.SelfPasswordResetNotAllowed, new PasswordChangedModel());
|
|
}
|
|
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
using IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
|
|
|
ICoreBackOfficeUserManager backOfficeUserManager = serviceScope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
|
|
|
|
var generatedPassword = backOfficeUserManager.GeneratePassword();
|
|
|
|
Attempt<PasswordChangedModel, UserOperationStatus> changePasswordAttempt =
|
|
await ChangePasswordAsync(performingUserKey, new ChangeUserPasswordModel
|
|
{
|
|
NewPassword = generatedPassword,
|
|
UserKey = userKey,
|
|
});
|
|
|
|
scope.Complete();
|
|
|
|
// todo tidy this up
|
|
// this should be part of the result of the ChangePasswordAsync() method
|
|
// but the model requires NewPassword
|
|
// and the passwordChanger does not have a codePath that deals with generating
|
|
if (changePasswordAttempt.Success)
|
|
{
|
|
changePasswordAttempt.Result.ResetPassword = generatedPassword;
|
|
}
|
|
|
|
return changePasswordAttempt;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Removes a specific section from all users
|
|
/// </summary>
|
|
/// <remarks>This is useful when an entire section is removed from config</remarks>
|
|
/// <param name="sectionAlias">Alias of the section to remove</param>
|
|
public void DeleteSectionFromAllUserGroups(string sectionAlias)
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
|
{
|
|
IEnumerable<IUserGroup> 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();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<Attempt<IEnumerable<NodePermissions>, UserOperationStatus>> GetMediaPermissionsAsync(Guid userKey, IEnumerable<Guid> mediaKeys)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
Attempt<Dictionary<Guid, int>?> idAttempt = CreateIdKeyMap(mediaKeys, UmbracoObjectTypes.Media);
|
|
|
|
if (idAttempt.Success is false || idAttempt.Result is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.MediaNodeNotFound, Enumerable.Empty<NodePermissions>());
|
|
}
|
|
|
|
Attempt<IEnumerable<NodePermissions>, UserOperationStatus> permissions = await GetPermissionsAsync(userKey, idAttempt.Result);
|
|
scope.Complete();
|
|
|
|
return permissions;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<Attempt<IEnumerable<NodePermissions>, UserOperationStatus>> GetDocumentPermissionsAsync(Guid userKey, IEnumerable<Guid> contentKeys)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
Attempt<Dictionary<Guid, int>?> idAttempt = CreateIdKeyMap(contentKeys, UmbracoObjectTypes.Document);
|
|
|
|
if (idAttempt.Success is false || idAttempt.Result is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.ContentNodeNotFound, Enumerable.Empty<NodePermissions>());
|
|
}
|
|
|
|
Attempt<IEnumerable<NodePermissions>, UserOperationStatus> permissions = await GetPermissionsAsync(userKey, idAttempt.Result);
|
|
scope.Complete();
|
|
|
|
return permissions;
|
|
}
|
|
|
|
|
|
private async Task<Attempt<IEnumerable<NodePermissions>, UserOperationStatus>> GetPermissionsAsync(Guid userKey, Dictionary<Guid, int> nodes)
|
|
{
|
|
IUser? user = await GetAsync(userKey);
|
|
|
|
if (user is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, Enumerable.Empty<NodePermissions>());
|
|
}
|
|
|
|
EntityPermissionCollection permissionsCollection = _userGroupRepository.GetPermissions(
|
|
user.Groups.ToArray(),
|
|
true,
|
|
nodes.Select(x => x.Value).ToArray());
|
|
|
|
var results = new List<NodePermissions>();
|
|
foreach (KeyValuePair<Guid, int> node in nodes)
|
|
{
|
|
ISet<string> permissions = permissionsCollection.GetAllPermissions(node.Value);
|
|
results.Add(new NodePermissions { NodeKey = node.Key, Permissions = permissions });
|
|
}
|
|
|
|
return Attempt.SucceedWithStatus<IEnumerable<NodePermissions>, UserOperationStatus>(UserOperationStatus.Success, results);
|
|
}
|
|
|
|
private Attempt<Dictionary<Guid, int>?> CreateIdKeyMap(IEnumerable<Guid> nodeKeys, UmbracoObjectTypes objectType)
|
|
{
|
|
// We'll return this as a dictionary we can link the id and key again later.
|
|
Dictionary<Guid, int> idKeys = new();
|
|
|
|
foreach (Guid key in nodeKeys)
|
|
{
|
|
Attempt<int> idAttempt = _entityService.GetId(key, objectType);
|
|
if (idAttempt.Success is false)
|
|
{
|
|
return Attempt.Fail<Dictionary<Guid, int>?>(null);
|
|
}
|
|
|
|
idKeys[key] = idAttempt.Result;
|
|
}
|
|
|
|
return Attempt.Succeed<Dictionary<Guid, int>?>(idKeys);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Attempt<IEnumerable<NodePermissions>, UserOperationStatus>> GetPermissionsAsync(Guid userKey, params Guid[] nodeKeys)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
|
|
|
IUser? user = await GetAsync(userKey);
|
|
|
|
if (user is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, Enumerable.Empty<NodePermissions>());
|
|
}
|
|
|
|
Guid[] keys = nodeKeys.ToArray();
|
|
if (keys.Length == 0)
|
|
{
|
|
return Attempt.SucceedWithStatus(UserOperationStatus.Success, Enumerable.Empty<NodePermissions>());
|
|
}
|
|
|
|
// We don't know what the entity type may be, so we have to get the entire entity :(
|
|
Dictionary<int, Guid> idKeyMap = new();
|
|
foreach (Guid key in keys)
|
|
{
|
|
IEntitySlim? entity = _entityService.Get(key);
|
|
|
|
if (entity is null)
|
|
{
|
|
return Attempt.FailWithStatus(UserOperationStatus.NodeNotFound, Enumerable.Empty<NodePermissions>());
|
|
}
|
|
|
|
idKeyMap[entity.Id] = key;
|
|
}
|
|
|
|
EntityPermissionCollection permissionCollection = _userGroupRepository.GetPermissions(user.Groups.ToArray(), true, idKeyMap.Keys.ToArray());
|
|
|
|
var results = new List<NodePermissions>();
|
|
foreach (int nodeId in idKeyMap.Keys)
|
|
{
|
|
ISet<string> permissions = permissionCollection.GetAllPermissions(nodeId);
|
|
results.Add(new NodePermissions { NodeKey = idKeyMap[nodeId], Permissions = permissions });
|
|
}
|
|
|
|
return Attempt.SucceedWithStatus<IEnumerable<NodePermissions>, UserOperationStatus>(UserOperationStatus.Success, results);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get explicitly assigned permissions for a user and optional node ids
|
|
/// </summary>
|
|
/// <param name="user">User to retrieve permissions for</param>
|
|
/// <param name="nodeIds">Specifying nothing will return all permissions for all nodes</param>
|
|
/// <returns>An enumerable list of <see cref="EntityPermission" /></returns>
|
|
public EntityPermissionCollection GetPermissions(IUser? user, params int[] nodeIds)
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
return _userGroupRepository.GetPermissions(user?.Groups.ToArray(), true, nodeIds);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get explicitly assigned permissions for a group and optional node Ids
|
|
/// </summary>
|
|
/// <param name="groups"></param>
|
|
/// <param name="fallbackToDefaultPermissions">
|
|
/// Flag indicating if we want to include the default group permissions for each result if there are not explicit
|
|
/// permissions set
|
|
/// </param>
|
|
/// <param name="nodeIds">Specifying nothing will return all permissions for all nodes</param>
|
|
/// <returns>An enumerable list of <see cref="EntityPermission" /></returns>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get explicitly assigned permissions for a group and optional node Ids
|
|
/// </summary>
|
|
/// <param name="groups">Groups to retrieve permissions for</param>
|
|
/// <param name="fallbackToDefaultPermissions">
|
|
/// Flag indicating if we want to include the default group permissions for each result if there are not explicit
|
|
/// permissions set
|
|
/// </param>
|
|
/// <param name="nodeIds">Specifying nothing will return all permissions for all nodes</param>
|
|
/// <returns>An enumerable list of <see cref="EntityPermission" /></returns>
|
|
private IEnumerable<EntityPermission> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the implicit/inherited permissions for the user for the given path
|
|
/// </summary>
|
|
/// <param name="user">User to check permissions for</param>
|
|
/// <param name="path">Path to check permissions for</param>
|
|
public EntityPermissionSet GetPermissionsForPath(IUser? user, string? path)
|
|
{
|
|
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);
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the permissions for the provided group and path
|
|
/// </summary>
|
|
/// <param name="groups"></param>
|
|
/// <param name="path">Path to check permissions for</param>
|
|
/// <param name="fallbackToDefaultPermissions">
|
|
/// Flag indicating if we want to include the default group permissions for each result if there are not explicit
|
|
/// permissions set
|
|
/// </param>
|
|
/// <returns>String indicating permissions for provided user and path</returns>
|
|
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);
|
|
}
|
|
|
|
public async Task<UserClientCredentialsOperationStatus> AddClientIdAsync(Guid userKey, string clientId)
|
|
{
|
|
if (ValidClientId().IsMatch(clientId) is false)
|
|
{
|
|
return UserClientCredentialsOperationStatus.InvalidClientId;
|
|
}
|
|
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
|
|
|
IEnumerable<string> currentClientIds = _userRepository.GetAllClientIds();
|
|
if (currentClientIds.InvariantContains(clientId))
|
|
{
|
|
return UserClientCredentialsOperationStatus.DuplicateClientId;
|
|
}
|
|
|
|
IUser? user = await GetAsync(userKey);
|
|
if (user is null || user.Kind != UserKind.Api)
|
|
{
|
|
return UserClientCredentialsOperationStatus.InvalidUser;
|
|
}
|
|
|
|
_userRepository.AddClientId(user.Id, clientId);
|
|
|
|
return UserClientCredentialsOperationStatus.Success;
|
|
}
|
|
|
|
public async Task<bool> RemoveClientIdAsync(Guid userKey, string clientId)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
|
|
|
var userId = await _userIdKeyResolver.GetAsync(userKey);
|
|
return _userRepository.RemoveClientId(userId, clientId);
|
|
}
|
|
|
|
public Task<IUser?> FindByClientIdAsync(string clientId)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
|
|
|
IUser? user = _userRepository.GetByClientId(clientId);
|
|
return Task.FromResult(user?.Kind == UserKind.Api ? user : null);
|
|
}
|
|
|
|
public async Task<IEnumerable<string>> GetClientIdsAsync(Guid userKey)
|
|
{
|
|
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
|
|
|
var userId = await _userIdKeyResolver.GetAsync(userKey);
|
|
return _userRepository.GetClientIds(userId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This performs the calculations for inherited nodes based on this
|
|
/// http://issues.umbraco.org/issue/U4-10075#comment=67-40085
|
|
/// </summary>
|
|
/// <param name="groupPermissions"></param>
|
|
/// <param name="pathIds"></param>
|
|
/// <returns></returns>
|
|
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<int, Dictionary<int, EntityPermission[]>> 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([]);
|
|
}
|
|
|
|
// get permissions for all nodes in the path by group
|
|
IEnumerable<IGrouping<int, EntityPermission>> 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)!);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the resulting permission set for a group for the path based on all permissions provided for the branch
|
|
/// </summary>
|
|
/// <param name="pathPermissions">
|
|
/// The collective set of permissions provided to calculate the resulting permissions set for the path
|
|
/// based on a single group
|
|
/// </param>
|
|
/// <param name="pathIds">Must be ordered deepest to shallowest (right to left)</param>
|
|
/// <param name="fallbackToDefaultPermissions">
|
|
/// Flag indicating if we want to include the default group permissions for each result if there are not explicit
|
|
/// permissions set
|
|
/// </param>
|
|
/// <returns></returns>
|
|
internal static EntityPermission? GetPermissionsForPathForGroup(
|
|
IEnumerable<EntityPermission> 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(ISet<string> assignedPermissions, ISet<string> additionalPermissions)
|
|
{
|
|
foreach (var additionalPermission in additionalPermissions)
|
|
{
|
|
assignedPermissions.Add(additionalPermission);
|
|
}
|
|
}
|
|
|
|
[GeneratedRegex(@"^[\w\d\-\._~]{1,100}$")]
|
|
private static partial Regex ValidClientId();
|
|
|
|
#endregion
|
|
}
|