Browse Source

Add shared user account infrastructure.

pull/24456/head
maliming 7 months ago
parent
commit
f21ffe274f
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 270
      framework/src/Volo.Abp.Data/Volo/Abp/Data/DataFilterExtensions.cs
  2. 2
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  3. 18
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpCompiledQueryCacheKeyGenerator.cs
  4. 6
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/IAbpEfCoreCompiledQueryCacheKeyProvider.cs
  5. 2
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/IAbpEfCoreDbFunctionContext.cs
  6. 38
      framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs
  7. 18
      framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbModule.cs
  8. 8
      framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpMultiTenancyOptions.cs
  9. 8
      framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantUserSharingStrategy.cs
  10. 3
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs
  11. 14
      modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserPasswordChangedEto.cs
  12. 176
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityUserValidator.cs
  13. 38
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs
  14. 42
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs
  15. 33
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs
  16. 60
      modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs
  17. 3
      modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs
  18. 53
      modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs
  19. 15
      modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityUserValidator_Tests.cs
  20. 14
      modules/users/src/Volo.Abp.Users.Abstractions/Volo/Abp/Users/InviteUserToTenantRequestedEto.cs

270
framework/src/Volo.Abp.Data/Volo/Abp/Data/DataFilterExtensions.cs

@ -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>()
});
}
}

2
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs

@ -956,7 +956,7 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
return expression;
}
protected virtual bool UseDbFunction()
public virtual bool UseDbFunction()
{
return LazyServiceProvider != null && GlobalFilterOptions.Value.UseDbFunction && DbContextOptions.FindExtension<AbpDbContextOptionsExtension>() != null;
}

18
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpCompiledQueryCacheKeyGenerator.cs

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.DependencyInjection;
namespace Volo.Abp.EntityFrameworkCore.GlobalFilters;
@ -23,7 +25,21 @@ public class AbpCompiledQueryCacheKeyGenerator : ICompiledQueryCacheKeyGenerator
var cacheKey = InnerCompiledQueryCacheKeyGenerator.GenerateCacheKey(query, async);
if (CurrentContext.Context is IAbpEfCoreDbFunctionContext abpEfCoreDbFunctionContext)
{
return new AbpCompiledQueryCacheKey(cacheKey, abpEfCoreDbFunctionContext.GetCompiledQueryCacheKey());
var abpCacheKey = abpEfCoreDbFunctionContext.GetCompiledQueryCacheKey();
var cacheKeyProviders = abpEfCoreDbFunctionContext.LazyServiceProvider.GetService<IEnumerable<IAbpEfCoreCompiledQueryCacheKeyProvider>>();
if (cacheKeyProviders != null)
{
foreach (var provider in cacheKeyProviders)
{
var key = provider.GetCompiledQueryCacheKey();
if (!key.IsNullOrWhiteSpace())
{
abpCacheKey += $":{key}";
}
}
}
return new AbpCompiledQueryCacheKey(cacheKey, abpCacheKey);
}
return cacheKey;

6
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/IAbpEfCoreCompiledQueryCacheKeyProvider.cs

@ -0,0 +1,6 @@
namespace Volo.Abp.EntityFrameworkCore.GlobalFilters;
public interface IAbpEfCoreCompiledQueryCacheKeyProvider
{
string? GetCompiledQueryCacheKey();
}

2
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/IAbpEfCoreDbFunctionContext.cs

@ -12,5 +12,7 @@ public interface IAbpEfCoreDbFunctionContext
IDataFilter DataFilter { get; }
bool UseDbFunction();
string GetCompiledQueryCacheKey();
}

38
framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs

