diff --git a/layers/auth/server/services/casl.ts b/layers/auth/server/services/casl.ts index 9299b548..73fc5d1d 100644 --- a/layers/auth/server/services/casl.ts +++ b/layers/auth/server/services/casl.ts @@ -57,12 +57,15 @@ async function evaluateAbilityString( return { allowed: session.abilities.includes(abilityStr) } } - if (!caslAbility.can(action, subjectName)) { - return { allowed: false } + if (scope !== 'self') { + // Check with an empty instance so conditional (`:self`) rules don't accidentally + // satisfy an unscoped gate — conditions like { user_id } won't match {}. + return { allowed: caslAbility.can(action, subject(subjectName, {})) } } - if (scope !== 'self') { - return { allowed: true } + // :self — quick type-level bail before the DB fetch + if (!caslAbility.can(action, subjectName)) { + return { allowed: false } } // :self — fetch the resource and let CASL verify the user_id condition @@ -85,7 +88,13 @@ async function evaluateAbilityString( return { allowed: false } } - if (!caslAbility.can(action, subject(subjectName, resource as Record))) { + // Ownership check: user owns the resource OR has unconditional manage. + // We cannot rely on caslAbility.can(action, instance) here because an unscoped + // ability (e.g. `project:write` for CREATE) would satisfy the conditional check, + // letting any member bypass the ownership gate. + const owns = resource.user_id === session.id + const hasManage = caslAbility.can('manage', subject(subjectName, {})) + if (!owns && !hasManage) { return { allowed: false } } diff --git a/test/__mocks__/hub-db-schema.ts b/test/__mocks__/hub-db-schema.ts index ac55d6c3..25ca8a49 100644 --- a/test/__mocks__/hub-db-schema.ts +++ b/test/__mocks__/hub-db-schema.ts @@ -13,6 +13,9 @@ export const organizationCreditTable = {} as any export const transactionTable = {} as any export const roleTable = {} as any export const organizationMemberRoleTable = {} as any +export const TICKET_KINDS = ['feedback', 'support'] as const +export const TICKET_CATEGORIES = ['account', 'billing', 'technical', 'other'] as const +export const TICKET_STATUSES = ['open', 'active', 'closed'] as const export const supportTicketTable = {} as any export const supportTicketMessageTable = {} as any export const projectTable = {} as any