1
0

BootstrapAdminTest.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. <?php
  2. /*
  3. * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
  4. * SPDX-License-Identifier: Apache-2.0
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * See the LICENSE file in the project root for the full license text.
  9. */
  10. declare(strict_types=1);
  11. namespace App\Tests\Auth;
  12. use App\Auth\BootstrapAdmin;
  13. use App\Tests\TestCase;
  14. /**
  15. * Lock-in tests for R01-N03 — the OIDC bootstrap matcher. The matcher
  16. * is the only thing standing between an unconfigured deploy and the
  17. * historical "first user wins" land-grab.
  18. */
  19. final class BootstrapAdminTest extends TestCase
  20. {
  21. /** @var array<string, string|false> */
  22. private array $envBackup = [];
  23. /** @var string[] */
  24. private array $envKeys = [
  25. 'BOOTSTRAP_ADMIN_OID',
  26. 'BOOTSTRAP_ADMIN_EMAIL',
  27. ];
  28. protected function setUp(): void
  29. {
  30. parent::setUp();
  31. foreach ($this->envKeys as $k) {
  32. $this->envBackup[$k] = getenv($k);
  33. putenv($k);
  34. }
  35. }
  36. protected function tearDown(): void
  37. {
  38. foreach ($this->envKeys as $k) {
  39. $prev = $this->envBackup[$k] ?? false;
  40. if ($prev === false) {
  41. putenv($k);
  42. } else {
  43. putenv("{$k}={$prev}");
  44. }
  45. }
  46. parent::tearDown();
  47. }
  48. public function testNotConfiguredByDefault(): void
  49. {
  50. self::assertFalse(BootstrapAdmin::isConfigured());
  51. self::assertFalse(
  52. BootstrapAdmin::matches('any-oid', 'any@example.com'),
  53. 'unconfigured matcher must never promote anyone'
  54. );
  55. }
  56. public function testMatchesByOid(): void
  57. {
  58. putenv('BOOTSTRAP_ADMIN_OID=00000000-0000-0000-0000-000000000001');
  59. self::assertTrue(BootstrapAdmin::isConfigured());
  60. self::assertTrue(BootstrapAdmin::matches(
  61. '00000000-0000-0000-0000-000000000001',
  62. 'unrelated@example.com'
  63. ));
  64. self::assertFalse(BootstrapAdmin::matches(
  65. '00000000-0000-0000-0000-000000000002',
  66. 'unrelated@example.com'
  67. ));
  68. }
  69. public function testMatchesByEmail(): void
  70. {
  71. putenv('BOOTSTRAP_ADMIN_EMAIL=admin@example.com');
  72. self::assertTrue(BootstrapAdmin::isConfigured());
  73. self::assertTrue(BootstrapAdmin::matches(
  74. 'oid-does-not-matter',
  75. 'admin@example.com'
  76. ));
  77. self::assertFalse(BootstrapAdmin::matches(
  78. 'oid-does-not-matter',
  79. 'attacker@example.com'
  80. ));
  81. }
  82. public function testEitherChannelMatchesWhenBothConfigured(): void
  83. {
  84. putenv('BOOTSTRAP_ADMIN_OID=oid-1');
  85. putenv('BOOTSTRAP_ADMIN_EMAIL=admin@example.com');
  86. self::assertTrue(BootstrapAdmin::matches('oid-1', 'someone-else@x'));
  87. self::assertTrue(BootstrapAdmin::matches('oid-99', 'admin@example.com'));
  88. self::assertFalse(BootstrapAdmin::matches('oid-99', 'someone-else@x'));
  89. }
  90. public function testMatchIsCaseInsensitiveAndTrimmed(): void
  91. {
  92. putenv('BOOTSTRAP_ADMIN_EMAIL=Admin@Example.COM');
  93. self::assertTrue(BootstrapAdmin::matches('', ' admin@example.com '));
  94. putenv('BOOTSTRAP_ADMIN_EMAIL');
  95. putenv('BOOTSTRAP_ADMIN_OID=ABCDEF-1234');
  96. self::assertTrue(BootstrapAdmin::matches(' abcdef-1234 ', ''));
  97. }
  98. public function testBlankIncomingFieldsNeverMatch(): void
  99. {
  100. // Blank email in env shouldn't match a blank claim — config is
  101. // OID-only here, so an empty email coming back from Entra must
  102. // not opportunistically match the absent BOOTSTRAP_ADMIN_EMAIL.
  103. putenv('BOOTSTRAP_ADMIN_OID=oid-1');
  104. self::assertFalse(BootstrapAdmin::matches('', ''));
  105. self::assertFalse(BootstrapAdmin::matches('', 'bob@x'));
  106. }
  107. public function testWhitespaceOnlyEnvTreatedAsUnset(): void
  108. {
  109. putenv('BOOTSTRAP_ADMIN_OID= ');
  110. putenv('BOOTSTRAP_ADMIN_EMAIL= ');
  111. self::assertFalse(BootstrapAdmin::isConfigured());
  112. self::assertFalse(BootstrapAdmin::matches('anything', 'anything@x'));
  113. }
  114. public function testOidValueOverEmailIsPreferred(): void
  115. {
  116. // Both are set; if the user's oid matches we promote regardless of
  117. // a (presumably stale) BOOTSTRAP_ADMIN_EMAIL. Sanity check that
  118. // either-channel-wins still holds.
  119. putenv('BOOTSTRAP_ADMIN_OID=primary-oid');
  120. putenv('BOOTSTRAP_ADMIN_EMAIL=admin@example.com');
  121. self::assertTrue(BootstrapAdmin::matches('primary-oid', 'noone@x'));
  122. }
  123. }