3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00
Files
snipe-it/tests/Feature/Authentication/CheckUserIsActivatedApiTest.php
2026-07-30 17:10:50 +01:00

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'));
}
}