Browse Source

feat: Add tenant impersonation implementation

pull/1525/head
colin 3 months ago
parent
commit
946c5fe241
  1. 4
      apps/vben5/apps/app-antd/src/layouts/basic.vue
  2. 90
      apps/vben5/apps/app-antd/src/views/saas/tenants/index.vue
  3. 3
      apps/vben5/packages/@abp/account/src/utils/auth.ts
  4. 19
      apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue
  5. 2
      apps/vben5/packages/@abp/saas/src/constants/permissions.ts
  6. 20
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.Impersonation/LINGYUN/Abp/OpenIddict/Impersonation/ImpersonationTokenExtensionGrant.cs

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

@ -28,8 +28,8 @@ import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue'; import LoginForm from '#/views/_core/authentication/login.vue';
const UserSettingsIcon = createIconifyIcon('tdesign:user-setting'); const UserSettingsIcon = createIconifyIcon('tdesign:user-setting');
const UserLinkIcon = createIconifyIcon('material-symbols-light:link'); const UserLinkIcon = createIconifyIcon('material-symbols:link');
const BckLoginIcon = createIconifyIcon('lets-icons:back-light'); const BckLoginIcon = createIconifyIcon('mingcute:back-line');
const notifications = ref<NotificationItem[]>([]); const notifications = ref<NotificationItem[]>([]);

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

@ -1,15 +1,101 @@
<script lang="ts" setup> <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 { TenantTable } from '@abp/saas';
import { useAuthStore } from '#/store/auth';
defineOptions({ defineOptions({
name: 'Vben5SaasTenants', 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> </script>
<template> <template>
<Page> <Page>
<TenantTable /> <TenantTable @impersonation="handleImpersonation" />
<Modal :title="$t('AbpSaas.ImpersonationTenant')">
<Form />
</Modal>
</Page> </Page>
</template> </template>

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

@ -88,6 +88,9 @@ class AbpUserManager extends UserManager {
if (params.userDelegationId) { if (params.userDelegationId) {
body.set('UserDelegationId', params.userDelegationId); body.set('UserDelegationId', params.userDelegationId);
} }
if (params.tenantId) {
body.set('TenantId', params.tenantId);
}
if (params.tenantUserName) { if (params.tenantUserName) {
body.set('TenantUserName', params.tenantUserName); body.set('TenantUserName', params.tenantUserName);
} }

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

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

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

@ -145,6 +145,9 @@ public class ImpersonationTokenExtensionGrant : ITokenExtensionGrant
var permissionChecker = GetRequiredService<IPermissionChecker>(context); var permissionChecker = GetRequiredService<IPermissionChecker>(context);
var options = GetRequiredService<IOptions<OpenIddictImpersonationOptions>>(context); var options = GetRequiredService<IOptions<OpenIddictImpersonationOptions>>(context);
var tenantId = currentTenant.Id;
var tenantName = currentTenant.Name;
using (currentTenant.Change(request.TenantId)) using (currentTenant.Change(request.TenantId))
{ {
if (currentUser.Id == request.UserId) if (currentUser.Id == request.UserId)
@ -171,7 +174,7 @@ public class ImpersonationTokenExtensionGrant : ITokenExtensionGrant
localizer["TargetUserNotFound", request.UserId.Value]); localizer["TargetUserNotFound", request.UserId.Value]);
} }
return await CreateImpersonatedSignInResult(context, principal, targetUser, request.TenantId); return await CreateImpersonatedSignInResult(context, principal, targetUser, tenantId, tenantName);
} }
} }
@ -198,6 +201,9 @@ public class ImpersonationTokenExtensionGrant : ITokenExtensionGrant
} }
} }
var tenantId = currentTenant.Id;
var tenantName = currentTenant.Name;
using (currentTenant.Change(request.TenantId)) using (currentTenant.Change(request.TenantId))
{ {
var adminUserName = request.TenantUserName ?? options.Value.DefaultTenantAdminUserName; var adminUserName = request.TenantUserName ?? options.Value.DefaultTenantAdminUserName;
@ -216,7 +222,7 @@ public class ImpersonationTokenExtensionGrant : ITokenExtensionGrant
localizer["TenantAdminUserNotFound", adminUserName]); localizer["TenantAdminUserNotFound", adminUserName]);
} }
return await CreateImpersonatedSignInResult(context, principal, adminUser, request.TenantId); return await CreateImpersonatedSignInResult(context, principal, adminUser, tenantId, tenantName);
} }
} }
@ -307,11 +313,11 @@ public class ImpersonationTokenExtensionGrant : ITokenExtensionGrant
ClaimsPrincipal currentPrincipal, ClaimsPrincipal currentPrincipal,
IdentityUser targetUser, IdentityUser targetUser,
Guid? tenantId, Guid? tenantId,
string? tenantName,
string action = "ImpersonateUser") string action = "ImpersonateUser")
{ {
var userClaimsPrincipalFactory = GetRequiredService<IUserClaimsPrincipalFactory<IdentityUser>>(context); var userClaimsPrincipalFactory = GetRequiredService<IUserClaimsPrincipalFactory<IdentityUser>>(context);
var currentUser = GetRequiredService<ICurrentUser>(context); var currentUser = GetRequiredService<ICurrentUser>(context);
var currentTenant = GetRequiredService<ICurrentTenant>(context);
var currentPrincipalAccessor = GetRequiredService<ICurrentPrincipalAccessor>(context); var currentPrincipalAccessor = GetRequiredService<ICurrentPrincipalAccessor>(context);
var principal = await userClaimsPrincipalFactory.CreateAsync(targetUser); var principal = await userClaimsPrincipalFactory.CreateAsync(targetUser);
@ -322,9 +328,13 @@ public class ImpersonationTokenExtensionGrant : ITokenExtensionGrant
claims.Add(new Claim(AbpClaimTypes.ImpersonatorUserId, currentUser.Id!.Value.ToString())); claims.Add(new Claim(AbpClaimTypes.ImpersonatorUserId, currentUser.Id!.Value.ToString()));
claims.Add(new Claim(AbpClaimTypes.ImpersonatorUserName, currentUser.UserName!)); claims.Add(new Claim(AbpClaimTypes.ImpersonatorUserName, currentUser.UserName!));
if (currentTenant.IsAvailable) if (tenantId.HasValue)
{
claims.Add(new Claim(AbpClaimTypes.ImpersonatorTenantId, tenantId.Value.ToString()));
}
if (!tenantName.IsNullOrWhiteSpace())
{ {
claims.Add(new Claim(AbpClaimTypes.ImpersonatorTenantId, currentTenant.Id!.Value.ToString())); claims.Add(new Claim(AbpClaimTypes.ImpersonatorTenantName, tenantName));
} }
} }

Loading…
Cancel
Save