@ -98,7 +98,7 @@ public class MongoDbRepository<TMongoDbContext, TEntity>
public IMongoDbBulkOperationProvider? BulkOperationProvider => LazyServiceProvider.LazyGetService<IMongoDbBulkOperationProvider>();
public IMongoDbRepositoryFilterer<TEntity> RepositoryFilterer => LazyServiceProvider.LazyGetService<IMongoDbRepositoryFilterer<TEntity>>()!;
public IEnumerable<IMongoDbRepositoryFilterer<TEntity>> RepositoryFilterers => LazyServiceProvider.LazyGetService<IEnumerable<IMongoDbRepositoryFilterer<TEntity>>>()!;
public MongoDbRepository(IMongoDbContextProvider<TMongoDbContext> dbContextProvider)
: base(AbpMongoDbConsts.ProviderName)
@ -774,7 +774,10 @@ public class MongoDbRepository<TMongoDbContext, TEntity>
{
if (typeof(TOtherEntity) == typeof(TEntity))
{
return base.ApplyDataFilters<TQueryable, TOtherEntity>((TQueryable)RepositoryFilterer.FilterQueryable(query.As<IQueryable<TEntity>>()));
foreach (var filterer in RepositoryFilterers)
{
query = (TQueryable) filterer.FilterQueryable(query.As<IQueryable<TEntity>>());
}
}
return base.ApplyDataFilters<TQueryable, TOtherEntity>(query);
}
@ -786,7 +789,7 @@ public class MongoDbRepository<TMongoDbContext, TEntity, TKey>
where TMongoDbContext : IAbpMongoDbContext
where TEntity : class, IEntity<TKey>
{
public IMongoDbRepositoryFilterer<TEntity, TKey> RepositoryFiltererWithKey => LazyServiceProvider.LazyGetService<IMongoDbRepositoryFilterer<TEntity, TKey>>()!;
public IEnumerable<IMongoDbRepositoryFilterer<TEntity, TKey>> RepositoryFiltererWithKeys => LazyServiceProvider.LazyGetService<IEnumerable<IMongoDbRepositoryFilterer<TEntity, TKey>>>()!;
public MongoDbRepository(IMongoDbContextProvider<TMongoDbContext> dbContextProvider)
: base(dbContextProvider)
@ -844,19 +847,38 @@ public class MongoDbRepository<TMongoDbContext, TEntity, TKey>
{
if (typeof(TOtherEntity) == typeof(TEntity))
{
return base.ApplyDataFilters<TQueryable, TOtherEntity>((TQueryable)RepositoryFiltererWithKey.FilterQueryable(query.As<IQueryable<TEntity>>()));
foreach (var filterer in RepositoryFiltererWithKeys)
{
query = (TQueryable) filterer.FilterQueryable(query.As<IQueryable<TEntity>>());
}
}
return base.ApplyDataFilters<TQueryable, TOtherEntity>(query);
}
protected async override Task<FilterDefinition<TEntity>> CreateEntityFilterAsync(TEntity entity, bool withConcurrencyStamp = false, string? concurrencyStamp = null)
protected override async Task<FilterDefinition<TEntity>> CreateEntityFilterAsync(TEntity entity, bool withConcurrencyStamp = false, string? concurrencyStamp = null)
{
return await RepositoryFiltererWithKey.CreateEntityFilterAsync(entity, withConcurrencyStamp, concurrencyStamp);
FilterDefinition<TEntity> fieldDefinition = Builders<TEntity>.Filter.Empty;
foreach (var filterer in RepositoryFiltererWithKeys)
{
fieldDefinition = Builders<TEntity>.Filter.And(
fieldDefinition,
await filterer.CreateEntityFilterAsync(entity, withConcurrencyStamp, concurrencyStamp)
);
}
return fieldDefinition;
}
protected async override Task<FilterDefinition<TEntity>> CreateEntitiesFilterAsync(IEnumerable<TEntity> entities, bool withConcurrencyStamp = false)
protected override async Task<FilterDefinition<TEntity>> CreateEntitiesFilterAsync(IEnumerable<TEntity> entities, bool withConcurrencyStamp = false)
{
return await RepositoryFiltererWithKey.CreateEntitiesFilterAsync(entities, withConcurrencyStamp);
FilterDefinition<TEntity> fieldDefinition = Builders<TEntity>.Filter.Empty;
foreach (var filterer in RepositoryFiltererWithKeys)
{
fieldDefinition = Builders<TEntity>.Filter.And(
fieldDefinition,
await filterer.CreateEntitiesFilterAsync(entities, withConcurrencyStamp)
);
}
return fieldDefinition;
}
}

