Commit 4bd0be51 by luoqi

fix(ai): honor explicit whole-tenant host administration grants

parent a33528bc
Pipeline #3807 failed in 0 seconds
......@@ -160,7 +160,11 @@ export function canManage(
return { ok: true };
}
export function hostOperatorReach(ids: readonly string[], nodes: readonly OrgNode[]): OperatorReach {
export function hostOperatorReach(ids: readonly string[], nodes: readonly OrgNode[], tenantGrant = false): OperatorReach {
const visible = new Set(ids);
// 全机构管理要求 AI 常设全租户授权,并且宿主本次范围覆盖该客户的全部有效节点。
// ⛔ 仅覆盖所有节点的 scoped 用户不能据此升级为全机构管理员。
const active = nodes.filter((node) => node.isActive);
if (tenantGrant && active.length > 0 && active.every((node) => visible.has(node.id))) return { all: true };
return { all: false, visible, subtree: new Set(ids.filter((id) => [...expandSubtree(nodes, [id])].every((child) => visible.has(child)))) };
}
......@@ -201,7 +201,8 @@ export class ScopeService {
async operatorReach(op: AuthUser): Promise<OperatorReach> {
if (op.hostScope) {
const nodes = await this.prisma.orgUnit.findMany({ where: { tenantId: op.tenantId }, select: { id: true, parentId: true, name: true, kind: true, isActive: true } });
return hostOperatorReach(op.hostScope.orgUnitIds, nodes);
return hostOperatorReach(op.hostScope.orgUnitIds, nodes,
op.dataScope === 'tenant' && op.hostScope.wholeTenant && op.hostScope.accountAll);
}
if (op.dataScope === 'tenant') return { all: true };
const [snap, anchors] = await Promise.all([
......
......@@ -68,7 +68,8 @@ export async function reachOf(
where: { tenantId: op.tenantId, userId: op.id },
select: { orgUnitId: true, includeSubtree: true },
});
return { reach: op.hostScope ? hostOperatorReach(op.hostScope.orgUnitIds, nodes) : operatorReach(op, anchors, nodes), anchors };
return { reach: op.hostScope ? hostOperatorReach(op.hostScope.orgUnitIds, nodes,
op.dataScope === 'tenant' && op.hostScope.wholeTenant && op.hostScope.accountAll) : operatorReach(op, anchors, nodes), anchors };
}
/**
......
import type { AuthUser } from '../src/common/guards/jwt-auth.guard';
import { permissionsOf } from '../src/common/permissions';
import { adminCan, reachOf } from '../src/modules/workbench/admin-common';
import { hostOperatorReach } from '../src/modules/scope/grant-policy';
import { ScopeService } from '../src/modules/scope/scope.service';
import type { OrgNode } from '../src/modules/scope/org-tree';
import { NotifySettingsService } from '../src/modules/workbench/notify-settings.service';
const nodes: OrgNode[] = [
{ id: 'root', parentId: null, name: '普尔', kind: 'group', isActive: true },
{ id: 'clinic', parentId: 'root', name: '门店', kind: 'clinic', isActive: true },
];
const owner = (): AuthUser => ({
id: 'user',
tenantId: 'puer',
role: 'owner',
displayName: '管理员',
dataScope: 'tenant',
orgUnitId: null,
jobTitle: null,
mustChangePassword: false,
permissions: permissionsOf('owner'),
hostScope: {
version: 2,
appId: 'test',
dataTenantId: 'friday-market',
sourceUserId: '5979',
launchId: 'launch',
rootOrgId: 'root',
orgScope: ['root'],
scopeOrgIds: ['root'],
role: 'admin',
capabilityCeiling: [],
readonlyCeiling: false,
capabilities: [],
modules: ['workbench'],
readOnly: false,
orgUnitIds: ['root', 'clinic'],
wholeTenant: true,
accountAll: true,
clinicNames: {},
},
});
function scopeService() {
const db = { orgUnit: { findMany: async () => nodes } };
return new ScopeService(db as never);
}
it('人工全租户授权与宿主完整客户范围同时成立,才允许全机构管理', () => {
expect(hostOperatorReach(['root', 'clinic'], nodes, true)).toEqual({ all: true });
expect(hostOperatorReach(['root', 'clinic'], nodes).all).toBe(false);
expect(hostOperatorReach(['clinic'], nodes, true).all).toBe(false);
expect(hostOperatorReach([], [], true).all).toBe(false);
});
it('只覆盖所有节点但仍是 scoped 的宿主管理员,不能升级为全机构', async () => {
const user = { ...owner(), dataScope: 'scoped' };
expect((await scopeService().operatorReach(user)).all).toBe(false);
expect(adminCan(user).editTree).toBe(false);
});
it('AI 全租户管理员经宿主缩窄到门店或失去全机构标记,仍不能改全机构', async () => {
for (const override of [
{ orgUnitIds: ['clinic'], wholeTenant: false, accountAll: false },
{ wholeTenant: false },
{ accountAll: false },
{ orgUnitIds: ['clinic'] },
]) {
const user = owner();
user.hostScope = { ...user.hostScope!, ...override };
expect((await scopeService().operatorReach(user)).all).toBe(false);
}
});
it('组织授权事务与提醒设置共用全机构判据', async () => {
const user = owner();
const db = { userOrgScope: { findMany: async () => [] } };
expect((await reachOf(db as never, user, nodes)).reach).toEqual({ all: true });
expect(adminCan(user)).toMatchObject({ editTree: true, tenantScope: true });
const scope = scopeService();
const notify = new NotifySettingsService({} as never, scope);
expect(await notify['can'](user)).toMatchObject({ edit: true, pick: true });
user.hostScope!.orgUnitIds = ['clinic'];
expect((await reachOf(db as never, user, nodes)).reach.all).toBe(false);
expect(await notify['can'](user)).toMatchObject({ edit: true, pick: false });
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment