fix(vpn): losing the operator roles closes the owner doors by itself

Ownership alone kept download, block and delete reachable for someone whose
roles were removed by any path other than revokeStaff() — a direct role edit,
say. Being staff is now part of ownership, so revocation closes these doors
without depending on whoever removed the role also remembering the tunnel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 22:50:13 +02:00
parent e5f19f8db3
commit 4ffdafc614
2 changed files with 29 additions and 1 deletions

View File

@ -67,9 +67,16 @@ class VpnPeerPolicy
&& ! $peer->trashed(); && ! $peer->trashed();
} }
/**
* isOperator() is part of ownership on purpose: roles can be taken away by
* other paths than revokeStaff() a direct role edit, for instance. Losing
* the console must close these doors by itself, not rely on whoever removed
* the role also remembering the tunnel.
*/
private function owns(User $user, VpnPeer $peer): bool private function owns(User $user, VpnPeer $peer): bool
{ {
return $peer->kind === VpnPeer::KIND_STAFF return $user->isOperator()
&& $peer->kind === VpnPeer::KIND_STAFF
&& $peer->user_id !== null && $peer->user_id !== null
&& $peer->user_id === $user->id; && $peer->user_id === $user->id;
} }

View File

@ -745,3 +745,24 @@ it('will not issue an access to someone who is no longer staff', function () {
expect(VpnPeer::withTrashed()->count())->toBe(0); expect(VpnPeer::withTrashed()->count())->toBe(0);
}); });
it('closes the owner doors as soon as the roles are gone', function () {
vpnHub();
$support = operator('Support');
$peer = VpnPeer::factory()->ownedBy($support)->create([
'config_secret' => App\Services\Wireguard\ConfigVault::encrypt("[Interface]\n"),
]);
expect($support->can('downloadConfig', $peer))->toBeTrue();
// Roles removed directly, not through revokeStaff() — ownership alone must
// not keep the modals reachable.
$support->syncRoles([]);
app(Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
$support = $support->fresh();
expect($support->can('downloadConfig', $peer))->toBeFalse()
->and($support->can('delete', $peer))->toBeFalse()
->and($support->can('block', $peer))->toBeFalse();
Livewire::actingAs($support)->test(VpnConfigAccess::class, ['uuid' => $peer->uuid])->assertForbidden();
});