mirror of https://github.com/abpframework/abp.git
20 changed files with 781 additions and 40 deletions
@ -0,0 +1,270 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.Data; |
||||
|
|
||||
|
public static class DataFilterExtensions |
||||
|
{ |
||||
|
private sealed class CompositeDisposable : IDisposable |
||||
|
{ |
||||
|
private readonly IDisposable[] _disposables; |
||||
|
private bool _disposed; |
||||
|
|
||||
|
public CompositeDisposable(IDisposable[] disposables) |
||||
|
{ |
||||
|
_disposables = disposables; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if (_disposed) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_disposed = true; |
||||
|
|
||||
|
foreach (var disposable in _disposables) |
||||
|
{ |
||||
|
disposable?.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Disable<T1, T2>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Disable<T1>(), |
||||
|
filter.Disable<T2>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Disable<T1, T2, T3>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Disable<T1>(), |
||||
|
filter.Disable<T2>(), |
||||
|
filter.Disable<T3>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Disable<T1, T2, T3, T4>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
where T4 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Disable<T1>(), |
||||
|
filter.Disable<T2>(), |
||||
|
filter.Disable<T3>(), |
||||
|
filter.Disable<T4>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Disable<T1, T2, T3, T4, T5>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
where T4 : class |
||||
|
where T5 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Disable<T1>(), |
||||
|
filter.Disable<T2>(), |
||||
|
filter.Disable<T3>(), |
||||
|
filter.Disable<T4>(), |
||||
|
filter.Disable<T5>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Disable<T1, T2, T3, T4, T5, T6>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
where T4 : class |
||||
|
where T5 : class |
||||
|
where T6 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Disable<T1>(), |
||||
|
filter.Disable<T2>(), |
||||
|
filter.Disable<T3>(), |
||||
|
filter.Disable<T4>(), |
||||
|
filter.Disable<T5>(), |
||||
|
filter.Disable<T6>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Disable<T1, T2, T3, T4, T5, T6, T7>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
where T4 : class |
||||
|
where T5 : class |
||||
|
where T6 : class |
||||
|
where T7 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Disable<T1>(), |
||||
|
filter.Disable<T2>(), |
||||
|
filter.Disable<T3>(), |
||||
|
filter.Disable<T4>(), |
||||
|
filter.Disable<T5>(), |
||||
|
filter.Disable<T6>(), |
||||
|
filter.Disable<T7>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Disable<T1, T2, T3, T4, T5, T6, T7, T8>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
where T4 : class |
||||
|
where T5 : class |
||||
|
where T6 : class |
||||
|
where T7 : class |
||||
|
where T8 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Disable<T1>(), |
||||
|
filter.Disable<T2>(), |
||||
|
filter.Disable<T3>(), |
||||
|
filter.Disable<T4>(), |
||||
|
filter.Disable<T5>(), |
||||
|
filter.Disable<T6>(), |
||||
|
filter.Disable<T7>(), |
||||
|
filter.Disable<T8>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Enable<T1, T2>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Enable<T1>(), |
||||
|
filter.Enable<T2>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Enable<T1, T2, T3>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Enable<T1>(), |
||||
|
filter.Enable<T2>(), |
||||
|
filter.Enable<T3>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Enable<T1, T2, T3, T4>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
where T4 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Enable<T1>(), |
||||
|
filter.Enable<T2>(), |
||||
|
filter.Enable<T3>(), |
||||
|
filter.Enable<T4>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Enable<T1, T2, T3, T4, T5>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
where T4 : class |
||||
|
where T5 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Enable<T1>(), |
||||
|
filter.Enable<T2>(), |
||||
|
filter.Enable<T3>(), |
||||
|
filter.Enable<T4>(), |
||||
|
filter.Enable<T5>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Enable<T1, T2, T3, T4, T5, T6>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
where T4 : class |
||||
|
where T5 : class |
||||
|
where T6 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Enable<T1>(), |
||||
|
filter.Enable<T2>(), |
||||
|
filter.Enable<T3>(), |
||||
|
filter.Enable<T4>(), |
||||
|
filter.Enable<T5>(), |
||||
|
filter.Enable<T6>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Enable<T1, T2, T3, T4, T5, T6, T7>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
where T4 : class |
||||
|
where T5 : class |
||||
|
where T6 : class |
||||
|
where T7 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Enable<T1>(), |
||||
|
filter.Enable<T2>(), |
||||
|
filter.Enable<T3>(), |
||||
|
filter.Enable<T4>(), |
||||
|
filter.Enable<T5>(), |
||||
|
filter.Enable<T6>(), |
||||
|
filter.Enable<T7>() |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Enable<T1, T2, T3, T4, T5, T6, T7, T8>(this IDataFilter filter) |
||||
|
where T1 : class |
||||
|
where T2 : class |
||||
|
where T3 : class |
||||
|
where T4 : class |
||||
|
where T5 : class |
||||
|
where T6 : class |
||||
|
where T7 : class |
||||
|
where T8 : class |
||||
|
{ |
||||
|
return new CompositeDisposable(new[] |
||||
|
{ |
||||
|
filter.Enable<T1>(), |
||||
|
filter.Enable<T2>(), |
||||
|
filter.Enable<T3>(), |
||||
|
filter.Enable<T4>(), |
||||
|
filter.Enable<T5>(), |
||||
|
filter.Enable<T6>(), |
||||
|
filter.Enable<T7>(), |
||||
|
filter.Enable<T8>() |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
namespace Volo.Abp.EntityFrameworkCore.GlobalFilters; |
||||
|
|
||||
|
public interface IAbpEfCoreCompiledQueryCacheKeyProvider |
||||
|
{ |
||||
|
string? GetCompiledQueryCacheKey(); |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
namespace Volo.Abp.MultiTenancy; |
||||
|
|
||||
|
public enum TenantUserSharingStrategy |
||||
|
{ |
||||
|
Isolated = 0, |
||||
|
|
||||
|
Shared = 1 |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
|
||||
|
namespace Volo.Abp.Identity; |
||||
|
|
||||
|
[Serializable] |
||||
|
public class IdentityUserPasswordChangedEto : IMultiTenant |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public Guid? TenantId { get; set; } |
||||
|
|
||||
|
public string Email { get; set; } |
||||
|
} |
||||
@ -1,66 +1,206 @@ |
|||||
|
using System; |
||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using System.Linq; |
||||
using System.Threading.Tasks; |
using System.Threading.Tasks; |
||||
using Microsoft.AspNetCore.Identity; |
using Microsoft.AspNetCore.Identity; |
||||
using Microsoft.Extensions.Localization; |
using Microsoft.Extensions.Options; |
||||
using Volo.Abp.Identity.Localization; |
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DistributedLocking; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
|
||||
namespace Volo.Abp.Identity |
namespace Volo.Abp.Identity |
||||
{ |
{ |
||||
public class AbpIdentityUserValidator : IUserValidator<IdentityUser> |
public class AbpIdentityUserValidator : IUserValidator<IdentityUser> |
||||
{ |
{ |
||||
protected IStringLocalizer<IdentityResource> Localizer { get; } |
protected IdentityErrorDescriber ErrorDescriber { get; } |
||||
|
protected IOptions<AbpMultiTenancyOptions> MultiTenancyOptions { get; } |
||||
|
protected IAbpDistributedLock DistributedLock { get; } |
||||
|
protected ICurrentTenant CurrentTenant { get; } |
||||
|
protected IDataFilter<IMultiTenant> TenantFilter { get; } |
||||
|
protected IIdentityUserRepository UserRepository { get; } |
||||
|
protected IUserValidator<IdentityUser> DefaultUserValidator { get; } |
||||
|
|
||||
public AbpIdentityUserValidator(IStringLocalizer<IdentityResource> localizer) |
public AbpIdentityUserValidator( |
||||
|
IdentityErrorDescriber errorDescriber, |
||||
|
IOptions<AbpMultiTenancyOptions> multiTenancyOptions, |
||||
|
IAbpDistributedLock distributedLock, |
||||
|
ICurrentTenant currentTenant, |
||||
|
IDataFilter<IMultiTenant> tenantFilter, |
||||
|
IIdentityUserRepository userRepository) |
||||
{ |
{ |
||||
Localizer = localizer; |
ErrorDescriber = errorDescriber; |
||||
|
MultiTenancyOptions = multiTenancyOptions; |
||||
|
DistributedLock = distributedLock; |
||||
|
CurrentTenant = currentTenant; |
||||
|
TenantFilter = tenantFilter; |
||||
|
UserRepository = userRepository; |
||||
|
DefaultUserValidator = new UserValidator<IdentityUser>(ErrorDescriber); |
||||
} |
} |
||||
|
|
||||
public virtual async Task<IdentityResult> ValidateAsync(UserManager<IdentityUser> manager, IdentityUser user) |
public virtual async Task<IdentityResult> ValidateAsync(UserManager<IdentityUser> manager, IdentityUser user) |
||||
{ |
{ |
||||
var describer = new IdentityErrorDescriber(); |
|
||||
|
|
||||
Check.NotNull(manager, nameof(manager)); |
Check.NotNull(manager, nameof(manager)); |
||||
Check.NotNull(user, nameof(user)); |
Check.NotNull(user, nameof(user)); |
||||
|
|
||||
|
return MultiTenancyOptions.Value.UserSharingStrategy == TenantUserSharingStrategy.Isolated |
||||
|
? await ValidateIsolatedUserAsync(manager, user) |
||||
|
: await ValidateSharedUserAsync(manager, user); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<IdentityResult> ValidateIsolatedUserAsync(UserManager<IdentityUser> manager, IdentityUser user) |
||||
|
{ |
||||
var errors = new List<IdentityError>(); |
var errors = new List<IdentityError>(); |
||||
|
|
||||
|
var defaultValidationResult = await DefaultUserValidator.ValidateAsync(manager, user); |
||||
|
if (!defaultValidationResult.Succeeded) |
||||
|
{ |
||||
|
return defaultValidationResult; |
||||
|
} |
||||
|
|
||||
var userName = await manager.GetUserNameAsync(user); |
var userName = await manager.GetUserNameAsync(user); |
||||
if (userName == null) |
if (userName == null) |
||||
{ |
{ |
||||
errors.Add(describer.InvalidUserName(null)); |
errors.Add(ErrorDescriber.InvalidUserName(null)); |
||||
} |
} |
||||
else |
else |
||||
{ |
{ |
||||
var owner = await manager.FindByEmailAsync(userName); |
var owner = await manager.FindByEmailAsync(userName); |
||||
if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user))) |
if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user))) |
||||
{ |
{ |
||||
errors.Add(new IdentityError |
errors.Add(ErrorDescriber.InvalidUserName(userName)); |
||||
{ |
|
||||
Code = "InvalidUserName", |
|
||||
Description = Localizer["Volo.Abp.Identity:InvalidUserName", userName] |
|
||||
}); |
|
||||
} |
} |
||||
} |
} |
||||
|
|
||||
var email = await manager.GetEmailAsync(user); |
var email = await manager.GetEmailAsync(user); |
||||
if (email == null) |
if (email == null) |
||||
{ |
{ |
||||
errors.Add(describer.InvalidEmail(null)); |
errors.Add(ErrorDescriber.InvalidEmail(null)); |
||||
} |
} |
||||
else |
else |
||||
{ |
{ |
||||
var owner = await manager.FindByNameAsync(email); |
var owner = await manager.FindByNameAsync(email); |
||||
if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user))) |
if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user))) |
||||
{ |
{ |
||||
errors.Add(new IdentityError |
errors.Add(ErrorDescriber.InvalidEmail(email)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return errors.Count > 0 ? IdentityResult.Failed(errors.ToArray()) : IdentityResult.Success; |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<IdentityResult> ValidateSharedUserAsync(UserManager<IdentityUser> manager, IdentityUser user) |
||||
|
{ |
||||
|
var errors = new List<IdentityError>(); |
||||
|
|
||||
|
var defaultValidationResult = await BuiltInValidateAsync(manager, user); |
||||
|
if (!defaultValidationResult.Succeeded) |
||||
|
{ |
||||
|
return defaultValidationResult; |
||||
|
} |
||||
|
|
||||
|
await using var handle = await DistributedLock.TryAcquireAsync(nameof(AbpIdentityUserValidator), TimeSpan.FromMinutes(1)); |
||||
|
if (handle == null) |
||||
|
{ |
||||
|
throw new AbpException("Could not acquire distributed lock for validating user uniqueness for shared user sharing strategy!"); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(null)) |
||||
|
{ |
||||
|
using (TenantFilter.Disable()) |
||||
|
{ |
||||
|
var owner = await manager.FindByIdAsync(user.Id.ToString()); |
||||
|
var normalizedUserName = manager.NormalizeName(user.UserName); |
||||
|
var normalizedEmail = manager.NormalizeEmail(user.Email); |
||||
|
var users = await UserRepository.GetUsersByNormalizedUserNamesAsync([normalizedUserName!, normalizedEmail], true); |
||||
|
users.RemoveAll(x => x.Id == user.Id); |
||||
|
if (owner != null) |
||||
{ |
{ |
||||
Code = "InvalidEmail", |
users.RemoveAll(x => x.NormalizedUserName == user.NormalizedUserName || x.NormalizedEmail == user.NormalizedEmail); |
||||
Description = Localizer["Volo.Abp.Identity:InvalidEmail", email] |
} |
||||
}); |
if (users.Any()) |
||||
|
{ |
||||
|
var userNames = users.Select(u => u.UserName).ToList(); |
||||
|
errors.Add(userNames.Contains(user.UserName) ? ErrorDescriber.InvalidUserName(user.UserName!) : ErrorDescriber.InvalidEmail(user.Email!)); |
||||
|
} |
||||
|
|
||||
|
users = await UserRepository.GetUsersByNormalizedEmailsAsync([normalizedUserName!, normalizedEmail], true); |
||||
|
users.RemoveAll(x => x.Id == user.Id); |
||||
|
if (owner != null) |
||||
|
{ |
||||
|
users.RemoveAll(x => x.NormalizedUserName == user.NormalizedUserName || x.NormalizedEmail == user.NormalizedEmail); |
||||
|
} |
||||
|
if (users.Any()) |
||||
|
{ |
||||
|
var emails = users.Select(u => u.Email).ToList(); |
||||
|
errors.Add(emails.Contains(user.Email) ? ErrorDescriber.InvalidEmail(user.Email!) : ErrorDescriber.InvalidUserName(user.UserName!)); |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
return errors.Count > 0 ? IdentityResult.Failed(errors.ToArray()) : IdentityResult.Success; |
return errors.Count > 0 ? IdentityResult.Failed(errors.ToArray()) : IdentityResult.Success; |
||||
} |
} |
||||
|
|
||||
|
public virtual async Task<IdentityResult> BuiltInValidateAsync(UserManager<IdentityUser> manager, IdentityUser user) |
||||
|
{ |
||||
|
var errors = await ValidateUserName(manager, user); |
||||
|
if (manager.Options.User.RequireUniqueEmail) |
||||
|
{ |
||||
|
errors.AddRange(await ValidateEmail(manager, user)); |
||||
|
} |
||||
|
return errors?.Count > 0 ? IdentityResult.Failed(errors.ToArray()) : IdentityResult.Success; |
||||
|
} |
||||
|
|
||||
|
private async Task<List<IdentityError>> ValidateUserName(UserManager<IdentityUser> manager, IdentityUser user) |
||||
|
{ |
||||
|
var errors = new List<IdentityError>(); |
||||
|
var userName = await manager.GetUserNameAsync(user); |
||||
|
if (string.IsNullOrWhiteSpace(userName)) |
||||
|
{ |
||||
|
errors.Add(ErrorDescriber.InvalidUserName(userName)); |
||||
|
} |
||||
|
else if (!string.IsNullOrEmpty(manager.Options.User.AllowedUserNameCharacters) && |
||||
|
userName.Any(c => !manager.Options.User.AllowedUserNameCharacters.Contains(c))) |
||||
|
{ |
||||
|
errors.Add(ErrorDescriber.InvalidUserName(userName)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var owner = await manager.FindByNameAsync(userName); |
||||
|
if (owner != null && |
||||
|
!string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user)) && |
||||
|
owner.TenantId == user.TenantId) |
||||
|
{ |
||||
|
errors.Add(ErrorDescriber.DuplicateUserName(userName)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return errors; |
||||
|
} |
||||
|
|
||||
|
// make sure email is not empty, valid, and unique
|
||||
|
private async Task<List<IdentityError>> ValidateEmail(UserManager<IdentityUser> manager, IdentityUser user) |
||||
|
{ |
||||
|
var errors = new List<IdentityError>(); |
||||
|
var email = await manager.GetEmailAsync(user); |
||||
|
if (string.IsNullOrWhiteSpace(email)) |
||||
|
{ |
||||
|
errors.Add(ErrorDescriber.InvalidEmail(email)); |
||||
|
return errors; |
||||
|
} |
||||
|
if (!new EmailAddressAttribute().IsValid(email)) |
||||
|
{ |
||||
|
errors.Add(ErrorDescriber.InvalidEmail(email)); |
||||
|
return errors; |
||||
|
} |
||||
|
var owner = await manager.FindByEmailAsync(email); |
||||
|
if (owner != null && |
||||
|
!string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user)) && |
||||
|
owner.TenantId == user.TenantId) |
||||
|
{ |
||||
|
errors.Add(ErrorDescriber.DuplicateEmail(email)); |
||||
|
} |
||||
|
return errors; |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,14 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.EventBus; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
|
||||
|
namespace Volo.Abp.Users; |
||||
|
|
||||
|
[Serializable] |
||||
|
[EventName("Volo.Abp.Users.InviteUserToTenantRequested")] |
||||
|
public class InviteUserToTenantRequestedEto : IMultiTenant |
||||
|
{ |
||||
|
public Guid? TenantId { get; set; } |
||||
|
|
||||
|
public string Email { get; set; } |
||||
|
} |
||||
Loading…
Reference in new issue