18
framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbModule.cs

@ -36,15 +36,17 @@ public class AbpMongoDbModule : AbpModule
typeof(UnitOfWorkMongoDbContextProvider<>)
);
context.Services.TryAddTransient(
typeof(IMongoDbRepositoryFilterer<>),
typeof(MongoDbRepositoryFilterer<>)
);
context.Services.TryAddEnumerable(
ServiceDescriptor.Transient(
typeof(IMongoDbRepositoryFilterer<>),
typeof(MongoDbRepositoryFilterer<>)
));
context.Services.TryAddTransient(
typeof(IMongoDbRepositoryFilterer<,>),
typeof(MongoDbRepositoryFilterer<,>)
);
context.Services.TryAddEnumerable(
ServiceDescriptor.Transient(
typeof(IMongoDbRepositoryFilterer<,>),
typeof(MongoDbRepositoryFilterer<,>)
));
context.Services.AddTransient(
typeof(IMongoDbContextEventOutbox<>),

8
framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpMultiTenancyOptions.cs

@ -4,7 +4,7 @@ public class AbpMultiTenancyOptions
{
/// <summary>
/// A central point to enable/disable multi-tenancy.
/// Default: false.
/// Default: false.
/// </summary>
public bool IsEnabled { get; set; }
@ -13,4 +13,10 @@ public class AbpMultiTenancyOptions
/// Default: <see cref="MultiTenancyDatabaseStyle.Hybrid"/>.
/// </summary>
public MultiTenancyDatabaseStyle DatabaseStyle { get; set; } = MultiTenancyDatabaseStyle.Hybrid;
/// <summary>
/// User sharing strategy between tenants.
/// Default: <see cref="TenantUserSharingStrategy.Isolated"/>.
/// </summary>
public TenantUserSharingStrategy UserSharingStrategy { get; set; } = TenantUserSharingStrategy.Isolated;
}

8
framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantUserSharingStrategy.cs

@ -0,0 +1,8 @@
namespace Volo.Abp.MultiTenancy;
public enum TenantUserSharingStrategy
{
Isolated = 0,
Shared = 1
}

3
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -47,6 +48,8 @@ public class AbpIdentityAspNetCoreModule : AbpModule
public override void PostConfigureServices(ServiceConfigurationContext context)
{
// Replace the default UserValidator with AbpIdentityUserValidator
context.Services.RemoveAll(x => x.ServiceType == typeof(IUserValidator<IdentityUser>) && x.ImplementationType == typeof(UserValidator<IdentityUser>));
context.Services.AddAbpOptions<SecurityStampValidatorOptions>()
.Configure<IServiceProvider>((securityStampValidatorOptions, serviceProvider) =>
{

14
modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserPasswordChangedEto.cs

@ -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; }
}

176
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityUserValidator.cs

@ -1,66 +1,206 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Localization;
using Volo.Abp.Identity.Localization;
using Microsoft.Extensions.Options;
using Volo.Abp.Data;
using Volo.Abp.DistributedLocking;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Identity
{
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)
{
var describer = new IdentityErrorDescriber();
Check.NotNull(manager, nameof(manager));
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 defaultValidationResult = await DefaultUserValidator.ValidateAsync(manager, user);
if (!defaultValidationResult.Succeeded)
{
return defaultValidationResult;
}
var userName = await manager.GetUserNameAsync(user);
if (userName == null)
{
errors.Add(describer.InvalidUserName(null));
errors.Add(ErrorDescriber.InvalidUserName(null));
}
else
{
var owner = await manager.FindByEmailAsync(userName);
if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user)))
{
errors.Add(new IdentityError
{
Code = "InvalidUserName",
Description = Localizer["Volo.Abp.Identity:InvalidUserName", userName]
});
errors.Add(ErrorDescriber.InvalidUserName(userName));
}
}
var email = await manager.GetEmailAsync(user);
if (email == null)
{
errors.Add(describer.InvalidEmail(null));
errors.Add(ErrorDescriber.InvalidEmail(null));
}
else
{
var owner = await manager.FindByNameAsync(email);
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",
Description = Localizer["Volo.Abp.Identity:InvalidEmail", email]
});
users.RemoveAll(x => x.NormalizedUserName == user.NormalizedUserName || x.NormalizedEmail == user.NormalizedEmail);
}
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;
}
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;
}
}
}

