From d04425551661fcdc00bfdf9c5287cf34c7495bf6 Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Sun, 30 Aug 2020 18:07:19 +0800 Subject: [PATCH] Add IdentityLinkUser aggregate root to Identity module. --- .../AbpIdentityServiceCollectionExtensions.cs | 5 +- .../Identity/AbpUserClaimsPrincipalFactory.cs | 34 +++++++- .../Identity/IIdentityLinkUserRepository.cs | 20 +++++ .../Volo/Abp/Identity/IdentityLinkUser.cs | 36 ++++++++ .../Volo/Abp/Identity/IdentityLinkUserInfo.cs | 17 ++++ .../Abp/Identity/IdentityUserLinkManager.cs | 86 +++++++++++++++++++ .../Abp/Identity/LinkUserTokenProvider.cs | 17 ++++ .../EfCoreIdentityLinkUserRepository.cs | 39 +++++++++ .../EntityFrameworkCore/IIdentityDbContext.cs | 2 + .../EntityFrameworkCore/IdentityDbContext.cs | 2 + ...IdentityDbContextModelBuilderExtensions.cs | 16 ++++ .../MongoDB/AbpIdentityMongoDbContext.cs | 2 + .../AbpIdentityMongoDbContextExtensions.cs | 5 ++ .../MongoDB/AbpIdentityMongoDbModule.cs | 1 + .../MongoDB/IAbpIdentityMongoDbContext.cs | 2 + .../MongoIdentityLinkUserRepository.cs | 38 ++++++++ .../AspNetCore/LinkUserTokenProvider_Tests.cs | 20 +++++ .../Identity/IdentityLinkUserManager_Tests.cs | 86 +++++++++++++++++++ .../IdentityLinkUserRepository_Tests.cs | 7 ++ .../IdentityLinkUserRepository_Tests.cs | 10 +++ .../Identity/AbpIdentityTestDataBuilder.cs | 20 ++++- .../IdentityLinkUserRepository_Tests.cs | 58 +++++++++++++ 22 files changed, 516 insertions(+), 7 deletions(-) create mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityLinkUserRepository.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityLinkUser.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityLinkUserInfo.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserLinkManager.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/LinkUserTokenProvider.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityLinkUserRepository.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityLinkUserRepository.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/LinkUserTokenProvider_Tests.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityLinkUserManager_Tests.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/IdentityLinkUserRepository_Tests.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentityLinkUserRepository_Tests.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityLinkUserRepository_Tests.cs diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/Extensions/DependencyInjection/AbpIdentityServiceCollectionExtensions.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/Extensions/DependencyInjection/AbpIdentityServiceCollectionExtensions.cs index c2d91eafd7..c37889a897 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/Extensions/DependencyInjection/AbpIdentityServiceCollectionExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/Extensions/DependencyInjection/AbpIdentityServiceCollectionExtensions.cs @@ -29,11 +29,12 @@ namespace Microsoft.Extensions.DependencyInjection //AbpRoleStore services.TryAddScoped(); services.TryAddScoped(typeof(IRoleStore), provider => provider.GetService(typeof(IdentityRoleStore))); - + return services .AddIdentityCore(setupAction) .AddRoles() - .AddClaimsPrincipalFactory(); + .AddClaimsPrincipalFactory() + .AddTokenProvider(LinkUserTokenProvider.LinkUserTokenProviderName); } } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpUserClaimsPrincipalFactory.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpUserClaimsPrincipalFactory.cs index 8db2b95942..aa6263175a 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpUserClaimsPrincipalFactory.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpUserClaimsPrincipalFactory.cs @@ -4,22 +4,31 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; using Volo.Abp.Security.Claims; using Volo.Abp.Uow; +using Volo.Abp.Users; namespace Volo.Abp.Identity { public class AbpUserClaimsPrincipalFactory : UserClaimsPrincipalFactory, ITransientDependency { + protected ICurrentImpersonatorUser CurrentImpersonatorUser { get; } + protected ICurrentImpersonatorTenant CurrentImpersonatorTenant { get; } + public AbpUserClaimsPrincipalFactory( UserManager userManager, - RoleManager roleManager, - IOptions options) + RoleManager roleManager, + IOptions options, + ICurrentImpersonatorUser currentImpersonatorUser, + ICurrentImpersonatorTenant currentImpersonatorTenant) : base( - userManager, - roleManager, + userManager, + roleManager, options) { + CurrentImpersonatorUser = currentImpersonatorUser; + CurrentImpersonatorTenant = currentImpersonatorTenant; } [UnitOfWork] @@ -34,6 +43,23 @@ namespace Volo.Abp.Identity .AddClaim(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString())); } + if (CurrentImpersonatorUser.Id != user.Id || CurrentImpersonatorTenant.Id != user.TenantId) + { + if (CurrentImpersonatorUser.Id.HasValue) + { + principal.Identities + .First() + .AddClaim(new Claim(AbpClaimTypes.ImpersonatorUserId, CurrentImpersonatorUser.Id.ToString())); + } + + if (CurrentImpersonatorTenant.Id.HasValue) + { + principal.Identities + .First() + .AddClaim(new Claim(AbpClaimTypes.ImpersonatorTenantId, CurrentImpersonatorTenant.Id.ToString())); + } + } + return principal; } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityLinkUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityLinkUserRepository.cs new file mode 100644 index 0000000000..2040924d10 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityLinkUserRepository.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Identity +{ + public interface IIdentityLinkUserRepository : IBasicRepository + { + Task FindAsync( + IdentityLinkUserInfo sourceLinkUserInfo, + IdentityLinkUserInfo targetLinkUserInfo, + CancellationToken cancellationToken = default); + + Task> GetListAsync( + IdentityLinkUserInfo linkUserInfo, + CancellationToken cancellationToken = default); + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityLinkUser.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityLinkUser.cs new file mode 100644 index 0000000000..ca8fe157d5 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityLinkUser.cs @@ -0,0 +1,36 @@ +using System; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.Identity +{ + public class IdentityLinkUser : AggregateRoot + { + public IdentityLinkUser(Guid id, IdentityLinkUserInfo sourceUser, IdentityLinkUserInfo targetUser) + : base(id) + { + SourceUserId = sourceUser.UserId; + SourceTenantId = sourceUser.TenantId; + + TargetUserId = targetUser.UserId; + TargetTenantId = targetUser.TenantId; + } + + public IdentityLinkUser(Guid id, Guid sourceUserId, Guid? sourceTenantId, Guid targetUserId, Guid? targetTenantId) + : base(id) + { + SourceUserId = sourceUserId; + SourceTenantId = sourceTenantId; + + TargetUserId = targetUserId; + TargetTenantId = targetTenantId; + } + + public virtual Guid SourceUserId { get; protected set; } + + public virtual Guid? SourceTenantId { get; protected set; } + + public virtual Guid TargetUserId { get; protected set; } + + public virtual Guid? TargetTenantId { get; protected set; } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityLinkUserInfo.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityLinkUserInfo.cs new file mode 100644 index 0000000000..68edd57f4a --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityLinkUserInfo.cs @@ -0,0 +1,17 @@ +using System; + +namespace Volo.Abp.Identity +{ + public class IdentityLinkUserInfo + { + public virtual Guid UserId { get; set; } + + public virtual Guid? TenantId { get; set; } + + public IdentityLinkUserInfo(Guid userId, Guid? tenantId) + { + UserId = userId; + TenantId = tenantId; + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserLinkManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserLinkManager.cs new file mode 100644 index 0000000000..a067b259f4 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserLinkManager.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Domain.Services; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Identity +{ + public class IdentityLinkUserManager : DomainService + { + protected IIdentityLinkUserRepository IdentityLinkUserRepository { get; } + + protected IdentityUserManager UserManager { get; } + + protected new ICurrentTenant CurrentTenant { get; } + + public IdentityLinkUserManager(IIdentityLinkUserRepository identityLinkUserRepository, IdentityUserManager userManager, ICurrentTenant currentTenant) + { + IdentityLinkUserRepository = identityLinkUserRepository; + UserManager = userManager; + CurrentTenant = currentTenant; + } + + public virtual async Task LinkAsync(IdentityLinkUserInfo sourceLinkUser, IdentityLinkUserInfo targetLinkUser) + { + if (sourceLinkUser.UserId == targetLinkUser.UserId && sourceLinkUser.TenantId == targetLinkUser.TenantId) + { + return; + } + + if (await IsLinkedAsync(sourceLinkUser, targetLinkUser)) + { + return; + } + + var userLink = new IdentityLinkUser( + GuidGenerator.Create(), + sourceLinkUser, + targetLinkUser); + await IdentityLinkUserRepository.InsertAsync(userLink, true); + } + + public virtual async Task IsLinkedAsync(IdentityLinkUserInfo sourceLinkUser, IdentityLinkUserInfo targetLinkUser) + { + return await IdentityLinkUserRepository.FindAsync(sourceLinkUser, targetLinkUser) != null; + } + + public virtual async Task UnlinkAsync(IdentityLinkUserInfo sourceLinkUser, IdentityLinkUserInfo targetLinkUser) + { + if (!await IsLinkedAsync(sourceLinkUser, targetLinkUser)) + { + return; + } + + var linkedUser = await IdentityLinkUserRepository.FindAsync(sourceLinkUser, targetLinkUser); + if (linkedUser != null) + { + await IdentityLinkUserRepository.DeleteAsync(linkedUser); + } + } + + public virtual async Task GenerateLinkTokenAsync(IdentityLinkUserInfo targetLinkUser) + { + using (CurrentTenant.Change(targetLinkUser.TenantId)) + { + var user = await UserManager.GetByIdAsync(targetLinkUser.UserId); + return await UserManager.GenerateUserTokenAsync( + user, + LinkUserTokenProvider.LinkUserTokenProviderName, + LinkUserTokenProvider.LinkUserTokenPurpose); + } + } + + public virtual async Task VerifyLinkTokenAsync(IdentityLinkUserInfo targetLinkUser, string token) + { + using (CurrentTenant.Change(targetLinkUser.TenantId)) + { + var user = await UserManager.GetByIdAsync(targetLinkUser.UserId); + return await UserManager.VerifyUserTokenAsync( + user, + LinkUserTokenProvider.LinkUserTokenProviderName, + LinkUserTokenProvider.LinkUserTokenPurpose, + token); + } + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/LinkUserTokenProvider.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/LinkUserTokenProvider.cs new file mode 100644 index 0000000000..706c045578 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/LinkUserTokenProvider.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace Volo.Abp.Identity +{ + public class LinkUserTokenProvider : TotpSecurityStampBasedTokenProvider + { + public const string LinkUserTokenProviderName = "AbpLinkUser"; + + public const string LinkUserTokenPurpose = "AbpLinkUserLogin"; + + public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, IdentityUser user) + { + return Task.FromResult(false); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityLinkUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityLinkUserRepository.cs new file mode 100644 index 0000000000..a1ea1fd7ba --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityLinkUserRepository.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Volo.Abp.Identity.EntityFrameworkCore +{ + public class EfCoreIdentityLinkUserRepository : EfCoreRepository, IIdentityLinkUserRepository + { + public EfCoreIdentityLinkUserRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + + } + + public async Task FindAsync(IdentityLinkUserInfo sourceLinkUserInfo, IdentityLinkUserInfo targetLinkUserInfo, CancellationToken cancellationToken = default) + { + return await DbSet.FirstOrDefaultAsync(x => + x.SourceUserId == sourceLinkUserInfo.UserId && x.SourceTenantId == sourceLinkUserInfo.TenantId && + x.TargetUserId == targetLinkUserInfo.UserId && x.TargetTenantId == targetLinkUserInfo.TenantId || + x.TargetUserId == sourceLinkUserInfo.UserId && x.TargetTenantId == sourceLinkUserInfo.TenantId && + x.SourceUserId == targetLinkUserInfo.UserId && x.SourceTenantId == targetLinkUserInfo.TenantId + , cancellationToken: GetCancellationToken(cancellationToken)); + } + + public async Task> GetListAsync(IdentityLinkUserInfo linkUserInfo, CancellationToken cancellationToken = default) + { + return await DbSet.Where(x => + x.SourceUserId == linkUserInfo.UserId && x.SourceTenantId == linkUserInfo.TenantId || + x.TargetUserId == linkUserInfo.UserId && x.TargetTenantId == linkUserInfo.TenantId) + .ToListAsync(cancellationToken: GetCancellationToken(cancellationToken)); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs index 48da4daf87..d0e279b7ff 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IIdentityDbContext.cs @@ -17,5 +17,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore DbSet OrganizationUnits { get; set; } DbSet IdentitySecurityLogs { get; set; } + + DbSet IdentityLinkUsers { get; set; } } } diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs index 16679429b8..d6f7d9a9b3 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs @@ -20,6 +20,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore public DbSet IdentitySecurityLogs { get; set; } + public DbSet IdentityLinkUsers { get; set; } + public IdentityDbContext(DbContextOptions options) : base(options) { diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs index 8efe82c5ce..a533ce6340 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs @@ -233,6 +233,22 @@ namespace Volo.Abp.Identity.EntityFrameworkCore b.HasIndex(x => new { x.TenantId, x.UserId }); }); + builder.Entity(b => + { + b.ToTable(options.TablePrefix + "LinkUsers", options.Schema); + + b.ConfigureByConvention(); + + b.HasIndex(x => new + { + UserId = x.SourceUserId, + TenantId = x.SourceTenantId, + LinkedUserId = x.TargetUserId, + LinkedTenantId = x.TargetTenantId + }).IsUnique(); + }); + + } } } diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs index 19c3316b88..3e62359e6b 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContext.cs @@ -17,6 +17,8 @@ namespace Volo.Abp.Identity.MongoDB public IMongoCollection IdentitySecurityLogs => Collection(); + public IMongoCollection IdentityLinkUsers => Collection(); + protected override void CreateModel(IMongoModelBuilder modelBuilder) { base.CreateModel(modelBuilder); diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs index 89aeddeb92..d9d926a3b8 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbContextExtensions.cs @@ -41,6 +41,11 @@ namespace Volo.Abp.Identity.MongoDB { b.CollectionName = options.CollectionPrefix + "SecurityLogs"; }); + + builder.Entity(b => + { + b.CollectionName = options.CollectionPrefix + "LinkUsers"; + }); } } } diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs index 3ceab94e23..188c8ff6ae 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/AbpIdentityMongoDbModule.cs @@ -19,6 +19,7 @@ namespace Volo.Abp.Identity.MongoDB options.AddRepository(); options.AddRepository(); options.AddRepository(); + options.AddRepository(); }); } } diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs index 54819ffa19..575451365d 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/IAbpIdentityMongoDbContext.cs @@ -16,5 +16,7 @@ namespace Volo.Abp.Identity.MongoDB IMongoCollection OrganizationUnits { get; } IMongoCollection IdentitySecurityLogs { get; } + + IMongoCollection IdentityLinkUsers { get; } } } diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityLinkUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityLinkUserRepository.cs new file mode 100644 index 0000000000..248458bb5d --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityLinkUserRepository.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Volo.Abp.Domain.Repositories.MongoDB; +using Volo.Abp.MongoDB; + +namespace Volo.Abp.Identity.MongoDB +{ + public class MongoIdentityLinkUserRepository : MongoDbRepository, IIdentityLinkUserRepository + { + public MongoIdentityLinkUserRepository(IMongoDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + + public async Task FindAsync(IdentityLinkUserInfo sourceLinkUserInfo, IdentityLinkUserInfo targetLinkUserInfo, CancellationToken cancellationToken = default) + { + return await GetMongoQueryable().FirstOrDefaultAsync(x => + x.SourceUserId == sourceLinkUserInfo.UserId && x.SourceTenantId == sourceLinkUserInfo.TenantId && + x.TargetUserId == targetLinkUserInfo.UserId && x.TargetTenantId == targetLinkUserInfo.TenantId || + x.TargetUserId == sourceLinkUserInfo.UserId && x.TargetTenantId == sourceLinkUserInfo.TenantId && + x.SourceUserId == targetLinkUserInfo.UserId && x.SourceTenantId == targetLinkUserInfo.TenantId + , cancellationToken: GetCancellationToken(cancellationToken)); + } + + public async Task> GetListAsync(IdentityLinkUserInfo linkUserInfo, CancellationToken cancellationToken = default) + { + return await GetMongoQueryable().Where(x => + x.SourceUserId == linkUserInfo.UserId && x.SourceTenantId == linkUserInfo.TenantId || + x.TargetUserId == linkUserInfo.UserId && x.TargetTenantId == linkUserInfo.TenantId) + .ToListAsync(cancellationToken: GetCancellationToken(cancellationToken)); + } + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/LinkUserTokenProvider_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/LinkUserTokenProvider_Tests.cs new file mode 100644 index 0000000000..2791de9a4f --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/LinkUserTokenProvider_Tests.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Shouldly; +using Xunit; + +namespace Volo.Abp.Identity.AspNetCore +{ + public class LinkUserTokenProvider_Tests : AbpIdentityAspNetCoreTestBase + { + [Fact] + public void LinkUserTokenProvider_Should_Be_Register() + { + var identityOptions = GetRequiredService>().Value; + + identityOptions.Tokens.ProviderMap.ShouldContain(x => + x.Key == LinkUserTokenProvider.LinkUserTokenProviderName && + x.Value.ProviderType == typeof(LinkUserTokenProvider)); + } + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityLinkUserManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityLinkUserManager_Tests.cs new file mode 100644 index 0000000000..633f419da5 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityLinkUserManager_Tests.cs @@ -0,0 +1,86 @@ +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Volo.Abp.Identity +{ + public class IdentityLinkUserManager_Tests : AbpIdentityDomainTestBase + { + protected IIdentityUserRepository UserRepository { get; } + protected IIdentityLinkUserRepository IdentityLinkUserRepository { get; } + protected IdentityLinkUserManager IdentityLinkUserManager { get; } + protected IdentityTestData TestData { get; } + + public IdentityLinkUserManager_Tests() + { + UserRepository = GetRequiredService(); + IdentityLinkUserRepository = GetRequiredService(); + IdentityLinkUserManager = GetRequiredService(); + TestData = GetRequiredService(); + } + + [Fact] + public virtual async Task LinkAsync() + { + var john = await UserRepository.GetAsync(TestData.UserJohnId); + var neo = await UserRepository.GetAsync(TestData.UserNeoId); + + (await IdentityLinkUserRepository.FindAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), + new IdentityLinkUserInfo(neo.Id, neo.TenantId))).ShouldBeNull(); + + await IdentityLinkUserManager.LinkAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), + new IdentityLinkUserInfo(neo.Id, neo.TenantId)); + + var linkUser = await IdentityLinkUserRepository.FindAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), + new IdentityLinkUserInfo(neo.Id, neo.TenantId)); + + linkUser.ShouldNotBeNull(); + linkUser.SourceUserId.ShouldBe(john.Id); + linkUser.SourceTenantId.ShouldBe(john.TenantId); + + linkUser.TargetUserId.ShouldBe(neo.Id); + linkUser.TargetTenantId.ShouldBe(neo.TenantId); + } + + [Fact] + public virtual async Task UnlinkAsync() + { + var john = await UserRepository.GetAsync(TestData.UserJohnId); + var david = await UserRepository.GetAsync(TestData.UserDavidId); + + (await IdentityLinkUserRepository.FindAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), + new IdentityLinkUserInfo(david.Id, david.TenantId))).ShouldNotBeNull(); + + await IdentityLinkUserManager.UnlinkAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), + new IdentityLinkUserInfo(david.Id, david.TenantId)); + + (await IdentityLinkUserRepository.FindAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), + new IdentityLinkUserInfo(david.Id, david.TenantId))).ShouldBeNull(); + } + + [Fact] + public virtual async Task IsLinkedAsync() + { + var john = await UserRepository.GetAsync(TestData.UserJohnId); + var david = await UserRepository.GetAsync(TestData.UserDavidId); + var neo = await UserRepository.GetAsync(TestData.UserNeoId); + + (await IdentityLinkUserManager.IsLinkedAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), + new IdentityLinkUserInfo(david.Id, david.TenantId))).ShouldBeTrue(); + + (await IdentityLinkUserManager.IsLinkedAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), + new IdentityLinkUserInfo(neo.Id, neo.TenantId))).ShouldBeFalse(); + } + + [Fact] + public virtual async Task GenerateAndVerifyLinkTokenAsync() + { + var john = await UserRepository.GetAsync(TestData.UserJohnId); + var token = await IdentityLinkUserManager.GenerateLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId)); + (await IdentityLinkUserManager.VerifyLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), token)).ShouldBeTrue(); + + (await IdentityLinkUserManager.VerifyLinkTokenAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), "123123")).ShouldBeFalse(); + } + + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/IdentityLinkUserRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/IdentityLinkUserRepository_Tests.cs new file mode 100644 index 0000000000..8e490fee8b --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/IdentityLinkUserRepository_Tests.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Identity.EntityFrameworkCore +{ + public class IdentityLinkUserRepository_Tests : IdentityLinkUserRepository_Tests + { + + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentityLinkUserRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentityLinkUserRepository_Tests.cs new file mode 100644 index 0000000000..c7c9f29a55 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/IdentityLinkUserRepository_Tests.cs @@ -0,0 +1,10 @@ +using Xunit; + +namespace Volo.Abp.Identity.MongoDB +{ + [Collection(MongoTestCollection.Name)] + public class IdentityLinkUserRepository_Tests : IdentityLinkUserRepository_Tests + { + + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs index 79dd858c20..708deaa68e 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs @@ -19,6 +19,8 @@ namespace Volo.Abp.Identity private readonly ILookupNormalizer _lookupNormalizer; private readonly IdentityTestData _testData; private readonly OrganizationUnitManager _organizationUnitManager; + private readonly IIdentityLinkUserRepository _identityLinkUserRepository; + private readonly IdentityLinkUserManager _identityLinkUserManager; private IdentityRole _adminRole; private IdentityRole _moderatorRole; @@ -36,7 +38,9 @@ namespace Volo.Abp.Identity IIdentitySecurityLogRepository identitySecurityLogRepository, ILookupNormalizer lookupNormalizer, IdentityTestData testData, - OrganizationUnitManager organizationUnitManager) + OrganizationUnitManager organizationUnitManager, + IIdentityLinkUserRepository identityLinkUserRepository, + IdentityLinkUserManager identityLinkUserManager) { _guidGenerator = guidGenerator; _userRepository = userRepository; @@ -46,6 +50,8 @@ namespace Volo.Abp.Identity _testData = testData; _organizationUnitRepository = organizationUnitRepository; _organizationUnitManager = organizationUnitManager; + _identityLinkUserRepository = identityLinkUserRepository; + _identityLinkUserManager = identityLinkUserManager; _identitySecurityLogRepository = identitySecurityLogRepository; } @@ -54,6 +60,7 @@ namespace Volo.Abp.Identity await AddRoles(); await AddOrganizationUnits(); await AddUsers(); + await AddLinkUsers(); await AddClaimTypes(); await AddSecurityLogs(); } @@ -128,6 +135,17 @@ namespace Volo.Abp.Identity await _userRepository.InsertAsync(neo); } + private async Task AddLinkUsers() + { + var john = await _userRepository.GetAsync(_testData.UserJohnId); + var david = await _userRepository.GetAsync(_testData.UserDavidId); + var neo = await _userRepository.GetAsync(_testData.UserNeoId); + + await _identityLinkUserManager.LinkAsync(new IdentityLinkUserInfo(john.Id, john.TenantId), + new IdentityLinkUserInfo(david.Id, david.TenantId)); + await _identityLinkUserManager.LinkAsync(new IdentityLinkUserInfo(david.Id, david.TenantId), + new IdentityLinkUserInfo(neo.Id, neo.TenantId)); + } private async Task AddClaimTypes() { diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityLinkUserRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityLinkUserRepository_Tests.cs new file mode 100644 index 0000000000..e5dd6dcd5f --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityLinkUserRepository_Tests.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Modularity; +using Xunit; + +namespace Volo.Abp.Identity +{ + public abstract class IdentityLinkUserRepository_Tests : AbpIdentityTestBase + where TStartupModule : IAbpModule + { + protected IIdentityUserRepository UserRepository { get; } + protected IIdentityLinkUserRepository IdentityLinkUserRepository { get; } + protected IdentityTestData TestData { get; } + + public IdentityLinkUserRepository_Tests() + { + UserRepository = GetRequiredService(); + IdentityLinkUserRepository = GetRequiredService(); + TestData = GetRequiredService(); + } + + [Fact] + public async Task FindAsync() + { + var john = await UserRepository.GetAsync(TestData.UserJohnId); + var david = await UserRepository.GetAsync(TestData.UserDavidId); + var neo = await UserRepository.GetAsync(TestData.UserNeoId); + + var johnAndDavidLinkUser = await IdentityLinkUserRepository.FindAsync( + new IdentityLinkUserInfo(john.Id, john.TenantId), + new IdentityLinkUserInfo(david.Id, david.TenantId)); + + johnAndDavidLinkUser.ShouldNotBeNull(); + johnAndDavidLinkUser.SourceUserId.ShouldBe(john.Id); + johnAndDavidLinkUser.SourceTenantId.ShouldBe(john.TenantId); + johnAndDavidLinkUser.TargetUserId.ShouldBe(david.Id); + johnAndDavidLinkUser.TargetTenantId.ShouldBe(david.TenantId); + + (await IdentityLinkUserRepository.FindAsync( + new IdentityLinkUserInfo(john.Id, john.TenantId), + new IdentityLinkUserInfo(neo.Id, neo.TenantId))).ShouldBeNull(); + } + + [Fact] + public async Task GetListAsync() + { + var john = await UserRepository.GetAsync(TestData.UserJohnId); + var david = await UserRepository.GetAsync(TestData.UserDavidId); + var neo = await UserRepository.GetAsync(TestData.UserNeoId); + + var davidLinkUsers = await IdentityLinkUserRepository.GetListAsync(new IdentityLinkUserInfo(david.Id, david.TenantId)); + davidLinkUsers.ShouldNotBeNull(); + + davidLinkUsers.ShouldContain(x => x.SourceUserId == john.Id && x.SourceTenantId == john.TenantId); + davidLinkUsers.ShouldContain(x => x.TargetUserId == neo.Id && x.TargetTenantId == neo.TenantId); + } + } +}