Per BACKLOG TECH-CHANNEL-AUTH-ORG-ADMIN.
WS-6 v1.3-delta D2 (PR #11 23a5696) introduced submission.{id} private
channel with submitter-only authorization, deferring org-admin auth
to a follow-up after the Spatie Permission helper convention was
audited. This commit closes that follow-up.
Authorization now permits (cheap-first short-circuit):
1. Submitter (submitted_by_user_id === user.id) — unchanged
2. super_admin (Spatie HasRoles app-wide bypass) — audit-surfaced bonus,
matches every analogous policy in the codebase
3. Organisation admins of the submission's organisation — new
Pattern: direct port of FormSubmissionActionFailurePolicy::canAccess.
Spatie teams is disabled in config/permission.php, so org-scoping
lives in the user_organisation pivot table's `role` column with
wherePivot('role', 'org_admin') — codebase canonical (used in 17+
policy sites). withoutGlobalScopes() preserved on both FormSubmission
and Organisation lookups so channel auth is a structural gate, not a
tenant-scoped query.
Inline TODO removed; the BACKLOG entry transitions to resolved in a
follow-up commit on this branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
88 lines
3.2 KiB
PHP
88 lines
3.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\FormBuilder\FormSubmission;
|
|
use App\Models\Organisation;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Broadcast;
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Broadcast Channel Authorization
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Channel-level authorization callbacks for private and presence channels.
|
|
| Laravel's broadcasting auth middleware invokes these on subscription
|
|
| attempts; returning truthy authorises, falsy denies.
|
|
|
|
|
| File registered in bootstrap/app.php via withRouting(channels: ...).
|
|
| Without that registration this file is dead code.
|
|
|
|
|
*/
|
|
|
|
/*
|
|
* Per RFC-WS-6 §Q1 v1.3 addition 2.
|
|
*
|
|
* Authorises private subscriptions to a form-submission's identity-match
|
|
* resolution channel. The TriggerPersonIdentityMatchOnFormSubmit listener
|
|
* dispatches FormSubmissionIdentityMatchResolved on this channel after
|
|
* writing the final identity_match_status; the frontend portal
|
|
* IdentityMatchBanner subscribes via Echo.private('submission.{id}') and
|
|
* refetches the submission resource on receipt.
|
|
*
|
|
* Authorisation paths (in order of cheap-first short-circuit):
|
|
* 1. Submitter (submitted_by_user_id === user.id) — common case for the
|
|
* portal banner; no DB lookup beyond the submission itself.
|
|
* 2. super_admin (Spatie HasRoles, app-wide bypass) — debugging,
|
|
* impersonation, platform-level support.
|
|
* 3. Organisation admin of the submission's organisation — pivot-table
|
|
* check on user_organisation with role='org_admin'. Codebase
|
|
* canonical pattern, mirroring FormSubmissionActionFailurePolicy::canAccess
|
|
* (Spatie teams is disabled in config/permission.php; org-scoping
|
|
* lives in the pivot, not in Spatie).
|
|
*
|
|
* Public (token-based) submitters are not on this channel; their flow is
|
|
* polling-based and they don't have a User to authenticate with.
|
|
*/
|
|
Broadcast::channel(
|
|
'submission.{submissionId}',
|
|
function (User $user, string $submissionId): bool {
|
|
$submission = FormSubmission::query()
|
|
->withoutGlobalScopes()
|
|
->find($submissionId);
|
|
|
|
if ($submission === null) {
|
|
return false;
|
|
}
|
|
|
|
// Submitter has access (authenticated session at submit time).
|
|
if ($submission->submitted_by_user_id === $user->id) {
|
|
return true;
|
|
}
|
|
|
|
// super_admin app-wide bypass (Spatie HasRoles, global role).
|
|
if ($user->hasRole('super_admin')) {
|
|
return true;
|
|
}
|
|
|
|
// Org admins of the submission's organisation. Pivot-table check
|
|
// matching the codebase's canonical pattern (see e.g.
|
|
// FormSubmissionActionFailurePolicy::canAccess). Spatie teams is
|
|
// disabled in config/permission.php, so org-scoping lives in the
|
|
// user_organisation pivot's `role` column, not Spatie.
|
|
$organisation = Organisation::query()
|
|
->withoutGlobalScopes()
|
|
->find($submission->organisation_id);
|
|
|
|
if (! $organisation instanceof Organisation) {
|
|
return false;
|
|
}
|
|
|
|
return $organisation->users()
|
|
->where('user_id', $user->id)
|
|
->wherePivot('role', 'org_admin')
|
|
->exists();
|
|
},
|
|
);
|