hasRole('super_admin')) { return true; } // Org admin in any organisation. Controllers in sessions 2/3 // restrict the result set per role. return $user->organisations() ->wherePivot('role', 'org_admin') ->exists(); } /** * RFC V3 — tenant gate for the org-scoped index endpoint. * The caller must be super_admin OR an org_admin on THIS specific * organisation; without this check the broader `viewAny` would let * orgB's admin enumerate orgA's failure rows via orgA's URL. * Denied → controller translates to 404 to keep the IDOR contract. */ public function viewAnyInOrganisation(User $user, Organisation $organisation): bool { if ($user->hasRole('super_admin')) { return true; } return $organisation->users() ->where('user_id', $user->id) ->wherePivot('role', 'org_admin') ->exists(); } public function view(User $user, FormSubmissionActionFailure $failure): bool { return $this->canAccess($user, $failure); } public function retry(User $user, FormSubmissionActionFailure $failure): bool { return $this->canAccess($user, $failure); } public function resolve(User $user, FormSubmissionActionFailure $failure): bool { return $this->canAccess($user, $failure); } public function dismiss(User $user, FormSubmissionActionFailure $failure): bool { return $this->canAccess($user, $failure); } private function canAccess(User $user, FormSubmissionActionFailure $failure): bool { // Load the submission without global scopes so cross-tenant // resolution works for super_admin and so the policy itself // does the tenant gating (RFC V3 — single source of truth for // tenant resolution, not OrganisationScope). $submission = \App\Models\FormBuilder\FormSubmission::query() ->withoutGlobalScopes() ->find($failure->form_submission_id); if ($submission === null) { return false; // parent submission deleted } if ($submission->deleted_at !== null) { return false; // soft-deleted parent — treat as gone } $orgId = (string) $submission->organisation_id; if ($orgId === '') { return false; } if ($user->hasRole('super_admin')) { return true; } // Tenant scope: user must be an org_admin in the failure's // organisation. RFC V3 — IDOR-class FK-chain enforcement. $organisation = \App\Models\Organisation::query() ->withoutGlobalScopes() ->find($orgId); if (! $organisation instanceof Organisation) { return false; } return $organisation->users() ->where('user_id', $user->id) ->wherePivot('role', 'org_admin') ->exists(); } }