task-l.3
This commit is contained in:
83
scripts/security/verify-crm-access.ts
Normal file
83
scripts/security/verify-crm-access.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
type Check = {
|
||||
name: string;
|
||||
file: string;
|
||||
patterns: string[];
|
||||
};
|
||||
|
||||
const checks: Check[] = [
|
||||
{
|
||||
name: 'dashboard route uses CRM security context',
|
||||
file: 'src/app/api/crm/dashboard/route.ts',
|
||||
patterns: ['buildCrmSecurityContext', 'getCrmDashboardData(']
|
||||
},
|
||||
{
|
||||
name: 'dashboard export blocks revenue export without pricing visibility',
|
||||
file: 'src/app/api/crm/dashboard/export/route.ts',
|
||||
patterns: ['canViewQuotationPricing', "section === 'revenue-analytics'"]
|
||||
},
|
||||
{
|
||||
name: 'quotation list route passes resolved scope context',
|
||||
file: 'src/app/api/crm/quotations/route.ts',
|
||||
patterns: ['buildCrmSecurityContext', 'listQuotations(organization.id', 'accessContext']
|
||||
},
|
||||
{
|
||||
name: 'quotation detail route passes resolved scope context',
|
||||
file: 'src/app/api/crm/quotations/[id]/route.ts',
|
||||
patterns: ['buildCrmSecurityContext', 'getQuotationDetail(id, organization.id, accessContext)']
|
||||
},
|
||||
{
|
||||
name: 'quotation PDF preview is pricing-gated',
|
||||
file: 'src/app/api/crm/quotations/[id]/pdf-preview/route.ts',
|
||||
patterns: ['canViewQuotationPricing', "action: 'pricing_hidden'"]
|
||||
},
|
||||
{
|
||||
name: 'quotation PDF download is pricing-gated',
|
||||
file: 'src/app/api/crm/quotations/[id]/pdf-download/route.ts',
|
||||
patterns: ['canViewQuotationPricing', "action: 'pricing_hidden'"]
|
||||
},
|
||||
{
|
||||
name: 'approved PDF route is pricing-gated',
|
||||
file: 'src/app/api/crm/quotations/[id]/approved-pdf/route.ts',
|
||||
patterns: ['canViewQuotationPricing', "action: 'pricing_hidden'"]
|
||||
},
|
||||
{
|
||||
name: 'artifact download route protects approved quotation artifacts',
|
||||
file: 'src/app/api/crm/document-artifacts/[id]/download/route.ts',
|
||||
patterns: ['getArtifact', "artifact.artifactType === 'approved_pdf'", 'canViewQuotationPricing']
|
||||
},
|
||||
{
|
||||
name: 'approval step actor resolution uses resolved CRM access',
|
||||
file: 'src/features/foundation/approval/server/service.ts',
|
||||
patterns: ['resolveCrmMembershipAccess', 'resolvedAccess.businessRoles.includes(roleCode)']
|
||||
},
|
||||
{
|
||||
name: 'quotation service applies scoped record access',
|
||||
file: 'src/features/crm/quotations/server/service.ts',
|
||||
patterns: ['canAccessScopedCrmRecord', 'buildQuotationAccessScopedFilters', 'Forbidden branch scope']
|
||||
}
|
||||
];
|
||||
|
||||
let hasFailure = false;
|
||||
|
||||
for (const check of checks) {
|
||||
const absolutePath = resolve(process.cwd(), check.file);
|
||||
const content = readFileSync(absolutePath, 'utf8');
|
||||
const missing = check.patterns.filter((pattern) => !content.includes(pattern));
|
||||
|
||||
if (missing.length > 0) {
|
||||
hasFailure = true;
|
||||
console.error(`FAIL ${check.name}`);
|
||||
console.error(` File: ${check.file}`);
|
||||
console.error(` Missing: ${missing.join(', ')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.info(`PASS ${check.name}`);
|
||||
}
|
||||
|
||||
if (hasFailure) {
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user