fix(vpn): an open modal stops serving the config once access is revoked
Once revealed, the plaintext sits in the handoff cache for ten minutes, and the modal read it from the token alone. A former owner could keep pulling the key out of an open modal after revocation — the exact window purgeSecret() exists to close. Every request now re-checks the policy and drops the handoff when it no longer holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
db7b8a6ee3
commit
04ba9dd2a9
|
|
@ -6,6 +6,7 @@ use App\Models\VpnPeer;
|
|||
use App\Services\Wireguard\ConfigHandoff;
|
||||
use App\Services\Wireguard\ConfigVault;
|
||||
use App\Services\Wireguard\QrCode;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
|
@ -42,6 +43,23 @@ class VpnConfigAccess extends ModalComponent
|
|||
$this->name = $peer->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every later request re-checks: once revealed, the plaintext sits in the
|
||||
* handoff cache for ten minutes, and an open modal would otherwise keep
|
||||
* handing it out after the access was revoked or reassigned — exactly the
|
||||
* window purgeSecret() exists to close.
|
||||
*/
|
||||
public function hydrate(): void
|
||||
{
|
||||
$peer = VpnPeer::withTrashed()->where('uuid', $this->uuid)->first();
|
||||
|
||||
if ($peer === null || Gate::denies('downloadConfig', $peer)) {
|
||||
ConfigHandoff::forget($this->token);
|
||||
$this->token = null;
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
public function reveal(): void
|
||||
{
|
||||
// A retry must not still be showing the previous attempt's complaint.
|
||||
|
|
|
|||
|
|
@ -667,3 +667,28 @@ it('removes a revoked colleague from the hub only after the rows are committed',
|
|||
expect($hub->peerIps())->toBe([])
|
||||
->and(VpnPeer::withTrashed()->count())->toBe(0); // tombstone cleared by the removal
|
||||
});
|
||||
|
||||
it('stops serving a revealed config once the access is revoked', function () {
|
||||
vpnHub();
|
||||
Queue::fake();
|
||||
$support = operator('Support');
|
||||
$support->forceFill(['password' => Hash::make('geheim-1234')])->save();
|
||||
$peer = VpnPeer::factory()->ownedBy($support)->create([
|
||||
'config_secret' => App\Services\Wireguard\ConfigVault::encrypt("[Interface]\nPrivateKey = XYZ\n"),
|
||||
]);
|
||||
|
||||
$modal = Livewire::actingAs($support)->test(VpnConfigAccess::class, ['uuid' => $peer->uuid])
|
||||
->set('password', 'geheim-1234')
|
||||
->call('reveal');
|
||||
$token = $modal->get('token');
|
||||
expect(App\Services\Wireguard\ConfigHandoff::get($token))->toContain('PrivateKey = XYZ');
|
||||
|
||||
// Revoked while the modal is still open on someone's screen.
|
||||
Livewire::actingAs(operator('Owner'))
|
||||
->test(ConfirmDeleteVpnPeer::class, ['uuid' => $peer->uuid])
|
||||
->call('delete');
|
||||
|
||||
// The open modal must not keep handing the key out of the cache.
|
||||
$modal->call('toggleQr')->assertForbidden();
|
||||
expect(App\Services\Wireguard\ConfigHandoff::get($token))->toBeNull();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue