Skip to content

Commit 592fdec

Browse files
os-zhuangclaude
andcommitted
fix(spec): cross-validate permission/profile object grants against declared objects
`validateCrossReferences` checked hook/view/seed/app-nav/action references against the declared object set — but NOT permission-set/profile `objects` grants, even though `validateNamespacePrefix`'s doc already assumed it did. So a profile that grants on a non-existent object (e.g. a short `lead` instead of the namespaced `crm_lead`) passed build/validate/test silently. The grant then applies to nothing: the authenticated path may namespace-resolve the short name, but the anonymous / explicit-permission-set path does not — so e.g. a public Web-to-Lead INSERT is denied for "roles []", with no diagnostic anywhere. Add the missing check: every `permissions[].objects` key must reference a declared object, or strict validation fails loudly at build time. - stack.zod.ts: validate permission/profile object grants → object references - stack.test.ts: +2 cases (short/undefined name fails; full name passes) 82/82 spec stack tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent be796ad commit 592fdec

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

packages/spec/src/stack.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,43 @@ describe('defineStack', () => {
485485
expect(() => defineStack(config, { strict: true })).not.toThrow();
486486
});
487487

488+
it('should detect permission/profile granting on an undefined object in strict mode', () => {
489+
const config = {
490+
manifest: baseManifest,
491+
objects: [
492+
{ name: 'crm_lead', fields: { email: { type: 'email' } } },
493+
],
494+
permissions: [
495+
{
496+
name: 'guest_portal',
497+
isProfile: true,
498+
// BUG: the object is `crm_lead`, not the short `lead` — the short name
499+
// exists nowhere, so this grant silently applies to nothing (and the
500+
// anonymous permission path denies the write). Must fail loudly.
501+
objects: { lead: { allowCreate: true } },
502+
},
503+
],
504+
};
505+
expect(() => defineStack(config, { strict: true })).toThrow("object 'lead'");
506+
});
507+
508+
it('should pass strict mode when a permission grants on a defined (full-name) object', () => {
509+
const config = {
510+
manifest: baseManifest,
511+
objects: [
512+
{ name: 'crm_lead', fields: { email: { type: 'email' } } },
513+
],
514+
permissions: [
515+
{
516+
name: 'guest_portal',
517+
isProfile: true,
518+
objects: { crm_lead: { allowCreate: true } },
519+
},
520+
],
521+
};
522+
expect(() => defineStack(config, { strict: true })).not.toThrow();
523+
});
524+
488525
it('should skip cross-reference validation when no objects are defined', () => {
489526
const config = {
490527
manifest: baseManifest,

packages/spec/src/stack.zod.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,29 @@ function validateCrossReferences(config: ObjectStackDefinition): string[] {
583583
}
584584
}
585585

586+
// Validate permission-set / profile object grants → object references.
587+
// A grant keyed by an object that isn't declared (e.g. a short `lead` instead
588+
// of the namespaced `crm_lead`) silently applies to NOTHING: the
589+
// authenticated path may namespace-resolve it, but the anonymous /
590+
// explicit-permission-set path does not — so the grant is simply lost (e.g. a
591+
// public Web-to-Lead INSERT is denied for "roles []"). Fail loudly at build
592+
// time. (`validateNamespacePrefix`'s doc already assumes this check lives here.)
593+
if (config.permissions) {
594+
for (const perm of config.permissions) {
595+
const grants = (perm as { objects?: Record<string, unknown> }).objects;
596+
if (grants && typeof grants === 'object') {
597+
for (const objName of Object.keys(grants)) {
598+
if (!objectNames.has(objName)) {
599+
errors.push(
600+
`Permission '${(perm as { name?: string }).name ?? '(unnamed)'}' grants on object ` +
601+
`'${objName}' which is not defined in objects.`,
602+
);
603+
}
604+
}
605+
}
606+
}
607+
}
608+
586609
// Validate app navigation → object/dashboard/page/report references
587610
if (config.apps) {
588611
const dashboardNames = new Set<string>();

0 commit comments

Comments
 (0)