Fixes the nine findings of security audit run 1 on 2.4.1 (T1-T11). F1: Uploads that are not complete now count only the bytes the server received, and a chunk that goes above the storage quota gets 422. An anonymous uploader can no longer fill the quota with declared sizes. F2: The share password and the admin login have a second limit without the address (20 attempts per minute per share and per e-mail), because a forged X-Forwarded-For changes the address. F3: The system password gate and the admin check now also run on Livewire updates, so an old uploader snapshot cannot register files. F4: The session keeps a fingerprint of the system password's hash. When the admin changes the system password, earlier verifications end. F5: The page after the upload opens only in the session that created the share, and only while recipients can still reach the share. F6: The share password limit counts an attempt before the password is checked, so parallel attempts cannot pass it. F7: The system password has the same two limits (5 per address, 20 in total per minute). F8: A password change or reset signs the user's other sessions out. F9: The only admin cannot delete the account, which would open /setup to every visitor. The setup wizard also holds the admin password to the password defaults, and Livewire applies EnsureAdmin to updates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
6.7 KiB
PHP
170 lines
6.7 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\AdminDashboard;
|
|
use App\Models\Share;
|
|
use App\Models\ShareFile;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Livewire;
|
|
|
|
test('admin dashboard requires authentication', function () {
|
|
$response = $this->get(route('admin.dashboard'));
|
|
|
|
$response->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('non-admin user cannot access admin dashboard', function () {
|
|
$user = User::factory()->create(['is_admin' => false]);
|
|
|
|
$response = $this->actingAs($user)->get(route('admin.dashboard'));
|
|
|
|
$response->assertForbidden();
|
|
});
|
|
|
|
test('admin can access dashboard', function () {
|
|
$admin = User::query()->where('is_admin', true)->first();
|
|
|
|
$response = $this->actingAs($admin)->get(route('admin.dashboard'));
|
|
|
|
$response->assertOk();
|
|
});
|
|
|
|
test('admin dashboard shows stats', function () {
|
|
$admin = User::query()->where('is_admin', true)->first();
|
|
|
|
$share = Share::factory()->create(['total_size' => 1024]);
|
|
ShareFile::factory()->create(['share_id' => $share->id]);
|
|
|
|
$response = $this->actingAs($admin)->get(route('admin.dashboard'));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('Total Shares');
|
|
$response->assertSee('Active Shares');
|
|
$response->assertSee('Total Files');
|
|
$response->assertSee('Disk Usage');
|
|
});
|
|
|
|
test('admin dashboard shows the installed version with its release notes and links to the website and noNameWEB', function () {
|
|
$admin = User::query()->where('is_admin', true)->first();
|
|
config(['app.version' => '9.8.7']);
|
|
|
|
$response = $this->actingAs($admin)->get(route('admin.dashboard'));
|
|
|
|
$response->assertSee('SealShare 9.8.7');
|
|
$response->assertSee('href="https://gitea.nonameweb.ch/noNameWEB/SealShare/releases/tag/v9.8.7"', false);
|
|
$response->assertSee('href="https://sealshare.nonameweb.ch"', false);
|
|
$response->assertSee('href="https://nonameweb.ch"', false);
|
|
});
|
|
|
|
test('admin can delete share', function () {
|
|
Storage::fake('shares');
|
|
|
|
$admin = User::query()->where('is_admin', true)->first();
|
|
|
|
$share = Share::factory()->create();
|
|
$shareId = $share->id;
|
|
|
|
Livewire::actingAs($admin)
|
|
->test(AdminDashboard::class)
|
|
->set('deletingShareId', $shareId)
|
|
->call('deleteShare', $shareId)
|
|
->assertSet('deletingShareId', null);
|
|
|
|
expect(Share::query()->find($shareId))->toBeNull();
|
|
});
|
|
|
|
test('admin dashboard lists the shares', function () {
|
|
$admin = User::query()->where('is_admin', true)->first();
|
|
|
|
$share = Share::factory()->create(['token' => 'testtoken12345678']);
|
|
|
|
$response = $this->actingAs($admin)->get(route('admin.dashboard'));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('testtoken12345678');
|
|
$response->assertSee('href="'.route('share.download', $share).'"', false);
|
|
});
|
|
|
|
test('the shares list shows downloads against the limit, and a share at its limit as closed and not active', function () {
|
|
$admin = User::query()->where('is_admin', true)->first();
|
|
Share::factory()->withMaxDownloads(3)->create(['token' => 'limitedshare0000', 'download_count' => 2, 'expires_at' => null]);
|
|
Share::factory()->withMaxDownloads(1)->create(['token' => 'limitreached0000', 'download_count' => 1, 'expires_at' => null]);
|
|
Share::factory()->create(['token' => 'unlimitedshare00', 'download_count' => 1, 'expires_at' => null]);
|
|
|
|
$dashboard = Livewire::actingAs($admin)->test(AdminDashboard::class);
|
|
|
|
$dashboard->assertSeeInOrder(['limitedshare0000', '2 of 3 downloads', 'Never expires'])
|
|
->assertSeeInOrder(['limitreached0000', '1 of 1 download', 'Download limit reached'])
|
|
->assertSeeInOrder(['unlimitedshare00', '1 download', 'Never expires'])
|
|
->assertViewHas('activeShares', 2);
|
|
});
|
|
|
|
test('the shares list sorts only by its own orders', function () {
|
|
$admin = User::query()->where('is_admin', true)->first();
|
|
|
|
Share::factory()->create(['token' => 'aaaaaaaaaaaaaaaa', 'download_count' => 1, 'created_at' => now()->subDay()]);
|
|
Share::factory()->create(['token' => 'zzzzzzzzzzzzzzzz', 'download_count' => 9, 'created_at' => now()->subDays(2)]);
|
|
|
|
Livewire::actingAs($admin)
|
|
->test(AdminDashboard::class)
|
|
->assertSeeInOrder(['aaaaaaaaaaaaaaaa', 'zzzzzzzzzzzzzzzz'])
|
|
->set('sort', 'most-downloaded')
|
|
->assertSeeInOrder(['zzzzzzzzzzzzzzzz', 'aaaaaaaaaaaaaaaa'])
|
|
->set('sort', 'token; drop table shares')
|
|
->assertOk()
|
|
->assertSeeInOrder(['aaaaaaaaaaaaaaaa', 'zzzzzzzzzzzzzzzz']);
|
|
|
|
expect(Share::query()->count())->toBe(2);
|
|
});
|
|
|
|
test('sorting by expiry puts shares that never expire last', function () {
|
|
$admin = User::query()->where('is_admin', true)->first();
|
|
|
|
Share::factory()->create(['token' => 'neverexpires0000', 'expires_at' => null]);
|
|
Share::factory()->create(['token' => 'expireslater0000', 'expires_at' => now()->addWeek()]);
|
|
Share::factory()->create(['token' => 'expiressoon00000', 'expires_at' => now()->addHour()]);
|
|
|
|
Livewire::actingAs($admin)
|
|
->test(AdminDashboard::class)
|
|
->set('sort', 'expiring')
|
|
->assertSeeInOrder(['expiressoon00000', 'expireslater0000', 'neverexpires0000']);
|
|
});
|
|
|
|
test('without shares the dashboard shows an empty state instead of the list', function () {
|
|
$admin = User::query()->where('is_admin', true)->first();
|
|
|
|
$this->actingAs($admin)->get(route('admin.dashboard'))
|
|
->assertOk()
|
|
->assertSee('No shares yet')
|
|
->assertDontSee('data-test="share-row"', false);
|
|
});
|
|
|
|
test('the shares list excludes the private text from its file count and shows it separately', function () {
|
|
$admin = User::query()->where('is_admin', true)->first();
|
|
$share = Share::factory()->create(['token' => 'textshare00000001']);
|
|
ShareFile::factory()->count(2)->for($share)->create();
|
|
ShareFile::factory()->for($share)->text()->create();
|
|
|
|
Livewire::actingAs($admin)
|
|
->test(AdminDashboard::class)
|
|
->assertSeeInOrder(['textshare00000001', '2 files', 'Text'])
|
|
->assertDontSee('3 files');
|
|
});
|
|
|
|
test('shares whose files are still uploading are neither listed nor counted, but only their received bytes count as used space', function () {
|
|
$admin = User::query()->where('is_admin', true)->first();
|
|
$completed = Share::factory()->create(['token' => 'completedshare01', 'total_size' => 1000]);
|
|
ShareFile::factory()->for($completed)->create();
|
|
$pending = Share::factory()->pending()->create(['token' => 'pendingshare0001', 'total_size' => 500]);
|
|
ShareFile::factory()->for($pending)->uploading()->create();
|
|
|
|
Livewire::actingAs($admin)
|
|
->test(AdminDashboard::class)
|
|
->assertSee('completedshare01')
|
|
->assertDontSee('pendingshare0001')
|
|
->assertViewHas('totalShares', 1)
|
|
->assertViewHas('activeShares', 1)
|
|
->assertViewHas('totalFiles', 1)
|
|
->assertViewHas('usedSpace', 1000);
|
|
});
|