38
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs

@ -165,4 +165,42 @@ public interface IIdentityUserRepository : IBasicRepository<IdentityUser, Guid>
byte[] credentialId,
bool includeDetails = true,
CancellationToken cancellationToken = default);
Task<List<IdentityUser>> GetUsersByNormalizedUserNameAsync(
[NotNull] string normalizedUserName,
bool includeDetails = false,
CancellationToken cancellationToken = default
);
Task<List<IdentityUser>> GetUsersByNormalizedUserNamesAsync(
[NotNull] string[] normalizedUserNames,
bool includeDetails = false,
CancellationToken cancellationToken = default
);
Task<List<IdentityUser>> GetUsersByNormalizedEmailAsync(
[NotNull] string normalizedEmail,
bool includeDetails = false,
CancellationToken cancellationToken = default
);
Task<List<IdentityUser>> GetUsersByNormalizedEmailsAsync(
[NotNull] string[] normalizedEmails,
bool includeDetails = false,
CancellationToken cancellationToken = default
);
Task<IdentityUser> FindByNormalizedUserNameAsync(
Guid? tenantId,
[NotNull] string normalizedUserName,
bool includeDetails = true,
CancellationToken cancellationToken = default
);
Task<IdentityUser> FindByNormalizedEmailAsync(
Guid? tenantId,
[NotNull] string normalizedEmail,
bool includeDetails = true,
CancellationToken cancellationToken = default
);
}

42
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs

@ -132,6 +132,11 @@ public class IdentityUser : FullAuditedAggregateRoot<Guid>, IUser, IHasEntityVer
/// </summary>
public virtual DateTimeOffset? LastSignInTime { get; protected set; }
/// <summary>
/// Gets or sets a flag indicating whether this user is leaved from tenant.
/// </summary>
public virtual bool Leaved { get; protected set; }
//TODO: Can we make collections readonly collection, which will provide encapsulation. But... can work for all ORMs?
/// <summary>
@ -435,6 +440,43 @@ public class IdentityUser : FullAuditedAggregateRoot<Guid>, IUser, IHasEntityVer
Passkeys.RemoveAll(x => x.CredentialId.SequenceEqual(credentialId));
}
/// <summary>
/// This method set the UserName and normalizedUserName without any validation.
/// Do not use it directly. Use UserManager to change the user name.
/// </summary>
public virtual void SetUserNameWithoutValidation(string userName, string normalizedUserName)
{
UserName = userName;
NormalizedUserName = normalizedUserName;
}
/// <summary>
/// This method set the Email and NormalizedEmail without any validation.
/// Do not use it directly. Use UserManager to change the email.
/// </summary>
/// <param name="email"></param>
/// <param name="normalizedEmail"></param>
public virtual void SetEmailWithoutValidation(string email, string normalizedEmail)
{
Email = email;
NormalizedEmail = normalizedEmail;
}
/// <summary>
/// This method set the PasswordHash without any validation.
/// Do not use it directly. Use UserManager to change the password.
/// </summary>
/// <param name="passwordHash"></param>
public virtual void SetPasswordHashWithoutValidation(string passwordHash)
{
PasswordHash = passwordHash;
}
public virtual void SetLeaved(bool leaved)
{
Leaved = leaved;
}
public override string ToString()
{
return $"{base.ToString()}, UserName = {UserName}";

33
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs

@ -116,7 +116,7 @@ public class IdentityUserManager : UserManager<IdentityUser>, IDomainService
/// <returns>A <see cref="IdentityResult"/> representing whether validation was successful.</returns>
public virtual async Task<IdentityResult> CallValidateUserAsync(IdentityUser user)
{
return await base.ValidateUserAsync(user);
return await ValidateUserAsync(user);
}
/// <summary>
@ -129,7 +129,20 @@ public class IdentityUserManager : UserManager<IdentityUser>, IDomainService
/// <returns>A <see cref="IdentityResult"/> representing whether validation was successful.</returns>
public virtual async Task<IdentityResult> CallValidatePasswordAsync(IdentityUser user, string password)
{
return await base.ValidatePasswordAsync(user, password);
return await ValidatePasswordAsync(user, password);
}
/// <summary>
/// This is to call the protection method UpdatePasswordHash
/// Updates a user's password hash.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="newPassword">The new password.</param>
/// <param name="validatePassword">Whether to validate the password.</param>
/// <returns>Whether the password has was successfully updated.</returns>
public virtual async Task<IdentityResult> CallUpdatePasswordHash(IdentityUser user, string newPassword, bool validatePassword)
{
return await UpdatePasswordHash(user, newPassword, validatePassword);
}
public virtual async Task<IdentityUser> GetByIdAsync(Guid id)
@ -396,6 +409,22 @@ public class IdentityUserManager : UserManager<IdentityUser>, IDomainService
return result;
}
public override async Task<IdentityResult> ChangePasswordAsync(IdentityUser user, string currentPassword, string newPassword)
{
var result = await base.ChangePasswordAsync(user, currentPassword, newPassword);
result.CheckErrors();
await DistributedEventBus.PublishAsync(new IdentityUserPasswordChangedEto
{
Id = user.Id,
TenantId = user.TenantId,
Email = user.Email,
});
return result;
}
public virtual async Task UpdateRoleAsync(Guid sourceRoleId, Guid? targetRoleId)
{
var sourceRole = await RoleRepository.GetAsync(sourceRoleId, cancellationToken: CancellationToken);

60
modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs

@ -455,6 +455,66 @@ public class EfCoreIdentityUserRepository : EfCoreRepository<IIdentityDbContext,
}
}
public virtual async Task<List<IdentityUser>> GetUsersByNormalizedUserNameAsync(string normalizedUserName, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.OrderBy(x => x.Id)
.Where(u => u.NormalizedUserName == normalizedUserName)
.ToListAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task<List<IdentityUser>> GetUsersByNormalizedUserNamesAsync(string[] normalizedUserNames, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.OrderBy(x => x.Id)
.Where(u => normalizedUserNames.Contains(u.NormalizedUserName))
.Distinct()
.ToListAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task<List<IdentityUser>> GetUsersByNormalizedEmailAsync(string normalizedEmail, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.OrderBy(x => x.Id)
.Where(u => u.NormalizedEmail == normalizedEmail)
.ToListAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task<List<IdentityUser>> GetUsersByNormalizedEmailsAsync(string[] normalizedEmails, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.OrderBy(x => x.Id)
.Where(u => normalizedEmails.Contains(u.NormalizedEmail))
.Distinct()
.ToListAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task<IdentityUser> FindByNormalizedUserNameAsync(Guid? tenantId, string normalizedUserName, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(
u => u.TenantId == tenantId && u.NormalizedUserName == normalizedUserName,
GetCancellationToken(cancellationToken)
);
}
public virtual async Task<IdentityUser> FindByNormalizedEmailAsync(Guid? tenantId, string normalizedEmail, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(
u => u.TenantId == tenantId && u.NormalizedEmail == normalizedEmail,
GetCancellationToken(cancellationToken)
);
}
protected virtual async Task<IQueryable<IdentityUser>> GetFilteredQueryableAsync(
string filter = null,
Guid? roleId = null,

3
modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs

@ -33,7 +33,8 @@ public static class IdentityDbContextModelBuilderExtensions
.HasColumnName(nameof(IdentityUser.TwoFactorEnabled));
b.Property(u => u.LockoutEnabled).HasDefaultValue(false)
.HasColumnName(nameof(IdentityUser.LockoutEnabled));
b.Property(u => u.Leaved).HasDefaultValue(false)
.HasColumnName(nameof(IdentityUser.Leaved));
b.Property(u => u.IsExternal).IsRequired().HasDefaultValue(false)
.HasColumnName(nameof(IdentityUser.IsExternal));

53
modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs

@ -451,6 +451,59 @@ public class MongoIdentityUserRepository : MongoDbRepository<IAbpIdentityMongoDb
.FirstOrDefaultAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task<List<IdentityUser>> GetUsersByNormalizedUserNameAsync(string normalizedUserName, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync(cancellationToken))
.OrderBy(x => x.Id)
.Where(u => u.NormalizedUserName == normalizedUserName)
.ToListAsync(cancellationToken: cancellationToken);
}
public virtual async Task<List<IdentityUser>> GetUsersByNormalizedUserNamesAsync(string[] normalizedUserNames, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync(cancellationToken))
.OrderBy(x => x.Id)
.Where(u => normalizedUserNames.Contains(u.NormalizedUserName))
.Distinct()
.ToListAsync(cancellationToken: cancellationToken);
}
public virtual async Task<List<IdentityUser>> GetUsersByNormalizedEmailAsync(string normalizedEmail, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync(cancellationToken))
.OrderBy(x => x.Id)
.Where(u => u.NormalizedEmail == normalizedEmail)
.ToListAsync(cancellationToken: cancellationToken);
}
public virtual async Task<List<IdentityUser>> GetUsersByNormalizedEmailsAsync(string[] normalizedEmails, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync(cancellationToken))
.OrderBy(x => x.Id)
.Where(u => normalizedEmails.Contains(u.NormalizedEmail))
.Distinct()
.ToListAsync(cancellationToken: cancellationToken);
}
public virtual async Task<IdentityUser> FindByNormalizedUserNameAsync(Guid? tenantId, string normalizedUserName, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync(cancellationToken))
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(
u => u.TenantId == tenantId && u.NormalizedUserName == normalizedUserName,
GetCancellationToken(cancellationToken)
);
}
public virtual async Task<IdentityUser> FindByNormalizedEmailAsync(Guid? tenantId, string normalizedEmail, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync(cancellationToken))
.OrderBy(x => x.Id).FirstOrDefaultAsync(
u => u.TenantId == tenantId && u.NormalizedEmail == normalizedEmail,
GetCancellationToken(cancellationToken)
);
}
protected virtual async Task<IQueryable<IdentityUser>> GetFilteredQueryableAsync(
string filter = null,
Guid? roleId = null,

15
modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityUserValidator_Tests.cs

@ -1,9 +1,12 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Shouldly;
using Volo.Abp.Identity.Localization;
using Volo.Abp.MultiTenancy;
using Xunit;
namespace Volo.Abp.Identity.AspNetCore;
@ -60,3 +63,15 @@ public class AbpIdentityUserValidator_Tests : AbpIdentityAspNetCoreTestBase
identityResult.Errors.First().Description.ShouldBe(Localizer["Volo.Abp.Identity:InvalidEmail", "user1@volosoft.com"]);
}
}
public class SharedTenantUserSharingStrategy_AbpIdentityUserValidator_Tests : AbpIdentityUserValidator_Tests
{
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
services.Configure<AbpMultiTenancyOptions>(options =>
{
options.IsEnabled = true;
options.UserSharingStrategy = TenantUserSharingStrategy.Shared;
});
}
}

14
modules/users/src/Volo.Abp.Users.Abstractions/Volo/Abp/Users/InviteUserToTenantRequestedEto.cs

@ -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…
Cancel
Save