Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/core/security/permission-sets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ export const ExecutivePermissionSet = {
},
};

// 财务(应付会计):供应商档案只读。银行账户(v0.2 M2)合入后在此加账号可读,核验只经 screen flow
export const FinancePermissionSet = {
name: 'srm_finance',
label: 'SRM 财务',
objects: {
srm_supplier: read,
srm_supplier_contact: read,
srm_supplier_certificate: read,
},
};

// 系统管理员:维护品类与工厂等配置
export const AdminPermissionSet = {
name: 'srm_admin',
Expand All @@ -85,5 +96,6 @@ export const SrmPermissionSets = [
SupplierAdminPermissionSet,
QualityPermissionSet,
ExecutivePermissionSet,
FinancePermissionSet,
AdminPermissionSet,
];
5 changes: 4 additions & 1 deletion src/core/security/positions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// 内部岗位(04 §2.1)的 v0.1 子集:只列 v0.1 流程用到的岗位,其余随对应版本加入。
// 内部岗位(04 §2.1)的子集:只列已上线流程用到的岗位,其余随对应版本加入。
// 挂载的权限集同样是子集:`srm_buyer`、`srm_sourcing` 等权限集随其对象上线后补挂。
export const SrmPositions = [
{ name: 'srm_procurement_director', label: '采购总监', permissionSets: ['srm_internal_base', 'srm_executive'] },
{ name: 'srm_category_manager', label: '品类经理', permissionSets: ['srm_internal_base', 'srm_supplier_admin'] },
{ name: 'srm_buyer', label: '采购员', permissionSets: ['srm_internal_base'] },
{ name: 'srm_buyer_lead', label: '采购主管', permissionSets: ['srm_internal_base', 'srm_supplier_admin'] },
{ name: 'srm_sqe', label: 'SQE', permissionSets: ['srm_internal_base', 'srm_quality'] },
{ name: 'srm_ap_accountant', label: '应付会计', permissionSets: ['srm_internal_base', 'srm_finance'] },
{ name: 'srm_admin', label: 'SRM 系统管理员', permissionSets: ['srm_internal_base', 'srm_admin'] },
];
80 changes: 80 additions & 0 deletions test/permissions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@ import { bootStack, type VerifyStack } from '@objectstack/verify';
import config from '../objectstack.config';
import { Supplier, SupplierApplication } from '../src/core/objects';
import { SrmPermissionSets } from '../src/core/security/permission-sets';
import { SrmPositions } from '../src/core/security/positions';

type Access = { allowCreate: boolean; allowRead: boolean; allowEdit: boolean; allowDelete: boolean };

// 岗位「能看到什么」:把岗位挂载的权限集按对象合并成 R/C/E/D 字符串,权限变更时对比快照(工程规范 §4)
const visibility = (position: string) => {
const sets = SrmPositions.find((p) => p.name === position)!.permissionSets.map(
(name) => SrmPermissionSets.find((set) => set.name === name)!,
);
const merged: Record<string, string> = {};
for (const set of sets) {
for (const [object, access] of Object.entries(set.objects as Record<string, Access>)) {
const flags = (merged[object] ?? '').split('');
if (access.allowRead) flags.push('R');
if (access.allowCreate) flags.push('C');
if (access.allowEdit) flags.push('E');
if (access.allowDelete) flags.push('D');
merged[object] = 'RCED'.split('').filter((f) => flags.includes(f)).join('');
}
}
return Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
};

// 内部岗位与权限集(04 §2):采购员只读,品类经理维护档案但不能直接改状态。
describe('internal permissions', () => {
Expand Down Expand Up @@ -30,6 +52,64 @@ describe('internal permissions', () => {
expect(SrmPermissionSets.filter((set) => 'isDefault' in set)).toEqual([]);
});

it('mounts only permission sets that exist', () => {
const names = new Set(SrmPermissionSets.map((set) => set.name));
for (const position of SrmPositions) {
expect(position.permissionSets.filter((name) => !names.has(name))).toEqual([]);
}
});

it('keeps what an AP accountant can see', () => {
expect(visibility('srm_ap_accountant')).toMatchInlineSnapshot(`
{
"srm_category": "R",
"srm_plant": "R",
"srm_supplier": "R",
"srm_supplier_category": "R",
"srm_supplier_certificate": "R",
"srm_supplier_contact": "R",
}
`);
});

it('keeps what a buyer lead can see', () => {
expect(visibility('srm_buyer_lead')).toMatchInlineSnapshot(`
{
"srm_category": "R",
"srm_plant": "R",
"srm_supplier": "RCE",
"srm_supplier_application": "RCE",
"srm_supplier_category": "RCE",
"srm_supplier_certificate": "RCE",
"srm_supplier_contact": "RCED",
}
`);
});

it('lets an AP accountant read suppliers but not edit or create one', async () => {
const manager = await person('srm_category_manager');
const supplier = await stack.hooks.run(Supplier.name, 'insert', supplierDoc(), { as: manager });
const accountant = await person('srm_ap_accountant');
const [row] = await stack.rows(Supplier.name, { id: supplier.id }, { as: accountant });
expect(row!.name).toBe(supplier.name);
await expect(
stack.hooks.run(Supplier.name, 'update', { id: supplier.id, short_name: '改名' }, { as: accountant }),
).rejects.toBeTruthy();
await expect(stack.hooks.run(Supplier.name, 'insert', supplierDoc(), { as: accountant })).rejects.toBeTruthy();
await expect(stack.rows(SupplierApplication.name, {}, { as: accountant })).rejects.toBeTruthy();
});

it('lets a buyer lead maintain suppliers but not set the status directly', async () => {
const lead = await person('srm_buyer_lead');
const supplier = await stack.hooks.run(Supplier.name, 'insert', supplierDoc(), { as: lead });
await stack.hooks.run(Supplier.name, 'update', { id: supplier.id, short_name: '简称' }, { as: lead });
await expect(
stack.hooks.run(Supplier.name, 'update', { id: supplier.id, status: 'eliminated' }, { as: lead }),
).rejects.toBeTruthy();
const [row] = await stack.rows(Supplier.name, { id: supplier.id }, { as: lead });
expect(row).toMatchObject({ short_name: '简称', status: 'potential' });
});

it('lets a buyer read suppliers but not create one', async () => {
const buyer = await person('srm_buyer');
expect((await stack.rows(Supplier.name, {}, { as: buyer })).length).toBeGreaterThan(0);
Expand Down
Loading