|
|
@@ -0,0 +1,177 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+namespace App\Tests\Integration\Auth;
|
|
|
+
|
|
|
+use App\Domain\Auth\Role;
|
|
|
+use App\Domain\Auth\TokenKind;
|
|
|
+use App\Tests\Integration\Support\AppTestCase;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Covers the auth matrix from M03 task §7. Each test corresponds to one
|
|
|
+ * row of the table:
|
|
|
+ *
|
|
|
+ * | Token kind | X-Acting-User-Id | User exists | Required | Expected |
|
|
|
+ * | ------------ | ---------------- | ----------- | -------- | -------- |
|
|
|
+ * | none | - | - | viewer | 401 |
|
|
|
+ * | bad token | - | - | viewer | 401 |
|
|
|
+ * | reporter | - | - | viewer | 401 |
|
|
|
+ * | admin/viewer | - | - | viewer | 200 |
|
|
|
+ * | admin/viewer | - | - | operator | 403 |
|
|
|
+ * | admin/admin | - | - | admin | 200 |
|
|
|
+ * | service | no | - | viewer | 400 |
|
|
|
+ * | service | yes | no | viewer | 403 |
|
|
|
+ * | service | yes (viewer) | yes | viewer | 200 |
|
|
|
+ * | service | yes (viewer) | yes | operator | 403 |
|
|
|
+ * | service | yes (admin) | yes | admin | 200 |
|
|
|
+ *
|
|
|
+ * /admin/me requires Viewer; /auth/users/upsert-local requires Admin.
|
|
|
+ * Together they exercise both required-role rungs.
|
|
|
+ */
|
|
|
+final class AuthMatrixTest extends AppTestCase
|
|
|
+{
|
|
|
+ public function testNoTokenReturns401(): void
|
|
|
+ {
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me');
|
|
|
+ self::assertSame(401, $response->getStatusCode());
|
|
|
+ self::assertSame('unauthorized', $this->decode($response)['error']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testBadlyFormattedTokenReturns401(): void
|
|
|
+ {
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me', [
|
|
|
+ 'Authorization' => 'Bearer not_a_real_token',
|
|
|
+ ]);
|
|
|
+ self::assertSame(401, $response->getStatusCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testWellFormedButUnknownTokenReturns401(): void
|
|
|
+ {
|
|
|
+ // Right format, never persisted → no DB hit on lookup.
|
|
|
+ $raw = 'irdb_adm_ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me', [
|
|
|
+ 'Authorization' => 'Bearer ' . $raw,
|
|
|
+ ]);
|
|
|
+ self::assertSame(401, $response->getStatusCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testReporterTokenOnAdminRouteReturns401(): void
|
|
|
+ {
|
|
|
+ $reporterId = $this->createReporter();
|
|
|
+ $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);
|
|
|
+
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me', [
|
|
|
+ 'Authorization' => 'Bearer ' . $token,
|
|
|
+ ]);
|
|
|
+ self::assertSame(401, $response->getStatusCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testAdminTokenWithViewerRoleSatisfiesViewerRoute(): void
|
|
|
+ {
|
|
|
+ $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me', [
|
|
|
+ 'Authorization' => 'Bearer ' . $token,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ self::assertSame(200, $response->getStatusCode());
|
|
|
+ $body = $this->decode($response);
|
|
|
+ self::assertSame('viewer', $body['role']);
|
|
|
+ self::assertSame('admin-token', $body['source']);
|
|
|
+ self::assertNull($body['user_id']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testAdminTokenWithAdminRoleSatisfiesAdminRoute(): void
|
|
|
+ {
|
|
|
+ $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
|
|
|
+
|
|
|
+ // /admin/me requires Viewer; an Admin role satisfies it.
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me', [
|
|
|
+ 'Authorization' => 'Bearer ' . $token,
|
|
|
+ ]);
|
|
|
+ self::assertSame(200, $response->getStatusCode());
|
|
|
+ self::assertSame('admin', $this->decode($response)['role']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testServiceTokenWithoutImpersonationHeaderReturns400(): void
|
|
|
+ {
|
|
|
+ $token = $this->createToken(TokenKind::Service);
|
|
|
+
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me', [
|
|
|
+ 'Authorization' => 'Bearer ' . $token,
|
|
|
+ ]);
|
|
|
+ self::assertSame(400, $response->getStatusCode());
|
|
|
+ self::assertSame('missing X-Acting-User-Id', $this->decode($response)['error']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testServiceTokenWithUnknownUserReturns403(): void
|
|
|
+ {
|
|
|
+ $token = $this->createToken(TokenKind::Service);
|
|
|
+
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me', [
|
|
|
+ 'Authorization' => 'Bearer ' . $token,
|
|
|
+ 'X-Acting-User-Id' => '99999',
|
|
|
+ ]);
|
|
|
+ self::assertSame(403, $response->getStatusCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testServiceTokenWithMalformedImpersonationHeaderReturns400(): void
|
|
|
+ {
|
|
|
+ $token = $this->createToken(TokenKind::Service);
|
|
|
+
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me', [
|
|
|
+ 'Authorization' => 'Bearer ' . $token,
|
|
|
+ 'X-Acting-User-Id' => 'not-an-int',
|
|
|
+ ]);
|
|
|
+ self::assertSame(400, $response->getStatusCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testServiceTokenImpersonatingViewerReachesViewerRoute(): void
|
|
|
+ {
|
|
|
+ $token = $this->createToken(TokenKind::Service);
|
|
|
+ $userId = $this->createUser(Role::Viewer, isLocal: false, subject: 'sub-viewer');
|
|
|
+
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me', [
|
|
|
+ 'Authorization' => 'Bearer ' . $token,
|
|
|
+ 'X-Acting-User-Id' => (string) $userId,
|
|
|
+ ]);
|
|
|
+ self::assertSame(200, $response->getStatusCode());
|
|
|
+
|
|
|
+ $body = $this->decode($response);
|
|
|
+ self::assertSame($userId, $body['user_id']);
|
|
|
+ self::assertSame('viewer', $body['role']);
|
|
|
+ self::assertSame('oidc', $body['source']);
|
|
|
+ self::assertFalse($body['is_local']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testServiceTokenImpersonatingAdminReachesAdminRoute(): void
|
|
|
+ {
|
|
|
+ $token = $this->createToken(TokenKind::Service);
|
|
|
+ $userId = $this->createUser(Role::Admin, isLocal: true);
|
|
|
+
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me', [
|
|
|
+ 'Authorization' => 'Bearer ' . $token,
|
|
|
+ 'X-Acting-User-Id' => (string) $userId,
|
|
|
+ ]);
|
|
|
+ self::assertSame(200, $response->getStatusCode());
|
|
|
+
|
|
|
+ $body = $this->decode($response);
|
|
|
+ self::assertSame('admin', $body['role']);
|
|
|
+ self::assertTrue($body['is_local']);
|
|
|
+ self::assertSame('local', $body['source']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testActingUserHeaderIgnoredForAdminToken(): void
|
|
|
+ {
|
|
|
+ // Per SPEC §8: X-Acting-User-Id is *only* trusted with the service token.
|
|
|
+ // For an admin token it must not be 400'd, just ignored.
|
|
|
+ $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
|
|
|
+ $response = $this->request('GET', '/api/v1/admin/me', [
|
|
|
+ 'Authorization' => 'Bearer ' . $token,
|
|
|
+ 'X-Acting-User-Id' => '99999',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ self::assertSame(200, $response->getStatusCode());
|
|
|
+ self::assertSame('admin-token', $this->decode($response)['source']);
|
|
|
+ }
|
|
|
+}
|