From 3386bb260a232b7c830d141935299fa6c0cb26d7 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Wed, 17 Jun 2026 10:23:59 -0400 Subject: [PATCH] fix(auth): grant admin role portal permission to submit evidence forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin-role users got "Access Denied" submitting evidence forms (e.g. /documents/risk-committee-meeting/new) while Owners could. The evidence-form submit/upload/my-submissions endpoints are gated on `portal:update` (evidence-forms.controller.ts) — chosen so employees can self-submit from the portal — but the built-in `admin` role had no `portal` statement at all. Owner and employee/contractor had `portal:['read','update']`; admin and auditor did not, so admins were denied. The CS workaround was granting the Employee role. Fix: add `portal:['read','update']` to the admin role so admins can submit evidence forms directly, mirroring owner. Auditor is intentionally left unchanged (read-only). Updated the two guarding specs that encoded the old "admins get portal only via the employee role" policy. Side effect (intended): a pure-admin user can now log into the employee portal, same as owner already could. Invite-email behavior is unchanged (it keys off the compliance obligation, which admin still lacks). Verified against the built @trycompai/auth dist running real better-auth: admin.authorize({portal:['update']}).success === true, auditor === false, owner/employee unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../training/permissions-regression.spec.ts | 6 ++-- apps/api/src/training/portal-access.spec.ts | 29 ++++++++++--------- packages/auth/src/permissions.ts | 3 ++ 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/api/src/training/permissions-regression.spec.ts b/apps/api/src/training/permissions-regression.spec.ts index 82f1010d4c..d578bcef05 100644 --- a/apps/api/src/training/permissions-regression.spec.ts +++ b/apps/api/src/training/permissions-regression.spec.ts @@ -195,8 +195,10 @@ describe('Built-in role permissions — regression', () => { ); }); - it('should NOT have portal permissions', () => { - expect(perms.portal).toBeUndefined(); + it('should have portal read/update', () => { + expect(perms.portal).toEqual( + expect.arrayContaining(['read', 'update']), + ); }); it('should have pentest create/read/delete', () => { diff --git a/apps/api/src/training/portal-access.spec.ts b/apps/api/src/training/portal-access.spec.ts index 90c64ded13..f1930ce000 100644 --- a/apps/api/src/training/portal-access.spec.ts +++ b/apps/api/src/training/portal-access.spec.ts @@ -93,7 +93,10 @@ describe('Portal access matrix', () => { ['employee'], ['contractor'], ['owner'], + ['admin'], ['admin,employee'], + ['admin,member'], + ['admin,auditor'], ['owner,employee'], ['employee,contractor'], ])('%s → ALLOW', (roleString) => { @@ -102,26 +105,26 @@ describe('Portal access matrix', () => { }); describe('roles that should NOT have portal access', () => { - it.each([ - ['admin'], - ['auditor'], - ['member'], - ['admin,member'], - ['admin,auditor'], - [''], - ])('%s → DENY', (roleString) => { - expect(hasPortalAccessForBuiltInRoles(roleString)).toBe(false); - }); + it.each([['auditor'], ['member'], ['']])( + '%s → DENY', + (roleString) => { + expect(hasPortalAccessForBuiltInRoles(roleString)).toBe(false); + }, + ); }); describe('portal access relies on RBAC, not role names', () => { - it('admin gets portal access only through the employee role', () => { - expect(BUILT_IN_ROLE_PERMISSIONS.admin?.portal).toBeUndefined(); - expect(BUILT_IN_ROLE_PERMISSIONS.employee?.portal).toEqual( + it('admin has portal access through its own permissions', () => { + expect(BUILT_IN_ROLE_PERMISSIONS.admin?.portal).toEqual( expect.arrayContaining(['read', 'update']), ); }); + it('auditor is denied portal access by RBAC, not by role name', () => { + expect(BUILT_IN_ROLE_PERMISSIONS.auditor?.portal).toBeUndefined(); + expect(BUILT_IN_ROLE_OBLIGATIONS.auditor?.compliance).toBeFalsy(); + }); + it('admin does not have compliance obligation', () => { expect(BUILT_IN_ROLE_OBLIGATIONS.admin?.compliance).toBeFalsy(); }); diff --git a/packages/auth/src/permissions.ts b/packages/auth/src/permissions.ts index 3d9bc2e076..58d5da483e 100644 --- a/packages/auth/src/permissions.ts +++ b/packages/auth/src/permissions.ts @@ -125,6 +125,9 @@ export const admin = ac.newRole({ pentest: ['create', 'read', 'update', 'delete'], // Training management training: ['read', 'update'], + // Portal self-service — admins manage GRC evidence, so they need to submit + // evidence forms (portal:update) just like owners do. + portal: ['read', 'update'], // Secrets manager — admin can fully manage decrypted credentials secret: ['create', 'read', 'update', 'delete'], });