mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
116 lines
4.6 KiB
PHP
116 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Authentication;
|
|
|
|
use App\Models\User;
|
|
use Laravel\Passport\Passport;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Confirms that Personal Access Tokens (or any Passport-authenticated API
|
|
* request) can't be used to call the API when the token's owner has been
|
|
* deactivated. The web session path has always been protected by
|
|
* CheckUserIsActivated in the `web` middleware group; the `api` group
|
|
* was missing it, so a deactivated user's already-issued token kept
|
|
* full permission-level access to the API - defeating "Activated" as an
|
|
* offboarding control. A deactivated account with users.edit could
|
|
* additionally re-activate itself via PATCH /api/v1/users/{own_id}.
|
|
* This file pins the fix (CheckUserIsActivated added to the api group).
|
|
*/
|
|
class CheckUserIsActivatedApiTest extends TestCase
|
|
{
|
|
public function test_activated_user_can_use_api(): void
|
|
{
|
|
Passport::actingAs(User::factory()->create(['activated' => 1]));
|
|
|
|
$this->getJson(route('api.users.me'))->assertOk();
|
|
}
|
|
|
|
public function test_deactivated_user_api_token_is_refused(): void
|
|
{
|
|
Passport::actingAs(User::factory()->create(['activated' => 0]));
|
|
|
|
$this->getJson(route('api.users.me'))->assertUnauthorized();
|
|
}
|
|
|
|
public function test_deactivated_user_cannot_write_via_api(): void
|
|
{
|
|
Passport::actingAs(User::factory()->superuser()->create(['activated' => 0]));
|
|
|
|
$this->postJson(route('api.users.store'), [
|
|
'first_name' => 'Should',
|
|
'last_name' => 'Not Persist',
|
|
'username' => 'shouldnotpersist',
|
|
'password' => 'This-Is-A-Long-Password-123!',
|
|
'password_confirmation' => 'This-Is-A-Long-Password-123!',
|
|
])->assertUnauthorized();
|
|
|
|
$this->assertDatabaseMissing('users', ['username' => 'shouldnotpersist']);
|
|
}
|
|
|
|
/**
|
|
* Escalation regression pin from the researcher's report: a
|
|
* deactivated account with users.edit used to be able to
|
|
* PATCH /api/v1/users/{own_id} {"activated": 1} and re-enable
|
|
* itself, permanently defeating the deactivation. The middleware
|
|
* now refuses the request before it reaches the controller.
|
|
*/
|
|
public function test_deactivated_admin_cannot_self_reactivate_via_api(): void
|
|
{
|
|
$user = User::factory()->admin()->create(['activated' => 0]);
|
|
|
|
Passport::actingAs($user);
|
|
|
|
$this->patchJson(route('api.users.update', $user), [
|
|
'activated' => 1,
|
|
])->assertUnauthorized();
|
|
|
|
$this->assertSame(0, (int) $user->fresh()->activated, 'Account must remain deactivated');
|
|
}
|
|
|
|
/**
|
|
* Response body deliberately does NOT distinguish "known token,
|
|
* deactivated account" from a generic 401. Locks in the generic
|
|
* message so a later change doesn't leak the specific reason (which
|
|
* would help an attacker validate that a token they hold is
|
|
* otherwise correct and just points at a disabled account).
|
|
*/
|
|
public function test_deactivated_response_is_generic_unauthorized(): void
|
|
{
|
|
Passport::actingAs(User::factory()->create(['activated' => 0]));
|
|
|
|
$response = $this->getJson(route('api.users.me'))->assertUnauthorized();
|
|
|
|
$body = $response->json();
|
|
$this->assertSame('error', $body['status'] ?? null);
|
|
// The generic 'general.unauthorized' translation; must NOT
|
|
// mention "activated" / "disabled" / "inactive".
|
|
$this->assertStringNotContainsStringIgnoringCase('activat', (string) ($body['messages'] ?? ''));
|
|
$this->assertStringNotContainsStringIgnoringCase('disabled', (string) ($body['messages'] ?? ''));
|
|
}
|
|
|
|
/**
|
|
* Rollbar regression pin: a Python client hitting /api/v1/hardware
|
|
* with a bearer token but without an Accept: application/json
|
|
* header used to fatal with BadMethodCallException. The middleware
|
|
* fell past its expectsJson() check (Python's requests library
|
|
* doesn't set Accept by default) and called Auth::logout() on
|
|
* Passport's TokenGuard, which doesn't define that method.
|
|
*
|
|
* A bearer token is now a sufficient signal that the caller is on
|
|
* the API surface, so the middleware returns a JSON 401 instead
|
|
* of trying to terminate a session that doesn't exist.
|
|
*/
|
|
public function test_bearer_authenticated_request_without_accept_header_returns_json_not_500(): void
|
|
{
|
|
Passport::actingAs(User::factory()->create(['activated' => 0]));
|
|
|
|
$response = $this
|
|
->withHeader('Authorization', 'Bearer fake-token-value')
|
|
->get(route('api.assets.index'));
|
|
|
|
$response->assertUnauthorized();
|
|
$this->assertSame('error', $response->json('status'));
|
|
}
|
|
}
|