mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-16 16:45:07 +00:00
6e47d76ba6
Client file sharing, rebuilt from the ground up: a private area per client, resumable uploads, folders, groups and categories, sharing with expiry dates and download limits, comments, file versions, an activity log, a REST API, and sixteen languages. This repository begins here. ProjectSend 2 was developed privately, and that development history is not published — the previous generation remains available, with its own history, at projectsend/legacy. Free software under the GNU General Public License v2, or (at your option) any later version.
141 lines
5.5 KiB
PHP
141 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Audit\Action;
|
|
use App\Modules\Audit\ActivityLog;
|
|
use App\Modules\Identity\Permissions\Permission;
|
|
|
|
beforeEach(function () {
|
|
$this->admin = User::factory()->create();
|
|
confirmPassword($this->admin);
|
|
});
|
|
|
|
test('minting a token requires a fresh password confirmation', function () {
|
|
// actingAs() swaps the user but keeps the session, so the beforeEach
|
|
// confirmation above would otherwise carry over and this would assert
|
|
// nothing. An unconfirmed session must not be able to mint a
|
|
// credential that outlives it.
|
|
$this->flushSession();
|
|
|
|
$unconfirmed = User::factory()->create();
|
|
|
|
$this->actingAs($unconfirmed)->post('/settings/api-tokens', [
|
|
'name' => 'From a stolen session',
|
|
'abilities' => [Permission::Upload->value],
|
|
'expires_in_days' => 30,
|
|
])->assertRedirect(route('password.confirm'));
|
|
|
|
expect($unconfirmed->tokens()->count())->toBe(0);
|
|
});
|
|
|
|
test('the token page lists only abilities the issuer actually holds', function () {
|
|
$limited = staffWithPermissions([Permission::Upload->value, Permission::EditFiles->value]);
|
|
|
|
$this->actingAs($limited)->get('/settings/api-tokens/create')
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('settings/api-tokens/create')
|
|
->where('available_abilities', function ($groups) {
|
|
$keys = collect($groups)->flatMap(fn ($group) => collect($group['abilities'])->pluck('key'))->all();
|
|
|
|
return $keys === [Permission::Upload->value, Permission::EditFiles->value];
|
|
}));
|
|
});
|
|
|
|
test('a token cannot be granted an ability its issuer lacks', function () {
|
|
$limited = staffWithPermissions([Permission::Upload->value]);
|
|
confirmPassword($limited);
|
|
|
|
$this->actingAs($limited)->post('/settings/api-tokens', [
|
|
'name' => 'Escalation attempt',
|
|
'abilities' => [Permission::DeleteOthersFiles->value],
|
|
'expires_in_days' => 30,
|
|
])->assertSessionHasErrors('abilities.0');
|
|
|
|
expect($limited->tokens()->count())->toBe(0);
|
|
});
|
|
|
|
test('the plaintext token is returned once and never stored', function () {
|
|
$response = $this->actingAs($this->admin)->post('/settings/api-tokens', [
|
|
'name' => 'Zapier',
|
|
'abilities' => [Permission::Upload->value],
|
|
'expires_in_days' => 30,
|
|
]);
|
|
|
|
$created = $response->assertRedirect()->getSession()->get('created_api_token');
|
|
expect($created['plain_text'])->toBeString()->not->toBeEmpty();
|
|
|
|
// What lands in the database is a hash of the secret, not the secret.
|
|
$stored = $this->admin->tokens()->firstOrFail();
|
|
expect($stored->token)->not->toContain($created['plain_text'])
|
|
->and($stored->token)->toBe(hash('sha256', explode('|', $created['plain_text'])[1]));
|
|
|
|
// It survives exactly one request — the redirect target that displays
|
|
// it — and is gone on the next load. That is the whole "shown once"
|
|
// guarantee: there is no second chance to read it out of the app.
|
|
$this->actingAs($this->admin)->get('/settings/api-tokens')
|
|
->assertInertia(fn ($page) => $page->where('created_token.plain_text', $created['plain_text']));
|
|
|
|
$this->actingAs($this->admin)->get('/settings/api-tokens')
|
|
->assertInertia(fn ($page) => $page->where('created_token', null));
|
|
});
|
|
|
|
test('an expiry is required unless never_expires is chosen explicitly', function () {
|
|
$this->actingAs($this->admin)->post('/settings/api-tokens', [
|
|
'name' => 'No expiry given',
|
|
'abilities' => [Permission::Upload->value],
|
|
])->assertSessionHasErrors('expires_in_days');
|
|
|
|
$this->actingAs($this->admin)->post('/settings/api-tokens', [
|
|
'name' => 'Deliberately eternal',
|
|
'abilities' => [Permission::Upload->value],
|
|
'never_expires' => true,
|
|
])->assertSessionHasNoErrors();
|
|
|
|
expect($this->admin->tokens()->firstOrFail()->expires_at)->toBeNull();
|
|
});
|
|
|
|
test('the expiry ceiling is enforced', function () {
|
|
$this->actingAs($this->admin)->post('/settings/api-tokens', [
|
|
'name' => 'Too long',
|
|
'abilities' => [Permission::Upload->value],
|
|
'expires_in_days' => (int) config('api.tokens.max_days') + 1,
|
|
])->assertSessionHasErrors('expires_in_days');
|
|
});
|
|
|
|
test('creating and revoking a token are both audited', function () {
|
|
$this->actingAs($this->admin)->post('/settings/api-tokens', [
|
|
'name' => 'Audited',
|
|
'abilities' => [Permission::Upload->value],
|
|
'expires_in_days' => 10,
|
|
]);
|
|
|
|
$token = $this->admin->tokens()->firstOrFail();
|
|
|
|
$this->actingAs($this->admin)->delete("/settings/api-tokens/{$token->getKey()}")->assertRedirect();
|
|
|
|
expect(ActivityLog::query()->where('action', Action::ApiTokenCreated)->exists())->toBeTrue()
|
|
->and(ActivityLog::query()->where('action', Action::ApiTokenRevoked)->exists())->toBeTrue()
|
|
->and($this->admin->tokens()->count())->toBe(0);
|
|
});
|
|
|
|
test('one staff member cannot revoke another staff member is token', function () {
|
|
$other = User::factory()->create();
|
|
$token = $other->createToken('Theirs', [Permission::Upload->value]);
|
|
|
|
$this->actingAs($this->admin)
|
|
->delete('/settings/api-tokens/'.$token->accessToken->getKey())
|
|
->assertRedirect();
|
|
|
|
// Silently a no-op rather than an error, but the token must survive.
|
|
expect($other->tokens()->count())->toBe(1);
|
|
});
|
|
|
|
test('clients cannot reach the token page at all', function () {
|
|
$client = User::factory()->client()->create();
|
|
|
|
$this->actingAs($client)->get('/settings/api-tokens')->assertRedirect();
|
|
});
|