Browse Source

Merge pull request #1525 from colinin/openiddict-impersonation

Openiddict impersonation
pull/1529/head
yx lin 3 months ago
committed by GitHub
parent
commit
2cb0127307
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 64
      apps/vben5/apps/app-antd/src/layouts/basic.vue
  2. 1
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  3. 1
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  4. 32
      apps/vben5/apps/app-antd/src/store/auth.ts
  5. 26
      apps/vben5/apps/app-antd/src/views/identity/users/index.vue
  6. 90
      apps/vben5/apps/app-antd/src/views/saas/tenants/index.vue
  7. 6
      apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts
  8. 14
      apps/vben5/packages/@abp/account/src/types/token.ts
  9. 30
      apps/vben5/packages/@abp/account/src/utils/auth.ts
  10. 36
      apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue
  11. 2
      apps/vben5/packages/@abp/identity/src/constants/permissions.ts
  12. 2
      apps/vben5/packages/@abp/identity/src/types/users.ts
  13. 19
      apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue
  14. 2
      apps/vben5/packages/@abp/saas/src/constants/permissions.ts
  15. 3
      aspnet-core/LINGYUN.MicroService.Aspire.slnx
  16. 2
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/AdminServiceMigrationsEntityFrameworkCoreModule.cs
  17. 2
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore/LINGYUN.Abp.MicroService.AdminService.EntityFrameworkCore.csproj
  18. 3
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeFeatureDefinitionManager.cs
  19. 3
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergePermissionDefinitionManager.cs
  20. 3
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeSettingDefinitionManager.cs
  21. 2
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AdminService/Overrides/MergeTemplateDefinitionManager.cs
  22. 6
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.EntityFrameworkCore/DataSeeds/OpenIddictDataSeeder.cs
  23. 13
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.Configure.cs
  24. 2
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/AuthServerModule.cs
  25. 1
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/LINGYUN.Abp.MicroService.AuthServer.csproj
  26. 254
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeFeatureDefinitionManager.cs
  27. 188
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergePermissionDefinitionManager.cs
  28. 73
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeSettingDefinitionManager.cs
  29. 86
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/Overrides/MergeTemplateDefinitionManager.cs
  30. 4
      aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer/appsettings.Development.json
  31. 5
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json
  32. 5
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json
  33. 1
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissionDefinitionProvider.cs
  34. 1
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Application.Contracts/LINGYUN/Abp/Identity/IdentityPermissions.cs
  35. 4
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json
  36. 4
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json
  37. 3
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xml
  38. 30
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/FodyWeavers.xsd
  39. 26
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN.Abp.OpenIddict.Impersonation.csproj
  40. 44
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/AbpOpenIddictImpersonationModule.cs
  41. 26
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationRequest.cs
  42. 463
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrant.cs
  43. 6
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrantConsts.cs
  44. 19
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/en.json
  45. 19
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/Localization/Resources/zh-Hans.json
  46. 21
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/OpenIddictImpersonationOptions.cs
  47. 11
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/Microsoft/Extensions/DependencyInjection/ImpersonationOpenIddictServerBuilderExtensions.cs
  48. 23
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/System/Security/Claims/ImpersonationClaimExtensions.cs
  49. 15
      aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissionDefinitionProvider.cs
  50. 9
      aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application.Contracts/LINGYUN/Abp/Saas/Permissions/AbpSaasPermissions.cs
  51. 4
      aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/en.json
  52. 4
      aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/zh-Hans.json

64
apps/vben5/apps/app-antd/src/layouts/basic.vue

