mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-25 19:07:25 +03:00
Added and updated tests to cover. Also updated API auth to a narrower focus of existing session instead of also existing user auth. This is mainly for tests, to ensure they're following the session process we'd see for activity in the UI.
195 lines
6.8 KiB
PHP
195 lines
6.8 KiB
PHP
<?php
|
|
|
|
namespace Tests\Api;
|
|
|
|
use BookStack\Permissions\Models\RolePermission;
|
|
use BookStack\Users\Models\Role;
|
|
use BookStack\Users\Models\User;
|
|
use Carbon\Carbon;
|
|
use Tests\TestCase;
|
|
|
|
class ApiAuthTest extends TestCase
|
|
{
|
|
use TestsApi;
|
|
|
|
protected string $endpoint = '/api/books';
|
|
|
|
public function test_requests_succeed_with_default_auth()
|
|
{
|
|
$viewer = $this->users->viewer();
|
|
$this->permissions->grantUserRolePermissions($viewer, ['access-api']);
|
|
|
|
$resp = $this->get($this->endpoint);
|
|
$resp->assertStatus(401);
|
|
|
|
$this->actingAs($viewer, 'standard');
|
|
|
|
$this->startSession();
|
|
$resp = $this->withCredentials()->get($this->endpoint);
|
|
$resp->assertStatus(200);
|
|
}
|
|
|
|
public function test_no_token_throws_error()
|
|
{
|
|
$resp = $this->get($this->endpoint);
|
|
$resp->assertStatus(401);
|
|
$resp->assertJson($this->errorResponse('No authorization token found on the request', 401));
|
|
}
|
|
|
|
public function test_bad_token_format_throws_error()
|
|
{
|
|
$resp = $this->get($this->endpoint, ['Authorization' => 'Token abc123']);
|
|
$resp->assertStatus(401);
|
|
$resp->assertJson($this->errorResponse('An authorization token was found on the request but the format appeared incorrect', 401));
|
|
}
|
|
|
|
public function test_token_with_non_existing_id_throws_error()
|
|
{
|
|
$resp = $this->get($this->endpoint, ['Authorization' => 'Token abc:123']);
|
|
$resp->assertStatus(401);
|
|
$resp->assertJson($this->errorResponse('No matching API token was found for the provided authorization token', 401));
|
|
}
|
|
|
|
public function test_token_with_bad_secret_value_throws_error()
|
|
{
|
|
$resp = $this->get($this->endpoint, ['Authorization' => "Token {$this->apiTokenId}:123"]);
|
|
$resp->assertStatus(401);
|
|
$resp->assertJson($this->errorResponse('The secret provided for the given used API token is incorrect', 401));
|
|
}
|
|
|
|
public function test_api_access_permission_required_to_access_api()
|
|
{
|
|
$resp = $this->get($this->endpoint, $this->apiAuthHeader());
|
|
$resp->assertStatus(200);
|
|
auth()->logout();
|
|
|
|
$accessApiPermission = RolePermission::getByName('access-api');
|
|
$editorRole = $this->users->editor()->roles()->first();
|
|
$editorRole->detachPermission($accessApiPermission);
|
|
|
|
$resp = $this->get($this->endpoint, $this->apiAuthHeader());
|
|
$resp->assertStatus(403);
|
|
$resp->assertJson($this->errorResponse('The owner of the used API token does not have permission to make API calls', 403));
|
|
}
|
|
|
|
public function test_api_access_permission_required_to_access_api_with_session_auth()
|
|
{
|
|
$editor = $this->users->editor();
|
|
$this->actingAs($editor, 'standard');
|
|
$this->startSession();
|
|
|
|
$resp = $this->get($this->endpoint);
|
|
$resp->assertStatus(200);
|
|
auth('standard')->logout();
|
|
|
|
$accessApiPermission = RolePermission::getByName('access-api');
|
|
$editorRole = $this->users->editor()->roles()->first();
|
|
$editorRole->detachPermission($accessApiPermission);
|
|
|
|
$editor = User::query()->where('id', '=', $editor->id)->first();
|
|
|
|
$this->actingAs($editor, 'standard');
|
|
$resp = $this->get($this->endpoint);
|
|
$resp->assertStatus(403);
|
|
$resp->assertJson($this->errorResponse('The owner of the used API token does not have permission to make API calls', 403));
|
|
}
|
|
|
|
public function test_access_prevented_for_guest_users_with_api_permission_while_public_access_disabled()
|
|
{
|
|
$this->disableCookieEncryption();
|
|
$publicRole = Role::getSystemRole('public');
|
|
$accessApiPermission = RolePermission::getByName('access-api');
|
|
$publicRole->attachPermission($accessApiPermission);
|
|
|
|
$this->withCookie('bookstack_session', 'abc123');
|
|
|
|
// Test API access when not public
|
|
setting()->put('app-public', false);
|
|
$resp = $this->get($this->endpoint);
|
|
$resp->assertStatus(403);
|
|
|
|
// Test API access when public
|
|
setting()->put('app-public', true);
|
|
$resp = $this->get($this->endpoint);
|
|
$resp->assertStatus(200);
|
|
}
|
|
|
|
public function test_only_get_requests_are_supported_with_session_auth()
|
|
{
|
|
$user = $this->users->admin();
|
|
$this->actingAs($user, 'standard');
|
|
$this->startSession();
|
|
|
|
$uriByMethods = [
|
|
'POST' => '/books',
|
|
'PUT' => '/books/1',
|
|
'DELETE' => '/books/1',
|
|
'HEAD' => '/books',
|
|
];
|
|
|
|
foreach ($uriByMethods as $method => $uri) {
|
|
$resp = $this->withCredentials()->json($method, "/api{$uri}");
|
|
$resp->assertStatus(403);
|
|
if ($method !== 'HEAD') {
|
|
$resp->assertJson($this->errorResponse('Only GET requests are allowed when using the API with cookie-based authentication', 403));
|
|
}
|
|
}
|
|
}
|
|
|
|
public function test_token_expiry_checked()
|
|
{
|
|
$editor = $this->users->editor();
|
|
$token = $editor->apiTokens()->first();
|
|
|
|
$resp = $this->get($this->endpoint, $this->apiAuthHeader());
|
|
$resp->assertStatus(200);
|
|
auth()->logout();
|
|
|
|
$token->expires_at = Carbon::now()->subDay()->format('Y-m-d');
|
|
$token->save();
|
|
|
|
$resp = $this->get($this->endpoint, $this->apiAuthHeader());
|
|
$resp->assertJson($this->errorResponse('The authorization token used has expired', 403));
|
|
}
|
|
|
|
public function test_email_confirmation_checked_using_api_auth()
|
|
{
|
|
$editor = $this->users->editor();
|
|
$editor->email_confirmed = false;
|
|
$editor->save();
|
|
|
|
// Set settings and get user instance
|
|
$this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
|
|
|
|
$resp = $this->get($this->endpoint, $this->apiAuthHeader());
|
|
$resp->assertStatus(401);
|
|
$resp->assertJson($this->errorResponse('The email address for the account in use needs to be confirmed', 401));
|
|
}
|
|
|
|
public function test_rate_limit_headers_active_on_requests()
|
|
{
|
|
$resp = $this->actingAsApiEditor()->get($this->endpoint);
|
|
$resp->assertHeader('x-ratelimit-limit', 180);
|
|
$resp->assertHeader('x-ratelimit-remaining', 179);
|
|
$resp = $this->actingAsApiEditor()->get($this->endpoint);
|
|
$resp->assertHeader('x-ratelimit-remaining', 178);
|
|
}
|
|
|
|
public function test_rate_limit_hit_gives_json_error()
|
|
{
|
|
config()->set(['api.requests_per_minute' => 1]);
|
|
$resp = $this->actingAsApiEditor()->get($this->endpoint);
|
|
$resp->assertStatus(200);
|
|
|
|
$resp = $this->actingAsApiEditor()->get($this->endpoint);
|
|
$resp->assertStatus(429);
|
|
$resp->assertHeader('x-ratelimit-remaining', 0);
|
|
$resp->assertHeader('retry-after');
|
|
$resp->assertJson([
|
|
'error' => [
|
|
'code' => 429,
|
|
],
|
|
]);
|
|
}
|
|
}
|