Browse Source

feat: Add user Impersonation implementation

pull/1525/head
colin 3 months ago
parent
commit
8f46ce7f90
  1. 3
      aspnet-core/LINGYUN.MicroService.Aspire.slnx
  2. 2
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/AdminServiceMigrationsEntityFrameworkCoreModule.cs
  3. 2
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore.csproj
  4. 3
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeFeatureDefinitionManager.cs
  5. 3
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergePermissionDefinitionManager.cs
  6. 3
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeSettingDefinitionManager.cs
  7. 2
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeTemplateDefinitionManager.cs
  8. 6
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.EntityFrameworkCore/DataSeeds/OpenIddictDataSeeder.cs
  9. 13
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.Configure.cs
  10. 2
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.cs
  11. 1
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/LINGYUN.Abp.MicroService.AuthServer.csproj
  12. 254
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeFeatureDefinitionManager.cs
  13. 188
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergePermissionDefinitionManager.cs
  14. 73
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeSettingDefinitionManager.cs
  15. 86
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeTemplateDefinitionManager.cs
  16. 4
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/appsettings.Development.json
  17. 5
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json
  18. 5
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json
  19. 1
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissionDefinitionProvider.cs
  20. 1
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissions.cs
  21. 4
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json
  22. 4
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json
  23. 3
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xml
  24. 30
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xsd
  25. 26
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN.Abp.OpenIddict.Impersonation.csproj
  26. 44
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/AbpOpenIddictImpersonationModule.cs
  27. 26
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationRequest.cs
  28. 453
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrant.cs
  29. 6
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrantConsts.cs
  30. 19
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/en.json
  31. 19
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/zh-Hans.json
  32. 21
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/OpenIddictImpersonationOptions.cs
  33. 11
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/Microsoft/Extensions/DependencyInjection/ImpersonationOpenIddictServerBuilderExtensions.cs
  34. 23
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/System/Security/Claims/ImpersonationClaimExtensions.cs
  35. 15
      aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissionDefinitionProvider.cs
  36. 9
      aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissions.cs
  37. 4
      aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/en.json
  38. 4
      aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/zh-Hans.json

3
aspnet-core/LINGYUN.MicroService.Aspire.slnx

@ -401,6 +401,7 @@
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.Dapr.Client/LINGYUN.Abp.OpenIddict.Dapr.Client.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.HttpApi.Client/LINGYUN.Abp.OpenIddict.HttpApi.Client.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.HttpApi/LINGYUN.Abp.OpenIddict.HttpApi.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN.Abp.OpenIddict.Impersonation.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.LinkUser/LINGYUN.Abp.OpenIddict.LinkUser.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.Portal/LINGYUN.Abp.OpenIddict.Portal.csproj" />
<Project Path="modules/openIddict/LINGYUN.Abp.OpenIddict.Sms/LINGYUN.Abp.OpenIddict.Sms.csproj" />
@ -411,6 +412,8 @@
<Project Path="modules/permissions-management/LINGYUN.Abp.PermissionManagement.Application.Contracts/LINGYUN.Abp.PermissionManagement.Application.Contracts.csproj" />
<Project Path="modules/permissions-management/LINGYUN.Abp.PermissionManagement.Application/LINGYUN.Abp.PermissionManagement.Application.csproj" />
<Project Path="modules/permissions-management/LINGYUN.Abp.PermissionManagement.Domain.OrganizationUnits/LINGYUN.Abp.PermissionManagement.Domain.OrganizationUnits.csproj" />
<Project Path="modules/permissions-management/LINGYUN.Abp.PermissionManagement.Domain/LINGYUN.Abp.PermissionManagement.Domain.csproj" />
<Project Path="modules/permissions-management/LINGYUN.Abp.PermissionManagement.EntityFrameworkCore/LINGYUN.Abp.PermissionManagement.EntityFrameworkCore.csproj" />
<Project Path="modules/permissions-management/LINGYUN.Abp.PermissionManagement.HttpApi/LINGYUN.Abp.PermissionManagement.HttpApi.csproj" />
</Folder>
<Folder Name="/modules/platform/">

2
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;

2
aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore.csproj

@ -18,7 +18,6 @@
<PackageReference Include="Volo.Abp.EntityFrameworkCore.PostgreSql" />
<PackageReference Include="Volo.Abp.FeatureManagement.EntityFrameworkCore" />
<PackageReference Include="Volo.Abp.SettingManagement.EntityFrameworkCore" />
<PackageReference Include="Volo.Abp.PermissionManagement.EntityFrameworkCore" />
</ItemGroup>
<ItemGroup>
@ -38,6 +37,7 @@
<ProjectReference Include="..\..\modules\openIddict\LINGYUN.Abp.OpenIddict.Application.Contracts\LINGYUN.Abp.OpenIddict.Application.Contracts.csproj" />
<ProjectReference Include="..\..\modules\blob-management\LINGYUN.Abp.BlobManagement.Application.Contracts\LINGYUN.Abp.BlobManagement.Application.Contracts.csproj" />
<ProjectReference Include="..\..\modules\permissions-management\LINGYUN.Abp.PermissionManagement.Application.Contracts\LINGYUN.Abp.PermissionManagement.Application.Contracts.csproj" />
<ProjectReference Include="..\..\modules\permissions-management\LINGYUN.Abp.PermissionManagement.EntityFrameworkCore\LINGYUN.Abp.PermissionManagement.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\modules\platform\LINGYUN.Platform.Application.Contracts\LINGYUN.Platform.Application.Contracts.csproj" />
<ProjectReference Include="..\..\modules\project\LINGYUN.Abp.ProjectManagement.Application.Contracts\LINGYUN.Abp.ProjectManagement.Application.Contracts.csproj" />
<ProjectReference Include="..\..\modules\realtime-message\LINGYUN.Abp.MessageService.Application.Contracts\LINGYUN.Abp.MessageService.Application.Contracts.csproj" />

3
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

3
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

3
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

2
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)]

6
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,

13
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<FeatureManagementOptions>(options =>
{
options.SaveStaticFeaturesToDatabase = false;
options.IsDynamicFeatureStoreEnabled = true;
});
}
@ -209,6 +212,7 @@ public partial class AuthServerModule
Configure<PermissionManagementOptions>(options =>
{
options.SaveStaticPermissionsToDatabase = false;
options.IsDynamicPermissionStoreEnabled = true;
});
}
@ -216,6 +220,7 @@ public partial class AuthServerModule
{
Configure<SettingManagementOptions>(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<OpenIddictImpersonationOptions>(options =>
{
// 模拟用户权限委派
options.ImpersonationPermission = IdentityPermissions.Users.Impersonation;
options.ImpersonationTenantPermission = "AbpSaas.Tenants.Impersonation";
});
}
private void ConfigureSecurity(IServiceCollection services, IConfiguration configuration, bool isDevelopment = false)
{

2
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),

1
aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/LINGYUN.Abp.MicroService.AuthServer.csproj

@ -50,6 +50,7 @@
<ProjectReference Include="..\..\framework\security\LINGYUN.Abp.Claims.Mapping\LINGYUN.Abp.Claims.Mapping.csproj" />
<ProjectReference Include="..\..\framework\tenants\LINGYUN.Abp.AspNetCore.MultiTenancy\LINGYUN.Abp.AspNetCore.MultiTenancy.csproj" />
<ProjectReference Include="..\..\modules\identity\LINGYUN.Abp.Identity.AspNetCore\LINGYUN.Abp.Identity.AspNetCore.csproj" />
<ProjectReference Include="..\..\modules\openIddict\LINGYUN.Abp.OpenIddict.Impersonation\LINGYUN.Abp.OpenIddict.Impersonation.csproj" />
</ItemGroup>
<ItemGroup>

254
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<IReadOnlyList<FeatureDefinition>> GetAllAsync()
{
var staticFeatures = await StaticStore.GetFeaturesAsync();
var dynamicFeatures = await DynamicStore.GetFeaturesAsync();
var mergedFeatures = new Dictionary<string, FeatureDefinition>();
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<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync()
{
var staticGroups = await StaticStore.GetGroupsAsync();
var dynamicGroups = await DynamicStore.GetGroupsAsync();
var mergedGroups = new Dictionary<string, FeatureGroupDefinition>();
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<FeatureDefinition> 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;
}
}

188
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<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync()
{
var staticGroups = await _staticStore.GetGroupsAsync();
var dynamicGroups = await _dynamicStore.GetGroupsAsync();
var mergedGroups = new Dictionary<string, PermissionGroupDefinition>();
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;
}
}

