diff --git a/tests/acceptance/TestHelpers/KeycloakHelper.php b/tests/acceptance/TestHelpers/KeycloakHelper.php index 3cdec0b5d47..55e8fd225df 100644 --- a/tests/acceptance/TestHelpers/KeycloakHelper.php +++ b/tests/acceptance/TestHelpers/KeycloakHelper.php @@ -345,6 +345,53 @@ public static function setAccessTokenForKeycloakOcisUser(array $user): array { return json_decode($tokenResponse->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); } + /** + * @return array + * @throws GuzzleException + * @throws JsonException + * @throws Exception + */ + public static function getRealm(): array { + $url = self::getKeycloakUrl() . '/admin/realms/oCIS'; + $response = HttpRequestHelper::get( + $url, + null, + null, + [ + 'Authorization' => 'Bearer ' . self::getAdminAccessToken(), + ], + ); + if ($response->getStatusCode() !== 200) { + throw new Exception("Failed to get realm roles."); + } + return json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); + } + + /** + * @param string $key + * @param string $value + * + * @return ResponseInterface + * @throws GuzzleException + * @throws JsonException + */ + public static function updateRealmAttribute(string $key, string $value): ResponseInterface { + $realm = self::getRealm(); + $attributes = $realm['attributes'] ?? []; + $attributes[$key] = $value; + $url = self::getKeycloakUrl() . '/admin/realms/oCIS'; + return HttpRequestHelper::put( + $url, + null, + null, + [ + 'Authorization' => 'Bearer ' . self::getAdminAccessToken(), + 'Content-Type' => 'application/json', + ], + json_encode(['attributes' => $attributes], JSON_THROW_ON_ERROR), + ); + } + /** * @param string $uuid * diff --git a/tests/acceptance/bootstrap/OcisConfigContext.php b/tests/acceptance/bootstrap/OcisConfigContext.php index 7308a2f8bf9..11276f6c977 100644 --- a/tests/acceptance/bootstrap/OcisConfigContext.php +++ b/tests/acceptance/bootstrap/OcisConfigContext.php @@ -23,6 +23,9 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Exception\GuzzleException; +use Psr\Http\Message\ResponseInterface; +use TestHelpers\HttpRequestHelper; +use TestHelpers\KeycloakHelper; use TestHelpers\OcisConfigHelper; use TestHelpers\GraphHelper; use PHPUnit\Framework\Assert; @@ -110,11 +113,9 @@ public function theConfigHasBeenSetTo( $response = OcisConfigHelper::reConfigureOcis($envs); } - $resBody = $response->getBody()->getContents(); - Assert::assertEquals( - 200, - $response->getStatusCode(), - "Failed to set config $configVariable=$configValue. Response: $resBody", + $this->assertOcisRestarted( + $response, + "Failed to set config $configVariable=$configValue.", ); } @@ -346,11 +347,66 @@ public function rollback(): void { */ public function rollbackOcis(): void { $response = OcisConfigHelper::rollbackOcis(); - $resBody = $response->getBody()->getContents(); + $this->assertOcisRestarted( + $response, + "Failed to rollback ocis server. Check if oCIS is started with ociswrapper.", + ); + } + + /** + * Asserts that an oCIS restart triggered by ociswrapper succeeded. + * In vault mode the wrapper's admin health-check uses basic-auth which is + * disabled (OIDC role assignment only), so the wrapper always returns HTTP 500 + * even after a successful restart. When Keycloak/vault mode is detected we + * poll the unauthenticated proxy debug readyz endpoint instead of failing. + * + * @param ResponseInterface $response + * @param string $errorMessage + * + * @return void + */ + private function assertOcisRestarted(ResponseInterface $response, string $errorMessage): void { + $statusCode = $response->getStatusCode(); + if ($statusCode === 200) { + return; + } + if (KeycloakHelper::isTestingWithKeycloak()) { + $this->waitForOcisProxyReady(); + return; + } Assert::assertEquals( 200, - $response->getStatusCode(), - "Failed to rollback ocis server. Check if oCIS is started with ociswrapper. Response: $resBody", + $statusCode, + $errorMessage . " Response: " . $response->getBody()->getContents(), + ); + } + + /** + * Poll the unauthenticated proxy debug readyz endpoint until oCIS is ready. + * Used in vault/Keycloak mode where basic-auth health checks are unavailable. + * + * @param int $timeoutSeconds + * + * @return void + * @throws GuzzleException + */ + private function waitForOcisProxyReady(int $timeoutSeconds = 60): void { + $readyzUrl = 'http://localhost:9205/readyz'; + $deadline = time() + $timeoutSeconds; + while (time() < $deadline) { + try { + $response = HttpRequestHelper::get($readyzUrl); + if ($response->getStatusCode() === 200) { + return; + } + echo "oCIS not ready yet. Retrying in 1s...\n"; + } catch (\Exception $e) { + throw new Exception("oCIS not ready. Error: $e"); + } + sleep(1); + } + throw new \RuntimeException( + "Timed out after {$timeoutSeconds}s waiting for oCIS proxy readyz at {$readyzUrl}", ); } diff --git a/tests/acceptance/bootstrap/VaultContext.php b/tests/acceptance/bootstrap/VaultContext.php new file mode 100644 index 00000000000..854f705bc83 --- /dev/null +++ b/tests/acceptance/bootstrap/VaultContext.php @@ -0,0 +1,123 @@ + + * @copyright Copyright (c) 2026 Prajwol Amatya prajwol@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use GuzzleHttp\Exception\GuzzleException; +use JsonException; +use PHPUnit\Framework\Assert; +use TestHelpers\BehatHelper; +use TestHelpers\KeycloakHelper; + +require_once 'bootstrap.php'; + +/** + * Context for ocis vault specific steps + */ +class VaultContext implements Context { + private FeatureContext $featureContext; + private array $originalRealmAttributes = []; + + /** + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + * + * @throws Exception + */ + public function before(BeforeScenarioScope $scope): void { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = BehatHelper::getContext($scope, $environment, 'FeatureContext'); + } + + /** + * @Given the administrator has set the Keycloak realm attribute :key to :value + * + * @param string $key + * @param string $value + * + * @return void + * @throws GuzzleException + * @throws JsonException + */ + public function theAdministratorHasSetTheKeycloakRealmAttributeTo(string $key, string $value): void { + $realm = KeycloakHelper::getRealm(); + $this->originalRealmAttributes[$key] = $realm['attributes'][$key] ?? null; + $response = KeycloakHelper::updateRealmAttribute($key, $value); + Assert::assertEquals( + 204, + $response->getStatusCode(), + "Failed to update Keycloak realm attribute $key. Response: " . $response->getBody()->getContents(), + ); + } + + /** + * @AfterScenario @keycloak-config + * + * @return void + * @throws GuzzleException + * @throws JsonException + */ + public function restoreKeycloakRealmAttributes(): void { + foreach ($this->originalRealmAttributes as $key => $value) { + if ($value === null) { + continue; + } + KeycloakHelper::updateRealmAttribute($key, $value); + } + $this->originalRealmAttributes = []; + } + + /** + * @Then user :user should have acr value :acr + * + * @param string $user + * @param string $acr + * + * @return void + * @throws Exception + */ + public function userShouldHaveAcrValue(string $user, string $acr): void { + $accessToken = $this->featureContext->getOcisUserToken($user)['token']['accessToken']; + + // Decode JWT token + $parts = explode('.', $accessToken); + if (\count($parts) !== 3) { + throw new Exception("Invalid JWT token format."); + } + $payload = $parts[1]; + $decodedPayload = base64_decode(strtr($payload, '-_', '+/'), true); + $payloadArray = json_decode($decodedPayload, true); + $actualAcr = $payloadArray['acr'] ?? null; + Assert::assertEquals( + $acr, + $actualAcr, + "Expected acr value to be $acr but got $actualAcr", + ); + } +} diff --git a/tests/acceptance/config/behat.yml b/tests/acceptance/config/behat.yml index 4f0d9a223f6..5103c8902af 100644 --- a/tests/acceptance/config/behat.yml +++ b/tests/acceptance/config/behat.yml @@ -493,6 +493,8 @@ default: context: *common_ldap_suite_context contexts: - FeatureContext: *common_feature_context_params + - VaultContext: + - OcisConfigContext: cliCommands: paths: diff --git a/tests/acceptance/features/apiVault/vault.feature b/tests/acceptance/features/apiVault/vault.feature index f5e86f19223..0fb5a1885cb 100644 --- a/tests/acceptance/features/apiVault/vault.feature +++ b/tests/acceptance/features/apiVault/vault.feature @@ -53,3 +53,12 @@ Feature: vault And for user "Alice" the space "Personal" should not contain these entries: | vaultFolder | | vaultFile.txt | + + @env-config @keycloak-config + Scenario: user can set custom auth level names + Given the administrator has set the Keycloak realm attribute "acr.loa.map" to '{"regular":"1","testing":"2"}' + And the config "OCIS_MFA_AUTH_LEVEL_NAMES" has been set to "testing" + And user "Alice" has logged in via web UI + When user "Alice" uploads a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault using the WebDAV API + Then the HTTP status code should be "201" + And user "Alice" should have acr value "testing"