diff --git a/.env.example b/.env.example index d913509..dcc5069 100644 --- a/.env.example +++ b/.env.example @@ -168,7 +168,6 @@ MONITORING_ATTEMPTS=2 # Erzeugen: head -c 32 /dev/urandom | base64 VPN_CONFIG_KEY= -ADMIN_HOSTS=admin.dev.clupilot.com,10.10.90.185 ADMIN_HOSTS=admin.dev.clupilot.com,10.10.90.185,localhost,127.0.0.1 # ── Nextcloud blueprint template ───────────────────────────────────────── diff --git a/app/Provisioning/Jobs/SyncVpnPeers.php b/app/Provisioning/Jobs/SyncVpnPeers.php index 29480e4..8ad5b56 100644 --- a/app/Provisioning/Jobs/SyncVpnPeers.php +++ b/app/Provisioning/Jobs/SyncVpnPeers.php @@ -99,6 +99,9 @@ class SyncVpnPeers implements ShouldBeUnique, ShouldQueue // never created through the console. 'allowed_ip' => strtok($snapshot->allowedIps, '/') ?: $snapshot->allowedIps, 'host_id' => $host?->id, + // Never "staff": a staff access has an owner, and nobody is + // behind a peer we merely found on the interface. + 'kind' => $host !== null ? VpnPeer::KIND_HOST : VpnPeer::KIND_SYSTEM, 'name' => $host?->name ?? __('vpn.unknown_peer'), 'enabled' => true, ]); @@ -112,6 +115,9 @@ class SyncVpnPeers implements ShouldBeUnique, ShouldQueue // (created_by is null exactly for sync-adopted rows). if ($peer->host_id === null && $host !== null) { $observed['host_id'] = $host->id; + if ($peer->kind === VpnPeer::KIND_SYSTEM) { + $observed['kind'] = VpnPeer::KIND_HOST; + } if ($peer->created_by === null) { $observed['name'] = $host->name; } diff --git a/database/migrations/2026_07_25_210000_add_ownership_to_vpn_peers.php b/database/migrations/2026_07_25_210000_add_ownership_to_vpn_peers.php index a238d0a..c0e4099 100644 --- a/database/migrations/2026_07_25_210000_add_ownership_to_vpn_peers.php +++ b/database/migrations/2026_07_25_210000_add_ownership_to_vpn_peers.php @@ -32,13 +32,18 @@ return new class extends Migration $table->unsignedInteger('download_count')->default(0); }); - // Existing rows: pipeline peers are host peers, the rest belong to - // whoever created them (created_by is the only ownership hint we have). + // Existing rows fall into three groups, and the difference matters: + // pipeline peers are host peers; peers someone created in the console + // belong to that person; and peers the sync adopted off the interface + // have no human behind them at all — calling those "staff" would break + // the very invariant this migration introduces (a staff access has an + // owner). DB::table('vpn_peers')->whereNotNull('host_id')->update(['kind' => 'host']); - DB::table('vpn_peers')->whereNull('host_id')->update([ + DB::table('vpn_peers')->whereNull('host_id')->whereNotNull('created_by')->update([ 'kind' => 'staff', 'user_id' => DB::raw('created_by'), ]); + DB::table('vpn_peers')->whereNull('host_id')->whereNull('created_by')->update(['kind' => 'system']); } public function down(): void diff --git a/tests/Feature/Admin/VpnTest.php b/tests/Feature/Admin/VpnTest.php index 776ee99..4966c16 100644 --- a/tests/Feature/Admin/VpnTest.php +++ b/tests/Feature/Admin/VpnTest.php @@ -223,6 +223,7 @@ it('adopts host peers from the hub and marks vanished ones absent', function () $adopted = VpnPeer::query()->where('public_key', $key)->firstOrFail(); expect($adopted->name)->toBe('pve-fsn1') // named after its host, not "unknown" + ->and($adopted->kind)->toBe(VpnPeer::KIND_HOST) ->and($adopted->host_id)->toBe($host->id) ->and($adopted->present)->toBeTrue() ->and($adopted->rx_bytes)->toBe(2048) @@ -456,13 +457,16 @@ it('renames a peer it adopted before the host link was known', function () { (new SyncVpnPeers)->handle($hub); $peer = VpnPeer::query()->where('public_key', $key)->firstOrFail(); expect($peer->name)->toBe(__('vpn.unknown_peer')) - ->and($peer->host_id)->toBeNull(); + ->and($peer->host_id)->toBeNull() + // Not staff: a staff access has an owner, and this one has nobody. + ->and($peer->kind)->toBe(VpnPeer::KIND_SYSTEM); Host::factory()->active()->create(['name' => 'pve-fsn1', 'wg_pubkey' => $key]); (new SyncVpnPeers)->handle($hub); expect($peer->fresh()->name)->toBe('pve-fsn1') - ->and($peer->fresh()->host_id)->not->toBeNull(); + ->and($peer->fresh()->host_id)->not->toBeNull() + ->and($peer->fresh()->kind)->toBe(VpnPeer::KIND_HOST); }); it('does not rename an access an operator named themselves', function () {