73
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<IReadOnlyList<SettingDefinition>> GetAllAsync()
{
var staticSettings = await _staticStore.GetAllAsync();
var dynamicSettings = await _dynamicStore.GetAllAsync();
var mergedSettings = new Dictionary<string, SettingDefinition>();
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);
}
}
}
}

86
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<IReadOnlyList<TemplateDefinition>> GetAllAsync()
{
var staticTemplates = await StaticStore.GetAllAsync();
var dynamicTemplates = await DynamicStore.GetAllAsync();
var mergedTemplates = new Dictionary<string, TemplateDefinition>();
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;
}
}

4
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": [

5
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!"
}
}

5
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": "您将从当前账户退出, 回到您之前登录的账户!"
}
}

1
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);

1
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

4
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"
}
}

4
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": "模拟用户"
}
}

3
aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

26
aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN.Abp.OpenIddict.Impersonation.csproj

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\configureawait.props" />
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>LINGYUN.Abp.OpenIddict.Impersonation</AssemblyName>
<PackageId>LINGYUN.Abp.OpenIddict.Impersonation</PackageId>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<Nullable>enable</Nullable>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<None Remove="LINGYUN\Abp\OpenIddict\Impersonation\Localization\Resources\*.json" />
<EmbeddedResource Include="LINGYUN\Abp\OpenIddict\Impersonation\Localization\Resources\*.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.OpenIddict.AspNetCore" />
</ItemGroup>
</Project>

44
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<OpenIddictServerBuilder>(builder =>
{
builder.AllowImpersonationFlow();
});
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpOpenIddictExtensionGrantsOptions>(options =>
{
options.Grants.TryAdd(
ImpersonationTokenExtensionGrantConsts.GrantType,
new ImpersonationTokenExtensionGrant());
});
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpOpenIddictImpersonationModule>();
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Get<AbpOpenIddictResource>()
.AddVirtualJson("/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources");
});
}
}

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

