fix(权限): 修复版本详情写入权限校验
This commit is contained in:
@@ -5,16 +5,19 @@ import type { CurrentUser } from './auth-context.service';
|
|||||||
describe('PermissionService', () => {
|
describe('PermissionService', () => {
|
||||||
const versionFindUnique = jest.fn();
|
const versionFindUnique = jest.fn();
|
||||||
const projectMemberFindUnique = jest.fn();
|
const projectMemberFindUnique = jest.fn();
|
||||||
|
const appDataFindUnique = jest.fn();
|
||||||
|
|
||||||
const prisma = {
|
const prisma = {
|
||||||
version: { findUnique: versionFindUnique },
|
version: { findUnique: versionFindUnique },
|
||||||
projectMember: { findUnique: projectMemberFindUnique },
|
projectMember: { findUnique: projectMemberFindUnique },
|
||||||
|
appData: { findUnique: appDataFindUnique },
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const service = new PermissionService(prisma);
|
const service = new PermissionService(prisma);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
appDataFindUnique.mockResolvedValue(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows the built-in super admin wildcard for any permission without a scope lookup', async () => {
|
it('allows the built-in super admin wildcard for any permission without a scope lookup', async () => {
|
||||||
@@ -26,13 +29,13 @@ describe('PermissionService', () => {
|
|||||||
expect(projectMemberFindUnique).not.toHaveBeenCalled();
|
expect(projectMemberFindUnique).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('requires scoped users to belong to the project resolved from a version', async () => {
|
it('denies version work mutations for project members outside the version members list', async () => {
|
||||||
versionFindUnique.mockResolvedValue({ id: 'version-1', projectId: 'project-1', members: [] });
|
versionFindUnique.mockResolvedValue({ id: 'version-1', projectId: 'project-1', members: [] });
|
||||||
projectMemberFindUnique.mockResolvedValue({ projectId: 'project-1', userId: 'dev-1', role: 'member' });
|
projectMemberFindUnique.mockResolvedValue({ projectId: 'project-1', userId: 'dev-1', role: 'member' });
|
||||||
|
|
||||||
await expect(service.can(currentUser({ id: 'dev-1', roleId: 'role-dev' }), 'version.devtask:manage', {
|
await expect(service.can(currentUser({ id: 'dev-1', roleId: 'role-dev' }), 'version.devtask:manage', {
|
||||||
versionId: 'version-1',
|
versionId: 'version-1',
|
||||||
})).resolves.toBe(true);
|
})).resolves.toBe(false);
|
||||||
|
|
||||||
expect(versionFindUnique).toHaveBeenCalledWith({
|
expect(versionFindUnique).toHaveBeenCalledWith({
|
||||||
where: { id: 'version-1' },
|
where: { id: 'version-1' },
|
||||||
@@ -43,6 +46,39 @@ describe('PermissionService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows version work mutations when the user has the role permission and is a version member', async () => {
|
||||||
|
versionFindUnique.mockResolvedValue({
|
||||||
|
id: 'version-1',
|
||||||
|
projectId: 'project-1',
|
||||||
|
members: [{ id: 'dev-1', name: 'Dev One' }],
|
||||||
|
});
|
||||||
|
projectMemberFindUnique.mockResolvedValue({ projectId: 'project-1', userId: 'dev-1', role: 'member' });
|
||||||
|
|
||||||
|
await expect(service.can(currentUser({ id: 'dev-1', name: 'Dev One', roleId: 'role-dev' }), 'version.devtask:manage', {
|
||||||
|
versionId: 'version-1',
|
||||||
|
})).resolves.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses configured role permissions before built-in role defaults', async () => {
|
||||||
|
appDataFindUnique.mockResolvedValue({
|
||||||
|
value: {
|
||||||
|
roles: [
|
||||||
|
{ id: 'role-dev', permissions: ['version.devtask:view'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
versionFindUnique.mockResolvedValue({
|
||||||
|
id: 'version-1',
|
||||||
|
projectId: 'project-1',
|
||||||
|
members: [{ id: 'dev-1', name: 'Dev One' }],
|
||||||
|
});
|
||||||
|
projectMemberFindUnique.mockResolvedValue({ projectId: 'project-1', userId: 'dev-1', role: 'member' });
|
||||||
|
|
||||||
|
await expect(service.can(currentUser({ id: 'dev-1', name: 'Dev One', roleId: 'role-dev' }), 'version.devtask:manage', {
|
||||||
|
versionId: 'version-1',
|
||||||
|
})).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('allows a version member when no project membership exists', async () => {
|
it('allows a version member when no project membership exists', async () => {
|
||||||
versionFindUnique.mockResolvedValue({
|
versionFindUnique.mockResolvedValue({
|
||||||
id: 'version-1',
|
id: 'version-1',
|
||||||
|
|||||||
@@ -101,13 +101,26 @@ const PROJECT_ROLE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const VERSION_WORK_MUTATION_PERMISSIONS = new Set([
|
||||||
|
'version.req:manage',
|
||||||
|
'version.research:manage',
|
||||||
|
'version.product_plan:manage',
|
||||||
|
'version.ui_plan:manage',
|
||||||
|
'version.devtask:manage',
|
||||||
|
'version.testcase:manage',
|
||||||
|
'version.bug:create',
|
||||||
|
'version.bug:edit',
|
||||||
|
'version.bug:delete',
|
||||||
|
]);
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PermissionService {
|
export class PermissionService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
async can(user: CurrentUser | null | undefined, permission: string, scope: ResourceScope = {}): Promise<boolean> {
|
async can(user: CurrentUser | null | undefined, permission: string, scope: ResourceScope = {}): Promise<boolean> {
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
const systemPermissions = SYSTEM_ROLE_PERMISSIONS[user.roleId] ?? [];
|
if ((SYSTEM_ROLE_PERMISSIONS[user.roleId] ?? []).includes('*')) return true;
|
||||||
|
const systemPermissions = await this.resolveRolePermissions(user.roleId);
|
||||||
if (systemPermissions.includes('*')) return true;
|
if (systemPermissions.includes('*')) return true;
|
||||||
|
|
||||||
const hasSystemPermission = systemPermissions.includes(permission);
|
const hasSystemPermission = systemPermissions.includes(permission);
|
||||||
@@ -119,6 +132,10 @@ export class PermissionService {
|
|||||||
where: { projectId_userId: { projectId: resolvedScope.projectId, userId: user.id } },
|
where: { projectId_userId: { projectId: resolvedScope.projectId, userId: user.id } },
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
|
if (requiresVersionWorkMembership(permission, resolvedScope)) {
|
||||||
|
return hasSystemPermission && isVersionMember(user, resolvedScope.versionMembers);
|
||||||
|
}
|
||||||
|
|
||||||
if (projectMember && roleAllows(PROJECT_ROLE_PERMISSIONS[projectMember.role] ?? [], permission)) {
|
if (projectMember && roleAllows(PROJECT_ROLE_PERMISSIONS[projectMember.role] ?? [], permission)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -146,6 +163,26 @@ export class PermissionService {
|
|||||||
versionMembers: version?.members,
|
versionMembers: version?.members,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveRolePermissions(roleId: string): Promise<string[]> {
|
||||||
|
const configuredPermissions = await this.findConfiguredRolePermissions(roleId);
|
||||||
|
return configuredPermissions ?? SYSTEM_ROLE_PERMISSIONS[roleId] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findConfiguredRolePermissions(roleId: string): Promise<string[] | null> {
|
||||||
|
const findUnique = (this.prisma as unknown as {
|
||||||
|
appData?: { findUnique?: (args: unknown) => Promise<{ value?: unknown } | null> };
|
||||||
|
}).appData?.findUnique;
|
||||||
|
if (!findUnique) return null;
|
||||||
|
|
||||||
|
const row = await findUnique({
|
||||||
|
where: { key: 'members' },
|
||||||
|
select: { value: true },
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
if (!row) return null;
|
||||||
|
return readRolePermissions(row.value, roleId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function std4(module: string): string[] {
|
function std4(module: string): string[] {
|
||||||
@@ -156,6 +193,25 @@ function roleAllows(permissions: string[], permission: string): boolean {
|
|||||||
return permissions.includes('*') || permissions.includes(permission);
|
return permissions.includes('*') || permissions.includes(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requiresVersionWorkMembership(permission: string, scope: ResourceScope): boolean {
|
||||||
|
return Boolean(scope.versionId && VERSION_WORK_MUTATION_PERMISSIONS.has(permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRolePermissions(value: unknown, roleId: string): string[] | null {
|
||||||
|
if (!value || typeof value !== 'object') return null;
|
||||||
|
const roles = (value as { roles?: unknown }).roles;
|
||||||
|
if (!Array.isArray(roles)) return null;
|
||||||
|
const role = roles.find((item) => (
|
||||||
|
item &&
|
||||||
|
typeof item === 'object' &&
|
||||||
|
(item as { id?: unknown }).id === roleId
|
||||||
|
));
|
||||||
|
if (!role || typeof role !== 'object') return null;
|
||||||
|
const permissions = (role as { permissions?: unknown }).permissions;
|
||||||
|
if (!Array.isArray(permissions)) return null;
|
||||||
|
return permissions.filter((item): item is string => typeof item === 'string');
|
||||||
|
}
|
||||||
|
|
||||||
function isVersionMember(user: CurrentUser, members: unknown): boolean {
|
function isVersionMember(user: CurrentUser, members: unknown): boolean {
|
||||||
if (!Array.isArray(members)) return false;
|
if (!Array.isArray(members)) return false;
|
||||||
return members.some((member) => {
|
return members.some((member) => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { formatDateTime, formatLocalDate } from '@/lib/format';
|
|||||||
import { getVersionLinkedRequirementCandidates } from '@/lib/requirement-selector';
|
import { getVersionLinkedRequirementCandidates } from '@/lib/requirement-selector';
|
||||||
import { getRequirementVersionSelectionPatch, getRequirementVersionUnlinkPatch } from '@/lib/requirement-version-link';
|
import { getRequirementVersionSelectionPatch, getRequirementVersionUnlinkPatch } from '@/lib/requirement-version-link';
|
||||||
import { calcBugSeverityRanking, calcPersonalEffortRanking, calcStageEffortMetrics, calcVersionOverviewEffortTotals } from '@/lib/version-overview';
|
import { calcBugSeverityRanking, calcPersonalEffortRanking, calcStageEffortMetrics, calcVersionOverviewEffortTotals } from '@/lib/version-overview';
|
||||||
import { addVersionMembers, DEFAULT_VERSION_MEMBER_ROLE, filterVersionMemberCandidates } from '@/lib/version-members';
|
import { addVersionMembers, canWriteVersionWork, DEFAULT_VERSION_MEMBER_ROLE, filterVersionMemberCandidates } from '@/lib/version-members';
|
||||||
import { addRecommendedVersionMembers, getDefaultRecommendedMemberNames, recommendVersionMembers, type MemberRecommendationGroup, type RecommendableRole } from '@/lib/member-recommendation';
|
import { addRecommendedVersionMembers, getDefaultRecommendedMemberNames, recommendVersionMembers, type MemberRecommendationGroup, type RecommendableRole } from '@/lib/member-recommendation';
|
||||||
import { getRequirementCoverageSummary } from '@/lib/version-plan';
|
import { getRequirementCoverageSummary } from '@/lib/version-plan';
|
||||||
import { buildVersionDataScope, selectVersionDataScope, type VersionDataScope } from '@/lib/version-data-scope';
|
import { buildVersionDataScope, selectVersionDataScope, type VersionDataScope } from '@/lib/version-data-scope';
|
||||||
@@ -85,6 +85,12 @@ const TABS = [
|
|||||||
{ key: 'bugs', label: 'BUG', permission: 'version.bug:view' },
|
{ key: 'bugs', label: 'BUG', permission: 'version.bug:view' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PLAN_MANAGE_PERMISSION = {
|
||||||
|
research: 'version.research:manage',
|
||||||
|
product: 'version.product_plan:manage',
|
||||||
|
ui: 'version.ui_plan:manage',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default function VersionDetailPage() {
|
export default function VersionDetailPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -302,6 +308,20 @@ export default function VersionDetailPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const versionMembers = version.members ?? [];
|
||||||
|
const canWriteVersionPermission = (permission: string) => canWriteVersionWork({
|
||||||
|
role: currentRole,
|
||||||
|
permission,
|
||||||
|
currentUserName,
|
||||||
|
versionMembers,
|
||||||
|
readOnly: versionReadonly,
|
||||||
|
});
|
||||||
|
const canManageRequirements = canWriteVersionPermission('version.req:manage');
|
||||||
|
const canManageDevTasks = canWriteVersionPermission('version.devtask:manage');
|
||||||
|
const canManageTestCases = canWriteVersionPermission('version.testcase:manage');
|
||||||
|
const canManageBugs = ['version.bug:create', 'version.bug:edit', 'version.bug:delete']
|
||||||
|
.some((permission) => canWriteVersionPermission(permission));
|
||||||
|
|
||||||
const scopedVersionData = versionScope!;
|
const scopedVersionData = versionScope!;
|
||||||
const appDataScopedVersionData = appDataVersionScope ?? scopedVersionData;
|
const appDataScopedVersionData = appDataVersionScope ?? scopedVersionData;
|
||||||
const displayRequirements = requirementsLoaded ? requirements : scopedVersionData.requirements;
|
const displayRequirements = requirementsLoaded ? requirements : scopedVersionData.requirements;
|
||||||
@@ -987,9 +1007,9 @@ export default function VersionDetailPage() {
|
|||||||
devTasks={displayDevTasks}
|
devTasks={displayDevTasks}
|
||||||
versionMembers={version.members ?? []}
|
versionMembers={version.members ?? []}
|
||||||
currentUserName={user?.name ?? ''}
|
currentUserName={user?.name ?? ''}
|
||||||
readOnly={versionReadonly || !requirementWriteReady}
|
readOnly={versionReadonly || !requirementWriteReady || !canManageRequirements}
|
||||||
onLink={(ids, addedBy) => {
|
onLink={(ids, addedBy) => {
|
||||||
if (versionReadonly || !requirementWriteReady) return;
|
if (versionReadonly || !requirementWriteReady || !canManageRequirements) return;
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
const requirement = displayRequirements.find((item) => item.id === id);
|
const requirement = displayRequirements.find((item) => item.id === id);
|
||||||
if (!requirement) return;
|
if (!requirement) return;
|
||||||
@@ -1000,7 +1020,7 @@ export default function VersionDetailPage() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onUnlink={(id) => {
|
onUnlink={(id) => {
|
||||||
if (versionReadonly || !requirementWriteReady) return;
|
if (versionReadonly || !requirementWriteReady || !canManageRequirements) return;
|
||||||
const requirement = displayRequirements.find((item) => item.id === id);
|
const requirement = displayRequirements.find((item) => item.id === id);
|
||||||
if (!requirement) return;
|
if (!requirement) return;
|
||||||
updateRequirement(id, {
|
updateRequirement(id, {
|
||||||
@@ -1009,7 +1029,7 @@ export default function VersionDetailPage() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onCreateChange={(data) => {
|
onCreateChange={(data) => {
|
||||||
if (versionReadonly || !requirementWriteReady) return;
|
if (versionReadonly || !requirementWriteReady || !canManageRequirements) return;
|
||||||
createRequirement({
|
createRequirement({
|
||||||
...data,
|
...data,
|
||||||
productId: version.productId,
|
productId: version.productId,
|
||||||
@@ -1043,9 +1063,9 @@ export default function VersionDetailPage() {
|
|||||||
versionMembers={version.members ?? []}
|
versionMembers={version.members ?? []}
|
||||||
linkedRequirements={versionLinkedReqs}
|
linkedRequirements={versionLinkedReqs}
|
||||||
allRequirements={displayRequirements}
|
allRequirements={displayRequirements}
|
||||||
readOnly={versionReadonly || !planWriteReady}
|
readOnly={versionReadonly || !planWriteReady || !canWriteVersionPermission(PLAN_MANAGE_PERMISSION[pt])}
|
||||||
onCreate={(data) => {
|
onCreate={(data) => {
|
||||||
if (versionReadonly || !planWriteReady) return;
|
if (versionReadonly || !planWriteReady || !canWriteVersionPermission(PLAN_MANAGE_PERMISSION[pt])) return;
|
||||||
createPlan(data);
|
createPlan(data);
|
||||||
if ((pt === 'product') && data.linkedRequirementIds?.length) {
|
if ((pt === 'product') && data.linkedRequirementIds?.length) {
|
||||||
data.linkedRequirementIds.forEach((rid) => {
|
data.linkedRequirementIds.forEach((rid) => {
|
||||||
@@ -1061,7 +1081,7 @@ export default function VersionDetailPage() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onUpdate={(id, data) => {
|
onUpdate={(id, data) => {
|
||||||
if (versionReadonly || !planWriteReady) return;
|
if (versionReadonly || !planWriteReady || !canWriteVersionPermission(PLAN_MANAGE_PERMISSION[pt])) return;
|
||||||
updatePlan(id, data);
|
updatePlan(id, data);
|
||||||
if ((pt === 'product') && data.linkedRequirementIds && data.owner) {
|
if ((pt === 'product') && data.linkedRequirementIds && data.owner) {
|
||||||
data.linkedRequirementIds.forEach((rid) => {
|
data.linkedRequirementIds.forEach((rid) => {
|
||||||
@@ -1071,11 +1091,11 @@ export default function VersionDetailPage() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onComplete={(id, result) => {
|
onComplete={(id, result) => {
|
||||||
if (versionReadonly || !planWriteReady) return;
|
if (versionReadonly || !planWriteReady || !canWriteVersionPermission(PLAN_MANAGE_PERMISSION[pt])) return;
|
||||||
return completePlan(id, result);
|
return completePlan(id, result);
|
||||||
}}
|
}}
|
||||||
onDelete={(id) => {
|
onDelete={(id) => {
|
||||||
if (versionReadonly || !planWriteReady) return;
|
if (versionReadonly || !planWriteReady || !canWriteVersionPermission(PLAN_MANAGE_PERMISSION[pt])) return;
|
||||||
deletePlan(id);
|
deletePlan(id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1086,7 +1106,7 @@ export default function VersionDetailPage() {
|
|||||||
versionId={version.id}
|
versionId={version.id}
|
||||||
requirementIds={displayRequirementIds}
|
requirementIds={displayRequirementIds}
|
||||||
versionDeadline={version.expectedReleaseDate ?? undefined}
|
versionDeadline={version.expectedReleaseDate ?? undefined}
|
||||||
readOnly={versionReadonly || !devTaskWriteReady}
|
readOnly={versionReadonly || !devTaskWriteReady || !canManageDevTasks}
|
||||||
versionTasks={displayDevTasks}
|
versionTasks={displayDevTasks}
|
||||||
versionRequirements={displayRequirements}
|
versionRequirements={displayRequirements}
|
||||||
/>
|
/>
|
||||||
@@ -1094,7 +1114,7 @@ export default function VersionDetailPage() {
|
|||||||
<TestCaseTab
|
<TestCaseTab
|
||||||
versionId={version.id}
|
versionId={version.id}
|
||||||
requirementIds={displayRequirementIds}
|
requirementIds={displayRequirementIds}
|
||||||
readOnly={versionReadonly || !testCaseWriteReady}
|
readOnly={versionReadonly || !testCaseWriteReady || !canManageTestCases}
|
||||||
versionCases={displayTestCases}
|
versionCases={displayTestCases}
|
||||||
versionBugs={displayBugs}
|
versionBugs={displayBugs}
|
||||||
versionDevTasks={displayDevTasks}
|
versionDevTasks={displayDevTasks}
|
||||||
@@ -1104,7 +1124,7 @@ export default function VersionDetailPage() {
|
|||||||
<BugTab
|
<BugTab
|
||||||
versionId={version.id}
|
versionId={version.id}
|
||||||
requirementIds={displayRequirementIds}
|
requirementIds={displayRequirementIds}
|
||||||
readOnly={versionReadonly || !bugWriteReady}
|
readOnly={versionReadonly || !bugWriteReady || !canManageBugs}
|
||||||
versionBugs={displayBugs}
|
versionBugs={displayBugs}
|
||||||
versionTestCases={displayTestCases}
|
versionTestCases={displayTestCases}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
import { addVersionMembers, filterVersionMemberCandidates, inferVersionMemberRole } from './version-members';
|
import { addVersionMembers, canWriteVersionWork, filterVersionMemberCandidates, inferVersionMemberRole } from './version-members';
|
||||||
|
|
||||||
test('filterVersionMemberCandidates searches people and excludes already added members', () => {
|
test('filterVersionMemberCandidates searches people and excludes already added members', () => {
|
||||||
const result = filterVersionMemberCandidates(
|
const result = filterVersionMemberCandidates(
|
||||||
@@ -63,3 +63,45 @@ test('addVersionMembers infers each selected person role from department', () =>
|
|||||||
{ name: 'QA', role: 'testing' },
|
{ name: 'QA', role: 'testing' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('canWriteVersionWork requires role permission and version membership', () => {
|
||||||
|
const devRole = { id: 'role-dev', name: '开发', permissions: ['version.devtask:manage'] } as any;
|
||||||
|
const testRole = { id: 'role-test', name: '测试', permissions: ['version.testcase:manage'] } as any;
|
||||||
|
const adminRole = { id: 'role-admin', name: '超管', permissions: ['*'] } as any;
|
||||||
|
const versionMembers = [{ name: 'Dev One', role: 'frontend' as const }];
|
||||||
|
|
||||||
|
assert.equal(canWriteVersionWork({
|
||||||
|
role: devRole,
|
||||||
|
permission: 'version.devtask:manage',
|
||||||
|
currentUserName: 'Dev One',
|
||||||
|
versionMembers,
|
||||||
|
}), true);
|
||||||
|
|
||||||
|
assert.equal(canWriteVersionWork({
|
||||||
|
role: devRole,
|
||||||
|
permission: 'version.devtask:manage',
|
||||||
|
currentUserName: 'Outside Dev',
|
||||||
|
versionMembers,
|
||||||
|
}), false);
|
||||||
|
|
||||||
|
assert.equal(canWriteVersionWork({
|
||||||
|
role: testRole,
|
||||||
|
permission: 'version.devtask:manage',
|
||||||
|
currentUserName: 'Dev One',
|
||||||
|
versionMembers,
|
||||||
|
}), false);
|
||||||
|
|
||||||
|
assert.equal(canWriteVersionWork({
|
||||||
|
role: devRole,
|
||||||
|
permission: 'version.devtask:manage',
|
||||||
|
currentUserName: 'Dev One',
|
||||||
|
versionMembers: [],
|
||||||
|
}), false);
|
||||||
|
|
||||||
|
assert.equal(canWriteVersionWork({
|
||||||
|
role: adminRole,
|
||||||
|
permission: 'version.devtask:manage',
|
||||||
|
currentUserName: 'Admin',
|
||||||
|
versionMembers: [],
|
||||||
|
}), true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -56,3 +56,23 @@ export function addVersionMembers(
|
|||||||
|
|
||||||
return [...currentMembers, ...additions];
|
return [...currentMembers, ...additions];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isVersionMemberName(currentUserName: string | undefined, versionMembers: VersionMember[]): boolean {
|
||||||
|
const name = currentUserName?.trim();
|
||||||
|
if (!name) return false;
|
||||||
|
return versionMembers.some((member) => member.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canWriteVersionWork(input: {
|
||||||
|
role?: { permissions?: string[] } | null;
|
||||||
|
permission: string;
|
||||||
|
currentUserName?: string;
|
||||||
|
versionMembers: VersionMember[];
|
||||||
|
readOnly?: boolean;
|
||||||
|
}): boolean {
|
||||||
|
if (input.readOnly) return false;
|
||||||
|
const permissions = input.role?.permissions ?? [];
|
||||||
|
if (permissions.includes('*')) return true;
|
||||||
|
if (!permissions.includes(input.permission)) return false;
|
||||||
|
return isVersionMemberName(input.currentUserName, input.versionMembers);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user