seed(RolePermissionSeeder::class); } protected function person(string $role): User { $user = User::factory()->create(); $user->syncRoles([$role]); return $user; } /** * @param array $abilities */ protected function tokenFor(User $user, array $abilities, ?\DateTimeInterface $expires = null): string { [, $plain] = ApiToken::issue($user, 'CI', $abilities, $expires ?? now()->addDay()); $this->app['auth']->forgetGuards(); $this->withHeaders(['Authorization' => 'Bearer '.$plain, 'Accept' => 'application/json']); return $plain; } #[Test] public function a_person_makes_a_token_and_sees_it_only_once(): void { $this->withHeaders($this->apiHeadersFor($this->person('editor'))); $response = $this->postJson('/api/v1/tokens', [ 'name' => 'Despliegue', 'abilities' => ['technologies.*', 'pages.view_any'], ]) ->assertCreated() ->assertJsonPath('data.status', 'active') ->assertJsonPath('data.abilities', ['technologies.*', 'pages.view_any']); $plain = $response->json('token'); $this->assertStringStartsWith(ApiToken::PREFIX, $plain); $this->assertSame(ApiToken::hash($plain), ApiToken::query()->sole()->token_hash); $this->getJson('/api/v1/tokens') ->assertJsonCount(1, 'data') ->assertJsonMissingPath('data.0.token'); } #[Test] public function a_token_cannot_be_given_what_its_maker_lacks(): void { $this->withHeaders($this->apiHeadersFor($this->person('writer'))); $this->postJson('/api/v1/tokens', ['name' => 'X', 'abilities' => ['users.update']]) ->assertUnprocessable() ->assertJsonValidationErrors('abilities.0'); $this->postJson('/api/v1/tokens', ['name' => 'X', 'abilities' => ['nothing.*']]) ->assertUnprocessable(); $this->postJson('/api/v1/tokens', [ 'name' => 'X', 'abilities' => ['pages.view_any'], 'expires_at' => now()->addYears(2)->toIso8601String(), ])->assertUnprocessable()->assertJsonValidationErrors('expires_at'); } #[Test] public function a_token_acts_as_its_maker_within_its_abilities(): void { Http::fake(); $editor = $this->person('editor'); $this->tokenFor($editor, ['technologies.*']); $this->postJson('/api/v1/technologies', ['name' => 'Vue', 'slug' => 'vue'])->assertCreated(); $this->getJson('/api/v1/pages')->assertForbidden(); $permissions = $this->getJson('/api/v1/me')->assertOk()->json('data.permissions'); $this->assertContains('technologies.create', $permissions); $this->assertNotContains('pages.view_any', $permissions); // Never sent to the identity server to be checked. Http::assertNothingSent(); } #[Test] public function a_super_admin_token_is_still_limited(): void { $this->tokenFor($this->person('super_admin'), ['pages.view_any', 'pages.view']); $page = Page::factory()->create(); $this->getJson('/api/v1/pages')->assertOk(); $this->getJson("/api/v1/pages/{$page->id}")->assertOk(); $this->deleteJson("/api/v1/pages/{$page->id}")->assertForbidden(); $this->postJson('/api/v1/technologies', ['name' => 'Vue', 'slug' => 'vue'])->assertForbidden(); } #[Test] public function losing_a_role_narrows_the_token_at_once(): void { $editor = $this->person('editor'); $this->tokenFor($editor, ['technologies.*']); $editor->syncRoles(['viewer']); $this->postJson('/api/v1/technologies', ['name' => 'Vue', 'slug' => 'vue'])->assertForbidden(); $this->assertSame(0, Technology::query()->count()); } #[Test] public function expired_revoked_or_deactivated_tokens_do_not_work(): void { $editor = $this->person('editor'); $this->tokenFor($editor, ['pages.*'], now()->subMinute()); $this->getJson('/api/v1/pages')->assertUnauthorized(); $plain = $this->tokenFor($editor, ['pages.*']); ApiToken::query()->where('token_hash', ApiToken::hash($plain))->sole()->revoke(); $this->getJson('/api/v1/pages')->assertUnauthorized(); $this->tokenFor($editor, ['pages.*']); $editor->update(['is_active' => false]); $this->getJson('/api/v1/pages')->assertUnauthorized(); } #[Test] public function a_token_cannot_make_or_revoke_tokens(): void { $editor = $this->person('editor'); $this->tokenFor($editor, ['pages.*']); $this->postJson('/api/v1/tokens', ['name' => 'Forever', 'abilities' => ['pages.*']])->assertForbidden(); $this->getJson('/api/v1/tokens')->assertForbidden(); } #[Test] public function a_revoked_token_stays_listed(): void { $editor = $this->person('editor'); [$token] = ApiToken::issue($editor, 'Old', ['pages.*'], now()->addDay()); $this->withHeaders($this->apiHeadersFor($editor)); $this->deleteJson("/api/v1/tokens/{$token->id}")->assertNoContent(); $this->getJson('/api/v1/tokens')->assertJsonPath('data.0.status', 'revoked'); // Someone else's is not theirs to revoke. [$other] = ApiToken::issue($this->person('editor'), 'Theirs', ['pages.*'], now()->addDay()); $this->deleteJson("/api/v1/tokens/{$other->id}")->assertForbidden(); } }