feat(broadcasting): extend submission.{id} channel auth to organisation admins
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>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
|
||||
@@ -30,11 +31,19 @@ use Illuminate\Support\Facades\Broadcast;
|
||||
* IdentityMatchBanner subscribes via Echo.private('submission.{id}') and
|
||||
* refetches the submission resource on receipt.
|
||||
*
|
||||
* v1 authz scope: only the submitter who created the submission via an
|
||||
* authenticated session is allowed to subscribe. Org-admin access is
|
||||
* deferred — see BACKLOG entry TECH-CHANNEL-AUTH-ORG-ADMIN. Public
|
||||
* (token-based) submitters are not on this channel; their flow is
|
||||
* already polling-based and they don't have a User to authenticate with.
|
||||
* 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}',
|
||||
@@ -47,11 +56,32 @@ Broadcast::channel(
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO TECH-CHANNEL-AUTH-ORG-ADMIN — extend to organisation admins
|
||||
// once we audit the Spatie Permission helper for an
|
||||
// organisation-scoped role check (hasRoleInOrganisation or
|
||||
// similar). Until that audit lands, only the submitter has
|
||||
// channel access.
|
||||
return $submission->submitted_by_user_id === $user->id;
|
||||
// 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();
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user