@ -4,7 +4,11 @@ import type { NotificationItem } from '@vben/layouts';
import { computed, defineAsyncComponent, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
import {
AuthenticationLoginExpiredModal,
confirm,
useVbenModal,
} from '@vben/common-ui';
import { useWatermark } from '@vben/hooks';
import { createIconifyIcon } from '@vben/icons';
import {
@ -24,7 +28,8 @@ import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
const UserSettingsIcon = createIconifyIcon('tdesign:user-setting');
const UserLinkIcon = createIconifyIcon('material-symbols-light:link');
const UserLinkIcon = createIconifyIcon('material-symbols:link');
const BckLoginIcon = createIconifyIcon('mingcute:back-line');
const notifications = ref<NotificationItem[]>([]);
@ -48,22 +53,32 @@ const [LinkUserModal, linkUserModalApi] = useVbenModal({
}),
});
const menus = computed(() => [
{
handler: () => {
linkUserModalApi.open();
const menus = computed(() => {
const basicMenus = [
{
handler: () => {
linkUserModalApi.open();
},
icon: UserLinkIcon,
text: $t('abp.account.linkAccount'),
},
icon: UserLinkIcon,
text: $t('abp.account.linkAccount'),
},
{
handler: () => {
router.replace('/account/my-settings');
{
handler: () => {
router.replace('/account/my-settings');
},
icon: UserSettingsIcon,
text: $t('abp.account.settings.title'),
},
icon: UserSettingsIcon,
text: $t('abp.account.settings.title'),
},
]);
];
if (abpStore.application?.currentUser.impersonatorUserId) {
basicMenus.push({
handler: handleBackUserLogin,
icon: BckLoginIcon,
text: $t('abp.account.revertImpersonation'),
});
}
return basicMenus;
});
const userInfo = computed(() => {
return userStore.userInfo;
@ -114,6 +129,23 @@ async function handleLinkLogin(userId: string, tenantId?: string) {
window.location.replace('/');
});
}
async function handleBackUserLogin() {
confirm({
beforeClose: async (scope) => {
if (scope.isConfirm) {
await authStore.impersonationUserLogin({}, () => {
tabbarStore.closeAllTabs(router);
window.location.replace('/');
});
}
return true;
},
centered: true,
content: $t('AbpAccount.RevertImpersonationWarning'),
title: $t('AbpUi.AreYouSure'),
});
}
watch(
() => preferences.app.watermark,
async (enable) => {

1
apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json

@ -80,6 +80,7 @@
"account": {
"title": "Account",
"linkAccount": "Link Account",
"revertImpersonation": "Revert My Account",
"settings": {
"title": "My Settings",
"basic": {

1
apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json

@ -80,6 +80,7 @@
"account": {
"title": "账户管理",
"linkAccount": "关联账户",
"revertImpersonation": "回到我的账户",
"settings": {
"title": "个人设置",
"basic": {

32
apps/vben5/apps/app-antd/src/store/auth.ts

@ -142,6 +142,37 @@ export const useAuthStore = defineStore('auth', () => {
}
}
async function impersonationUserLogin(
params: {
tenantId?: string;
tenantUserName?: string;
userDelegationId?: string;
userId?: string;
},
onSuccess?: () => Promise<void> | void,
) {
try {
loginLoading.value = true;
const accessToken = await oAuthService.getAccessToken();
const user = await oAuthService.loginByImpersonation({
accessToken,
...params,
});
return await _loginSuccess(
{
accessToken: user.access_token,
tokenType: user.token_type,
refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!,
},
onSuccess,
);
} finally {
loginLoading.value = false;
}
}
/**
* 异步处理登录操作
* Asynchronously handle the login process
@ -300,6 +331,7 @@ export const useAuthStore = defineStore('auth', () => {
oidcCallback,
fetchUserInfo,
linkUseLogin,
impersonationUserLogin,
loginLoading,
logout,
refreshSession,

26
apps/vben5/apps/app-antd/src/views/identity/users/index.vue

@ -1,15 +1,39 @@
<script lang="ts" setup>
import type { IdentityUserDto } from '@abp/identity';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useTabbarStore } from '@vben/stores';
import { UserTable } from '@abp/identity';
import { useAuthStore } from '#/store/auth';
defineOptions({
name: 'Vben5IdentityUsers',
});
const router = useRouter();
const authStore = useAuthStore();
const tabbarStore = useTabbarStore();
async function handleImpersonation(user: IdentityUserDto) {
await authStore.impersonationUserLogin(
{
userId: user.id,
tenantId: user.tenantId,
},
() => {
tabbarStore.closeAllTabs(router);
window.location.replace('/');
},
);
}
</script>
<template>
<Page>
<UserTable />
<UserTable @impersonation="handleImpersonation" />
</Page>
</template>

90
apps/vben5/apps/app-antd/src/views/saas/tenants/index.vue

@ -1,15 +1,101 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { TenantDto } from '@abp/saas';
import { useRouter } from 'vue-router';
import { Page, useVbenForm, useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useTabbarStore } from '@vben/stores';
import { TenantTable } from '@abp/saas';
import { useAuthStore } from '#/store/auth';
defineOptions({
name: 'Vben5SaasTenants',
});
interface ImpersonationModalState {
tenantId: string;
}
const router = useRouter();
const authStore = useAuthStore();
const tabbarStore = useTabbarStore();
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
showDefaultActions: false,
schema: [
{
fieldName: 'tenantId',
component: 'Input',
hide: true,
label: $t('AbpSaas.ImpersonationTenant'),
rules: 'required',
},
{
fieldName: 'userName',
component: 'Input',
componentProps: {
allowClear: true,
},
defaultValue: 'admin',
label: $t('AbpIdentity.DisplayName:UserName'),
rules: 'required',
},
],
handleSubmit: onImpersonationSubmit,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
await formApi.validateAndSubmitForm();
},
async onOpenChange(isOpen) {
if (isOpen) {
await formApi.resetForm();
}
},
});
async function handleImpersonation(tenant: TenantDto) {
modalApi.setData<ImpersonationModalState>({
tenantId: tenant.id,
});
modalApi.open();
}
async function onImpersonationSubmit(values: Record<string, string>) {
const state = modalApi.getData<ImpersonationModalState>();
try {
modalApi.lock();
await authStore.impersonationUserLogin(
{
tenantUserName: values.userName,
tenantId: state.tenantId,
},
() => {
modalApi.unlock();
tabbarStore.closeAllTabs(router);
window.location.replace('/');
},
);
} finally {
modalApi.unlock();
}
}
</script>
<template>
<Page>
<TenantTable />
<TenantTable @impersonation="handleImpersonation" />
<Modal :title="$t('AbpSaas.ImpersonationTenant')">
<Form />
</Modal>
</Page>
</template>

6
apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts

@ -1,6 +1,7 @@
import type { SigninRedirectArgs, SignoutRedirectArgs } from 'oidc-client-ts';
import type {
ImpersonationTokenRequest,
LinkUserTokenRequest,
PasswordTokenRequestModel,
PhoneNumberTokenRequest,
@ -18,6 +19,10 @@ export function useOAuthService() {
return userManager.signinLinkUser(input);
}
async function loginByImpersonation(input: ImpersonationTokenRequest) {
return userManager.signinImpersonation(input);
}
async function loginByPassword(input: PasswordTokenRequestModel) {
return userManager.signinResourceOwnerCredentials(input);
}
@ -67,6 +72,7 @@ export function useOAuthService() {
loginBySmsCode,
loginByQrCode,
logout,
loginByImpersonation,
refreshToken,
revokeTokens,
getAccessToken,

14
apps/vben5/packages/@abp/account/src/types/token.ts

@ -47,6 +47,19 @@ interface LinkUserTokenRequest {
/** 关联用户Id */
linkUserId: string;
}
/** 模拟用户授权请求数据模型 */
interface ImpersonationTokenRequest {
/** 当前用户访问令牌 */
accessToken?: string;
/** 模拟租户Id */
tenantId?: string;
/** 模拟租户用户名 */
tenantUserName?: string;
/** 委托用户Id */
userDelegationId?: string;
/** 模拟用户Id */
userId?: string;
}
/** 令牌撤销请求数据类型 */
interface RevokeTokenRequest {
/** 令牌 */
@ -101,6 +114,7 @@ interface ShouldChangePasswordError extends OAuthError {
}
export type {
ImpersonationTokenRequest,
LinkUserTokenRequest,
OAuthError,
OAuthTokenRefreshModel,

30
apps/vben5/packages/@abp/account/src/utils/auth.ts

@ -1,6 +1,7 @@
import type { Logger, UserManagerSettings } from 'oidc-client-ts';
import type {
ImpersonationTokenRequest,
LinkUserTokenRequest,
PasswordTokenRequestModel,
PhoneNumberTokenRequest,
@ -72,6 +73,34 @@ class AbpUserManager extends UserManager {
params.set('userId', model.userId);
}
}
async signinImpersonation(params: ImpersonationTokenRequest) {
const logger = this._logger.create('signinImpersonationUser');
const body = new URLSearchParams({
grant_type: 'impersonation',
client_id: this.settings.client_id,
});
if (params.accessToken) {
body.set('access_token', params.accessToken);
}
if (params.userId) {
body.set('UserId', params.userId);
}
if (params.userDelegationId) {
body.set('UserDelegationId', params.userDelegationId);
}
if (params.tenantId) {
body.set('TenantId', params.tenantId);
}
if (params.tenantUserName) {
body.set('TenantUserName', params.tenantUserName);
}
const client_secret = this.settings.client_secret;
if (client_secret) {
body.set('client_secret', client_secret);
}
this._writeTwoFactorToken(body, params);
return await this._fetchUser(logger, body);
}
async signinLinkUser(params: LinkUserTokenRequest) {
const logger = this._logger.create('signinLinkUser');
const body = new URLSearchParams({
@ -89,7 +118,6 @@ class AbpUserManager extends UserManager {
if (client_secret) {
body.set('client_secret', client_secret);
}
this._writeUserId(body, params);
this._writeTenantId(body, params);
this._writeTwoFactorToken(body, params);
return await this._fetchUser(logger, body);

36
apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue

@ -23,6 +23,7 @@ import {
EditOutlined,
EllipsisOutlined,
LockOutlined,
LoginOutlined,
PlusOutlined,
UnlockOutlined,
} from '@ant-design/icons-vue';
@ -38,6 +39,10 @@ defineOptions({
name: 'UserTable',
});
const emit = defineEmits<{
(event: 'impersonation', user: IdentityUserDto): void;
}>();
const UserModal = defineAsyncComponent(() => import('./UserModal.vue'));
const LockModal = defineAsyncComponent(() => import('./UserLockModal.vue'));
const ClaimModal = defineAsyncComponent(() => import('./UserClaimModal.vue'));
@ -46,6 +51,7 @@ const PasswordModal = defineAsyncComponent(
);
const MenuItem = Menu.Item;
const MenuDivider = Menu.Divider;
const CheckIcon = createIconifyIcon('ant-design:check-outlined');
const CloseIcon = createIconifyIcon('ant-design:close-outlined');
const PasswordIcon = createIconifyIcon('carbon:password');
@ -240,6 +246,17 @@ const handleDelete = (row: IdentityUserDto) => {
});
};
const handleImpersonationUser = (row: IdentityUserDto) => {
Modal.confirm({
centered: true,
content: $t('AbpAccount.ImpersonatorWarning', [row.userName]),
onOk: () => {
emit('impersonation', row);
},
title: $t('AbpUi.AreYouSure'),
});
};
const handleUnlock = async (row: IdentityUserDto) => {
await unLockApi(row.id);
await gridApi.query();
@ -261,6 +278,10 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
userChangeDrawerApi.open();
break;
}
case 'impersonation': {
handleImpersonationUser(row);
break;
}
case 'lock': {
lockModalApi.setData(row);
lockModalApi.open();
@ -374,7 +395,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
<div class="basis-1/3">
<Dropdown>
<template #overlay>
<Menu @click="(info) => handleMenuClick(row, info)">
<Menu @click="(info: MenuInfo) => handleMenuClick(row, info)">
<MenuItem
v-if="
hasAccessByCodes([IdentityUserPermissions.Update]) &&
@ -397,6 +418,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
>
{{ $t('AbpIdentity.UnLock') }}
</MenuItem>
<MenuDivider />
<MenuItem
v-if="
hasAccessByCodes([
@ -424,6 +446,16 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
>
{{ $t('AbpIdentity.ManageClaim') }}
</MenuItem>
<MenuItem
v-if="
hasAccessByCodes([IdentityUserPermissions.Impersonation])
"
key="impersonation"
:icon="h(LoginOutlined)"
>
{{ $t('AbpIdentity.ImpersonationUser') }}
</MenuItem>
<MenuDivider />
<MenuItem
v-if="hasAccessByCodes([IdentityUserPermissions.Update])"
key="password"
@ -431,6 +463,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
>
{{ $t('AbpIdentity.SetPassword') }}
</MenuItem>
<MenuDivider />
<MenuItem
v-if="hasAccessByCodes(['Platform.Menu.ManageUsers'])"
key="menus"
@ -438,6 +471,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
>
{{ $t('AppPlatform.Menu:Manage') }}
</MenuItem>
<MenuDivider />
<MenuItem
v-if="
isEnabled('AbpAuditing.Logging.AuditLog') &&

2
apps/vben5/packages/@abp/identity/src/constants/permissions.ts

@ -10,6 +10,8 @@ export const IdentityUserPermissions = {
ManagePermissions: 'AbpIdentity.Users.ManagePermissions',
/** 更新 */
Update: 'AbpIdentity.Users.Update',
/** 模拟用户 */
Impersonation: 'AbpIdentity.Users.Impersonation',
};
/** 角色权限 */
export const IdentityRolePermissions = {

2
apps/vben5/packages/@abp/identity/src/types/users.ts

@ -64,7 +64,7 @@ interface IdentityUserDto
/** 角色列表 */
roleNames: string[];
/** 租户标识 */
tenentId?: string;
tenantId?: string;
}
/** 用户实体数据视图对象 */
type IdentityUserVto = Omit<

19
apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue

@ -21,6 +21,7 @@ import {
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
LoginOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import { Button, Dropdown, Menu, message, Modal } from 'ant-design-vue';
@ -32,7 +33,12 @@ defineOptions({
name: 'EditionTable',
});
const emit = defineEmits<{
(event: 'impersonation', tenant: TenantDto): void;
}>();
const MenuItem = Menu.Item;
const MenuDivider = Menu.Divider;
const CheckIcon = createIconifyIcon('ant-design:check-outlined');
const CloseIcon = createIconifyIcon('ant-design:close-outlined');
const AuditLogIcon = createIconifyIcon('fluent-mdl2:compliance-audit');
@ -211,6 +217,10 @@ const onMenuClick = (row: TenantDto, info: MenuInfo) => {
providerName: 'T',
});
tenantFeatureModalApi.open();
break;
}
case 'impersonation': {
emit('impersonation', row);
}
}
};
@ -274,6 +284,15 @@ const onMenuClick = (row: TenantDto, info: MenuInfo) => {
>
{{ $t('AbpSaas.ManageFeatures') }}
</MenuItem>
<MenuDivider />
<MenuItem
v-if="hasAccessByCodes([TenantsPermissions.Impersonation])"
key="impersonation"
:icon="h(LoginOutlined)"
>
{{ $t('AbpSaas.ImpersonationTenant') }}
</MenuItem>
<MenuDivider />
<MenuItem
v-if="
isEnabled('AbpAuditing.Logging.AuditLog') &&

2
apps/vben5/packages/@abp/saas/src/constants/permissions.ts

@ -25,4 +25,6 @@ export const TenantsPermissions = {
ManageFeatures: 'AbpSaas.Tenants.ManageFeatures',
/** 更新 */
Update: 'AbpSaas.Tenants.Update',
/** 模拟租户 */
Impersonation: 'AbpSaas.Tenants.Impersonation',
};

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

463
aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrant.cs

@ -0,0 +1,463 @@
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);
var tenantId = currentTenant.Id;
var tenantName = currentTenant.Name;
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, tenantId, tenantName);
}
}
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"]);
}
}
var tenantId = currentTenant.Id;
var tenantName = currentTenant.Name;
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, tenantId, tenantName);
}
}
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? tenantName,
string action = "ImpersonateUser")
{
var userClaimsPrincipalFactory = GetRequiredService<IUserClaimsPrincipalFactory<IdentityUser>>(context);
var currentUser = GetRequiredService<ICurrentUser>(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 (tenantId.HasValue)
{
claims.Add(new Claim(AbpClaimTypes.ImpersonatorTenantId, tenantId.Value.ToString()));
}
if (!tenantName.IsNullOrWhiteSpace())
{
claims.Add(new Claim(AbpClaimTypes.ImpersonatorTenantName, tenantName));
}
}
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