From 8f46ce7f90b7658b5eeafdebac4b7c0d63d32c31 Mon Sep 17 00:00:00 2001 From: colin Date: Wed, 24 Jun 2026 17:54:56 +0800 Subject: [PATCH] feat: Add user Impersonation implementation --- aspnet-core/LINGYUN.MicroService.Aspire.slnx | 3 + ...viceMigrationsEntityFrameworkCoreModule.cs | 2 +- ...ce.AdminService.EntityFrameworkCore.csproj | 2 +- .../MergeFeatureDefinitionManager.cs | 3 +- .../MergePermissionDefinitionManager.cs | 3 +- .../MergeSettingDefinitionManager.cs | 3 +- .../MergeTemplateDefinitionManager.cs | 2 +- .../DataSeeds/OpenIddictDataSeeder.cs | 6 +- .../AuthServerModule.Configure.cs | 13 + .../AuthServerModule.cs | 2 + ...LINGYUN.Abp.MicroService.AuthServer.csproj | 1 + .../MergeFeatureDefinitionManager.cs | 254 ++++++++++ .../MergePermissionDefinitionManager.cs | 188 ++++++++ .../MergeSettingDefinitionManager.cs | 73 +++ .../MergeTemplateDefinitionManager.cs | 86 ++++ .../appsettings.Development.json | 4 +- .../Account/Localization/Resources/en.json | 5 +- .../Localization/Resources/zh-Hans.json | 5 +- .../IdentityPermissionDefinitionProvider.cs | 1 + .../Abp/Identity/IdentityPermissions.cs | 1 + .../LINGYUN/Abp/Identity/Localization/en.json | 4 +- .../Abp/Identity/Localization/zh-Hans.json | 4 +- .../FodyWeavers.xml | 3 + .../FodyWeavers.xsd | 30 ++ ...INGYUN.Abp.OpenIddict.Impersonation.csproj | 26 + .../AbpOpenIddictImpersonationModule.cs | 44 ++ .../Impersonation/ImpersonationRequest.cs | 26 + .../ImpersonationTokenExtensionGrant.cs | 453 ++++++++++++++++++ .../ImpersonationTokenExtensionGrantConsts.cs | 6 + .../Localization/Resources/en.json | 19 + .../Localization/Resources/zh-Hans.json | 19 + .../OpenIddictImpersonationOptions.cs | 21 + ...nationOpenIddictServerBuilderExtensions.cs | 11 + .../Claims/ImpersonationClaimExtensions.cs | 23 + .../AbpSaasPermissionDefinitionProvider.cs | 15 +- .../Saas/Permissions/AbpSaasPermissions.cs | 9 +- .../Abp/Saas/Localization/Resources/en.json | 4 +- .../Saas/Localization/Resources/zh-Hans.json | 4 +- 38 files changed, 1348 insertions(+), 30 deletions(-) create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeFeatureDefinitionManager.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergePermissionDefinitionManager.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeSettingDefinitionManager.cs create mode 100644 aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeTemplateDefinitionManager.cs create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xml create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xsd create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN.Abp.OpenIddict.Impersonation.csproj create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/AbpOpenIddictImpersonationModule.cs create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationRequest.cs create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrant.cs create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrantConsts.cs create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/en.json create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/zh-Hans.json create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/OpenIddictImpersonationOptions.cs create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/Microsoft/Extensions/DependencyInjection/ImpersonationOpenIddictServerBuilderExtensions.cs create mode 100644 aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/System/Security/Claims/ImpersonationClaimExtensions.cs diff --git a/aspnet-core/LINGYUN.MicroService.Aspire.slnx b/aspnet-core/LINGYUN.MicroService.Aspire.slnx index bf4fcb03f..e4a7bf1c3 100644 --- a/aspnet-core/LINGYUN.MicroService.Aspire.slnx +++ b/aspnet-core/LINGYUN.MicroService.Aspire.slnx @@ -401,6 +401,7 @@ + @@ -411,6 +412,8 @@ + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/AdminServiceMigrationsEntityFrameworkCoreModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/AdminServiceMigrationsEntityFrameworkCoreModule.cs index 7c9944faa..3fd95bc16 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/AdminServiceMigrationsEntityFrameworkCoreModule.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/AdminServiceMigrationsEntityFrameworkCoreModule.cs @@ -15,6 +15,7 @@ using LINGYUN.Abp.LocalizationManagement.EntityFrameworkCore; using LINGYUN.Abp.MessageService; using LINGYUN.Abp.Notifications; using LINGYUN.Abp.OpenIddict; +using LINGYUN.Abp.PermissionManagement.EntityFrameworkCore; using LINGYUN.Abp.ProjectManagement; using LINGYUN.Abp.RulesEngineManagement; using LINGYUN.Abp.Saas; @@ -32,7 +33,6 @@ using Volo.Abp.EntityFrameworkCore.PostgreSql; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement; -using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; namespace LINGYUN.Abp.MicroService.AdminService; diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore.csproj index 9f44a4cad..feeef0c8d 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore.csproj +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore.csproj @@ -18,7 +18,6 @@ - @@ -38,6 +37,7 @@ + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeFeatureDefinitionManager.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeFeatureDefinitionManager.cs index 1bcefc35c..e7c395b1f 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeFeatureDefinitionManager.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeFeatureDefinitionManager.cs @@ -1,8 +1,7 @@ using System.Collections.Immutable; using Volo.Abp.DependencyInjection; -using Volo.Abp.Features; -namespace LINGYUN.Abp.MicroService.AdminService.Overrides; +namespace Volo.Abp.Features; [Dependency(ReplaceServices = true)] public class MergeFeatureDefinitionManager : FeatureDefinitionManager diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergePermissionDefinitionManager.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergePermissionDefinitionManager.cs index 5d3f92222..f224a1dd1 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergePermissionDefinitionManager.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergePermissionDefinitionManager.cs @@ -1,9 +1,8 @@ using System.Collections.Immutable; -using Volo.Abp.Authorization.Permissions; using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; -namespace LINGYUN.Abp.MicroService.AdminService.Overrides; +namespace Volo.Abp.Authorization.Permissions; [Dependency(ReplaceServices = true)] public class MergePermissionDefinitionManager : PermissionDefinitionManager diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeSettingDefinitionManager.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeSettingDefinitionManager.cs index 7eb4ee257..6ed3c40de 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeSettingDefinitionManager.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeSettingDefinitionManager.cs @@ -1,8 +1,7 @@ using System.Collections.Immutable; using Volo.Abp.DependencyInjection; -using Volo.Abp.Settings; -namespace LINGYUN.Abp.MicroService.AdminService.Overrides; +namespace Volo.Abp.Settings; [Dependency(ReplaceServices = true)] public class MergeSettingDefinitionManager : SettingDefinitionManager diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeTemplateDefinitionManager.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeTemplateDefinitionManager.cs index 9fe58573a..467966c11 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeTemplateDefinitionManager.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeTemplateDefinitionManager.cs @@ -2,7 +2,7 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.TextTemplating; -namespace LINGYUN.Abp.MicroService.AdminService.Overrides; +namespace Volo.Abp.TextTemplatin; [Dependency(ReplaceServices = true)] diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.EntityFrameworkCore/DataSeeds/OpenIddictDataSeeder.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.EntityFrameworkCore/DataSeeds/OpenIddictDataSeeder.cs index 29398ecb0..6bd44bf85 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.EntityFrameworkCore/DataSeeds/OpenIddictDataSeeder.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.EntityFrameworkCore/DataSeeds/OpenIddictDataSeeder.cs @@ -480,7 +480,8 @@ public class OpenIddictDataSeeder : OpenIddictDataSeedContributorBase, ITransien OpenIddictConstants.GrantTypes.Password, OpenIddictConstants.GrantTypes.RefreshToken, // TODO: 引用项目? - "link_user"], + "link_user", + "impersonation"], [OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.Email, OpenIddictConstants.Scopes.Roles, @@ -544,7 +545,8 @@ public class OpenIddictDataSeeder : OpenIddictDataSeedContributorBase, ITransien [OpenIddictConstants.GrantTypes.AuthorizationCode, OpenIddictConstants.GrantTypes.RefreshToken, // TODO: 引用项目? - "link_user"], + "link_user", + "impersonation"], [OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.Email, OpenIddictConstants.Scopes.Roles, diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.Configure.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.Configure.cs index e281f164c..cce2f4351 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.Configure.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.Configure.cs @@ -1,9 +1,11 @@ using DotNetCore.CAP; using LINGYUN.Abp.AspNetCore.MultiTenancy; +using LINGYUN.Abp.Identity; using LINGYUN.Abp.Localization.CultureMap; using LINGYUN.Abp.LocalizationManagement; using LINGYUN.Abp.MicroService.AuthServer.Ui.Branding; using LINGYUN.Abp.OpenIddict.AspNetCore.Session; +using LINGYUN.Abp.OpenIddict.Impersonation; using LINGYUN.Abp.OpenIddict.LinkUser; using LINGYUN.Abp.OpenIddict.Portal; using LINGYUN.Abp.OpenIddict.Sms; @@ -200,6 +202,7 @@ public partial class AuthServerModule { Configure(options => { + options.SaveStaticFeaturesToDatabase = false; options.IsDynamicFeatureStoreEnabled = true; }); } @@ -209,6 +212,7 @@ public partial class AuthServerModule Configure(options => { options.SaveStaticPermissionsToDatabase = false; + options.IsDynamicPermissionStoreEnabled = true; }); } @@ -216,6 +220,7 @@ public partial class AuthServerModule { Configure(options => { + options.SaveStaticSettingsToDatabase = false; options.IsDynamicSettingStoreEnabled = true; }); } @@ -295,6 +300,7 @@ public partial class AuthServerModule options.PersistentSessionGrantTypes.Add(SmsTokenExtensionGrantConsts.GrantType); options.PersistentSessionGrantTypes.Add(PortalTokenExtensionGrantConsts.GrantType); options.PersistentSessionGrantTypes.Add(LinkUserTokenExtensionGrantConsts.GrantType); + options.PersistentSessionGrantTypes.Add(ImpersonationTokenExtensionGrantConsts.GrantType); options.PersistentSessionGrantTypes.Add(WeChatTokenExtensionGrantConsts.OfficialGrantType); options.PersistentSessionGrantTypes.Add(WeChatTokenExtensionGrantConsts.MiniProgramGrantType); options.PersistentSessionGrantTypes.Add(AbpWeChatWorkGlobalConsts.GrantType); @@ -393,6 +399,13 @@ public partial class AuthServerModule options.RefreshTokenReuseLeeway = lifetime.GetValue("RefreshTokenReuseLeeway", options.RefreshTokenReuseLeeway); options.UserCodeLifetime = lifetime.GetValue("UserCode", options.UserCodeLifetime); }); + + Configure(options => + { + // 模拟用户权限委派 + options.ImpersonationPermission = IdentityPermissions.Users.Impersonation; + options.ImpersonationTenantPermission = "AbpSaas.Tenants.Impersonation"; + }); } private void ConfigureSecurity(IServiceCollection services, IConfiguration configuration, bool isDevelopment = false) { diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.cs index 521ef2220..98eb4313f 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.cs @@ -18,6 +18,7 @@ using LINGYUN.Abp.Identity.OrganizaztionUnits; using LINGYUN.Abp.Identity.Session.AspNetCore; using LINGYUN.Abp.Localization.CultureMap; using LINGYUN.Abp.OpenIddict.AspNetCore.Session; +using LINGYUN.Abp.OpenIddict.Impersonation; using LINGYUN.Abp.OpenIddict.LinkUser; using LINGYUN.Abp.OpenIddict.Portal; using LINGYUN.Abp.OpenIddict.Sms; @@ -61,6 +62,7 @@ namespace LINGYUN.Abp.MicroService.AuthServer; typeof(AbpOpenIddictSmsModule), typeof(AbpOpenIddictWeChatModule), typeof(AbpOpenIddictLinkUserModule), + typeof(AbpOpenIddictImpersonationModule), typeof(AbpOpenIddictPortalModule), typeof(AbpOpenIddictWeChatWorkModule), typeof(AbpIdentityOrganizaztionUnitsModule), diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/LINGYUN.Abp.MicroService.AuthServer.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/LINGYUN.Abp.MicroService.AuthServer.csproj index cd720576e..e6fa3dfda 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/LINGYUN.Abp.MicroService.AuthServer.csproj +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/LINGYUN.Abp.MicroService.AuthServer.csproj @@ -50,6 +50,7 @@ + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeFeatureDefinitionManager.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeFeatureDefinitionManager.cs new file mode 100644 index 000000000..887001b2f --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeFeatureDefinitionManager.cs @@ -0,0 +1,254 @@ +using JetBrains.Annotations; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Features; + +[Dependency(ReplaceServices = true)] +public class MergeFeatureDefinitionManager : FeatureDefinitionManager +{ + public MergeFeatureDefinitionManager( + IStaticFeatureDefinitionStore staticStore, + IDynamicFeatureDefinitionStore dynamicStore) + : base(staticStore, dynamicStore) + { + } + + public async override Task> GetAllAsync() + { + var staticFeatures = await StaticStore.GetFeaturesAsync(); + var dynamicFeatures = await DynamicStore.GetFeaturesAsync(); + + var mergedFeatures = new Dictionary(); + + foreach (var staticFeature in staticFeatures) + { + mergedFeatures[staticFeature.Name] = staticFeature; + } + + foreach (var dynamicFeature in dynamicFeatures) + { + if (mergedFeatures.TryGetValue(dynamicFeature.Name, out var existingFeature)) + { + MergeFeatureMetadata(existingFeature, dynamicFeature); + + foreach (var child in dynamicFeature.Children) + { + MergeChildFeature(existingFeature, child); + } + } + else + { + mergedFeatures[dynamicFeature.Name] = dynamicFeature; + } + } + + return mergedFeatures.Values.ToImmutableList(); + } + + public async override Task> GetGroupsAsync() + { + var staticGroups = await StaticStore.GetGroupsAsync(); + var dynamicGroups = await DynamicStore.GetGroupsAsync(); + + var mergedGroups = new Dictionary(); + + foreach (var staticGroup in staticGroups) + { + mergedGroups[staticGroup.Name] = staticGroup; + } + + foreach (var dynamicGroup in dynamicGroups) + { + if (mergedGroups.TryGetValue(dynamicGroup.Name, out var existingGroup)) + { + MergeGroupFeatures(existingGroup, dynamicGroup); + } + else + { + mergedGroups[dynamicGroup.Name] = dynamicGroup; + } + } + + return mergedGroups.Values.ToImmutableList(); + } + + private static void MergeGroupFeatures(FeatureGroupDefinition target, FeatureGroupDefinition source) + { + foreach (var sourceFeature in source.Features) + { + var existingFeature = GetFeatureOrNull(target, sourceFeature.Name); + + if (existingFeature == null) + { + var newFeature = target.AddFeature( + sourceFeature.Name, + sourceFeature.DefaultValue, + sourceFeature.DisplayName, + sourceFeature.Description, + sourceFeature.ValueType, + sourceFeature.IsVisibleToClients, + sourceFeature.IsAvailableToHost + ); + + CopyFeatureDetails(sourceFeature, newFeature); + + foreach (var child in sourceFeature.Children) + { + AddChildFeatureRecursively(newFeature, child); + } + } + else + { + MergeFeatureMetadata(existingFeature, sourceFeature); + + foreach (var child in sourceFeature.Children) + { + MergeChildFeature(existingFeature, child); + } + } + } + } + + private static void AddChildFeatureRecursively(FeatureDefinition parent, FeatureDefinition sourceChild) + { + var newChild = parent.CreateChild( + sourceChild.Name, + sourceChild.DefaultValue, + sourceChild.DisplayName, + sourceChild.Description, + sourceChild.ValueType, + sourceChild.IsVisibleToClients, + sourceChild.IsAvailableToHost + ); + + CopyFeatureDetails(sourceChild, newChild); + + foreach (var grandchild in sourceChild.Children) + { + AddChildFeatureRecursively(newChild, grandchild); + } + } + + private static void MergeChildFeature(FeatureDefinition parent, FeatureDefinition sourceChild) + { + var existingChild = parent.Children.FirstOrDefault(c => c.Name == sourceChild.Name); + + if (existingChild == null) + { + var newChild = parent.CreateChild( + sourceChild.Name, + sourceChild.DefaultValue, + sourceChild.DisplayName, + sourceChild.Description, + sourceChild.ValueType, + sourceChild.IsVisibleToClients, + sourceChild.IsAvailableToHost + ); + CopyFeatureDetails(sourceChild, newChild); + + foreach (var grandchild in sourceChild.Children) + { + AddChildFeatureRecursively(newChild, grandchild); + } + } + else + { + MergeFeatureMetadata(existingChild, sourceChild); + + foreach (var grandchild in sourceChild.Children) + { + MergeChildFeature(existingChild, grandchild); + } + } + } + + private static void CopyFeatureDetails(FeatureDefinition source, FeatureDefinition target) + { + foreach (var property in source.Properties) + { + target.Properties[property.Key] = property.Value; + } + + foreach (var provider in source.AllowedProviders) + { + if (!target.AllowedProviders.Contains(provider)) + { + target.AllowedProviders.Add(provider); + } + } + } + + private static void MergeFeatureMetadata(FeatureDefinition target, FeatureDefinition source) + { + if (source.DisplayName != null) + { + target.DisplayName = source.DisplayName; + } + + if (source.Description != null) + { + target.Description = source.Description; + } + + if (source.DefaultValue != null) + { + target.DefaultValue = source.DefaultValue; + } + + if (source.ValueType != null) + { + target.ValueType = source.ValueType; + } + + foreach (var property in source.Properties) + { + target.Properties[property.Key] = property.Value; + } + + foreach (var provider in source.AllowedProviders) + { + if (!target.AllowedProviders.Contains(provider)) + { + target.AllowedProviders.Add(provider); + } + } + + target.IsVisibleToClients = target.IsVisibleToClients || source.IsVisibleToClients; + target.IsAvailableToHost = target.IsAvailableToHost || source.IsAvailableToHost; + } + + + public static FeatureDefinition GetFeatureOrNull( + FeatureGroupDefinition group, + [NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + return GetFeatureOrNullRecursively(group.Features, name); + } + + private static FeatureDefinition GetFeatureOrNullRecursively( + IReadOnlyList features, + string name) + { + foreach (var feature in features) + { + if (feature.Name == name) + { + return feature; + } + + var childFeature = GetFeatureOrNullRecursively(feature.Children, name); + if (childFeature != null) + { + return childFeature; + } + } + + return null; + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergePermissionDefinitionManager.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergePermissionDefinitionManager.cs new file mode 100644 index 000000000..68b34e48b --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergePermissionDefinitionManager.cs @@ -0,0 +1,188 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Authorization.Permissions; + +[Dependency(ReplaceServices = true)] +public class MergePermissionDefinitionManager : PermissionDefinitionManager +{ + private readonly IStaticPermissionDefinitionStore _staticStore; + private readonly IDynamicPermissionDefinitionStore _dynamicStore; + + public MergePermissionDefinitionManager( + IStaticPermissionDefinitionStore staticStore, + IDynamicPermissionDefinitionStore dynamicStore) : base(staticStore, dynamicStore) + { + _staticStore = staticStore; + _dynamicStore = dynamicStore; + } + + public async override Task> GetGroupsAsync() + { + var staticGroups = await _staticStore.GetGroupsAsync(); + var dynamicGroups = await _dynamicStore.GetGroupsAsync(); + + var mergedGroups = new Dictionary(); + + foreach (var staticGroup in staticGroups) + { + mergedGroups[staticGroup.Name] = staticGroup; + } + + foreach (var dynamicGroup in dynamicGroups) + { + if (mergedGroups.TryGetValue(dynamicGroup.Name, out var existingGroup)) + { + MergeGroupPermissions(existingGroup, dynamicGroup); + } + else + { + mergedGroups[dynamicGroup.Name] = dynamicGroup; + } + } + + return mergedGroups.Values.ToImmutableList(); + } + + private static void MergeGroupPermissions(PermissionGroupDefinition target, PermissionGroupDefinition source) + { + foreach (var sourcePermission in source.Permissions) + { + var existingPermission = target.GetPermissionOrNull(sourcePermission.Name); + + if (existingPermission == null) + { + var newPermission = target.AddPermission( + sourcePermission.Name, + sourcePermission.DisplayName, + sourcePermission.MultiTenancySide, + sourcePermission.IsEnabled + ); + + CopyPermissionDetails(sourcePermission, newPermission); + + foreach (var child in sourcePermission.Children) + { + AddChildPermissionRecursively(newPermission, child); + } + } + else + { + MergePermissionMetadata(existingPermission, sourcePermission); + + foreach (var sourceChild in sourcePermission.Children) + { + MergeChildPermissions(existingPermission, sourceChild); + } + } + } + } + + private static void AddChildPermissionRecursively(PermissionDefinition parent, PermissionDefinition sourceChild) + { + var newChild = parent.AddChild( + sourceChild.Name, + sourceChild.DisplayName, + sourceChild.MultiTenancySide, + sourceChild.IsEnabled + ); + + CopyPermissionDetails(sourceChild, newChild); + + foreach (var grandchild in sourceChild.Children) + { + AddChildPermissionRecursively(newChild, grandchild); + } + } + + private static void MergeChildPermissions(PermissionDefinition parent, PermissionDefinition sourceChild) + { + var existingChild = parent.Children.FirstOrDefault(c => c.Name == sourceChild.Name); + + if (existingChild == null) + { + var newChild = parent.AddChild( + sourceChild.Name, + sourceChild.DisplayName, + sourceChild.MultiTenancySide, + sourceChild.IsEnabled + ); + CopyPermissionDetails(sourceChild, newChild); + + foreach (var grandchild in sourceChild.Children) + { + AddChildPermissionRecursively(newChild, grandchild); + } + } + else + { + MergePermissionMetadata(existingChild, sourceChild); + + foreach (var grandchild in sourceChild.Children) + { + MergeChildPermissions(existingChild, grandchild); + } + } + } + + private static void CopyPermissionDetails(PermissionDefinition source, PermissionDefinition target) + { + foreach (var property in source.Properties) + { + target.Properties[property.Key] = property.Value; + } + + foreach (var provider in source.Providers) + { + if (!target.Providers.Contains(provider)) + { + target.Providers.Add(provider); + } + } + + foreach (var checker in source.StateCheckers) + { + if (!target.StateCheckers.Contains(checker)) + { + target.StateCheckers.Add(checker); + } + } + } + + private static void MergePermissionMetadata(PermissionDefinition target, PermissionDefinition source) + { + target.DisplayName = source.DisplayName; + + foreach (var property in source.Properties) + { + target.Properties[property.Key] = property.Value; + } + + foreach (var provider in source.Providers) + { + if (!target.Providers.Contains(provider)) + { + target.Providers.Add(provider); + } + } + + foreach (var checker in source.StateCheckers) + { + if (!target.StateCheckers.Contains(checker)) + { + target.StateCheckers.Add(checker); + } + } + + if (source.MultiTenancySide != MultiTenancySides.Both) + { + target.MultiTenancySide |= source.MultiTenancySide; + } + + target.IsEnabled = target.IsEnabled || source.IsEnabled; + } +} \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeSettingDefinitionManager.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeSettingDefinitionManager.cs new file mode 100644 index 000000000..c3c479f4f --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeSettingDefinitionManager.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Settings; + +[Dependency(ReplaceServices = true)] +public class MergeSettingDefinitionManager : SettingDefinitionManager +{ + private readonly IStaticSettingDefinitionStore _staticStore; + private readonly IDynamicSettingDefinitionStore _dynamicStore; + + public MergeSettingDefinitionManager( + IStaticSettingDefinitionStore staticStore, + IDynamicSettingDefinitionStore dynamicStore) + : base(staticStore, dynamicStore) + { + _staticStore = staticStore; + _dynamicStore = dynamicStore; + } + + public async override Task> GetAllAsync() + { + var staticSettings = await _staticStore.GetAllAsync(); + var dynamicSettings = await _dynamicStore.GetAllAsync(); + + var mergedSettings = new Dictionary(); + + foreach (var staticSetting in staticSettings) + { + mergedSettings[staticSetting.Name] = staticSetting; + } + + foreach (var dynamicSetting in dynamicSettings) + { + if (mergedSettings.TryGetValue(dynamicSetting.Name, out var existingSetting)) + { + MergeSetting(existingSetting, dynamicSetting); + } + else + { + mergedSettings[dynamicSetting.Name] = dynamicSetting; + } + } + + return mergedSettings.Values.ToImmutableList(); + } + + private static void MergeSetting(SettingDefinition target, SettingDefinition source) + { + target.DisplayName = source.DisplayName; + target.Description = source.Description ?? target.Description; + target.DefaultValue = source.DefaultValue ?? target.DefaultValue; + target.IsVisibleToClients = target.IsVisibleToClients || source.IsVisibleToClients; + target.IsInherited = target.IsInherited || source.IsInherited; + target.IsEncrypted = target.IsEncrypted || source.IsEncrypted; + + foreach (var property in source.Properties) + { + target.Properties[property.Key] = property.Value; + } + + foreach (var provider in source.Providers) + { + if (!target.Providers.Contains(provider)) + { + target.Providers.Add(provider); + } + } + } +} + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeTemplateDefinitionManager.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeTemplateDefinitionManager.cs new file mode 100644 index 000000000..0a45a8908 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeTemplateDefinitionManager.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.TextTemplating; + +namespace Volo.Abp.TextTemplatin; + + +[Dependency(ReplaceServices = true)] +public class MergeTemplateDefinitionManager : TemplateDefinitionManager +{ + public MergeTemplateDefinitionManager( + IStaticTemplateDefinitionStore staticStore, + IDynamicTemplateDefinitionStore dynamicStore) + : base(staticStore, dynamicStore) + { + } + public async override Task> GetAllAsync() + { + var staticTemplates = await StaticStore.GetAllAsync(); + var dynamicTemplates = await DynamicStore.GetAllAsync(); + + var mergedTemplates = new Dictionary(); + + foreach (var staticTemplate in staticTemplates) + { + mergedTemplates[staticTemplate.Name] = staticTemplate; + } + + foreach (var dynamicTemplate in dynamicTemplates) + { + if (mergedTemplates.TryGetValue(dynamicTemplate.Name, out var existingTemplate)) + { + MergeTemplate(existingTemplate, dynamicTemplate); + } + else + { + mergedTemplates[dynamicTemplate.Name] = dynamicTemplate; + } + } + + return mergedTemplates.Values.ToImmutableList(); + } + + private static TemplateDefinition MergeTemplate(TemplateDefinition staticTemplate, TemplateDefinition dynamicTemplate) + { + var localizationResourceName = dynamicTemplate.LocalizationResourceName ?? staticTemplate.LocalizationResourceName; + var defaultCultureName = dynamicTemplate.DefaultCultureName ?? staticTemplate.DefaultCultureName; + var displayName = dynamicTemplate.DisplayName ?? staticTemplate.DisplayName; + var isLayout = dynamicTemplate.IsLayout || staticTemplate.IsLayout; + var layout = dynamicTemplate.Layout ?? staticTemplate.Layout; + + var mergedTemplate = new TemplateDefinition( + staticTemplate.Name, + localizationResourceName, + displayName, + isLayout, + layout, + defaultCultureName + ); + + foreach (var property in staticTemplate.Properties) + { + mergedTemplate.Properties[property.Key] = property.Value; + } + + foreach (var property in dynamicTemplate.Properties) + { + mergedTemplate.Properties[property.Key] = property.Value; + } + + if (!string.IsNullOrEmpty(dynamicTemplate.RenderEngine)) + { + mergedTemplate.RenderEngine = dynamicTemplate.RenderEngine; + } + else if (!string.IsNullOrEmpty(staticTemplate.RenderEngine)) + { + mergedTemplate.RenderEngine = staticTemplate.RenderEngine; + } + + mergedTemplate.IsInlineLocalized = dynamicTemplate.IsInlineLocalized || staticTemplate.IsInlineLocalized; + + return mergedTemplate; + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/appsettings.Development.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/appsettings.Development.json index 128c684cf..09e85a1bd 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/appsettings.Development.json +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/appsettings.Development.json @@ -134,11 +134,11 @@ }, "Serilog": { "MinimumLevel": { - "Default": "Warning", + "Default": "Debug", "Override": { "System": "Warning", "Microsoft": "Warning", - "DotNetCore": "Warning" + "DotNetCore": "Debug" } }, "WriteTo": [ diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json index c9d9c61fc..7f06b61ac 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json @@ -78,6 +78,9 @@ "LinkAccountWillBeDeleteWarning": "Are you sure you want to delete the associated account \"{0}\"?", "LinkLogged": "Accounts are linked", "StayWithCurrentAccount": "Stay with the current account", - "Settings:Identity.Account": "Account" + "Settings:Identity.Account": "Account", + "ImpersonatorUser": "Impersonator", + "ImpersonatorWarning": "You will log out from your current account and access the system as {0}!", + "RevertImpersonationWarning": "You will log out of your current account and return to the account you previously logged in with!" } } \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json index 64de06182..791208eb4 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json @@ -78,6 +78,9 @@ "LinkAccountWillBeDeleteWarning": "您确定要删除关联帐户\"{0}\"吗?", "LinkLogged": "帐户已经关联", "StayWithCurrentAccount": "继续使用当前帐户", - "Settings:Identity.Account": "用户账户" + "Settings:Identity.Account": "用户账户", + "ImpersonatorUser": "模拟用户", + "ImpersonatorWarning": "您将从当前账户退出, 使用 {0} 的身份访问系统!", + "RevertImpersonationWarning": "您将从当前账户退出, 回到您之前登录的账户!" } } \ No newline at end of file diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissionDefinitionProvider.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissionDefinitionProvider.cs index 562b8c7b5..3fac2259d 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissionDefinitionProvider.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissionDefinitionProvider.cs @@ -18,6 +18,7 @@ public class IdentityPermissionDefinitionProvider : PermissionDefinitionProvider userPermission.AddChild(IdentityPermissions.Users.ResetPassword, L("Permission:ResetPassword")); userPermission.AddChild(IdentityPermissions.Users.ManageClaims, L("Permission:ManageClaims")); userPermission.AddChild(IdentityPermissions.Users.ManageOrganizationUnits, L("Permission:ManageOrganizationUnits")); + userPermission.AddChild(IdentityPermissions.Users.Impersonation, L("Permission:Impersonation")); } var rolePermission = identityGroup.GetPermissionOrNull(Volo.Abp.Identity.IdentityPermissions.Roles.Default); diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissions.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissions.cs index 0bb2ec123..6292a276e 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissions.cs +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissions.cs @@ -15,6 +15,7 @@ public class IdentityPermissions public const string ResetPassword = Volo.Abp.Identity.IdentityPermissions.Users.Default + ".ResetPassword"; public const string ManageClaims = Volo.Abp.Identity.IdentityPermissions.Users.Default + ".ManageClaims"; public const string ManageOrganizationUnits = Volo.Abp.Identity.IdentityPermissions.Users.Default + ".ManageOrganizationUnits"; + public const string Impersonation = Volo.Abp.Identity.IdentityPermissions.Users.Default + ".Impersonation"; } public static class OrganizationUnits diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json index 1059016d6..65c5f31cf 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json @@ -10,6 +10,7 @@ "Permission:IdentityClaimTypeManagement": "Management claim types", "Permission:IdentitySessions": "Management sessions", "Permission:RevokeSession": "Revoke session", + "Permission:Impersonation": "Impersonation", "OrganizationUnits": "Organization Units", "OrganizationUnit:RefreshRoot": "Refresh Root", "OrganizationUnit:Tree": "Organization tree", @@ -124,6 +125,7 @@ "Settings:Identity.Password": "Password", "Settings:Identity.OrganizationUnit": "OrganizationUnit", "Settings:Identity.Session": "Session", - "Settings:Identity.Link": "Link" + "Settings:Identity.Link": "Link", + "ImpersonationUser": "Impersonation" } } \ No newline at end of file diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json index 1d4b0dacd..3298324b0 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json @@ -10,6 +10,7 @@ "Permission:IdentityClaimTypeManagement": "管理声明类型", "Permission:IdentitySessions": "管理会话", "Permission:RevokeSession": "撤销会话", + "Permission:Impersonation": "模拟用户", "OrganizationUnits": "组织机构", "OrganizationUnit:RefreshRoot": "刷新根组织", "OrganizationUnit:Tree": "组织机构树", @@ -124,6 +125,7 @@ "Settings:Identity.Password": "密码配置", "Settings:Identity.OrganizationUnit": "组织机构", "Settings:Identity.Session": "会话配置", - "Settings:Identity.Link": "链接" + "Settings:Identity.Link": "链接", + "ImpersonationUser": "模拟用户" } } \ No newline at end of file diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xml b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xsd b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN.Abp.OpenIddict.Impersonation.csproj b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN.Abp.OpenIddict.Impersonation.csproj new file mode 100644 index 000000000..c18ffa627 --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN.Abp.OpenIddict.Impersonation.csproj @@ -0,0 +1,26 @@ + + + + + + + net10.0 + LINGYUN.Abp.OpenIddict.Impersonation + LINGYUN.Abp.OpenIddict.Impersonation + false + false + false + enable + + + + + + + + + + + + + diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/AbpOpenIddictImpersonationModule.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/AbpOpenIddictImpersonationModule.cs new file mode 100644 index 000000000..ed8a16fda --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/AbpOpenIddictImpersonationModule.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.OpenIddict; +using Volo.Abp.OpenIddict.ExtensionGrantTypes; +using Volo.Abp.OpenIddict.Localization; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.OpenIddict.Impersonation; + +[DependsOn( + typeof(AbpOpenIddictAspNetCoreModule))] +public class AbpOpenIddictImpersonationModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(builder => + { + builder.AllowImpersonationFlow(); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Grants.TryAdd( + ImpersonationTokenExtensionGrantConsts.GrantType, + new ImpersonationTokenExtensionGrant()); + }); + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources"); + }); + } +} diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationRequest.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationRequest.cs new file mode 100644 index 000000000..55fc9d238 --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationRequest.cs @@ -0,0 +1,26 @@ +using System; + +namespace LINGYUN.Abp.OpenIddict.Impersonation; + +public class ImpersonationRequest +{ + public Guid? UserId { get; set; } + public Guid? TenantId { get; set; } + public Guid? UserDelegationId { get; set; } + public string? TenantUserName { get; set; } + + public bool HasImpersonationTarget() + { + return UserId.HasValue || TenantId.HasValue || UserDelegationId.HasValue; + } + + public bool IsUserImpersonation() + { + return UserId.HasValue && !UserDelegationId.HasValue; + } + + public bool IsTenantImpersonation() + { + return !UserId.HasValue && TenantId.HasValue && !UserDelegationId.HasValue; + } +} \ No newline at end of file diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrant.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrant.cs new file mode 100644 index 000000000..bb4b16970 --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrant.cs @@ -0,0 +1,453 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OpenIddict.Abstractions; +using OpenIddict.Server; +using OpenIddict.Server.AspNetCore; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Identity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.OpenIddict; +using Volo.Abp.OpenIddict.ExtensionGrantTypes; +using Volo.Abp.OpenIddict.Localization; +using Volo.Abp.Security.Claims; +using Volo.Abp.Users; +using IdentityUser = Volo.Abp.Identity.IdentityUser; +using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult; + +namespace LINGYUN.Abp.OpenIddict.Impersonation; + +public class ImpersonationTokenExtensionGrant : ITokenExtensionGrant +{ + public string Name => ImpersonationTokenExtensionGrantConsts.GrantType; + + public async virtual Task HandleAsync(ExtensionGrantContext context) + { + var localizer = GetRequiredService>(context); + + var principal = await ValidateAccessTokenAsync(context); + if (principal == null) + { + return ForbidWithError( + OpenIddictConstants.Errors.InvalidToken, + localizer["InvalidAccessToken"]); + } + + var currentPrincipalAccessor = GetRequiredService(context); + using (currentPrincipalAccessor.Change(principal)) + { + return await ProcessImpersonationRequestAsync(context, principal); + } + } + + protected async virtual Task ValidateAccessTokenAsync(ExtensionGrantContext context) + { + var factory = GetRequiredService(context); + var dispatcher = GetRequiredService(context); + var logger = GetRequiredService>(context); + + var transaction = await factory.CreateTransactionAsync(); + transaction.EndpointType = OpenIddictServerEndpointType.Introspection; + transaction.Request = new OpenIddictRequest + { + ClientId = context.Request.ClientId, + ClientSecret = context.Request.ClientSecret, + Token = context.Request.AccessToken, + TokenTypeHint = OpenIddictConstants.TokenTypeHints.AccessToken + }; + + var authContext = new OpenIddictServerEvents.ProcessAuthenticationContext(transaction); + await dispatcher.DispatchAsync(authContext); + + if (authContext.IsRejected) + { + logger.LogWarning("Token introspection rejected: {Error}", authContext.Error); + return null; + } + + return authContext.GenericTokenPrincipal; + } + + protected async virtual Task ProcessImpersonationRequestAsync( + ExtensionGrantContext context, + ClaimsPrincipal principal) + { + var request = context.Request; + var localizer = GetRequiredService>(context); + var currentPrincipalAccessor = GetRequiredService(context); + + var isCurrentlyImpersonating = currentPrincipalAccessor.Principal.IsImpersonating(); + + var impersonationRequest = ParseImpersonationRequest(request); + + if (isCurrentlyImpersonating && !impersonationRequest.HasImpersonationTarget()) + { + return await RevertToOriginalUserAsync(context, principal); + } + + if (isCurrentlyImpersonating && impersonationRequest.HasImpersonationTarget()) + { + return ForbidWithError( + OpenIddictConstants.Errors.InvalidRequest, + localizer["NestedImpersonationNotAllowed"]); + } + + if (impersonationRequest.UserDelegationId.HasValue) + { + return await HandleDelegatedImpersonationAsync(context, principal, impersonationRequest.UserDelegationId.Value); + } + + if (impersonationRequest.IsTenantImpersonation()) + { + return await HandleTenantImpersonationAsync(context, principal, impersonationRequest); + } + + if (impersonationRequest.IsUserImpersonation()) + { + return await HandleUserImpersonationAsync(context, principal, impersonationRequest); + } + + return ForbidWithError( + OpenIddictConstants.Errors.InvalidRequest, + localizer["InvalidImpersonationRequest"]); + } + + protected virtual ImpersonationRequest ParseImpersonationRequest(OpenIddictRequest request) + { + return new ImpersonationRequest + { + UserId = ParseGuidParameter(request, "UserId"), + TenantId = ParseGuidParameter(request, "TenantId"), + UserDelegationId = ParseGuidParameter(request, "UserDelegationId"), + TenantUserName = request.GetParameter("TenantUserName")?.ToString() + }; + } + + protected async virtual Task HandleUserImpersonationAsync( + ExtensionGrantContext context, + ClaimsPrincipal principal, + ImpersonationRequest request) + { + var localizer = GetRequiredService>(context); + var currentUser = GetRequiredService(context); + var currentTenant = GetRequiredService(context); + var userManager = GetRequiredService(context); + var permissionChecker = GetRequiredService(context); + var options = GetRequiredService>(context); + + using (currentTenant.Change(request.TenantId)) + { + if (currentUser.Id == request.UserId) + { + return ForbidWithError( + OpenIddictConstants.Errors.InvalidRequest, + localizer["CannotImpersonateYourself"]); + } + + var impersonationPermission = options.Value.ImpersonationPermission; + if (!impersonationPermission.IsNullOrWhiteSpace() && + !await permissionChecker.IsGrantedAsync(impersonationPermission)) + { + return ForbidWithError( + OpenIddictConstants.Errors.InsufficientAccess, + localizer["InsufficientImpersonationPermission"]); + } + + var targetUser = await userManager.FindByIdAsync(request.UserId!.Value.ToString()!); + if (targetUser == null) + { + return ForbidWithError( + OpenIddictConstants.Errors.InvalidRequest, + localizer["TargetUserNotFound", request.UserId.Value]); + } + + return await CreateImpersonatedSignInResult(context, principal, targetUser, request.TenantId); + } + } + + protected async virtual Task HandleTenantImpersonationAsync( + ExtensionGrantContext context, + ClaimsPrincipal principal, + ImpersonationRequest request) + { + var localizer = GetRequiredService>(context); + var currentTenant = GetRequiredService(context); + var userManager = GetRequiredService(context); + var permissionChecker = GetRequiredService(context); + var options = GetRequiredService>(context); + + using (currentTenant.Change(null)) + { + var impersonationPermission = options.Value.ImpersonationTenantPermission; + if (!impersonationPermission.IsNullOrWhiteSpace() && + !await permissionChecker.IsGrantedAsync(impersonationPermission)) + { + return ForbidWithError( + OpenIddictConstants.Errors.InsufficientAccess, + localizer["RequirePermissionToImpersonateUser"]); + } + } + + using (currentTenant.Change(request.TenantId)) + { + var adminUserName = request.TenantUserName ?? options.Value.DefaultTenantAdminUserName; + if (adminUserName.IsNullOrWhiteSpace()) + { + return ForbidWithError( + OpenIddictConstants.Errors.InvalidRequest, + localizer["TenantAdminUserNameNotSpecified"]); + } + + var adminUser = await userManager.FindByNameAsync(adminUserName); + if (adminUser == null) + { + return ForbidWithError( + OpenIddictConstants.Errors.InvalidRequest, + localizer["TenantAdminUserNotFound", adminUserName]); + } + + return await CreateImpersonatedSignInResult(context, principal, adminUser, request.TenantId); + } + } + + protected async virtual Task HandleDelegatedImpersonationAsync( + ExtensionGrantContext context, + ClaimsPrincipal principal, + Guid userDelegationId) + { + var localizer = GetRequiredService>(context); + var currentUser = GetRequiredService(context); + var currentTenant = GetRequiredService(context); + var userManager = GetRequiredService(context); + var delegationManager = GetRequiredService(context); + + var delegation = await delegationManager.FindActiveDelegationByIdAsync(userDelegationId); + if (delegation == null) + { + return ForbidWithError( + OpenIddictConstants.Errors.InvalidRequest, + localizer["InvalidUserDelegation"]); + } + + if (currentUser.Id != delegation.TargetUserId) + { + return ForbidWithError( + OpenIddictConstants.Errors.InsufficientAccess, + localizer["NotAuthorizedForDelegation"]); + } + + if (delegation.SourceUserId == currentUser.Id) + { + return ForbidWithError( + OpenIddictConstants.Errors.InvalidRequest, + localizer["CannotImpersonateYourself"]); + } + + var sourceUser = await userManager.FindByIdAsync(delegation.SourceUserId.ToString()); + if (sourceUser == null) + { + return ForbidWithError( + OpenIddictConstants.Errors.InvalidRequest, + localizer["DelegationSourceUserNotFound"]); + } + + return await CreateImpersonatedSignInResult( + context, + principal, + sourceUser, + currentTenant.Id, + "DelegatedImpersonation"); + } + + protected async virtual Task RevertToOriginalUserAsync( + ExtensionGrantContext context, + ClaimsPrincipal principal) + { + var localizer = GetRequiredService>(context); + var currentPrincipalAccessor = GetRequiredService(context); + var userManager = GetRequiredService(context); + var currentTenant = GetRequiredService(context); + + var impersonatorUserId = currentPrincipalAccessor.Principal.FindImpersonatorUserId(); + var impersonatorTenantId = currentPrincipalAccessor.Principal.FindImpersonatorTenantId(); + + if (!impersonatorUserId.HasValue) + { + return ForbidWithError( + OpenIddictConstants.Errors.InvalidRequest, + localizer["NoImpersonationContext"]); + } + + using (currentTenant.Change(impersonatorTenantId)) + { + var originalUser = await userManager.FindByIdAsync(impersonatorUserId.Value.ToString()); + if (originalUser == null) + { + return ForbidWithError( + OpenIddictConstants.Errors.InvalidRequest, + localizer["OriginalUserNotFound"]); + } + + return await CreateAuthenticatedSignInResult(context, principal, originalUser); + } + } + + protected async virtual Task CreateImpersonatedSignInResult( + ExtensionGrantContext context, + ClaimsPrincipal currentPrincipal, + IdentityUser targetUser, + Guid? tenantId, + string action = "ImpersonateUser") + { + var userClaimsPrincipalFactory = GetRequiredService>(context); + var currentUser = GetRequiredService(context); + var currentTenant = GetRequiredService(context); + var currentPrincipalAccessor = GetRequiredService(context); + + var principal = await userClaimsPrincipalFactory.CreateAsync(targetUser); + var claims = new List(); + + if (currentUser.Id.HasValue && currentUser.Id?.ToString() != currentPrincipalAccessor.Principal.FindImpersonatorUserId()?.ToString()) + { + claims.Add(new Claim(AbpClaimTypes.ImpersonatorUserId, currentUser.Id!.Value.ToString())); + claims.Add(new Claim(AbpClaimTypes.ImpersonatorUserName, currentUser.UserName!)); + + if (currentTenant.IsAvailable) + { + claims.Add(new Claim(AbpClaimTypes.ImpersonatorTenantId, currentTenant.Id!.Value.ToString())); + } + } + + var rememberMeClaim = currentPrincipal.Claims.FirstOrDefault(c => c.Type == AbpClaimTypes.RememberMe); + if (rememberMeClaim != null) + { + claims.Add(rememberMeClaim); + } + + var identity = principal.Identities.FirstOrDefault(); + if (identity != null && claims.Count != 0) + { + identity.AddClaims(claims); + } + + return await FinalizeSignInAsync(context, principal, currentPrincipal.GetScopes(), action); + } + + protected async Task CreateAuthenticatedSignInResult( + ExtensionGrantContext context, + ClaimsPrincipal currentPrincipal, + IdentityUser targetUser) + { + var userClaimsPrincipalFactory = GetRequiredService>(context); + var principal = await userClaimsPrincipalFactory.CreateAsync(targetUser); + + return await FinalizeSignInAsync(context, principal, currentPrincipal.GetScopes(), "RevertImpersonation"); + } + + protected async virtual Task FinalizeSignInAsync( + ExtensionGrantContext context, + ClaimsPrincipal principal, + IEnumerable scopes, + string action) + { + var currentPrincipalAccessor = GetRequiredService(context); + + using (currentPrincipalAccessor.Change(principal)) + { + await SaveSecurityLogAsync(context, principal, action); + } + + principal.SetScopes(scopes); + principal.SetResources(await GetResourcesAsync(context, scopes)); + + await SetClaimsDestinationsAsync(context, principal); + + return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal); + } + + protected async virtual Task SaveSecurityLogAsync( + ExtensionGrantContext context, + ClaimsPrincipal principal, + string action) + { + var identitySecurityLogManager = GetRequiredService(context); + var currentUser = GetRequiredService(context); + var currentTenant = GetRequiredService(context); + + var logContext = new IdentitySecurityLogContext + { + Identity = OpenIddictSecurityLogIdentityConsts.OpenIddict, + Action = action, + UserName = principal.FindFirstValue(ClaimTypes.Name), + ClientId = context.Request.ClientId + }; + logContext.WithProperty("GrantType", Name); + logContext.WithProperty("ImpersonatorUserId", currentUser.Id?.ToString()); + logContext.WithProperty("ImpersonatorTenantId", currentTenant.Id?.ToString()); + + await identitySecurityLogManager.SaveAsync(logContext); + } + + protected async virtual Task> GetResourcesAsync( + ExtensionGrantContext context, + IEnumerable scopes) + { + var resources = new List(); + if (scopes.Any()) + { + var scopeManager = GetRequiredService(context); + + await foreach (var resource in scopeManager.ListResourcesAsync(scopes.ToImmutableArray())) + { + resources.Add(resource); + } + } + + return resources; + } + + protected async virtual Task SetClaimsDestinationsAsync( + ExtensionGrantContext context, + ClaimsPrincipal principal) + { + var manager = GetRequiredService(context); + await manager.HandleAsync(context.Request, principal); + } + + protected virtual IActionResult ForbidWithError(string error, string description) + { + return new ForbidResult( + new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, + new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = error, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = description + })); + } + + protected virtual T GetRequiredService(ExtensionGrantContext context) where T : notnull + { + return context.HttpContext.RequestServices.GetRequiredService(); + } + + protected static Guid? ParseGuidParameter(OpenIddictRequest request, string name) + { + var parameter = request.GetParameter(name); + if (parameter == null) + { + return null; + } + + return Guid.TryParse(parameter.Value.ToString(), out var result) ? result : null; + } +} \ No newline at end of file diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrantConsts.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrantConsts.cs new file mode 100644 index 000000000..286c1cfea --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrantConsts.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Abp.OpenIddict.Impersonation; + +public static class ImpersonationTokenExtensionGrantConsts +{ + public const string GrantType = "impersonation"; +} diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/en.json b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/en.json new file mode 100644 index 000000000..7cffa3b5e --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/en.json @@ -0,0 +1,19 @@ +{ + "culture": "en", + "texts": { + "InvalidAccessToken": "Invalid access token, token validation failed", + "NestedImpersonationNotAllowed": "Nested impersonation is not allowed. You are currently in impersonation mode. Please revert to your original identity first", + "InvalidImpersonationRequest": "Invalid impersonation request. Please provide valid impersonation target parameters (User ID, Tenant ID, or Delegation ID)", + "CannotImpersonateYourself": "You cannot impersonate yourself. Please select a different user to impersonate", + "InsufficientImpersonationPermission": "Insufficient permission to perform user impersonation", + "TargetUserNotFound": "Target user not found: {0}", + "RequirePermissionToImpersonateUser": "Authorization is required to simulate users/tenants!", + "TenantAdminUserNameNotSpecified": "Tenant admin user name is not specified. Tenant impersonation requires an admin user name", + "TenantAdminUserNotFound": "Tenant admin user not found: {0}. Please verify the admin user name is correct", + "InvalidUserDelegation": "Invalid user delegation. The delegation record does not exist or has expired", + "NotAuthorizedForDelegation": "Delegation authorization failed. Current user is not the target of this delegation", + "DelegationSourceUserNotFound": "Delegation source user not found. Unable to complete delegated impersonation", + "NoImpersonationContext": "No impersonation context found. Unable to revert to original user identity", + "OriginalUserNotFound": "Original user not found. The impersonator's user record may have been deleted" + } +} \ No newline at end of file diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/zh-Hans.json b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..74c419f5c --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "culture": "zh-Hans", + "texts": { + "InvalidAccessToken": "无效的访问令牌,令牌验证失败!", + "NestedImpersonationNotAllowed": "不允许嵌套模拟,当前已处于模拟登录状态,请先返回原始用户身份!", + "InvalidImpersonationRequest": "无效的模拟请求,请提供正确的模拟目标参数(用户ID、租户ID或委托ID)!", + "CannotImpersonateYourself": "不能模拟自己,请选择其他用户进行模拟!", + "InsufficientImpersonationPermission": "模拟权限不足,您没有执行用户模拟操作的权限!", + "TargetUserNotFound": "目标用户不存在:{0}!", + "RequirePermissionToImpersonateUser": "需要授权才能模拟用户/租户!", + "TenantAdminUserNameNotSpecified": "未指定租户管理员用户名,租户模拟需要提供管理员用户名!", + "TenantAdminUserNotFound": "未找到租户管理员用户:{0},请确认管理员用户名是否正确!", + "InvalidUserDelegation": "无效的用户委托,委托记录不存在或已失效!", + "NotAuthorizedForDelegation": "委托授权失败,当前用户不是该委托的目标用户!", + "DelegationSourceUserNotFound": "委托源用户不存在,无法完成委托模拟!", + "NoImpersonationContext": "未找到模拟上下文,无法返回原始用户身份!", + "OriginalUserNotFound": "原始用户不存在,模拟者的用户记录可能已被删除!" + } +} \ No newline at end of file diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/OpenIddictImpersonationOptions.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/OpenIddictImpersonationOptions.cs new file mode 100644 index 000000000..e09ac506a --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/OpenIddictImpersonationOptions.cs @@ -0,0 +1,21 @@ +namespace LINGYUN.Abp.OpenIddict.Impersonation; + +public class OpenIddictImpersonationOptions +{ + /// + /// 模拟用户时检查权限 + /// + public string? ImpersonationPermission { get; set; } + /// + /// 模拟租户时检查权限 + /// + public string? ImpersonationTenantPermission { get; set; } + /// + /// 宿主端模拟租户时默认管理员用户名 + /// + public string DefaultTenantAdminUserName { get; set; } + public OpenIddictImpersonationOptions() + { + DefaultTenantAdminUserName = "admin"; + } +} diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/Microsoft/Extensions/DependencyInjection/ImpersonationOpenIddictServerBuilderExtensions.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/Microsoft/Extensions/DependencyInjection/ImpersonationOpenIddictServerBuilderExtensions.cs new file mode 100644 index 000000000..b9f962117 --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/Microsoft/Extensions/DependencyInjection/ImpersonationOpenIddictServerBuilderExtensions.cs @@ -0,0 +1,11 @@ +using LINGYUN.Abp.OpenIddict.Impersonation; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ImpersonationOpenIddictServerBuilderExtensions +{ + public static OpenIddictServerBuilder AllowImpersonationFlow(this OpenIddictServerBuilder builder) + { + return builder.AllowCustomFlow(ImpersonationTokenExtensionGrantConsts.GrantType); + } +} diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/System/Security/Claims/ImpersonationClaimExtensions.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/System/Security/Claims/ImpersonationClaimExtensions.cs new file mode 100644 index 000000000..9a09adcb1 --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/System/Security/Claims/ImpersonationClaimExtensions.cs @@ -0,0 +1,23 @@ +using Volo.Abp.Security.Claims; + +namespace System.Security.Claims; + +public static class ImpersonationClaimExtensions +{ + public static bool IsImpersonating(this ClaimsPrincipal principal) + { + return principal.FindImpersonatorUserId().HasValue; + } + + public static Guid? FindImpersonatorUserId(this ClaimsPrincipal principal) + { + var value = principal.FindFirstValue(AbpClaimTypes.ImpersonatorUserId); + return Guid.TryParse(value, out var result) ? result : null; + } + + public static Guid? FindImpersonatorTenantId(this ClaimsPrincipal principal) + { + var value = principal.FindFirstValue(AbpClaimTypes.ImpersonatorTenantId); + return Guid.TryParse(value, out var result) ? result : null; + } +} diff --git a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissionDefinitionProvider.cs b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissionDefinitionProvider.cs index 0776aab55..9027e268a 100644 --- a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissionDefinitionProvider.cs +++ b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissionDefinitionProvider.cs @@ -1,10 +1,10 @@ using LINGYUN.Abp.Saas.Localization; -using Volo.Abp.Authorization.Permissions; -using Volo.Abp.Localization; -using Volo.Abp.MultiTenancy; - -namespace LINGYUN.Abp.Saas; - +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Localization; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.Saas; + public class AbpSaasPermissionDefinitionProvider : PermissionDefinitionProvider { public override void Define(IPermissionDefinitionContext context) @@ -23,10 +23,11 @@ public class AbpSaasPermissionDefinitionProvider : PermissionDefinitionProvider tenantsPermission.AddChild(AbpSaasPermissions.Tenants.Delete, L("Permission:Delete"), multiTenancySide: MultiTenancySides.Host); tenantsPermission.AddChild(AbpSaasPermissions.Tenants.ManageFeatures, L("Permission:ManageFeatures"), multiTenancySide: MultiTenancySides.Host); tenantsPermission.AddChild(AbpSaasPermissions.Tenants.ManageConnectionStrings, L("Permission:ManageConnectionStrings"), multiTenancySide: MultiTenancySides.Host); + tenantsPermission.AddChild(AbpSaasPermissions.Tenants.Impersonation, L("Permission:Impersonation"), multiTenancySide: MultiTenancySides.Host); } private static LocalizableString L(string name) { return LocalizableString.Create(name); } -} +} diff --git a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissions.cs b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissions.cs index 729e1be49..fa37e2a4d 100644 --- a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissions.cs +++ b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissions.cs @@ -1,7 +1,7 @@ -using Volo.Abp.Reflection; - -namespace LINGYUN.Abp.Saas; - +using Volo.Abp.Reflection; + +namespace LINGYUN.Abp.Saas; + public static class AbpSaasPermissions { public const string GroupName = "AbpSaas"; @@ -23,6 +23,7 @@ public static class AbpSaasPermissions public const string Delete = Default + ".Delete"; public const string ManageFeatures = Default + ".ManageFeatures"; public const string ManageConnectionStrings = Default + ".ManageConnectionStrings"; + public const string Impersonation = Default + ".Impersonation"; } public static string[] GetAll() diff --git a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/en.json b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/en.json index 8d976749b..bee5ad9e5 100644 --- a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/en.json +++ b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/en.json @@ -42,6 +42,7 @@ "Permission:Delete": "Delete", "Permission:ManageConnectionStrings": "Manage connection strings", "Permission:ManageFeatures": "Manage features", + "Permission:Impersonation": "Impersonation", "DisplayName:AdminEmailAddress": "Admin Email Address", "DisplayName:AdminPassword": "Admin Password", "Features:RecycleStrategy": "Recycle Strategy", @@ -54,6 +55,7 @@ "RecycleStrategy:Reserve": "Reserve", "RecycleStrategy:Recycle": "Recycle", "ManageFeatures": "Manage Features", - "IfUseCustomDataBaseDefaultConnectionStringIsRequiredMessage": "If you use a custom database, the default connection string must be required!" + "IfUseCustomDataBaseDefaultConnectionStringIsRequiredMessage": "If you use a custom database, the default connection string must be required!", + "ImpersonationTenant": "Impersonation" } } \ No newline at end of file diff --git a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/zh-Hans.json b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/zh-Hans.json index 21dba4be6..d6a882b83 100644 --- a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/zh-Hans.json +++ b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/zh-Hans.json @@ -42,6 +42,7 @@ "Permission:Delete": "删除", "Permission:ManageConnectionStrings": "管理连接字符串", "Permission:ManageFeatures": "管理功能", + "Permission:Impersonation": "模拟租户", "DisplayName:AdminEmailAddress": "管理员电子邮件地址", "DisplayName:AdminPassword": "管理员密码", "Features:RecycleStrategy": "资源回收策略", @@ -54,6 +55,7 @@ "RecycleStrategy:Reserve": "保留", "RecycleStrategy:Recycle": "回收", "ManageFeatures": "管理功能", - "IfUseCustomDataBaseDefaultConnectionStringIsRequiredMessage": "如果使用自定义数据库,默认连接字符串必须输入!" + "IfUseCustomDataBaseDefaultConnectionStringIsRequiredMessage": "如果使用自定义数据库,默认连接字符串必须输入!", + "ImpersonationTenant": "模拟租户" } } \ No newline at end of file