AuthMatrixTest.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Integration\Auth;
  4. use App\Domain\Auth\Role;
  5. use App\Domain\Auth\TokenKind;
  6. use App\Tests\Integration\Support\AppTestCase;
  7. /**
  8. * Covers the auth matrix from M03 task §7. Each test corresponds to one
  9. * row of the table:
  10. *
  11. * | Token kind | X-Acting-User-Id | User exists | Required | Expected |
  12. * | ------------ | ---------------- | ----------- | -------- | -------- |
  13. * | none | - | - | viewer | 401 |
  14. * | bad token | - | - | viewer | 401 |
  15. * | reporter | - | - | viewer | 401 |
  16. * | admin/viewer | - | - | viewer | 200 |
  17. * | admin/viewer | - | - | operator | 403 |
  18. * | admin/admin | - | - | admin | 200 |
  19. * | service | no | - | viewer | 400 |
  20. * | service | yes | no | viewer | 403 |
  21. * | service | yes (viewer) | yes | viewer | 200 |
  22. * | service | yes (viewer) | yes | operator | 403 |
  23. * | service | yes (admin) | yes | admin | 200 |
  24. *
  25. * /admin/me requires Viewer; /auth/users/upsert-local requires Admin.
  26. * Together they exercise both required-role rungs.
  27. */
  28. final class AuthMatrixTest extends AppTestCase
  29. {
  30. public function testNoTokenReturns401(): void
  31. {
  32. $response = $this->request('GET', '/api/v1/admin/me');
  33. self::assertSame(401, $response->getStatusCode());
  34. self::assertSame('unauthorized', $this->decode($response)['error']);
  35. }
  36. public function testBadlyFormattedTokenReturns401(): void
  37. {
  38. $response = $this->request('GET', '/api/v1/admin/me', [
  39. 'Authorization' => 'Bearer not_a_real_token',
  40. ]);
  41. self::assertSame(401, $response->getStatusCode());
  42. }
  43. public function testWellFormedButUnknownTokenReturns401(): void
  44. {
  45. // Right format, never persisted → no DB hit on lookup.
  46. $raw = 'irdb_adm_ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  47. $response = $this->request('GET', '/api/v1/admin/me', [
  48. 'Authorization' => 'Bearer ' . $raw,
  49. ]);
  50. self::assertSame(401, $response->getStatusCode());
  51. }
  52. public function testReporterTokenOnAdminRouteReturns401(): void
  53. {
  54. $reporterId = $this->createReporter();
  55. $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);
  56. $response = $this->request('GET', '/api/v1/admin/me', [
  57. 'Authorization' => 'Bearer ' . $token,
  58. ]);
  59. self::assertSame(401, $response->getStatusCode());
  60. }
  61. public function testAdminTokenWithViewerRoleSatisfiesViewerRoute(): void
  62. {
  63. $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
  64. $response = $this->request('GET', '/api/v1/admin/me', [
  65. 'Authorization' => 'Bearer ' . $token,
  66. ]);
  67. self::assertSame(200, $response->getStatusCode());
  68. $body = $this->decode($response);
  69. self::assertSame('viewer', $body['role']);
  70. self::assertSame('admin-token', $body['source']);
  71. self::assertNull($body['user_id']);
  72. }
  73. public function testAdminTokenWithAdminRoleSatisfiesAdminRoute(): void
  74. {
  75. $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
  76. // /admin/me requires Viewer; an Admin role satisfies it.
  77. $response = $this->request('GET', '/api/v1/admin/me', [
  78. 'Authorization' => 'Bearer ' . $token,
  79. ]);
  80. self::assertSame(200, $response->getStatusCode());
  81. self::assertSame('admin', $this->decode($response)['role']);
  82. }
  83. public function testServiceTokenWithoutImpersonationHeaderReturns400(): void
  84. {
  85. $token = $this->createToken(TokenKind::Service);
  86. $response = $this->request('GET', '/api/v1/admin/me', [
  87. 'Authorization' => 'Bearer ' . $token,
  88. ]);
  89. self::assertSame(400, $response->getStatusCode());
  90. self::assertSame('missing X-Acting-User-Id', $this->decode($response)['error']);
  91. }
  92. public function testServiceTokenWithUnknownUserReturns403(): void
  93. {
  94. $token = $this->createToken(TokenKind::Service);
  95. $response = $this->request('GET', '/api/v1/admin/me', [
  96. 'Authorization' => 'Bearer ' . $token,
  97. 'X-Acting-User-Id' => '99999',
  98. ]);
  99. self::assertSame(403, $response->getStatusCode());
  100. }
  101. public function testServiceTokenWithMalformedImpersonationHeaderReturns400(): void
  102. {
  103. $token = $this->createToken(TokenKind::Service);
  104. $response = $this->request('GET', '/api/v1/admin/me', [
  105. 'Authorization' => 'Bearer ' . $token,
  106. 'X-Acting-User-Id' => 'not-an-int',
  107. ]);
  108. self::assertSame(400, $response->getStatusCode());
  109. }
  110. public function testServiceTokenImpersonatingViewerReachesViewerRoute(): void
  111. {
  112. $token = $this->createToken(TokenKind::Service);
  113. $userId = $this->createUser(Role::Viewer, isLocal: false, subject: 'sub-viewer');
  114. $response = $this->request('GET', '/api/v1/admin/me', [
  115. 'Authorization' => 'Bearer ' . $token,
  116. 'X-Acting-User-Id' => (string) $userId,
  117. ]);
  118. self::assertSame(200, $response->getStatusCode());
  119. $body = $this->decode($response);
  120. self::assertSame($userId, $body['user_id']);
  121. self::assertSame('viewer', $body['role']);
  122. self::assertSame('oidc', $body['source']);
  123. self::assertFalse($body['is_local']);
  124. }
  125. public function testServiceTokenImpersonatingAdminReachesAdminRoute(): void
  126. {
  127. $token = $this->createToken(TokenKind::Service);
  128. $userId = $this->createUser(Role::Admin, isLocal: true);
  129. $response = $this->request('GET', '/api/v1/admin/me', [
  130. 'Authorization' => 'Bearer ' . $token,
  131. 'X-Acting-User-Id' => (string) $userId,
  132. ]);
  133. self::assertSame(200, $response->getStatusCode());
  134. $body = $this->decode($response);
  135. self::assertSame('admin', $body['role']);
  136. self::assertTrue($body['is_local']);
  137. self::assertSame('local', $body['source']);
  138. }
  139. public function testActingUserHeaderIgnoredForAdminToken(): void
  140. {
  141. // Per SPEC §8: X-Acting-User-Id is *only* trusted with the service token.
  142. // For an admin token it must not be 400'd, just ignored.
  143. $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
  144. $response = $this->request('GET', '/api/v1/admin/me', [
  145. 'Authorization' => 'Bearer ' . $token,
  146. 'X-Acting-User-Id' => '99999',
  147. ]);
  148. self::assertSame(200, $response->getStatusCode());
  149. self::assertSame('admin-token', $this->decode($response)['source']);
  150. }
  151. }