453
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<IActionResult> HandleAsync(ExtensionGrantContext context)
{
var localizer = GetRequiredService<IStringLocalizer<AbpOpenIddictResource>>(context);
var principal = await ValidateAccessTokenAsync(context);
if (principal == null)
{
return ForbidWithError(
OpenIddictConstants.Errors.InvalidToken,
localizer["InvalidAccessToken"]);
}
var currentPrincipalAccessor = GetRequiredService<ICurrentPrincipalAccessor>(context);
using (currentPrincipalAccessor.Change(principal))
{
return await ProcessImpersonationRequestAsync(context, principal);
}
}
protected async virtual Task<ClaimsPrincipal?> ValidateAccessTokenAsync(ExtensionGrantContext context)
{
var factory = GetRequiredService<IOpenIddictServerFactory>(context);
var dispatcher = GetRequiredService<IOpenIddictServerDispatcher>(context);
var logger = GetRequiredService<ILogger<ImpersonationTokenExtensionGrant>>(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<IActionResult> ProcessImpersonationRequestAsync(
ExtensionGrantContext context,
ClaimsPrincipal principal)
{
var request = context.Request;
var localizer = GetRequiredService<IStringLocalizer<AbpOpenIddictResource>>(context);
var currentPrincipalAccessor = GetRequiredService<ICurrentPrincipalAccessor>(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<IActionResult> HandleUserImpersonationAsync(
ExtensionGrantContext context,
ClaimsPrincipal principal,
ImpersonationRequest request)
{
var localizer = GetRequiredService<IStringLocalizer<AbpOpenIddictResource>>(context);
var currentUser = GetRequiredService<ICurrentUser>(context);
var currentTenant = GetRequiredService<ICurrentTenant>(context);
var userManager = GetRequiredService<IdentityUserManager>(context);
var permissionChecker = GetRequiredService<IPermissionChecker>(context);
var options = GetRequiredService<IOptions<OpenIddictImpersonationOptions>>(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<IActionResult> HandleTenantImpersonationAsync(
ExtensionGrantContext context,
ClaimsPrincipal principal,
ImpersonationRequest request)
{
var localizer = GetRequiredService<IStringLocalizer<AbpOpenIddictResource>>(context);
var currentTenant = GetRequiredService<ICurrentTenant>(context);
var userManager = GetRequiredService<IdentityUserManager>(context);
var permissionChecker = GetRequiredService<IPermissionChecker>(context);
var options = GetRequiredService<IOptions<OpenIddictImpersonationOptions>>(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<IActionResult> HandleDelegatedImpersonationAsync(
ExtensionGrantContext context,
ClaimsPrincipal principal,
Guid userDelegationId)
{
var localizer = GetRequiredService<IStringLocalizer<AbpOpenIddictResource>>(context);
var currentUser = GetRequiredService<ICurrentUser>(context);
var currentTenant = GetRequiredService<ICurrentTenant>(context);
var userManager = GetRequiredService<IdentityUserManager>(context);
var delegationManager = GetRequiredService<IdentityUserDelegationManager>(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<IActionResult> RevertToOriginalUserAsync(
ExtensionGrantContext context,
ClaimsPrincipal principal)
{
var localizer = GetRequiredService<IStringLocalizer<AbpOpenIddictResource>>(context);
var currentPrincipalAccessor = GetRequiredService<ICurrentPrincipalAccessor>(context);
var userManager = GetRequiredService<IdentityUserManager>(context);
var currentTenant = GetRequiredService<ICurrentTenant>(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<IActionResult> CreateImpersonatedSignInResult(
ExtensionGrantContext context,
ClaimsPrincipal currentPrincipal,
IdentityUser targetUser,
Guid? tenantId,
string action = "ImpersonateUser")
{
var userClaimsPrincipalFactory = GetRequiredService<IUserClaimsPrincipalFactory<IdentityUser>>(context);
var currentUser = GetRequiredService<ICurrentUser>(context);
var currentTenant = GetRequiredService<ICurrentTenant>(context);
var currentPrincipalAccessor = GetRequiredService<ICurrentPrincipalAccessor>(context);
var principal = await userClaimsPrincipalFactory.CreateAsync(targetUser);
var claims = new List<Claim>();
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<IActionResult> CreateAuthenticatedSignInResult(
ExtensionGrantContext context,
ClaimsPrincipal currentPrincipal,
IdentityUser targetUser)
{
var userClaimsPrincipalFactory = GetRequiredService<IUserClaimsPrincipalFactory<IdentityUser>>(context);
var principal = await userClaimsPrincipalFactory.CreateAsync(targetUser);
return await FinalizeSignInAsync(context, principal, currentPrincipal.GetScopes(), "RevertImpersonation");
}
protected async virtual Task<IActionResult> FinalizeSignInAsync(
ExtensionGrantContext context,
ClaimsPrincipal principal,
IEnumerable<string> scopes,
string action)
{
var currentPrincipalAccessor = GetRequiredService<ICurrentPrincipalAccessor>(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<IdentitySecurityLogManager>(context);
var currentUser = GetRequiredService<ICurrentUser>(context);
var currentTenant = GetRequiredService<ICurrentTenant>(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<IEnumerable<string>> GetResourcesAsync(
ExtensionGrantContext context,
IEnumerable<string> scopes)
{
var resources = new List<string>();
if (scopes.Any())
{
var scopeManager = GetRequiredService<IOpenIddictScopeManager>(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<AbpOpenIddictClaimsPrincipalManager>(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<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = error,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = description
}));
}
protected virtual T GetRequiredService<T>(ExtensionGrantContext context) where T : notnull
{
return context.HttpContext.RequestServices.GetRequiredService<T>();
}
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;
}
}

6
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";
}

19
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"
}
}

19
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": "原始用户不存在,模拟者的用户记录可能已被删除!"
}
}

21
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
{
/// <summary>
/// 模拟用户时检查权限
/// </summary>
public string? ImpersonationPermission { get; set; }
/// <summary>
/// 模拟租户时检查权限
/// </summary>
public string? ImpersonationTenantPermission { get; set; }
/// <summary>
/// 宿主端模拟租户时默认管理员用户名
/// </summary>
public string DefaultTenantAdminUserName { get; set; }
public OpenIddictImpersonationOptions()
{
DefaultTenantAdminUserName = "admin";
}
}

11
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);
}
}

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

15
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<AbpSaasResource>(name);
}
}
}

9
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()

4
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"
}
}

4
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": "模拟租户"
}
}
Loading…
Cancel
Save