mirror of https://github.com/Squidex/squidex.git
104 changed files with 1903 additions and 1260 deletions
@ -0,0 +1,408 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Security; |
||||
|
using Squidex.Log; |
||||
|
using Squidex.Shared; |
||||
|
using Squidex.Shared.Identity; |
||||
|
using Squidex.Shared.Users; |
||||
|
|
||||
|
namespace Squidex.Domain.Users |
||||
|
{ |
||||
|
public sealed class DefaultUserService : IUserService |
||||
|
{ |
||||
|
private readonly UserManager<IdentityUser> userManager; |
||||
|
private readonly IUserFactory userFactory; |
||||
|
private readonly IEnumerable<IUserEvents> userEvents; |
||||
|
private readonly ISemanticLog log; |
||||
|
|
||||
|
public DefaultUserService(UserManager<IdentityUser> userManager, IUserFactory userFactory, |
||||
|
IEnumerable<IUserEvents> userEvents, ISemanticLog log) |
||||
|
{ |
||||
|
Guard.NotNull(userManager, nameof(userManager)); |
||||
|
Guard.NotNull(userFactory, nameof(userFactory)); |
||||
|
Guard.NotNull(userEvents, nameof(userEvents)); |
||||
|
Guard.NotNull(log, nameof(log)); |
||||
|
|
||||
|
this.userManager = userManager; |
||||
|
this.userFactory = userFactory; |
||||
|
this.userEvents = userEvents; |
||||
|
|
||||
|
this.log = log; |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> IsEmptyAsync() |
||||
|
{ |
||||
|
var result = await QueryAsync(null, 0, 0); |
||||
|
|
||||
|
return result.Total == 0; |
||||
|
} |
||||
|
|
||||
|
public string GetUserId(ClaimsPrincipal user) |
||||
|
{ |
||||
|
Guard.NotNull(user, nameof(user)); |
||||
|
|
||||
|
return userManager.GetUserId(user); |
||||
|
} |
||||
|
|
||||
|
public async Task<IResultList<IUser>> QueryAsync(IEnumerable<string> ids) |
||||
|
{ |
||||
|
Guard.NotNull(ids, nameof(ids)); |
||||
|
|
||||
|
ids = ids.Where(userFactory.IsId); |
||||
|
|
||||
|
if (!ids.Any()) |
||||
|
{ |
||||
|
return ResultList.CreateFrom<IUser>(0); |
||||
|
} |
||||
|
|
||||
|
var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList(); |
||||
|
|
||||
|
var resolved = await ResolveAsync(users); |
||||
|
|
||||
|
return ResultList.Create(users.Count, resolved); |
||||
|
} |
||||
|
|
||||
|
public async Task<IResultList<IUser>> QueryAsync(string? query, int take, int skip) |
||||
|
{ |
||||
|
IQueryable<IdentityUser> QueryUsers(string? email = null) |
||||
|
{ |
||||
|
var result = userManager.Users; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(email)) |
||||
|
{ |
||||
|
var normalizedEmail = userManager.NormalizeEmail(email); |
||||
|
|
||||
|
result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
var userItems = QueryUsers(query).Take(take).Skip(skip).ToList(); |
||||
|
var userTotal = QueryUsers(query).LongCount(); |
||||
|
|
||||
|
var resolved = await ResolveAsync(userItems); |
||||
|
|
||||
|
return ResultList.Create(userTotal, resolved); |
||||
|
} |
||||
|
|
||||
|
public Task<IList<UserLoginInfo>> GetLoginsAsync(IUser user) |
||||
|
{ |
||||
|
Guard.NotNull(user, nameof(user)); |
||||
|
|
||||
|
return userManager.GetLoginsAsync((IdentityUser)user.Identity); |
||||
|
} |
||||
|
|
||||
|
public Task<bool> HasPasswordAsync(IUser user) |
||||
|
{ |
||||
|
Guard.NotNull(user, nameof(user)); |
||||
|
|
||||
|
return userManager.HasPasswordAsync((IdentityUser)user.Identity); |
||||
|
} |
||||
|
|
||||
|
public async Task<IUser?> FindByLoginAsync(string provider, string key) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(provider, nameof(provider)); |
||||
|
|
||||
|
var user = await userManager.FindByLoginAsync(provider, key); |
||||
|
|
||||
|
return await ResolveOptionalAsync(user); |
||||
|
} |
||||
|
|
||||
|
public async Task<IUser?> FindByEmailAsync(string email) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(email, nameof(email)); |
||||
|
|
||||
|
var user = await userManager.FindByEmailAsync(email); |
||||
|
|
||||
|
return await ResolveOptionalAsync(user); |
||||
|
} |
||||
|
|
||||
|
public async Task<IUser?> GetAsync(ClaimsPrincipal principal) |
||||
|
{ |
||||
|
Guard.NotNull(principal, nameof(principal)); |
||||
|
|
||||
|
var user = await userManager.GetUserAsync(principal); |
||||
|
|
||||
|
return await ResolveOptionalAsync(user); |
||||
|
} |
||||
|
|
||||
|
public async Task<IUser?> FindByIdAsync(string id) |
||||
|
{ |
||||
|
if (!userFactory.IsId(id)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var user = await userManager.FindByIdAsync(id); |
||||
|
|
||||
|
return await ResolveOptionalAsync(user); |
||||
|
} |
||||
|
|
||||
|
public async Task<IUser> CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(email, nameof(email)); |
||||
|
|
||||
|
var isFirst = !userManager.Users.Any(); |
||||
|
|
||||
|
var user = userFactory.Create(email); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await userManager.CreateAsync(user).Throw(log); |
||||
|
|
||||
|
values ??= new UserValues(); |
||||
|
|
||||
|
if (string.IsNullOrWhiteSpace(values.DisplayName)) |
||||
|
{ |
||||
|
values.DisplayName = email; |
||||
|
} |
||||
|
|
||||
|
if (isFirst) |
||||
|
{ |
||||
|
var permissions = values.Permissions?.ToIds().ToList() ?? new List<string>(); |
||||
|
|
||||
|
permissions.Add(Permissions.Admin); |
||||
|
|
||||
|
values.Permissions = new PermissionSet(permissions); |
||||
|
} |
||||
|
|
||||
|
await userManager.SyncClaims(user, values).Throw(log); |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(values.Password)) |
||||
|
{ |
||||
|
await userManager.AddPasswordAsync(user, values.Password).Throw(log); |
||||
|
} |
||||
|
|
||||
|
if (!isFirst && lockAutomatically) |
||||
|
{ |
||||
|
await userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (userFactory.IsId(user.Id)) |
||||
|
{ |
||||
|
await userManager.DeleteAsync(user); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex2) |
||||
|
{ |
||||
|
log.LogError(ex2, w => w |
||||
|
.WriteProperty("action", "CleanupUser") |
||||
|
.WriteProperty("status", "Failed")); |
||||
|
} |
||||
|
|
||||
|
throw; |
||||
|
} |
||||
|
|
||||
|
var resolved = await ResolveAsync(user); |
||||
|
|
||||
|
foreach (var @events in userEvents) |
||||
|
{ |
||||
|
@events.OnUserRegistered(resolved); |
||||
|
} |
||||
|
|
||||
|
if (HasConsentGiven(values, null!)) |
||||
|
{ |
||||
|
foreach (var @events in userEvents) |
||||
|
{ |
||||
|
@events.OnConsentGiven(resolved); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return resolved; |
||||
|
} |
||||
|
|
||||
|
public Task<IUser> SetPasswordAsync(string id, string password, string? oldPassword) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(id, nameof(id)); |
||||
|
|
||||
|
return ForUserAsync(id, async user => |
||||
|
{ |
||||
|
if (await userManager.HasPasswordAsync(user)) |
||||
|
{ |
||||
|
await userManager.ChangePasswordAsync(user, oldPassword!, password).Throw(log); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
await userManager.AddPasswordAsync(user, password).Throw(log); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async Task<IUser> UpdateAsync(string id, UserValues values) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(id, nameof(id)); |
||||
|
Guard.NotNull(values, nameof(values)); |
||||
|
|
||||
|
var user = await GetUserAsync(id); |
||||
|
|
||||
|
var oldUser = await ResolveAsync(user); |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) |
||||
|
{ |
||||
|
await userManager.SetEmailAsync(user, values.Email).Throw(log); |
||||
|
await userManager.SetUserNameAsync(user, values.Email).Throw(log); |
||||
|
} |
||||
|
|
||||
|
await userManager.SyncClaims(user, values).Throw(log); |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(values.Password)) |
||||
|
{ |
||||
|
if (await userManager.HasPasswordAsync(user)) |
||||
|
{ |
||||
|
await userManager.RemovePasswordAsync(user).Throw(log); |
||||
|
} |
||||
|
|
||||
|
await userManager.AddPasswordAsync(user, values.Password).Throw(log); |
||||
|
} |
||||
|
|
||||
|
var resolved = await ResolveAsync(user); |
||||
|
|
||||
|
foreach (var @events in userEvents) |
||||
|
{ |
||||
|
@events.OnUserUpdated(resolved); |
||||
|
} |
||||
|
|
||||
|
if (HasConsentGiven(values, oldUser)) |
||||
|
{ |
||||
|
foreach (var @events in userEvents) |
||||
|
{ |
||||
|
@events.OnConsentGiven(resolved); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return resolved; |
||||
|
} |
||||
|
|
||||
|
public Task<IUser> LockAsync(string id) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(id, nameof(id)); |
||||
|
|
||||
|
return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log)); |
||||
|
} |
||||
|
|
||||
|
public Task<IUser> UnlockAsync(string id) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(id, nameof(id)); |
||||
|
|
||||
|
return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, null).Throw(log)); |
||||
|
} |
||||
|
|
||||
|
public Task<IUser> AddLoginAsync(string id, ExternalLoginInfo externalLogin) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(id, nameof(id)); |
||||
|
|
||||
|
return ForUserAsync(id, user => userManager.AddLoginAsync(user, externalLogin).Throw(log)); |
||||
|
} |
||||
|
|
||||
|
public Task<IUser> RemoveLoginAsync(string id, string loginProvider, string providerKey) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(id, nameof(id)); |
||||
|
|
||||
|
return ForUserAsync(id, user => userManager.RemoveLoginAsync(user, loginProvider, providerKey).Throw(log)); |
||||
|
} |
||||
|
|
||||
|
public async Task DeleteAsync(string id) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(id, nameof(id)); |
||||
|
|
||||
|
var user = await GetUserAsync(id); |
||||
|
|
||||
|
var resolved = await ResolveAsync(user); |
||||
|
|
||||
|
await userManager.DeleteAsync(user).Throw(log); |
||||
|
|
||||
|
foreach (var @events in userEvents) |
||||
|
{ |
||||
|
@events.OnUserDeleted(resolved); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task<IUser> ForUserAsync(string id, Func<IdentityUser, Task> action) |
||||
|
{ |
||||
|
var user = await GetUserAsync(id); |
||||
|
|
||||
|
await action(user); |
||||
|
|
||||
|
return await ResolveAsync(user); |
||||
|
} |
||||
|
|
||||
|
private async Task<IdentityUser> GetUserAsync(string id) |
||||
|
{ |
||||
|
if (!userFactory.IsId(id)) |
||||
|
{ |
||||
|
throw new DomainObjectNotFoundException(id); |
||||
|
} |
||||
|
|
||||
|
var user = await userManager.FindByIdAsync(id); |
||||
|
|
||||
|
if (user == null) |
||||
|
{ |
||||
|
throw new DomainObjectNotFoundException(id); |
||||
|
} |
||||
|
|
||||
|
return user; |
||||
|
} |
||||
|
|
||||
|
private Task<IUser[]> ResolveAsync(IEnumerable<IdentityUser> users) |
||||
|
{ |
||||
|
return Task.WhenAll(users.Select(async user => |
||||
|
{ |
||||
|
return await ResolveAsync(user); |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
private async Task<IUser> ResolveAsync(IdentityUser user) |
||||
|
{ |
||||
|
var claims = await userManager.GetClaimsAsync(user); |
||||
|
|
||||
|
if (!claims.Any(x => string.Equals(x.Type, SquidexClaimTypes.DisplayName, StringComparison.OrdinalIgnoreCase))) |
||||
|
{ |
||||
|
claims.Add(new Claim(SquidexClaimTypes.DisplayName, user.Email)); |
||||
|
} |
||||
|
|
||||
|
return new UserWithClaims(user, claims.ToList()); |
||||
|
} |
||||
|
|
||||
|
private async Task<IUser?> ResolveOptionalAsync(IdentityUser? user) |
||||
|
{ |
||||
|
if (user == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return await ResolveAsync(user); |
||||
|
} |
||||
|
|
||||
|
private static bool HasConsentGiven(UserValues values, IUser? oldUser) |
||||
|
{ |
||||
|
if (values.Consent == true && oldUser?.Claims.HasConsent() != true) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return values.ConsentForEmails == true && oldUser?.Claims.HasConsentForEmails() != true; |
||||
|
} |
||||
|
|
||||
|
private static DateTimeOffset LockoutDate() |
||||
|
{ |
||||
|
return DateTimeOffset.UtcNow.AddYears(100); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,26 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using Squidex.Shared.Users; |
|
||||
|
|
||||
namespace Squidex.Domain.Users |
|
||||
{ |
|
||||
public interface IUserEventHandler |
|
||||
{ |
|
||||
void OnUserRegistered(IUser user) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
void OnUserUpdated(IUser user) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
void OnConsentGiven(IUser user) |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,55 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Shared.Users; |
||||
|
|
||||
|
namespace Squidex.Domain.Users |
||||
|
{ |
||||
|
public interface IUserService |
||||
|
{ |
||||
|
Task<IResultList<IUser>> QueryAsync(IEnumerable<string> ids); |
||||
|
|
||||
|
Task<IResultList<IUser>> QueryAsync(string? query = null, int take = 10, int skip = 0); |
||||
|
|
||||
|
string GetUserId(ClaimsPrincipal user); |
||||
|
|
||||
|
Task<IList<UserLoginInfo>> GetLoginsAsync(IUser user); |
||||
|
|
||||
|
Task<bool> HasPasswordAsync(IUser user); |
||||
|
|
||||
|
Task<bool> IsEmptyAsync(); |
||||
|
|
||||
|
Task<IUser> CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false); |
||||
|
|
||||
|
Task<IUser?> GetAsync(ClaimsPrincipal principal); |
||||
|
|
||||
|
Task<IUser?> FindByEmailAsync(string email); |
||||
|
|
||||
|
Task<IUser?> FindByIdAsync(string id); |
||||
|
|
||||
|
Task<IUser?> FindByLoginAsync(string provider, string key); |
||||
|
|
||||
|
Task<IUser> SetPasswordAsync(string id, string password, string? oldPassword = null); |
||||
|
|
||||
|
Task<IUser> AddLoginAsync(string id, ExternalLoginInfo externalLogin); |
||||
|
|
||||
|
Task<IUser> RemoveLoginAsync(string id, string loginProvider, string providerKey); |
||||
|
|
||||
|
Task<IUser> LockAsync(string id); |
||||
|
|
||||
|
Task<IUser> UnlockAsync(string id); |
||||
|
|
||||
|
Task<IUser> UpdateAsync(string id, UserValues values); |
||||
|
|
||||
|
Task DeleteAsync(string id); |
||||
|
} |
||||
|
} |
||||
@ -1,49 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Collections.Generic; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Shared.Users; |
|
||||
|
|
||||
namespace Squidex.Domain.Users |
|
||||
{ |
|
||||
public sealed class UserEvents : IUserEvents |
|
||||
{ |
|
||||
private readonly IEnumerable<IUserEventHandler> userEventHandlers; |
|
||||
|
|
||||
public UserEvents(IEnumerable<IUserEventHandler> userEventHandlers) |
|
||||
{ |
|
||||
Guard.NotNull(userEventHandlers, nameof(userEventHandlers)); |
|
||||
|
|
||||
this.userEventHandlers = userEventHandlers; |
|
||||
} |
|
||||
|
|
||||
public void OnUserRegistered(IUser user) |
|
||||
{ |
|
||||
foreach (var handler in userEventHandlers) |
|
||||
{ |
|
||||
handler.OnUserRegistered(user); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void OnUserUpdated(IUser user) |
|
||||
{ |
|
||||
foreach (var handler in userEventHandlers) |
|
||||
{ |
|
||||
handler.OnUserUpdated(user); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void OnConsentGiven(IUser user) |
|
||||
{ |
|
||||
foreach (var handler in userEventHandlers) |
|
||||
{ |
|
||||
handler.OnConsentGiven(user); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,47 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Security.Claims; |
|
||||
using Squidex.Infrastructure.Security; |
|
||||
|
|
||||
namespace Squidex.Shared.Identity |
|
||||
{ |
|
||||
public static class ClaimsPrincipalExtensions |
|
||||
{ |
|
||||
public static void SetDisplayName(this ClaimsIdentity identity, string displayName) |
|
||||
{ |
|
||||
identity.AddClaim(new Claim(SquidexClaimTypes.DisplayName, displayName)); |
|
||||
} |
|
||||
|
|
||||
public static void SetPictureUrl(this ClaimsIdentity identity, string pictureUrl) |
|
||||
{ |
|
||||
identity.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl)); |
|
||||
} |
|
||||
|
|
||||
public static PermissionSet Permissions(this ClaimsPrincipal principal) |
|
||||
{ |
|
||||
return new PermissionSet(principal.Claims |
|
||||
.Where(x => |
|
||||
(x.Type == SquidexClaimTypes.Permissions || |
|
||||
x.Type == SquidexClaimTypes.PermissionsClient) && |
|
||||
!string.IsNullOrWhiteSpace(x.Value)) |
|
||||
.Select(x => new Permission(x.Value))); |
|
||||
} |
|
||||
|
|
||||
public static IEnumerable<Claim> GetSquidexClaims(this ClaimsPrincipal principal) |
|
||||
{ |
|
||||
return principal.Claims |
|
||||
.Where(x => |
|
||||
(x.Type.StartsWith(SquidexClaimTypes.Prefix, StringComparison.Ordinal) || |
|
||||
x.Type.StartsWith(SquidexClaimTypes.PrefixClient, StringComparison.Ordinal)) && |
|
||||
!string.IsNullOrWhiteSpace(x.Value)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,159 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Security.Claims; |
||||
|
using Squidex.Infrastructure.Security; |
||||
|
|
||||
|
namespace Squidex.Shared.Identity |
||||
|
{ |
||||
|
public static class SquidexClaimsExtensions |
||||
|
{ |
||||
|
private const string ClientPrefix = "client_"; |
||||
|
|
||||
|
public static PermissionSet Permissions(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
return new PermissionSet(user.GetClaims(SquidexClaimTypes.Permissions).Select(x => new Permission(x.Value))); |
||||
|
} |
||||
|
|
||||
|
public static bool IsHidden(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
return user.HasClaimValue(SquidexClaimTypes.Hidden, "true"); |
||||
|
} |
||||
|
|
||||
|
public static bool HasConsent(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
return user.HasClaimValue(SquidexClaimTypes.Consent, "true"); |
||||
|
} |
||||
|
|
||||
|
public static bool HasConsentForEmails(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true"); |
||||
|
} |
||||
|
|
||||
|
public static bool HasDisplayName(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
return user.HasClaim(SquidexClaimTypes.DisplayName); |
||||
|
} |
||||
|
|
||||
|
public static bool HasPictureUrl(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
return user.HasClaim(SquidexClaimTypes.PictureUrl); |
||||
|
} |
||||
|
|
||||
|
public static bool IsPictureUrlStored(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); |
||||
|
} |
||||
|
|
||||
|
public static string? ClientSecret(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
return user.GetClaimValue(SquidexClaimTypes.ClientSecret); |
||||
|
} |
||||
|
|
||||
|
public static string? PictureUrl(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
return user.GetClaimValue(SquidexClaimTypes.PictureUrl); |
||||
|
} |
||||
|
|
||||
|
public static string? DisplayName(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
return user.GetClaimValue(SquidexClaimTypes.DisplayName); |
||||
|
} |
||||
|
|
||||
|
public static bool HasClaim(this IEnumerable<Claim> user, string type) |
||||
|
{ |
||||
|
return user.GetClaims(type).Any(); |
||||
|
} |
||||
|
|
||||
|
public static bool HasClaimValue(this IEnumerable<Claim> user, string type, string value) |
||||
|
{ |
||||
|
return user.GetClaims(type).Any(x => string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase)); |
||||
|
} |
||||
|
|
||||
|
public static IEnumerable<Claim> GetSquidexClaims(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
const string prefix = "urn:squidex:"; |
||||
|
|
||||
|
foreach (var claim in user) |
||||
|
{ |
||||
|
var type = GetType(claim); |
||||
|
|
||||
|
if (type.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
yield return claim; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static IEnumerable<(string Name, string Value)> GetCustomProperties(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
foreach (var claim in user) |
||||
|
{ |
||||
|
var type = GetType(claim); |
||||
|
|
||||
|
if (type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
var name = type[(SquidexClaimTypes.CustomPrefix.Length + 1)..].ToString(); |
||||
|
|
||||
|
yield return (name, claim.Value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static string? PictureNormalizedUrl(this IEnumerable<Claim> user) |
||||
|
{ |
||||
|
var url = user.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) |
||||
|
{ |
||||
|
if (url.Contains("?")) |
||||
|
{ |
||||
|
url += "&d=404"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
url += "?d=404"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return url; |
||||
|
} |
||||
|
|
||||
|
private static string? GetClaimValue(this IEnumerable<Claim> user, string type) |
||||
|
{ |
||||
|
return user.GetClaims(type).FirstOrDefault()?.Value; |
||||
|
} |
||||
|
|
||||
|
private static IEnumerable<Claim> GetClaims(this IEnumerable<Claim> user, string request) |
||||
|
{ |
||||
|
foreach (var claim in user) |
||||
|
{ |
||||
|
var type = GetType(claim); |
||||
|
|
||||
|
if (type.Equals(request, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
yield return claim; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static ReadOnlySpan<char> GetType(Claim claim) |
||||
|
{ |
||||
|
var type = claim.Type.AsSpan(); |
||||
|
|
||||
|
if (type.StartsWith(ClientPrefix, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
type = type[ClientPrefix.Length..]; |
||||
|
} |
||||
|
|
||||
|
return type; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,121 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using Squidex.Infrastructure.Security; |
|
||||
using Squidex.Shared.Identity; |
|
||||
|
|
||||
namespace Squidex.Shared.Users |
|
||||
{ |
|
||||
public static class UserExtensions |
|
||||
{ |
|
||||
public static PermissionSet Permissions(this IUser user) |
|
||||
{ |
|
||||
return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x))); |
|
||||
} |
|
||||
|
|
||||
public static bool IsInvited(this IUser user) |
|
||||
{ |
|
||||
return user.HasClaimValue(SquidexClaimTypes.Invited, "true"); |
|
||||
} |
|
||||
|
|
||||
public static bool IsHidden(this IUser user) |
|
||||
{ |
|
||||
return user.HasClaimValue(SquidexClaimTypes.Hidden, "true"); |
|
||||
} |
|
||||
|
|
||||
public static bool HasConsent(this IUser user) |
|
||||
{ |
|
||||
return user.HasClaimValue(SquidexClaimTypes.Consent, "true"); |
|
||||
} |
|
||||
|
|
||||
public static bool HasConsentForEmails(this IUser user) |
|
||||
{ |
|
||||
return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true"); |
|
||||
} |
|
||||
|
|
||||
public static bool HasDisplayName(this IUser user) |
|
||||
{ |
|
||||
return user.HasClaim(SquidexClaimTypes.DisplayName); |
|
||||
} |
|
||||
|
|
||||
public static bool HasPictureUrl(this IUser user) |
|
||||
{ |
|
||||
return user.HasClaim(SquidexClaimTypes.PictureUrl); |
|
||||
} |
|
||||
|
|
||||
public static bool IsPictureUrlStored(this IUser user) |
|
||||
{ |
|
||||
return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); |
|
||||
} |
|
||||
|
|
||||
public static string? ClientSecret(this IUser user) |
|
||||
{ |
|
||||
return user.GetClaimValue(SquidexClaimTypes.ClientSecret); |
|
||||
} |
|
||||
|
|
||||
public static string? PictureUrl(this IUser user) |
|
||||
{ |
|
||||
return user.GetClaimValue(SquidexClaimTypes.PictureUrl); |
|
||||
} |
|
||||
|
|
||||
public static string? DisplayName(this IUser user) |
|
||||
{ |
|
||||
return user.GetClaimValue(SquidexClaimTypes.DisplayName); |
|
||||
} |
|
||||
|
|
||||
public static string? GetClaimValue(this IUser user, string type) |
|
||||
{ |
|
||||
return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value; |
|
||||
} |
|
||||
|
|
||||
public static string[] GetClaimValues(this IUser user, string type) |
|
||||
{ |
|
||||
return user.Claims.Where(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)) |
|
||||
.Select(x => x.Value).ToArray(); |
|
||||
} |
|
||||
|
|
||||
public static List<(string Name, string Value)> GetCustomProperties(this IUser user) |
|
||||
{ |
|
||||
return user.Claims.Where(x => x.Type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase)) |
|
||||
.Select(x => (x.Type[(SquidexClaimTypes.CustomPrefix.Length + 1)..], x.Value)).ToList(); |
|
||||
} |
|
||||
|
|
||||
public static bool HasClaim(this IUser user, string type) |
|
||||
{ |
|
||||
return user.Claims.Any(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); |
|
||||
} |
|
||||
|
|
||||
public static bool HasClaimValue(this IUser user, string type, string value) |
|
||||
{ |
|
||||
return user.Claims.Any(x => |
|
||||
string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase) && |
|
||||
string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase)); |
|
||||
} |
|
||||
|
|
||||
public static string? PictureNormalizedUrl(this IUser user) |
|
||||
{ |
|
||||
var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value; |
|
||||
|
|
||||
if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) |
|
||||
{ |
|
||||
if (url.Contains("?")) |
|
||||
{ |
|
||||
url += "&d=404"; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
url += "?d=404"; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return url; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,607 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Log; |
||||
|
using Squidex.Shared; |
||||
|
using Squidex.Shared.Identity; |
||||
|
using Squidex.Shared.Users; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Users |
||||
|
{ |
||||
|
public class DefaultUserServiceTests |
||||
|
{ |
||||
|
private readonly UserManager<IdentityUser> userManager = A.Fake<UserManager<IdentityUser>>(); |
||||
|
private readonly IUserFactory userFactory = A.Fake<IUserFactory>(); |
||||
|
private readonly IUserEvents userEvents = A.Fake<IUserEvents>(); |
||||
|
private readonly DefaultUserService sut; |
||||
|
|
||||
|
public DefaultUserServiceTests() |
||||
|
{ |
||||
|
A.CallTo(() => userFactory.IsId(A<string>._)) |
||||
|
.Returns(true); |
||||
|
|
||||
|
A.CallTo(userManager).WithReturnType<Task<IdentityResult>>() |
||||
|
.Returns(IdentityResult.Success); |
||||
|
|
||||
|
sut = new DefaultUserService(userManager, userFactory, Enumerable.Repeat(userEvents, 1), A.Fake<ISemanticLog>()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_resolve_identity_if_id_not_valid() |
||||
|
{ |
||||
|
var invalidId = "__"; |
||||
|
|
||||
|
A.CallTo(() => userFactory.IsId(invalidId)) |
||||
|
.Returns(false); |
||||
|
|
||||
|
var result = await sut.FindByIdAsync(invalidId); |
||||
|
|
||||
|
Assert.Null(result); |
||||
|
|
||||
|
A.CallTo(() => userManager.FindByIdAsync(invalidId)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_identity_by_id_if_found() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
var result = await sut.FindByIdAsync(identity.Id); |
||||
|
|
||||
|
Assert.Same(identity, result?.Identity); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_null_if_identity_by_id_not_found() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
var result = await sut.FindByIdAsync(identity.Id); |
||||
|
|
||||
|
Assert.Null(result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_identity_by_email_if_found() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
var result = await sut.FindByEmailAsync(identity.Email); |
||||
|
|
||||
|
Assert.Same(identity, result?.Identity); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_null_if_identity_by_email_not_found() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
var result = await sut.FindByEmailAsync(identity.Email); |
||||
|
|
||||
|
Assert.Null(result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_identity_by_login_if_found() |
||||
|
{ |
||||
|
var provider = "my-provider"; |
||||
|
var providerKey = "key"; |
||||
|
|
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) |
||||
|
.Returns(identity); |
||||
|
|
||||
|
var result = await sut.FindByLoginAsync(provider, providerKey); |
||||
|
|
||||
|
Assert.Same(identity, result?.Identity); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_return_null_if_identity_by_login_not_found() |
||||
|
{ |
||||
|
var provider = "my-provider"; |
||||
|
var providerKey = "key"; |
||||
|
|
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) |
||||
|
.Returns(Task.FromResult<IdentityUser>(null!)); |
||||
|
|
||||
|
var result = await sut.FindByLoginAsync(provider, providerKey); |
||||
|
|
||||
|
Assert.Null(result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_provide_password_existence() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
var user = A.Fake<IUser>(); |
||||
|
|
||||
|
A.CallTo(() => user.Identity) |
||||
|
.Returns(identity); |
||||
|
|
||||
|
A.CallTo(() => userManager.HasPasswordAsync(identity)) |
||||
|
.Returns(true); |
||||
|
|
||||
|
var result = await sut.HasPasswordAsync(user); |
||||
|
|
||||
|
Assert.True(result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_provide_logins() |
||||
|
{ |
||||
|
var logins = new List<UserLoginInfo>(); |
||||
|
|
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
var user = A.Fake<IUser>(); |
||||
|
|
||||
|
A.CallTo(() => user.Identity) |
||||
|
.Returns(identity); |
||||
|
|
||||
|
A.CallTo(() => userManager.GetLoginsAsync(identity)) |
||||
|
.Returns(logins); |
||||
|
|
||||
|
var result = await sut.GetLoginsAsync(user); |
||||
|
|
||||
|
Assert.Same(logins, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Create_should_add_user() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
var values = new UserValues |
||||
|
{ |
||||
|
Email = identity.Email |
||||
|
}; |
||||
|
|
||||
|
SetupCreation(identity, values, 1); |
||||
|
|
||||
|
await sut.CreateAsync(values.Email, values); |
||||
|
|
||||
|
A.CallTo(() => userEvents.OnUserRegistered(A<IUser>.That.Matches(x => x.Identity == identity))) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => userEvents.OnConsentGiven(A<IUser>.That.Matches(x => x.Identity == identity))) |
||||
|
.MustNotHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions))) |
||||
|
.MustNotHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => userManager.AddPasswordAsync(identity, A<string>._)) |
||||
|
.MustNotHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A<DateTimeOffset>._)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Create_should_raise_event_if_consent_given() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
var values = new UserValues |
||||
|
{ |
||||
|
Consent = true, |
||||
|
}; |
||||
|
|
||||
|
SetupCreation(identity, values, 1); |
||||
|
|
||||
|
await sut.CreateAsync(identity.Email, values); |
||||
|
|
||||
|
A.CallTo(() => userEvents.OnConsentGiven(A<IUser>.That.Matches(x => x.Identity == identity))) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Create_should_set_admin_if_first_user() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
var values = new UserValues |
||||
|
{ |
||||
|
Consent = true, |
||||
|
}; |
||||
|
|
||||
|
SetupCreation(identity, values, 0); |
||||
|
|
||||
|
await sut.CreateAsync(identity.Email, values); |
||||
|
|
||||
|
A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions, Permissions.Admin))) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Create_should_not_lock_first_user() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
var values = new UserValues |
||||
|
{ |
||||
|
Consent = true, |
||||
|
}; |
||||
|
|
||||
|
SetupCreation(identity, values, 0); |
||||
|
|
||||
|
await sut.CreateAsync(identity.Email, values, true); |
||||
|
|
||||
|
A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A<DateTimeOffset>._)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Create_should_lock_second_user() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
var values = new UserValues |
||||
|
{ |
||||
|
Consent = true, |
||||
|
}; |
||||
|
|
||||
|
SetupCreation(identity, values, 1); |
||||
|
|
||||
|
await sut.CreateAsync(identity.Email, values, true); |
||||
|
|
||||
|
A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, InFuture())) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Create_should_add_password() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
var values = new UserValues |
||||
|
{ |
||||
|
Password = "password" |
||||
|
}; |
||||
|
|
||||
|
SetupCreation(identity, values, 1); |
||||
|
|
||||
|
await sut.CreateAsync(identity.Email, values, false); |
||||
|
|
||||
|
A.CallTo(() => userManager.AddPasswordAsync(identity, values.Password)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Update_should_throw_exception_if_not_found() |
||||
|
{ |
||||
|
var update = new UserValues |
||||
|
{ |
||||
|
Email = "new@email.com" |
||||
|
}; |
||||
|
|
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.UpdateAsync(identity.Id, update)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Update_should_do_nothing_for_new_update() |
||||
|
{ |
||||
|
var update = new UserValues(); |
||||
|
|
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
await sut.UpdateAsync(identity.Id, update); |
||||
|
|
||||
|
A.CallTo(() => userEvents.OnUserUpdated(A<IUser>.That.Matches(x => x.Identity == identity))) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Update_should_change_password_if_changed() |
||||
|
{ |
||||
|
var update = new UserValues |
||||
|
{ |
||||
|
Password = "password" |
||||
|
}; |
||||
|
|
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
A.CallTo(() => userManager.HasPasswordAsync(identity)) |
||||
|
.Returns(true); |
||||
|
|
||||
|
await sut.UpdateAsync(identity.Id, update); |
||||
|
|
||||
|
A.CallTo(() => userManager.RemovePasswordAsync(identity)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => userManager.AddPasswordAsync(identity, update.Password)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Update_should_change_email_if_changed() |
||||
|
{ |
||||
|
var update = new UserValues |
||||
|
{ |
||||
|
Email = "new@email.com" |
||||
|
}; |
||||
|
|
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
await sut.UpdateAsync(identity.Id, update); |
||||
|
|
||||
|
A.CallTo(() => userManager.SetEmailAsync(identity, update.Email)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => userManager.SetUserNameAsync(identity, update.Email)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Update_should_set_claim_if_consent_given() |
||||
|
{ |
||||
|
var update = new UserValues |
||||
|
{ |
||||
|
Consent = true |
||||
|
}; |
||||
|
|
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
await sut.UpdateAsync(identity.Id, update); |
||||
|
|
||||
|
A.CallTo<Task<IdentityResult>>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Consent))) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => userEvents.OnConsentGiven(A<IUser>.That.Matches(x => x.Identity == identity))) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Update_should_set_claim_if_email_consent_given() |
||||
|
{ |
||||
|
var update = new UserValues |
||||
|
{ |
||||
|
ConsentForEmails = true |
||||
|
}; |
||||
|
|
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
await sut.UpdateAsync(identity.Id, update); |
||||
|
|
||||
|
A.CallTo<Task<IdentityResult>>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.ConsentForEmails))) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => userEvents.OnConsentGiven(A<IUser>.That.Matches(x => x.Identity == identity))) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task SetPassword_should_throw_exception_if_not_found() |
||||
|
{ |
||||
|
var password = "password"; |
||||
|
|
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.SetPasswordAsync(identity.Id, password, null)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task SetPassword_should_succeed_if_found() |
||||
|
{ |
||||
|
var password = "password"; |
||||
|
|
||||
|
var identity = CreateIdentity(found: true);; |
||||
|
|
||||
|
await sut.SetPasswordAsync(identity.Id, password, null); |
||||
|
|
||||
|
A.CallTo(() => userManager.AddPasswordAsync(identity, password)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task SetPassword_should_change_password_if_identity_has_password() |
||||
|
{ |
||||
|
var password = "password"; |
||||
|
|
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
A.CallTo(() => userManager.HasPasswordAsync(identity)) |
||||
|
.Returns(true); |
||||
|
|
||||
|
await sut.SetPasswordAsync(identity.Id, password, "old"); |
||||
|
|
||||
|
A.CallTo(() => userManager.ChangePasswordAsync(identity, "old", password)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task AddLogin_should_throw_exception_if_not_found() |
||||
|
{ |
||||
|
var login = A.Fake<ExternalLoginInfo>(); |
||||
|
|
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.AddLoginAsync(identity.Id, login)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task AddLogin_should_succeed_if_found() |
||||
|
{ |
||||
|
var login = A.Fake<ExternalLoginInfo>(); |
||||
|
|
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
await sut.AddLoginAsync(identity.Id, login); |
||||
|
|
||||
|
A.CallTo(() => userManager.AddLoginAsync(identity, login)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task RemoveLogin_should_throw_exception_if_not_found() |
||||
|
{ |
||||
|
var provider = "my-provider"; |
||||
|
var providerKey = "key"; |
||||
|
|
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.RemoveLoginAsync(identity.Id, provider, providerKey)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task RemoveLogin_should_succeed_if_found() |
||||
|
{ |
||||
|
var provider = "my-provider"; |
||||
|
var providerKey = "key"; |
||||
|
|
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
await sut.RemoveLoginAsync(identity.Id, provider, providerKey); |
||||
|
|
||||
|
A.CallTo(() => userManager.RemoveLoginAsync(identity, provider, providerKey)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Lock_should_throw_exception_if_not_found() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.LockAsync(identity.Id)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Lock_should_succeed_if_found() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
await sut.LockAsync(identity.Id); |
||||
|
|
||||
|
A.CallTo<Task<IdentityResult>>(() => userManager.SetLockoutEndDateAsync(identity, InFuture())) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Unlock_should_throw_exception_if_not_found() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.UnlockAsync(identity.Id)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Unlock_should_succeeed_if_found() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
await sut.UnlockAsync(identity.Id); |
||||
|
|
||||
|
A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, null)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Delete_should_throw_exception_if_not_found() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: false); |
||||
|
|
||||
|
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.DeleteAsync(identity.Id)); |
||||
|
|
||||
|
A.CallTo(() => userEvents.OnUserDeleted(A<IUser>._)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Delete_should_succeed_if_found() |
||||
|
{ |
||||
|
var identity = CreateIdentity(found: true); |
||||
|
|
||||
|
await sut.DeleteAsync(identity.Id); |
||||
|
|
||||
|
A.CallTo(() => userManager.DeleteAsync(identity)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => userEvents.OnUserDeleted(A<IUser>.That.Matches(x => x.Identity == identity))) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
private IdentityUser CreateIdentity(bool found, string id = "123") |
||||
|
{ |
||||
|
var identity = CreatePendingUser(id); |
||||
|
|
||||
|
if (found) |
||||
|
{ |
||||
|
A.CallTo(() => userManager.FindByIdAsync(identity.Id)) |
||||
|
.Returns(identity); |
||||
|
|
||||
|
A.CallTo(() => userManager.FindByEmailAsync(identity.Email)) |
||||
|
.Returns(identity); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
A.CallTo(() => userManager.FindByIdAsync(identity.Id)) |
||||
|
.Returns(Task.FromResult<IdentityUser>(null!)); |
||||
|
|
||||
|
A.CallTo(() => userManager.FindByEmailAsync(identity.Email)) |
||||
|
.Returns(Task.FromResult<IdentityUser>(null!)); |
||||
|
} |
||||
|
|
||||
|
return identity; |
||||
|
} |
||||
|
|
||||
|
private void SetupCreation(IdentityUser identity, UserValues values, int numCurrentUsers) |
||||
|
{ |
||||
|
var users = new List<IdentityUser>(); |
||||
|
|
||||
|
for (var i = 0; i < numCurrentUsers; i++) |
||||
|
{ |
||||
|
users.Add(CreatePendingUser(i.ToString())); |
||||
|
} |
||||
|
|
||||
|
A.CallTo(() => userManager.Users) |
||||
|
.Returns(users.AsQueryable()); |
||||
|
|
||||
|
A.CallTo(() => userFactory.Create(identity.Email)) |
||||
|
.Returns(identity); |
||||
|
} |
||||
|
|
||||
|
private static IEnumerable<Claim> HasClaim(string claim) |
||||
|
{ |
||||
|
return A<IEnumerable<Claim>>.That.Matches(x => x.Any(y => y.Type == claim)); |
||||
|
} |
||||
|
|
||||
|
private static IEnumerable<Claim> HasClaim(string claim, string value) |
||||
|
{ |
||||
|
return A<IEnumerable<Claim>>.That.Matches(x => x.Any(y => y.Type == claim && y.Value == value)); |
||||
|
} |
||||
|
|
||||
|
private static DateTimeOffset InFuture() |
||||
|
{ |
||||
|
return A<DateTimeOffset>.That.Matches(x => x >= DateTimeOffset.UtcNow.AddYears(1)); |
||||
|
} |
||||
|
|
||||
|
private static IdentityUser CreatePendingUser(string id = "123") |
||||
|
{ |
||||
|
return new IdentityUser |
||||
|
{ |
||||
|
Id = id, |
||||
|
Email = $"{id}@email.com" |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue