From b1b97122fa7f0a093fb3642c083e1370f9490702 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Fri, 20 Dec 2024 16:52:00 +0100 Subject: [PATCH 01/38] Add SRAM Testfilter Add processSRAMInterrupt route Working SRAM interrupt route Manipulate attributes in interrupt Add token parameter Rename endpoints --- config/packages/parameters.yml.dist | 1 + .../EngineBlock/Application/DiContainer.php | 6 + library/EngineBlock/Corto/Adapter.php | 5 + .../Corto/Filter/Command/SRAMTestFilter.php | 81 ++++++++ library/EngineBlock/Corto/Filter/Input.php | 7 + .../Module/Service/AssertionConsumer.php | 18 +- .../Corto/Module/Service/SRAMInterrupt.php | 190 ++++++++++++++++++ library/EngineBlock/Corto/Module/Services.php | 7 + library/EngineBlock/Corto/ProxyServer.php | 23 ++- .../Saml2/ResponseAnnotationDecorator.php | 30 +++ .../InvalidSRAMConfigurationException.php | 26 +++ .../EngineBlock/SRAM/SRAMEndpoint.php | 88 ++++++++ .../Service/ProcessingStateHelper.php | 3 +- .../ProcessingStateHelperInterface.php | 1 + .../Controller/IdentityProviderController.php | 13 ++ 15 files changed, 495 insertions(+), 4 deletions(-) create mode 100644 library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php create mode 100644 library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php create mode 100644 src/OpenConext/EngineBlock/Exception/InvalidSRAMConfigurationException.php create mode 100644 src/OpenConext/EngineBlock/SRAM/SRAMEndpoint.php diff --git a/config/packages/parameters.yml.dist b/config/packages/parameters.yml.dist index f8fe5741e8..383947b00d 100644 --- a/config/packages/parameters.yml.dist +++ b/config/packages/parameters.yml.dist @@ -223,6 +223,7 @@ parameters: feature_enable_idp_initiated_flow: true feature_stepup_sfo_override_engine_entityid: false feature_stepup_send_user_attributes: false + feature_enable_sram_interrupt: true ########################################################################################## ## PROFILE SETTINGS diff --git a/library/EngineBlock/Application/DiContainer.php b/library/EngineBlock/Application/DiContainer.php index b4ded1095f..feacd4f738 100644 --- a/library/EngineBlock/Application/DiContainer.php +++ b/library/EngineBlock/Application/DiContainer.php @@ -541,6 +541,12 @@ protected function getStepupEndpoint() return $this->container->get(\OpenConext\EngineBlock\Stepup\StepupEndpoint::class); } + /** @return \OpenConext\EngineBlock\SRAM\SRAMEndpoint $sramEndpoint */ + public function getSRAMEndpoint() + { + return $this->container->get('engineblock.configuration.sram.endpoint'); + } + /** @return string */ public function getStepupEntityIdOverrideValue() { diff --git a/library/EngineBlock/Corto/Adapter.php b/library/EngineBlock/Corto/Adapter.php index 3f825f277f..a61627a54f 100644 --- a/library/EngineBlock/Corto/Adapter.php +++ b/library/EngineBlock/Corto/Adapter.php @@ -127,6 +127,11 @@ public function processWayf() $this->_callCortoServiceUri('continueToIdp'); } + public function processSRAMInterrupt() + { + $this->_callCortoServiceUri('SRAMInterruptService'); + } + public function processConsent() { $this->_callCortoServiceUri('processConsentService'); diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php new file mode 100644 index 0000000000..782137b326 --- /dev/null +++ b/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php @@ -0,0 +1,81 @@ +_responseAttributes; + } + + public function execute(): void + { + + $application = EngineBlock_ApplicationSingleton::getInstance(); + + $sramEndpoint = $application->getDiContainer()->getSRAMEndpoint(); + $sramApiToken = $sramEndpoint->getApiToken(); + $sramAuthzLocation = $sramEndpoint->getAuthzLocation(); + // $sramAuthzLocation = 'http://192.168.0.1:12345/api'; + + error_log("SRAMTestFilter execute"); + + $attributes = $this->getResponseAttributes(); + + $uid = $attributes['urn:mace:dir:attribute-def:uid'][0]; + $id = $this->_request->getId(); + $continue_url = $this->_server->getUrl('SRAMInterruptService', '') . "?ID=$id"; + + $headers = array( + "Authorization: $sramApiToken" + ); + + $post = array( + 'uid' => $uid, + 'continue_url' => $continue_url, + ); + + $options = [ + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $post, + ]; + + + $ch = curl_init($sramAuthzLocation); + curl_setopt_array($ch, $options); + + $data = curl_exec($ch); + curl_close($ch); + + $body = json_decode($data); + error_log("SRAMTestFilter " . var_export($body, true)); + + $msg = $body->msg; + if ('interrupt' == $msg) { + $this->_response->setSRAMInterruptNonce($body->nonce); + } + + } +} diff --git a/library/EngineBlock/Corto/Filter/Input.php b/library/EngineBlock/Corto/Filter/Input.php index 3c55a067d6..39e04ef495 100644 --- a/library/EngineBlock/Corto/Filter/Input.php +++ b/library/EngineBlock/Corto/Filter/Input.php @@ -95,8 +95,15 @@ public function getCommands() // Apply the Attribute Release Policy before we do consent. new EngineBlock_Corto_Filter_Command_AttributeReleasePolicy(), + ); + // SRAM Test filter + // When feature_enable_sram_interrupt enabled + if ($featureConfiguration->isEnabled('eb.feature_enable_sram_interrupt')) { + $commands[] = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); + } + if (!$featureConfiguration->isEnabled('eb.run_all_manipulations_prior_to_consent')) { return $commands; } diff --git a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php index 42f99d996e..fbb74a7647 100644 --- a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php @@ -171,6 +171,21 @@ public function serve($serviceName, Request $httpRequest) $originalAssertions = clone $receivedResponse->getAssertions()[0]; $this->_server->filterInputAssertionAttributes($receivedResponse, $receivedRequest); + // Send SRAM Interrupt call + if ($receivedResponse->getSRAMInterruptNonce() != Null) { + $log->info('Handle SRAM Interrupt callout'); + + // Add the SRAM step + $currentProcessStep = $this->_processingStateHelper->addStep( + $receivedRequest->getId(), + ProcessingStateHelperInterface::STEP_SRAM, + $this->getEngineSpRole($this->_server), + $receivedResponse + ); + + $this->_server->sendSRAMInterruptRequest($receivedResponse, $receivedRequest); + } + // Add the consent step $currentProcessStep = $this->_processingStateHelper->addStep( $receivedRequest->getId(), @@ -216,7 +231,8 @@ public function serve($serviceName, Request $httpRequest) $sp->getCoins()->isStepupForceAuthn(), $originalAssertions ); - } + + } /** * @return AuthenticationState diff --git a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php new file mode 100644 index 0000000000..03408b8f12 --- /dev/null +++ b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php @@ -0,0 +1,190 @@ +_server = $server; + $this->_authenticationStateHelper = $stateHelper; + $this->_processingStateHelper = $processingStateHelper; + $this->_stepupGatewayCallOutHelper = $stepupGatewayCallOutHelper; + } + + /** + * @param $serviceName + * @param Request $httpRequest + */ + public function serve($serviceName, Request $httpRequest) + { + + $application = EngineBlock_ApplicationSingleton::getInstance(); + + $sramEndpoint = $application->getDiContainer()->getSRAMEndpoint(); + $sramApiToken = $sramEndpoint->getApiToken(); + $sramEntitlementsLocation = $sramEndpoint->getEntitlementsLocation(); + // $sramEntitlementsLocation = 'http://192.168.0.1:12345/entitlements'; + + $log = $application->getLogInstance(); + + error_log("EngineBlock_Corto_Module_Service_SRAMInterrupt"); + + // Get active request + $id = $httpRequest->get('ID'); + + $nextProcessStep = $this->_processingStateHelper->getStepByRequestId( + $id, + ProcessingStateHelperInterface::STEP_SRAM + ); + + $receivedResponse = $nextProcessStep->getResponse(); + $receivedRequest = $this->_server->getReceivedRequestFromResponse($receivedResponse); + + /* + * TODO Add SRAM stuff + * Manipulate attributes + */ + $attributes = $receivedResponse->getAssertion()->getAttributes(); + $nonce = $receivedResponse->getSRAMInterruptNonce(); + + $headers = array( + "Authorization: $sramApiToken" + ); + + $post = array( + 'nonce' => $nonce + ); + + $options = [ + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $post, + ]; + + + $ch = curl_init($sramEntitlementsLocation); + curl_setopt_array($ch, $options); + + $data = curl_exec($ch); + curl_close($ch); + + $body = json_decode($data); + $entitlements = $body->entitlements; + + + if ($entitlements) { + $attributes['eduPersonEntitlement'] = $entitlements; + $receivedResponse->getAssertion()->setAttributes($attributes); + } + + /* + * Continue to Consent/StepUp + */ + + // Flush log if SP or IdP has additional logging enabled + $issuer = $receivedResponse->getIssuer() ? $receivedResponse->getIssuer()->getValue() : ''; + $idp = $this->_server->getRepository()->fetchIdentityProviderByEntityId($issuer); + + if ($receivedRequest->isDebugRequest()) { + $sp = $this->_server->getEngineSpRole($this->_server); + } else { + $issuer = $receivedRequest->getIssuer() ? $receivedRequest->getIssuer()->getValue() : ''; + $sp = $this->_server->getRepository()->fetchServiceProviderByEntityId($issuer); + } + + // When dealing with an SP that acts as a trusted proxy, we should use the proxying SP and not the proxy itself. + if ($sp->getCoins()->isTrustedProxy()) { + // Overwrite the trusted proxy SP instance with that of the SP that uses the trusted proxy. + $sp = $this->_server->findOriginalServiceProvider($receivedRequest, $log); + } + + $pdpLoas = $receivedResponse->getPdpRequestedLoas(); + $loaRepository = $application->getDiContainer()->getLoaRepository(); + $authnRequestLoas = $receivedRequest->getStepupObligations($loaRepository->getStepUpLoas()); + + $shouldUseStepup = $this->_stepupGatewayCallOutHelper->shouldUseStepup($idp, $sp, $authnRequestLoas, $pdpLoas); + + // Goto consent if no Stepup authentication is needed + if (!$shouldUseStepup) { + $this->_server->sendConsentAuthenticationRequest($receivedResponse, $receivedRequest, $nextProcessStep->getRole(), $this->_authenticationStateHelper->getAuthenticationState()); + return; + } + + $log->info('Handle Stepup authentication callout'); + + // Add Stepup authentication step + $currentProcessStep = $this->_processingStateHelper->addStep( + $receivedRequest->getId(), + ProcessingStateHelperInterface::STEP_STEPUP, + $application->getDiContainer()->getStepupIdentityProvider($this->_server), + $receivedResponse + ); + + // Get mapped AuthnClassRef and get NameId + $nameId = clone $receivedResponse->getNameId(); + $authnClassRef = $this->_stepupGatewayCallOutHelper->getStepupLoa($idp, $sp, $authnRequestLoas, $pdpLoas); + + + + $this->_server->sendStepupAuthenticationRequest( + $receivedRequest, + $currentProcessStep->getRole(), + $authnClassRef, + $nameId, + $sp->getCoins()->isStepupForceAuthn() + ); + + + } +} diff --git a/library/EngineBlock/Corto/Module/Services.php b/library/EngineBlock/Corto/Module/Services.php index 763469a5aa..82363f40f2 100644 --- a/library/EngineBlock/Corto/Module/Services.php +++ b/library/EngineBlock/Corto/Module/Services.php @@ -96,6 +96,13 @@ private function factoryService($className, EngineBlock_Corto_ProxyServer $serve $diContainer->getAuthenticationStateHelper(), $diContainer->getProcessingStateHelper() ); + case EngineBlock_Corto_Module_Service_SRAMInterrupt::class : + return new EngineBlock_Corto_Module_Service_SRAMInterrupt( + $server, + $diContainer->getAuthenticationStateHelper(), + $diContainer->getProcessingStateHelper(), + $diContainer->getStepupGatewayCallOutHelper() + ); case EngineBlock_Corto_Module_Service_AssertionConsumer::class : return new EngineBlock_Corto_Module_Service_AssertionConsumer( $server, diff --git a/library/EngineBlock/Corto/ProxyServer.php b/library/EngineBlock/Corto/ProxyServer.php index 8e596dff65..c39460156a 100644 --- a/library/EngineBlock/Corto/ProxyServer.php +++ b/library/EngineBlock/Corto/ProxyServer.php @@ -43,6 +43,7 @@ use SAML2\XML\saml\SubjectConfirmation; use SAML2\XML\saml\SubjectConfirmationData; use Twig\Environment; +use Symfony\Component\HttpFoundation\RedirectResponse; class EngineBlock_Corto_ProxyServer { @@ -73,7 +74,8 @@ class EngineBlock_Corto_ProxyServer 'idpMetadataService' => '/authentication/idp/metadata', 'spMetadataService' => '/authentication/sp/metadata', 'stepupMetadataService' => '/authentication/stepup/metadata', - 'singleLogoutService' => '/logout' + 'singleLogoutService' => '/logout', + 'SRAMInterruptService' => '/authentication/idp/process-sraminterrupt' ); // Todo: Make this mapping obsolete by updating all proxyserver getUrl callers. If they would reference the correct @@ -92,7 +94,8 @@ class EngineBlock_Corto_ProxyServer 'idpMetadataService' => 'metadata_idp', 'spMetadataService' => 'metadata_sp', 'stepupMetadataService' => 'metadata_stepup', - 'singleLogoutService' => 'authentication_logout' + 'singleLogoutService' => 'authentication_logout', + 'SRAMInterruptService' => 'authentication_idp_process_sraminterrupt' ); protected $_servicesNotNeedingSession = array( @@ -574,6 +577,22 @@ function sendConsentAuthenticationRequest( $this->_server->getBindingsModule()->send($newResponse, $serviceProvider); } + function sendSRAMInterruptRequest($response, $request) { + $id = $request->getId(); + $nonce = $response->getSRAMInterruptNonce(); + + $sramEndpoint = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getSRAMEndpoint(); + $interruptLocation = $sramEndpoint->getInterruptLocation(); + // $interruptLocation = 'http://localhost:12345/interrupt'; + + $redirect_url = "$interruptLocation?nonce=$nonce"; + // $redirect_url = $this->getUrl('SRAMInterruptService', '') . "?ID=$id&nonce=$nonce"; + + error_log("sendSRAMInterruptRequest: " . $redirect_url); + + $this->redirect($redirect_url, ''); + } + //////// RESPONSE HANDLING //////// public function createProxyCountExceededResponse(EngineBlock_Saml2_AuthnRequestAnnotationDecorator $request) diff --git a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php index 7ba6f9856e..dccfe4fe48 100644 --- a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php +++ b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php @@ -93,6 +93,10 @@ class EngineBlock_Saml2_ResponseAnnotationDecorator extends EngineBlock_Saml2_Me */ private ?string $_serializableRelayState = null; + /** @TODO JOHAN: Make nice */ + protected $SRAMInterruptNonce = null; + private ?bool $SRAMInterrupt = null; + /** * @param Response $response */ @@ -343,6 +347,8 @@ public function __sleep() 'isTransparentErrorResponse', '_serializableSspMessageXml', '_serializableRelayState', + 'SRAMInterruptNonce', + 'SRAMInterrupt', ]; } @@ -366,4 +372,28 @@ public function __wakeup() unset($this->_serializableSspMessageXml); } } + + /** + * @param bool + */ + public function setSRAMInterrupt(bool $SRAMInterrupt) + { + $this->SRAMInterrupt = $SRAMInterrupt; + } + + /** + * @param string + */ + public function setSRAMInterruptNonce(string $SRAMInterruptNonce) + { + $this->SRAMInterruptNonce = $SRAMInterruptNonce; + } + /** + * @return string + */ + public function getSRAMInterruptNonce(): string + { + return $this->SRAMInterruptNonce; + } + } diff --git a/src/OpenConext/EngineBlock/Exception/InvalidSRAMConfigurationException.php b/src/OpenConext/EngineBlock/Exception/InvalidSRAMConfigurationException.php new file mode 100644 index 0000000000..7a3d1ae319 --- /dev/null +++ b/src/OpenConext/EngineBlock/Exception/InvalidSRAMConfigurationException.php @@ -0,0 +1,26 @@ +apiToken = $apiToken; + $this->authzLocation = $authzLocation; + $this->interruptLocation = $interruptLocation; + $this->entitlementsLocation = $entitlementsLocation; + } + + /** + * @throws InvalidSRAMConfigurationException + */ + public function getApiToken() : string + { + return $this->apiToken; + } + + /** + * @throws InvalidSRAMConfigurationException + */ + public function getAuthzLocation() : string + { + return $this->authzLocation; + } + + /** + * @throws InvalidSRAMConfigurationException + */ + public function getInterruptLocation() : string + { + return $this->interruptLocation; + } + + /** + * @throws InvalidSRAMConfigurationException + */ + public function getEntitlementsLocation() : string + { + return $this->entitlementsLocation; + } + +} diff --git a/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php b/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php index 9e0d31929e..ee81b806d2 100644 --- a/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php +++ b/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php @@ -73,6 +73,7 @@ public function addStep( public function getStepByRequestId($requestId, $name) { $processing = $this->session->get(self::SESSION_KEY); + if (empty($processing)) { throw new EngineBlock_Corto_Module_Services_SessionLostException('Session lost after consent'); } @@ -83,7 +84,7 @@ public function getStepByRequestId($requestId, $name) } if (!isset($processing[$requestId][$name])) { throw new EngineBlock_Corto_Module_Services_Exception( - sprintf('Process step requested for ResponseID "%s" not found', $requestId) + sprintf('Process step requested for ResponseID "%s" with "%s" not found', $requestId, $name) ); } diff --git a/src/OpenConext/EngineBlock/Service/ProcessingStateHelperInterface.php b/src/OpenConext/EngineBlock/Service/ProcessingStateHelperInterface.php index d88a14e252..a82cafe25c 100644 --- a/src/OpenConext/EngineBlock/Service/ProcessingStateHelperInterface.php +++ b/src/OpenConext/EngineBlock/Service/ProcessingStateHelperInterface.php @@ -30,6 +30,7 @@ interface ProcessingStateHelperInterface { const STEP_CONSENT = 'consent'; const STEP_STEPUP = 'stepup'; + const STEP_SRAM = 'sram'; /** * @param string $requestId diff --git a/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php b/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php index 70f1699310..66557c0499 100644 --- a/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php +++ b/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php @@ -218,6 +218,19 @@ public function processConsentAction() * * @Route("/authentication/idp/requestAccess", name="authentication_idp_request_access", methods={"GET"}) */ + public function processSRAMInterrupt(Request $request) + { + $proxyServer = new EngineBlock_Corto_Adapter(); + $proxyServer->processSRAMInterrupt(); + + return ResponseFactory::fromEngineBlockResponse($this->engineBlockApplicationSingleton->getHttpResponse()); + } + + /** + * @param Request $request + * @return Response + * @throws \EngineBlock_Exception + */ public function requestAccessAction(Request $request) { $body = $this->twig->render( From 2f8e55037e463da6d327c20da3a960ed4961f9a8 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Thu, 23 Jan 2025 12:00:50 +0100 Subject: [PATCH 02/38] Add sbs-stub Align EB/stub with real SBS authz parameters Use SBS api namespace --- config/packages/parameters.yml.dist | 9 ++ .../Corto/Filter/Command/SRAMTestFilter.php | 15 ++- .../Module/Service/AssertionConsumer.php | 2 +- .../Corto/Module/Service/SRAMInterrupt.php | 6 +- .../Saml2/ResponseAnnotationDecorator.php | 2 +- sbs-stub/requirements.txt | 1 + sbs-stub/sbs.py | 97 +++++++++++++++++++ sbs-stub/start | 2 + sbs-stub/templates/interrupt.j2 | 9 ++ .../EngineBlock/SRAM/SRAMEndpoint.php | 13 ++- .../TestFeatureConfiguration.php | 1 + 11 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 sbs-stub/requirements.txt create mode 100755 sbs-stub/sbs.py create mode 100755 sbs-stub/start create mode 100644 sbs-stub/templates/interrupt.j2 diff --git a/config/packages/parameters.yml.dist b/config/packages/parameters.yml.dist index 383947b00d..ca3de4feca 100644 --- a/config/packages/parameters.yml.dist +++ b/config/packages/parameters.yml.dist @@ -308,3 +308,12 @@ parameters: # used in the authentication log record. The attributeName will be searched in the response attributes and if present # the log data will be enriched. The values of the response attributes are the final values after ARP and Attribute Manipulation. auth.log.attributes: [] + + ########################################################################################## + ## SRAM Settings + ########################################################################################## + ## Currently this is used for the outgoing requests with the PDP and AA client + sram.api_token: "xxx" + sram.authz_location: "http://127.0.0.1:12345/api" + sram.interrupt_location: "http://127.0.0.1:12345/interrupt" + sram.entitlements_location: "http://127.0.0.1:12345/entitlements" diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php index 782137b326..48a6d648c3 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php @@ -41,17 +41,22 @@ public function execute(): void $attributes = $this->getResponseAttributes(); - $uid = $attributes['urn:mace:dir:attribute-def:uid'][0]; $id = $this->_request->getId(); + + $user_id = $attributes['urn:mace:dir:attribute-def:uid'][0]; $continue_url = $this->_server->getUrl('SRAMInterruptService', '') . "?ID=$id"; + $service_id = $this->_serviceProvider->entityId; + $issuer_id = $this->_identityProvider->entityId; $headers = array( "Authorization: $sramApiToken" ); $post = array( - 'uid' => $uid, + 'user_id' => $user_id, 'continue_url' => $continue_url, + 'service_id' => $service_id, + 'issuer_id' => $issuer_id ); $options = [ @@ -70,11 +75,15 @@ public function execute(): void curl_close($ch); $body = json_decode($data); - error_log("SRAMTestFilter " . var_export($body, true)); + // error_log("SRAMTestFilter " . var_export($body, true)); $msg = $body->msg; if ('interrupt' == $msg) { $this->_response->setSRAMInterruptNonce($body->nonce); + } else { + if ($body->attributes) { + $this->_responseAttributes = array_merge_recursive($this->_responseAttributes, (array) $body->attributes); + } } } diff --git a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php index fbb74a7647..55503546cc 100644 --- a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php @@ -172,7 +172,7 @@ public function serve($serviceName, Request $httpRequest) $this->_server->filterInputAssertionAttributes($receivedResponse, $receivedRequest); // Send SRAM Interrupt call - if ($receivedResponse->getSRAMInterruptNonce() != Null) { + if ("" != $receivedResponse->getSRAMInterruptNonce()) { $log->info('Handle SRAM Interrupt callout'); // Add the SRAM step diff --git a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php index 03408b8f12..4487875cd9 100644 --- a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php +++ b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php @@ -120,11 +120,9 @@ public function serve($serviceName, Request $httpRequest) curl_close($ch); $body = json_decode($data); - $entitlements = $body->entitlements; - - if ($entitlements) { - $attributes['eduPersonEntitlement'] = $entitlements; + if ($body->attributes) { + $attributes = array_merge_recursive($attributes, (array) $body->attributes); $receivedResponse->getAssertion()->setAttributes($attributes); } diff --git a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php index dccfe4fe48..18effa3323 100644 --- a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php +++ b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php @@ -94,8 +94,8 @@ class EngineBlock_Saml2_ResponseAnnotationDecorator extends EngineBlock_Saml2_Me private ?string $_serializableRelayState = null; /** @TODO JOHAN: Make nice */ - protected $SRAMInterruptNonce = null; private ?bool $SRAMInterrupt = null; + protected string $SRAMInterruptNonce = ""; /** * @param Response $response diff --git a/sbs-stub/requirements.txt b/sbs-stub/requirements.txt new file mode 100644 index 0000000000..7e1060246f --- /dev/null +++ b/sbs-stub/requirements.txt @@ -0,0 +1 @@ +flask diff --git a/sbs-stub/sbs.py b/sbs-stub/sbs.py new file mode 100755 index 0000000000..047b66068b --- /dev/null +++ b/sbs-stub/sbs.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +import json +import logging +import secrets + +from flask import Flask, Response, request, render_template + +logging.getLogger().setLevel(logging.DEBUG) +logging.getLogger('flask_pyoidc').setLevel(logging.ERROR) +logging.getLogger('oic').setLevel(logging.ERROR) +logging.getLogger('jwkest').setLevel(logging.ERROR) +logging.getLogger('urllib3').setLevel(logging.ERROR) +logging.getLogger('werkzeug').setLevel(logging.ERROR) + +app = Flask(__name__, template_folder='templates', static_folder='static') + +nonces = {} + + +def debug(request): + for header in request.headers: + logging.debug(header) + for key, value in request.form.items(): + logging.debug(f'POST {key}: {value}') + + +@app.route('/api/users/proxy_authz_eb', methods=['POST']) +def api(): + logging.debug('-> /api/users/proxy_authz_eb') + debug(request) + + uid = request.form.get('user_id') + continue_url = request.form.get('continue_url') + service_entity_id = request.form.get('service_id') + issuer_id = request.form.get('issuer_id') + + nonce = secrets.token_urlsafe() + nonces[nonce] = (uid, continue_url, service_entity_id, issuer_id) + + response = Response(status=200) + body = { + 'msg': 'interrupt', + # 'msg': 'skip', + 'nonce': nonce, + 'attributes': { + 'urn:mace:dir:attribute-def:eduPersonEntitlement': [ + uid, + nonce, + 'urn:foobar' + ] + } + } + + logging.debug(f'<- {body}') + response.data = json.dumps(body) + + return response + + +@app.route('/api/users/interrupt', methods=['GET']) +def interrupt(): + logging.debug('-> /api/users/interrupt') + nonce = request.args.get('nonce') + (uid, continue_url, service_entity_id, issuer_id) = nonces.get(nonce, ('unknown', '/', '/', '')) + response = render_template('interrupt.j2', uid=uid, + service_entity_id=service_entity_id, issuer_id=issuer_id, url=continue_url) + + return response + + +@app.route('/api/users/attributes', methods=['POST']) +def entitlements(): + logging.debug('-> /api/users/attributes') + debug(request) + + nonce = request.form.get('nonce') + (uid, _, _, _) = nonces.pop(nonce) + + response = Response(status=200) + body = { + 'attributes': { + 'urn:mace:dir:attribute-def:eduPersonEntitlement': [ + uid, + nonce, + 'urn:foobar', + ] + } + } + + logging.debug(f'<- {body}') + response.data = json.dumps(body) + + return response + + +if __name__ == "__main__": + app.run(host='0.0.0.0', port=12345, debug=True) diff --git a/sbs-stub/start b/sbs-stub/start new file mode 100755 index 0000000000..d0a61dad17 --- /dev/null +++ b/sbs-stub/start @@ -0,0 +1,2 @@ +#!/bin/sh +./venv/bin/python sbs.py diff --git a/sbs-stub/templates/interrupt.j2 b/sbs-stub/templates/interrupt.j2 new file mode 100644 index 0000000000..cd9a506668 --- /dev/null +++ b/sbs-stub/templates/interrupt.j2 @@ -0,0 +1,9 @@ + + +

Hello {{uid}}!!

+

Coming from {{issuer_id}}

+

Going to {{service_entity_id}}

+

Accept AUP

+

Continue

+ + diff --git a/src/OpenConext/EngineBlock/SRAM/SRAMEndpoint.php b/src/OpenConext/EngineBlock/SRAM/SRAMEndpoint.php index ff124a58b2..fe501cb758 100644 --- a/src/OpenConext/EngineBlock/SRAM/SRAMEndpoint.php +++ b/src/OpenConext/EngineBlock/SRAM/SRAMEndpoint.php @@ -41,12 +41,12 @@ class SRAMEndpoint */ private $entitlementsLocation; - public function __construct(?string $apiToken, - ?string $authzLocation, - ?string $interruptLocation, - ?string $entitlementsLocation - ) - { + public function __construct( + ?string $apiToken, + ?string $authzLocation, + ?string $interruptLocation, + ?string $entitlementsLocation + ) { $this->apiToken = $apiToken; $this->authzLocation = $authzLocation; $this->interruptLocation = $interruptLocation; @@ -84,5 +84,4 @@ public function getEntitlementsLocation() : string { return $this->entitlementsLocation; } - } diff --git a/src/OpenConext/EngineBlockBundle/Configuration/TestFeatureConfiguration.php b/src/OpenConext/EngineBlockBundle/Configuration/TestFeatureConfiguration.php index 9866bd8f74..e6da271247 100644 --- a/src/OpenConext/EngineBlockBundle/Configuration/TestFeatureConfiguration.php +++ b/src/OpenConext/EngineBlockBundle/Configuration/TestFeatureConfiguration.php @@ -48,6 +48,7 @@ public function __construct() $this->setFeature(new Feature('eb.stepup.sfo.override_engine_entityid', false)); $this->setFeature(new Feature('eb.feature_enable_idp_initiated_flow', true)); $this->setFeature(new Feature('eb.stepup.send_user_attributes', true)); + $this->setFeature(new Feature('eb.feature_enable_sram_interrupt', true)); } public function setFeature(Feature $feature): void From d4df9a63c33ab4c702dd2936ae320e39e1d99f1b Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Thu, 27 Feb 2025 14:32:40 +0100 Subject: [PATCH 03/38] Change the ValidateAllowedConnection input filter so that collab_enabled-services skip the IdP-SP check. I.e., if coin:collab_enabled is set, skip the check if the correct IdP is connected, and always allow the flow to continue. --- .../Corto/Filter/Command/EnforcePolicy.php | 2 +- .../Corto/Filter/Command/SRAMTestFilter.php | 20 +-- .../Command/ValidateAllowedConnection.php | 12 ++ library/EngineBlock/Corto/Filter/Input.php | 2 + .../Module/Service/AssertionConsumer.php | 2 +- .../Saml2/ResponseAnnotationDecorator.php | 15 +-- .../EngineBlockBundle/Pdp/PdpClient.php | 2 +- .../Pdp/PdpClientInterface.php | 2 +- .../EngineBlockBundle/Sbs/SbsClient.php | 83 +++++++++++++ .../Sbs/SbsClientInterface.php | 27 ++++ .../Fixtures/FunctionalTestingPdpClient.php | 4 +- .../Filter/Command/EnforcePolicyTest.php | 2 +- .../Filter/Command/SRAMTestFilterTest.php | 117 ++++++++++++++++++ .../Command/ValidateAllowedConnectionTest.php | 68 ++++++++++ .../EngineBlockBundle/Pdp/PdpClientTest.php | 2 +- 15 files changed, 332 insertions(+), 28 deletions(-) create mode 100644 src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php create mode 100644 src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php create mode 100644 tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMTestFilterTest.php diff --git a/library/EngineBlock/Corto/Filter/Command/EnforcePolicy.php b/library/EngineBlock/Corto/Filter/Command/EnforcePolicy.php index 00bdf439e8..e5f73aa023 100644 --- a/library/EngineBlock/Corto/Filter/Command/EnforcePolicy.php +++ b/library/EngineBlock/Corto/Filter/Command/EnforcePolicy.php @@ -64,7 +64,7 @@ public function execute() try { $pdp = $this->getPdpClient(); - $policyDecision = $pdp->requestDecisionFor($pdpRequest); + $policyDecision = $pdp->requestInterruptDecisionFor($pdpRequest); } catch (\OpenConext\EngineBlock\Http\Exception\HttpException $e) { throw new EngineBlock_Exception_PdpCheckFailed( 'Policy Enforcement Point: Could not perform PDP check: ' . $e->getMessage() diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php index 48a6d648c3..3b49220e01 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php @@ -29,7 +29,6 @@ public function getResponseAttributes() public function execute(): void { - $application = EngineBlock_ApplicationSingleton::getInstance(); $sramEndpoint = $application->getDiContainer()->getSRAMEndpoint(); @@ -46,8 +45,15 @@ public function execute(): void $user_id = $attributes['urn:mace:dir:attribute-def:uid'][0]; $continue_url = $this->_server->getUrl('SRAMInterruptService', '') . "?ID=$id"; $service_id = $this->_serviceProvider->entityId; + // @TODO at the very start of this function, check if the SP has `coin:collab_enabled`, skip otherwise? $issuer_id = $this->_identityProvider->entityId; + /*** + * @TODO Move all curl related things to new HttpClient. See PDPClient as an example. + * @TODO Make sure it has tests + * @TODO add tests for this Input Filter + */ + $headers = array( "Authorization: $sramApiToken" ); @@ -74,16 +80,16 @@ public function execute(): void $data = curl_exec($ch); curl_close($ch); - $body = json_decode($data); + $body = json_decode($data, false); // error_log("SRAMTestFilter " . var_export($body, true)); + // @TODO Add integration test: Assert the redirect url on the saml response is SRAM + $msg = $body->msg; - if ('interrupt' == $msg) { + if ($msg === 'interrupt') { $this->_response->setSRAMInterruptNonce($body->nonce); - } else { - if ($body->attributes) { - $this->_responseAttributes = array_merge_recursive($this->_responseAttributes, (array) $body->attributes); - } + } elseif ($body->attributes) { + $this->_responseAttributes = array_merge_recursive($this->_responseAttributes, (array) $body->attributes); } } diff --git a/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php b/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php index bb81bdbfe5..ec03586f70 100644 --- a/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php +++ b/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php @@ -16,6 +16,8 @@ * limitations under the License. */ +use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; + /** * Validate if the IDP sending this response is allowed to connect to the SP that made the request. **/ @@ -24,6 +26,11 @@ class EngineBlock_Corto_Filter_Command_ValidateAllowedConnection extends EngineB public function execute() { $sp = $this->_serviceProvider; + + if ($this->sbsFlowActive($sp)) { + return; + } + // When dealing with an SP that acts as a trusted proxy, we should perform the validatoin on the proxying SP // and not the proxy itself. if ($sp->getCoins()->isTrustedProxy()) { @@ -41,4 +48,9 @@ public function execute() ); } } + + private function sbsFlowActive(ServiceProvider $sp) + { + return $sp->getCoins()->collabEnabled(); + } } diff --git a/library/EngineBlock/Corto/Filter/Input.php b/library/EngineBlock/Corto/Filter/Input.php index 39e04ef495..bdcd1a0ca1 100644 --- a/library/EngineBlock/Corto/Filter/Input.php +++ b/library/EngineBlock/Corto/Filter/Input.php @@ -100,6 +100,8 @@ public function getCommands() // SRAM Test filter // When feature_enable_sram_interrupt enabled + // @TODO Should this check be here, or in the filter itself like \EngineBlock_Corto_Filter_Command_SsoNotificationCookieFilter + // @TODO if it stays here, add test to make sure it's in the command[] or not if ($featureConfiguration->isEnabled('eb.feature_enable_sram_interrupt')) { $commands[] = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); } diff --git a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php index 55503546cc..7fc6fd3bde 100644 --- a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php @@ -172,7 +172,7 @@ public function serve($serviceName, Request $httpRequest) $this->_server->filterInputAssertionAttributes($receivedResponse, $receivedRequest); // Send SRAM Interrupt call - if ("" != $receivedResponse->getSRAMInterruptNonce()) { + if ($receivedResponse->getSRAMInterruptNonce() !== "") { $log->info('Handle SRAM Interrupt callout'); // Add the SRAM step diff --git a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php index 18effa3323..956812ffbf 100644 --- a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php +++ b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php @@ -94,7 +94,6 @@ class EngineBlock_Saml2_ResponseAnnotationDecorator extends EngineBlock_Saml2_Me private ?string $_serializableRelayState = null; /** @TODO JOHAN: Make nice */ - private ?bool $SRAMInterrupt = null; protected string $SRAMInterruptNonce = ""; /** @@ -373,24 +372,14 @@ public function __wakeup() } } - /** - * @param bool - */ - public function setSRAMInterrupt(bool $SRAMInterrupt) - { - $this->SRAMInterrupt = $SRAMInterrupt; - } - /** * @param string */ - public function setSRAMInterruptNonce(string $SRAMInterruptNonce) + public function setSRAMInterruptNonce(string $SRAMInterruptNonce): void { $this->SRAMInterruptNonce = $SRAMInterruptNonce; } - /** - * @return string - */ + public function getSRAMInterruptNonce(): string { return $this->SRAMInterruptNonce; diff --git a/src/OpenConext/EngineBlockBundle/Pdp/PdpClient.php b/src/OpenConext/EngineBlockBundle/Pdp/PdpClient.php index bd50b680e8..5618303f5a 100644 --- a/src/OpenConext/EngineBlockBundle/Pdp/PdpClient.php +++ b/src/OpenConext/EngineBlockBundle/Pdp/PdpClient.php @@ -43,7 +43,7 @@ public function __construct( $this->policyDecisionPointPath = $policyDecisionPointPath; } - public function requestDecisionFor(Request $request) : PolicyDecision + public function requestInterruptDecisionFor(Request $request) : PolicyDecision { $jsonData = $this->httpClient->post( json_encode($request), diff --git a/src/OpenConext/EngineBlockBundle/Pdp/PdpClientInterface.php b/src/OpenConext/EngineBlockBundle/Pdp/PdpClientInterface.php index 568e332c46..9909bff5da 100644 --- a/src/OpenConext/EngineBlockBundle/Pdp/PdpClientInterface.php +++ b/src/OpenConext/EngineBlockBundle/Pdp/PdpClientInterface.php @@ -22,5 +22,5 @@ interface PdpClientInterface { - public function requestDecisionFor(Request $request) : PolicyDecision; + public function requestInterruptDecisionFor(Request $request) : PolicyDecision; } diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php new file mode 100644 index 0000000000..5882be74a2 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php @@ -0,0 +1,83 @@ +httpClient = $httpClient; + $this->interruptLocation = $interruptLocation; + $this->entitlementsLocation = $entitlementsLocation; + } + + public function requestInterruptDecisionFor(Request $request) : PolicyDecision + { + $jsonData = $this->httpClient->post( + json_encode($request), + $this->interruptLocation, + [], + [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ] + ); + $response = Response::fromData($jsonData); + + return PolicyDecision::fromResponse($response); + } + public function requestEntitlementsFor(Request $request) : PolicyDecision + { + $jsonData = $this->httpClient->post( + json_encode($request), + $this->entitlementsLocation, + [], + [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ] + ); + $response = Response::fromData($jsonData); + + return PolicyDecision::fromResponse($response); + } +} diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php new file mode 100644 index 0000000000..193ce016dc --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php @@ -0,0 +1,27 @@ +status = new Status(); $pdpResponse->status->statusDetail = << diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/EnforcePolicyTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/EnforcePolicyTest.php index 67d2398626..59eb90e73b 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/EnforcePolicyTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/EnforcePolicyTest.php @@ -159,7 +159,7 @@ private function mockRequest(): EngineBlock_Saml2_AuthnRequestAnnotationDecorato private function mockPdpClientWithException(Throwable $exception): void { $pdpClient = Mockery::mock(PdpClientInterface::class); - $pdpClient->expects('requestDecisionFor')->andThrow($exception); + $pdpClient->expects('requestInterruptDecisionFor')->andThrow($exception); /** @var EngineBlock_Application_TestDiContainer $container */ $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMTestFilterTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMTestFilterTest.php new file mode 100644 index 0000000000..8c70bf9501 --- /dev/null +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMTestFilterTest.php @@ -0,0 +1,117 @@ +fail('TODO: Implement me'); + } + + public function testItDoesNothingIfSpDoesNotHaveCollabEnabled(): void + { + $this->fail('TODO: Implement me'); + } + + public function testItCallsSram(): void + { + $this->fail('TODO: Implement me'); + } + + public function testItAddsSramAttributesOnStatusAuthorized(): void + { + $this->fail('TODO: Implement me'); + } + + public function testItAdds() + { + $this->fail('TODO: Implement me'); + } + + public function testThrowsEngineBlockExceptionIfPolicyCannotBeChecked() + { +// $this->expectException('EngineBlock_Exception_PdpCheckFailed'); +// $this->expectExceptionMessage('Policy Enforcement Point: Could not perform PDP check: Resource could not be read (status code "503")'); +// +// $this->mockPdpClientWithException(new UnreadableResourceException('Resource could not be read (status code "503")')); +// +// $policy = new EngineBlock_Corto_Filter_Command_EnforcePolicy(); +// +// $request = $this->mockRequest(); +// $policy->setRequest($request); +// +// $repo = Mockery::mock(MetadataRepositoryInterface::class); +// $server = Mockery::mock(EngineBlock_Corto_ProxyServer::class); +// $server->expects('getRepository')->andReturn($repo); +// +// $sp = $this->mockServiceProvider(); +// +// $policy->setServiceProvider($sp); +// $policy->setProxyServer($server); +// $policy->setResponseAttributes([]); +// +// $policy->setCollabPersonId('foo'); +// +// $idp = Mockery::mock(IdentityProvider::class); +// $idp->entityId = 'bar'; +// $policy->setIdentityProvider($idp); +// +// $policy->execute(); + } + + private function mockServiceProvider(): ServiceProvider + { + $sp = Mockery::mock(ServiceProvider::class); + $sp->entityId = 'bar'; + $sp->shouldReceive('getCoins->isTrustedProxy')->andReturn(false); + $sp->shouldReceive('getCoins->policyEnforcementDecisionRequired')->andReturn(true); + return $sp; + } + + private function mockRequest(): EngineBlock_Saml2_AuthnRequestAnnotationDecorator + { + $assertion = new Assertion(); + $request = new AuthnRequest(); + $response = new SAML2\Response(); + $response->setAssertions(array($assertion)); + return new EngineBlock_Saml2_AuthnRequestAnnotationDecorator($request); + } + +// private function mockPdpClientWithException(Throwable $exception): void +// { +// $pdpClient = Mockery::mock(PdpClientInterface::class); +// $pdpClient->expects('requestDecisionFor')->andThrow($exception); +// +// /** @var EngineBlock_Application_TestDiContainer $container */ +// $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); +// $container->setPdpClient($pdpClient); +// } + +} diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php index fa21557415..914c58d752 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php @@ -86,6 +86,7 @@ public function testItShouldRunInNormalConditionsWithTrustedProxy() $verifier->setProxyServer($server); $verifier->setRequest(m::mock(EngineBlock_Saml2_AuthnRequestAnnotationDecorator::class)); $sp->shouldReceive('getCoins->isTrustedProxy')->andReturn(true); + $sp->shouldReceive('getCoins->collabEnabled')->andReturn(false); $server->shouldReceive('findOriginalServiceProvider')->andReturn($sp); $server->shouldReceive('getLogger')->andReturn($logger); $verifier->setIdentityProvider(new IdentityProvider('OpenConext')); @@ -105,4 +106,71 @@ public function testNotAllowed() self::expectExceptionMessage('Disallowed response by SP configuration. Response from IdP "OpenConext" to SP "FoobarSP"'); $verifier->execute(); } + + public function testIsAllowedWhenCollabEnabledCoinIsTrueEvenWhenNotAllowed() + { + $verifier = new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection(); + $verifier->setResponse($this->response); + + /** + * @TODO Use PHP8 named parameters to pass collabEnabled: true, all other params are default. + */ + $sp = new ServiceProvider( + 'FoobarSP', + null, + null, + null, + null, + null, + false, + [], + [], + '', + '', + '', + false, + '', + '', + '', + '', + '', + '', + null, + '', + '', + '', + null, + [], + false, + '', + '', + [], + false, + [], + false, + null, + true, + false, + false, + null, + false, + false, + false, + false, + '', + null, + null, + null, + null, + null, + null, + false, + true + ); + $sp->allowAll = false; + + $verifier->setIdentityProvider(new IdentityProvider('OpenConext')); + $verifier->setServiceProvider($sp); + $verifier->execute(); + } } diff --git a/tests/unit/OpenConext/EngineBlockBundle/Pdp/PdpClientTest.php b/tests/unit/OpenConext/EngineBlockBundle/Pdp/PdpClientTest.php index c84ce4d82d..af5ec938d4 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Pdp/PdpClientTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Pdp/PdpClientTest.php @@ -47,7 +47,7 @@ public function a_pdp_client_gives_policy_decisions_based_on_pdp_responses_to_pd $guzzle = new Client(['handler' => $mockHandler]); $pdpClient = new PdpClient(new HttpClient($guzzle), '/pdp/api/decide/policy'); - $policyDecision = $pdpClient->requestDecisionFor($pdpRequest); + $policyDecision = $pdpClient->requestInterruptDecisionFor($pdpRequest); $this->assertInstanceOf(PolicyDecision::class, $policyDecision); } From c268af77794198382258fb2de51432e5b76976ab Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Tue, 4 Mar 2025 14:48:14 +0100 Subject: [PATCH 04/38] Implement sbs integration flow See https://github.com/OpenConext/OpenConext-engineblock/issues/1804 --- config/packages/parameters.yml.dist | 18 +- .../functional_testing/functional_testing.yml | 15 + config/services/ci/services.yml | 15 + docs/filter_commands.md | 17 +- .../EngineBlock/Application/DiContainer.php | 17 +- .../Application/TestDiContainer.php | 34 ++- .../Corto/Filter/Command/EnforcePolicy.php | 2 +- .../Corto/Filter/Command/SRAMTestFilter.php | 109 +++---- library/EngineBlock/Corto/Filter/Input.php | 11 +- .../Module/Service/AssertionConsumer.php | 4 +- .../Corto/Module/Service/SRAMInterrupt.php | 74 ++--- library/EngineBlock/Corto/Module/Services.php | 3 +- library/EngineBlock/Corto/ProxyServer.php | 12 +- .../EngineBlock/Exception/SbsCheckFailed.php | 21 ++ sbs-stub/requirements.txt | 1 - sbs-stub/sbs.py | 97 ------- sbs-stub/start | 2 - sbs-stub/templates/interrupt.j2 | 9 - .../EngineBlock/Http/HttpClient.php | 7 +- .../EngineBlock/SRAM/SRAMEndpoint.php | 87 ------ .../Service/ProcessingStateHelper.php | 1 - .../Exception/InvalidSbsResponseException.php | 23 ++ .../EngineBlockBundle/Pdp/PdpClient.php | 2 +- .../Pdp/PdpClientInterface.php | 2 +- .../EngineBlockBundle/Sbs/AuthzResponse.php | 70 +++++ .../Sbs/Dto/AuthzRequest.php | 75 +++++ .../Sbs/Dto/EntitlementsRequest.php | 48 ++++ .../Sbs/EntitlementsResponse.php | 44 +++ .../Sbs/SbsAttributeMerger.php | 79 +++++ .../EngineBlockBundle/Sbs/SbsClient.php | 87 ++++-- .../Sbs/SbsClientInterface.php | 15 +- .../Controllers/SbsController.php | 83 ++++++ .../Features/Context/EngineBlockContext.php | 59 ++++ .../Features/Context/MockSpContext.php | 12 + .../{Context => }/Discoveries.feature | 0 .../Features/SbsFlowIntegration.feature | 93 ++++++ .../Fixtures/FunctionalTestingPdpClient.php | 2 +- .../Fixtures/SbsClientStateManager.php | 108 +++++++ .../Fixtures/ServiceRegistryFixture.php | 7 + tests/behat.yml | 3 +- .../Filter/Command/EnforcePolicyTest.php | 11 +- .../Filter/Command/SRAMTestFilterTest.php | 271 ++++++++++++++---- .../Command/ValidateAllowedConnectionTest.php | 1 + .../EngineBlockBundle/Pdp/PdpClientTest.php | 2 +- .../Sbs/AuthzResponseTest.php | 109 +++++++ .../Sbs/EntitlementsResponseTest.php | 54 ++++ .../Sbs/SbsAttributeMergerTest.php | 95 ++++++ .../EngineBlockBundle/Sbs/SbsClientTest.php | 99 +++++++ 48 files changed, 1586 insertions(+), 424 deletions(-) create mode 100644 library/EngineBlock/Exception/SbsCheckFailed.php delete mode 100644 sbs-stub/requirements.txt delete mode 100755 sbs-stub/sbs.py delete mode 100755 sbs-stub/start delete mode 100644 sbs-stub/templates/interrupt.j2 delete mode 100644 src/OpenConext/EngineBlock/SRAM/SRAMEndpoint.php create mode 100644 src/OpenConext/EngineBlockBundle/Exception/InvalidSbsResponseException.php create mode 100644 src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php create mode 100644 src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php create mode 100644 src/OpenConext/EngineBlockBundle/Sbs/Dto/EntitlementsRequest.php create mode 100644 src/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponse.php create mode 100644 src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php create mode 100644 src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php rename src/OpenConext/EngineBlockFunctionalTestingBundle/Features/{Context => }/Discoveries.feature (100%) create mode 100644 src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature create mode 100644 src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponseTest.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php create mode 100644 tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php diff --git a/config/packages/parameters.yml.dist b/config/packages/parameters.yml.dist index ca3de4feca..13abc4e307 100644 --- a/config/packages/parameters.yml.dist +++ b/config/packages/parameters.yml.dist @@ -312,8 +312,16 @@ parameters: ########################################################################################## ## SRAM Settings ########################################################################################## - ## Currently this is used for the outgoing requests with the PDP and AA client - sram.api_token: "xxx" - sram.authz_location: "http://127.0.0.1:12345/api" - sram.interrupt_location: "http://127.0.0.1:12345/interrupt" - sram.entitlements_location: "http://127.0.0.1:12345/entitlements" + ## Config for connecting with SBS server + ## base_url must end with /. Locations must not start with /. + sram.api_token: xxx + sram.base_url: 'https://engine.dev.openconext.local/functional-testing/' + sram.authz_location: authz + sram.interrupt_location: interrupt + sram.entitlements_location: entitlements + sram.verify_peer: false + sram.allowed_attributes: + - eduPersonEntitlement + - eduPersonPrincipalName + - uid + - sshkey diff --git a/config/routes/functional_testing/functional_testing.yml b/config/routes/functional_testing/functional_testing.yml index ccf8164843..bbf4c4d814 100644 --- a/config/routes/functional_testing/functional_testing.yml +++ b/config/routes/functional_testing/functional_testing.yml @@ -69,3 +69,18 @@ functional_testing_gateway: path: "/gateway/second-factor-only/single-sign-on" defaults: _controller: engineblock.functional_test.controller.stepup_mock::ssoAction + +functional_testing_sram_authz: + path: "/authz" + defaults: + _controller: engineblock.functional_test.controller.sbs::authzAction + +functional_testing_sram_interrupt: + path: "/interrupt" + defaults: + _controller: engineblock.functional_test.controller.sbs::interruptAction + +functional_testing_sram_entitlements: + path: "/entitlements" + defaults: + _controller: engineblock.functional_test.controller.sbs::entitlementsAction diff --git a/config/services/ci/services.yml b/config/services/ci/services.yml index 3a1c81c6e2..d7a6a75f4e 100644 --- a/config/services/ci/services.yml +++ b/config/services/ci/services.yml @@ -7,6 +7,8 @@ parameters: engineblock.functional_testing.attribute_aggregation_data_store.file: "/tmp/eb-fixtures/attribute_aggregation.json" engineblock.functional_testing.stepup_gateway_mock_data_store.file: "/tmp/eb-fixtures/stepup_gateway_mock.json" engineblock.functional_testing.translator_mock_data_store.file: "/tmp/eb-fixtures/translator_mock.json" + engineblock.functional_testing.sbs_client_state_manager_data_store.file: "/tmp/eb-fixtures/sbs_client_state_manager.json" + engineblock.functional_testing.sbs_controller_data_store.file: "/tmp/eb-fixtures/sbs_server_state.json" services: _defaults: @@ -58,6 +60,11 @@ services: - '@engineblock.mock_entities.sp_factory' - "@engineblock.compat.application" + engineblock.functional_testing.fixture.sbs_client_state_manager: + class: OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\SbsClientStateManager + arguments: + - "@engineblock.functional_testing.data_store.sbs_client_state_mananger" + #endregion Fixtures #region Data Stores @@ -77,6 +84,14 @@ services: class: OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\DataStore\JsonDataStore arguments: ['%engineblock.functional_testing.authentication_loop_guard_data_store.file%'] + engineblock.functional_testing.data_store.sbs_client_state_mananger: + class: OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\DataStore\JsonDataStore + arguments: ['%engineblock.functional_testing.sbs_client_state_manager_data_store.file%'] + + engineblock.functional_testing.data_store.sbs_server_state: + class: OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\DataStore\JsonDataStore + arguments: [ '%engineblock.functional_testing.sbs_controller_data_store.file%' ] + engineblock.function_testing.data_store.attribute_aggregation_client: class: OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\DataStore\JsonDataStore arguments: ['%engineblock.functional_testing.attribute_aggregation_data_store.file%'] diff --git a/docs/filter_commands.md b/docs/filter_commands.md index 07a5807dfe..188ac8ac08 100644 --- a/docs/filter_commands.md +++ b/docs/filter_commands.md @@ -1,8 +1,8 @@ # EngineBlock Input and Output Command Chains EngineBlock pre-processes incoming and outgoing SAML Responses using so-called Filters. These filters provide specific, -critical functionality, by invoking a sequence of Filter Commands. However, it is not easily discoverable what these -Filters and Filter Commands exactly do and how they work. This document outlines how these Filters and Filter Commands +critical functionality, by invoking a sequence of Filter Commands. However, it is not easily discoverable what these +Filters and Filter Commands exactly do and how they work. This document outlines how these Filters and Filter Commands work and what each filter command does. The chains are: @@ -13,11 +13,11 @@ The specific commands can be found in the [`library\EngineBlock\Corto\Filter\Com ## Input and Output Filters -These are called by [`ProxyServer`][ps], through [`filterOutputAssertionAttributes`][fOAA] and +These are called by [`ProxyServer`][ps], through [`filterOutputAssertionAttributes`][fOAA] and [`filterInputAssertionAttributes`][fIAA] calling [`callAttributeFilter`][cAF], which invokes the actual Filter Commands. Each Filter then executes Filter Commands in a specified order for Input (between receiving Assertion from IdP and -Consent) and Output (after Consent, before sending Response to SP). +Consent) and Output (after Consent, before sending Response to SP). What the filter does is: ``` Loop over given Filter Commands, for each Command: @@ -30,7 +30,7 @@ Loop over given Filter Commands, for each Command: set the collabPersonId (either: string stored in session, string found in Response, string found in responseAttributes, string found in nameId response or null, in that order) execute the command ``` -During the loop, the Response, responseAttributes and collabPersonId are retrieved from the previous command and are +During the loop, the Response, responseAttributes and collabPersonId are retrieved from the previous command and are used by the commands that follows. A command can also stop filtering by calling `$this->stopFiltering();` @@ -67,7 +67,7 @@ Uses: - EngineBlock_Saml2_ResponseAnnotationDecorator - responseAttributes -### NormalizeAttributes +### NormalizeAttributes Convert all OID attributes to URN and remove the OID variant Depends on: @@ -193,7 +193,7 @@ Modifies: See: [Engineblock Attribute Aggregation](attribute_aggregation.md) for more information. ### EnforcePolicy -Makes a call to the external PolicyDecisionPoint service. This returns a response which details whether or not the +Makes a call to the external PolicyDecisionPoint service. This returns a response which details whether or not the current User is allowed access to the Service Provider. For more information see [the PDP repository README][pdp-repo] Depends On: @@ -343,7 +343,8 @@ Uses: - OpenConext\EngineBlock\Metadata\Entity\IdentityProvider - EngineBlock_Saml2_AuthnRequestAnnotationDecorator - +### SRAM test filter +When enabled and the SP has the collab_enabled coin, the SBS integration flow will be activated allowing SRAM integration. diff --git a/library/EngineBlock/Application/DiContainer.php b/library/EngineBlock/Application/DiContainer.php index feacd4f738..6d288395b7 100644 --- a/library/EngineBlock/Application/DiContainer.php +++ b/library/EngineBlock/Application/DiContainer.php @@ -26,6 +26,7 @@ use OpenConext\EngineBlock\Stepup\StepupEntityFactory; use OpenConext\EngineBlock\Stepup\StepupGatewayCallOutHelper; use OpenConext\EngineBlock\Validator\AllowedSchemeValidator; +use OpenConext\EngineBlockBundle\Sbs\SbsAttributeMerger; use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface; use Symfony\Component\Mailer\MailerInterface; use Twig\Environment; @@ -309,6 +310,16 @@ protected function getSymfonyContainer() return $this->container; } + public function getSbsAttributeMerger(): SbsAttributeMerger + { + return $this->container->get('engineblack.sbs.attribute_merger'); + } + + public function getSbsClient(): \OpenConext\EngineBlockBundle\Sbs\SbsClientInterface + { + return $this->container->get('engineblock.sbs.sbs_client'); + } + public function getPdpClient() { return $this->container->get(\OpenConext\EngineBlockBundle\Pdp\PdpClient::class); @@ -541,12 +552,6 @@ protected function getStepupEndpoint() return $this->container->get(\OpenConext\EngineBlock\Stepup\StepupEndpoint::class); } - /** @return \OpenConext\EngineBlock\SRAM\SRAMEndpoint $sramEndpoint */ - public function getSRAMEndpoint() - { - return $this->container->get('engineblock.configuration.sram.endpoint'); - } - /** @return string */ public function getStepupEntityIdOverrideValue() { diff --git a/library/EngineBlock/Application/TestDiContainer.php b/library/EngineBlock/Application/TestDiContainer.php index a2957d1427..df6e4c430c 100644 --- a/library/EngineBlock/Application/TestDiContainer.php +++ b/library/EngineBlock/Application/TestDiContainer.php @@ -17,7 +17,9 @@ */ use OpenConext\EngineBlock\Stepup\StepupEndpoint; +use OpenConext\EngineBlockBundle\Configuration\FeatureConfigurationInterface; use OpenConext\EngineBlockBundle\Pdp\PdpClientInterface; +use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; /** * Creates mocked versions of dependencies for unit testing @@ -29,6 +31,16 @@ class EngineBlock_Application_TestDiContainer extends EngineBlock_Application_Di */ private $pdpClient; + /** + * @var SbsClientInterface|null + */ + private $sbsClient; + + /** + * @var FeatureConfigurationInterface|null + */ + private $featureConfiguration; + public function getXmlConverter(): EngineBlock_Corto_XmlToArray { return Phake::mock('EngineBlock_Corto_XmlToArray'); @@ -49,11 +61,31 @@ public function getPdpClient() return $this->pdpClient ?? parent::getPdpClient(); } - public function setPdpClient(PdpClientInterface $pdpClient) + public function setPdpClient(?PdpClientInterface $pdpClient) { $this->pdpClient = $pdpClient; } + public function setSbsClient(?SbsClientInterface $sbsClient) + { + $this->sbsClient = $sbsClient; + } + + public function getSbsClient(): SbsClientInterface + { + return $this->sbsClient ?? parent::getSbsClient(); + } + + public function setFeatureConfiguration(?FeatureConfigurationInterface $featureConfiguration) + { + $this->featureConfiguration = $featureConfiguration; + } + + public function getFeatureConfiguration(): FeatureConfigurationInterface + { + return $this->featureConfiguration ?? parent::getFeatureConfiguration(); + } + public function getConsentFactory(): EngineBlock_Corto_Model_Consent_Factory { $consentFactoryMock = Phake::mock('EngineBlock_Corto_Model_Consent_Factory'); diff --git a/library/EngineBlock/Corto/Filter/Command/EnforcePolicy.php b/library/EngineBlock/Corto/Filter/Command/EnforcePolicy.php index e5f73aa023..00bdf439e8 100644 --- a/library/EngineBlock/Corto/Filter/Command/EnforcePolicy.php +++ b/library/EngineBlock/Corto/Filter/Command/EnforcePolicy.php @@ -64,7 +64,7 @@ public function execute() try { $pdp = $this->getPdpClient(); - $policyDecision = $pdp->requestInterruptDecisionFor($pdpRequest); + $policyDecision = $pdp->requestDecisionFor($pdpRequest); } catch (\OpenConext\EngineBlock\Http\Exception\HttpException $e) { throw new EngineBlock_Exception_PdpCheckFailed( 'Policy Enforcement Point: Could not perform PDP check: ' . $e->getMessage() diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php index 3b49220e01..a5196cd82d 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php @@ -1,5 +1,10 @@ _responseAttributes; } + public function getResponse() + { + return $this->_response; + } + public function execute(): void { - $application = EngineBlock_ApplicationSingleton::getInstance(); + if (!$this->getFeatureConfiguration()->isEnabled('eb.feature_enable_sram_interrupt')) { + return; + } - $sramEndpoint = $application->getDiContainer()->getSRAMEndpoint(); - $sramApiToken = $sramEndpoint->getApiToken(); - $sramAuthzLocation = $sramEndpoint->getAuthzLocation(); - // $sramAuthzLocation = 'http://192.168.0.1:12345/api'; + if ($this->_serviceProvider->getCoins()->collabEnabled() === false) { + return; + } - error_log("SRAMTestFilter execute"); + try { + $request = $this->buildRequest(); - $attributes = $this->getResponseAttributes(); + $interruptResponse = $this->getSbsClient()->authz($request); + + if ($interruptResponse->msg === 'interrupt') { + $this->_response->setSRAMInterruptNonce($interruptResponse->nonce); + } elseif ($interruptResponse->msg === 'authorized' && !empty($interruptResponse->attributes)) { + $this->_responseAttributes = $this->getSbsAttributeMerger()->mergeAttributes($this->_responseAttributes, $interruptResponse->attributes); + } else { + throw new InvalidSbsResponseException(sprintf('Invalid SBS response received: %s', $interruptResponse->msg)); + } + }catch (Throwable $e){ + throw new EngineBlock_Exception_SbsCheckFailed('The SBS server could not be queried: ' . $e->getMessage()); + } + } + private function getSbsClient() + { + return EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getSbsClient(); + } + + private function getFeatureConfiguration(): FeatureConfigurationInterface + { + return EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getFeatureConfiguration(); + } + + private function getSbsAttributeMerger(): SbsAttributeMerger + { + return EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getSbsAttributeMerger(); + } + + /** + * @return AuthzRequest + * @throws EngineBlock_Corto_ProxyServer_Exception + */ + private function buildRequest(): AuthzRequest + { + $attributes = $this->getResponseAttributes(); $id = $this->_request->getId(); $user_id = $attributes['urn:mace:dir:attribute-def:uid'][0]; $continue_url = $this->_server->getUrl('SRAMInterruptService', '') . "?ID=$id"; $service_id = $this->_serviceProvider->entityId; - // @TODO at the very start of this function, check if the SP has `coin:collab_enabled`, skip otherwise? $issuer_id = $this->_identityProvider->entityId; - /*** - * @TODO Move all curl related things to new HttpClient. See PDPClient as an example. - * @TODO Make sure it has tests - * @TODO add tests for this Input Filter - */ - - $headers = array( - "Authorization: $sramApiToken" - ); - - $post = array( - 'user_id' => $user_id, - 'continue_url' => $continue_url, - 'service_id' => $service_id, - 'issuer_id' => $issuer_id + return AuthzRequest::create( + $user_id, + $continue_url, + $service_id, + $issuer_id ); - - $options = [ - CURLOPT_HEADER => false, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $post, - ]; - - - $ch = curl_init($sramAuthzLocation); - curl_setopt_array($ch, $options); - - $data = curl_exec($ch); - curl_close($ch); - - $body = json_decode($data, false); - // error_log("SRAMTestFilter " . var_export($body, true)); - - // @TODO Add integration test: Assert the redirect url on the saml response is SRAM - - $msg = $body->msg; - if ($msg === 'interrupt') { - $this->_response->setSRAMInterruptNonce($body->nonce); - } elseif ($body->attributes) { - $this->_responseAttributes = array_merge_recursive($this->_responseAttributes, (array) $body->attributes); - } - } } diff --git a/library/EngineBlock/Corto/Filter/Input.php b/library/EngineBlock/Corto/Filter/Input.php index bdcd1a0ca1..f4f40b6254 100644 --- a/library/EngineBlock/Corto/Filter/Input.php +++ b/library/EngineBlock/Corto/Filter/Input.php @@ -90,22 +90,15 @@ public function getCommands() $diContainer->getAttributeAggregationClient() ), + new EngineBlock_Corto_Filter_Command_SRAMTestFilter(), + // Check if the Policy Decision Point needs to be consulted for this request new EngineBlock_Corto_Filter_Command_EnforcePolicy(), // Apply the Attribute Release Policy before we do consent. new EngineBlock_Corto_Filter_Command_AttributeReleasePolicy(), - ); - // SRAM Test filter - // When feature_enable_sram_interrupt enabled - // @TODO Should this check be here, or in the filter itself like \EngineBlock_Corto_Filter_Command_SsoNotificationCookieFilter - // @TODO if it stays here, add test to make sure it's in the command[] or not - if ($featureConfiguration->isEnabled('eb.feature_enable_sram_interrupt')) { - $commands[] = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); - } - if (!$featureConfiguration->isEnabled('eb.run_all_manipulations_prior_to_consent')) { return $commands; } diff --git a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php index 7fc6fd3bde..ea42b90170 100644 --- a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php @@ -176,13 +176,14 @@ public function serve($serviceName, Request $httpRequest) $log->info('Handle SRAM Interrupt callout'); // Add the SRAM step - $currentProcessStep = $this->_processingStateHelper->addStep( + $this->_processingStateHelper->addStep( $receivedRequest->getId(), ProcessingStateHelperInterface::STEP_SRAM, $this->getEngineSpRole($this->_server), $receivedResponse ); + // Redirect to SRAM $this->_server->sendSRAMInterruptRequest($receivedResponse, $receivedRequest); } @@ -231,7 +232,6 @@ public function serve($serviceName, Request $httpRequest) $sp->getCoins()->isStepupForceAuthn(), $originalAssertions ); - } /** diff --git a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php index 4487875cd9..325e5de47e 100644 --- a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php +++ b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php @@ -1,7 +1,7 @@ _server = $server; $this->_authenticationStateHelper = $stateHelper; $this->_processingStateHelper = $processingStateHelper; $this->_stepupGatewayCallOutHelper = $stepupGatewayCallOutHelper; + $this->sbsAttributeMerger = $sbsAttributeMerger; } /** + * route that receives the user when they get back from their SBS interrupt, + * fetches the attribues from SBS, + * and resumes the AuthN flow. + * * @param $serviceName * @param Request $httpRequest */ public function serve($serviceName, Request $httpRequest) { - $application = EngineBlock_ApplicationSingleton::getInstance(); - $sramEndpoint = $application->getDiContainer()->getSRAMEndpoint(); - $sramApiToken = $sramEndpoint->getApiToken(); - $sramEntitlementsLocation = $sramEndpoint->getEntitlementsLocation(); - // $sramEntitlementsLocation = 'http://192.168.0.1:12345/entitlements'; - - $log = $application->getLogInstance(); - - error_log("EngineBlock_Corto_Module_Service_SRAMInterrupt"); - // Get active request $id = $httpRequest->get('ID'); @@ -89,40 +90,14 @@ public function serve($serviceName, Request $httpRequest) $receivedResponse = $nextProcessStep->getResponse(); $receivedRequest = $this->_server->getReceivedRequestFromResponse($receivedResponse); - /* - * TODO Add SRAM stuff - * Manipulate attributes - */ $attributes = $receivedResponse->getAssertion()->getAttributes(); $nonce = $receivedResponse->getSRAMInterruptNonce(); - $headers = array( - "Authorization: $sramApiToken" - ); - - $post = array( - 'nonce' => $nonce - ); - - $options = [ - CURLOPT_HEADER => false, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $post, - ]; - - - $ch = curl_init($sramEntitlementsLocation); - curl_setopt_array($ch, $options); - - $data = curl_exec($ch); - curl_close($ch); - - $body = json_decode($data); + $request = EntitlementsRequest::create($nonce); + $interruptResponse = $this->getSbsClient()->requestEntitlementsFor($request); - if ($body->attributes) { - $attributes = array_merge_recursive($attributes, (array) $body->attributes); + if (!empty($interruptResponse->attributes)) { + $attributes = $this->sbsAttributeMerger->mergeAttributes($attributes, $interruptResponse->attributes); $receivedResponse->getAssertion()->setAttributes($attributes); } @@ -144,7 +119,7 @@ public function serve($serviceName, Request $httpRequest) // When dealing with an SP that acts as a trusted proxy, we should use the proxying SP and not the proxy itself. if ($sp->getCoins()->isTrustedProxy()) { // Overwrite the trusted proxy SP instance with that of the SP that uses the trusted proxy. - $sp = $this->_server->findOriginalServiceProvider($receivedRequest, $log); + $sp = $this->_server->findOriginalServiceProvider($receivedRequest, $this->_server->getLogger()); } $pdpLoas = $receivedResponse->getPdpRequestedLoas(); @@ -159,7 +134,7 @@ public function serve($serviceName, Request $httpRequest) return; } - $log->info('Handle Stepup authentication callout'); + $this->_server->getLogger()->info('Handle Stepup authentication callout'); // Add Stepup authentication step $currentProcessStep = $this->_processingStateHelper->addStep( @@ -173,8 +148,6 @@ public function serve($serviceName, Request $httpRequest) $nameId = clone $receivedResponse->getNameId(); $authnClassRef = $this->_stepupGatewayCallOutHelper->getStepupLoa($idp, $sp, $authnRequestLoas, $pdpLoas); - - $this->_server->sendStepupAuthenticationRequest( $receivedRequest, $currentProcessStep->getRole(), @@ -182,7 +155,10 @@ public function serve($serviceName, Request $httpRequest) $nameId, $sp->getCoins()->isStepupForceAuthn() ); + } - + private function getSbsClient() + { + return EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getSbsClient(); } } diff --git a/library/EngineBlock/Corto/Module/Services.php b/library/EngineBlock/Corto/Module/Services.php index 82363f40f2..279415ccb0 100644 --- a/library/EngineBlock/Corto/Module/Services.php +++ b/library/EngineBlock/Corto/Module/Services.php @@ -101,7 +101,8 @@ private function factoryService($className, EngineBlock_Corto_ProxyServer $serve $server, $diContainer->getAuthenticationStateHelper(), $diContainer->getProcessingStateHelper(), - $diContainer->getStepupGatewayCallOutHelper() + $diContainer->getStepupGatewayCallOutHelper(), + $diContainer->getSbsAttributeMerger() ); case EngineBlock_Corto_Module_Service_AssertionConsumer::class : return new EngineBlock_Corto_Module_Service_AssertionConsumer( diff --git a/library/EngineBlock/Corto/ProxyServer.php b/library/EngineBlock/Corto/ProxyServer.php index c39460156a..26db1d7559 100644 --- a/library/EngineBlock/Corto/ProxyServer.php +++ b/library/EngineBlock/Corto/ProxyServer.php @@ -578,18 +578,10 @@ function sendConsentAuthenticationRequest( } function sendSRAMInterruptRequest($response, $request) { - $id = $request->getId(); $nonce = $response->getSRAMInterruptNonce(); - $sramEndpoint = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getSRAMEndpoint(); - $interruptLocation = $sramEndpoint->getInterruptLocation(); - // $interruptLocation = 'http://localhost:12345/interrupt'; - - $redirect_url = "$interruptLocation?nonce=$nonce"; - // $redirect_url = $this->getUrl('SRAMInterruptService', '') . "?ID=$id&nonce=$nonce"; - - error_log("sendSRAMInterruptRequest: " . $redirect_url); - + $sbsClient = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getSbsClient(); + $redirect_url = $sbsClient->getInterruptLocationLink($nonce); $this->redirect($redirect_url, ''); } diff --git a/library/EngineBlock/Exception/SbsCheckFailed.php b/library/EngineBlock/Exception/SbsCheckFailed.php new file mode 100644 index 0000000000..250f442279 --- /dev/null +++ b/library/EngineBlock/Exception/SbsCheckFailed.php @@ -0,0 +1,21 @@ + /api/users/proxy_authz_eb') - debug(request) - - uid = request.form.get('user_id') - continue_url = request.form.get('continue_url') - service_entity_id = request.form.get('service_id') - issuer_id = request.form.get('issuer_id') - - nonce = secrets.token_urlsafe() - nonces[nonce] = (uid, continue_url, service_entity_id, issuer_id) - - response = Response(status=200) - body = { - 'msg': 'interrupt', - # 'msg': 'skip', - 'nonce': nonce, - 'attributes': { - 'urn:mace:dir:attribute-def:eduPersonEntitlement': [ - uid, - nonce, - 'urn:foobar' - ] - } - } - - logging.debug(f'<- {body}') - response.data = json.dumps(body) - - return response - - -@app.route('/api/users/interrupt', methods=['GET']) -def interrupt(): - logging.debug('-> /api/users/interrupt') - nonce = request.args.get('nonce') - (uid, continue_url, service_entity_id, issuer_id) = nonces.get(nonce, ('unknown', '/', '/', '')) - response = render_template('interrupt.j2', uid=uid, - service_entity_id=service_entity_id, issuer_id=issuer_id, url=continue_url) - - return response - - -@app.route('/api/users/attributes', methods=['POST']) -def entitlements(): - logging.debug('-> /api/users/attributes') - debug(request) - - nonce = request.form.get('nonce') - (uid, _, _, _) = nonces.pop(nonce) - - response = Response(status=200) - body = { - 'attributes': { - 'urn:mace:dir:attribute-def:eduPersonEntitlement': [ - uid, - nonce, - 'urn:foobar', - ] - } - } - - logging.debug(f'<- {body}') - response.data = json.dumps(body) - - return response - - -if __name__ == "__main__": - app.run(host='0.0.0.0', port=12345, debug=True) diff --git a/sbs-stub/start b/sbs-stub/start deleted file mode 100755 index d0a61dad17..0000000000 --- a/sbs-stub/start +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -./venv/bin/python sbs.py diff --git a/sbs-stub/templates/interrupt.j2 b/sbs-stub/templates/interrupt.j2 deleted file mode 100644 index cd9a506668..0000000000 --- a/sbs-stub/templates/interrupt.j2 +++ /dev/null @@ -1,9 +0,0 @@ - - -

Hello {{uid}}!!

-

Coming from {{issuer_id}}

-

Going to {{service_entity_id}}

-

Accept AUP

-

Continue

- - diff --git a/src/OpenConext/EngineBlock/Http/HttpClient.php b/src/OpenConext/EngineBlock/Http/HttpClient.php index e94f103460..e5fb0c9cb3 100644 --- a/src/OpenConext/EngineBlock/Http/HttpClient.php +++ b/src/OpenConext/EngineBlock/Http/HttpClient.php @@ -23,7 +23,7 @@ use OpenConext\EngineBlock\Http\Exception\MalformedResponseException; use OpenConext\EngineBlock\Http\Exception\UnreadableResourceException; -final class HttpClient +class HttpClient { /** * @var ClientInterface @@ -89,13 +89,14 @@ public function read($path, array $parameters = [], array $headers = []) * @param array $headers * @return mixed */ - public function post($data, $path, $parameters = [], array $headers = []) + public function post($data, $path, $parameters = [], array $headers = [], bool $verify = true) { $resource = ResourcePathFormatter::format($path, $parameters); $response = $this->httpClient->request('POST', $resource, [ 'exceptions' => false, 'body' => $data, - 'headers' => $headers + 'headers' => $headers, + 'verify' => $verify, ]); $statusCode = $response->getStatusCode(); diff --git a/src/OpenConext/EngineBlock/SRAM/SRAMEndpoint.php b/src/OpenConext/EngineBlock/SRAM/SRAMEndpoint.php deleted file mode 100644 index fe501cb758..0000000000 --- a/src/OpenConext/EngineBlock/SRAM/SRAMEndpoint.php +++ /dev/null @@ -1,87 +0,0 @@ -apiToken = $apiToken; - $this->authzLocation = $authzLocation; - $this->interruptLocation = $interruptLocation; - $this->entitlementsLocation = $entitlementsLocation; - } - - /** - * @throws InvalidSRAMConfigurationException - */ - public function getApiToken() : string - { - return $this->apiToken; - } - - /** - * @throws InvalidSRAMConfigurationException - */ - public function getAuthzLocation() : string - { - return $this->authzLocation; - } - - /** - * @throws InvalidSRAMConfigurationException - */ - public function getInterruptLocation() : string - { - return $this->interruptLocation; - } - - /** - * @throws InvalidSRAMConfigurationException - */ - public function getEntitlementsLocation() : string - { - return $this->entitlementsLocation; - } -} diff --git a/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php b/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php index ee81b806d2..02589eed9b 100644 --- a/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php +++ b/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php @@ -73,7 +73,6 @@ public function addStep( public function getStepByRequestId($requestId, $name) { $processing = $this->session->get(self::SESSION_KEY); - if (empty($processing)) { throw new EngineBlock_Corto_Module_Services_SessionLostException('Session lost after consent'); } diff --git a/src/OpenConext/EngineBlockBundle/Exception/InvalidSbsResponseException.php b/src/OpenConext/EngineBlockBundle/Exception/InvalidSbsResponseException.php new file mode 100644 index 0000000000..9e15bd0d66 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Exception/InvalidSbsResponseException.php @@ -0,0 +1,23 @@ +policyDecisionPointPath = $policyDecisionPointPath; } - public function requestInterruptDecisionFor(Request $request) : PolicyDecision + public function requestDecisionFor(Request $request) : PolicyDecision { $jsonData = $this->httpClient->post( json_encode($request), diff --git a/src/OpenConext/EngineBlockBundle/Pdp/PdpClientInterface.php b/src/OpenConext/EngineBlockBundle/Pdp/PdpClientInterface.php index 9909bff5da..568e332c46 100644 --- a/src/OpenConext/EngineBlockBundle/Pdp/PdpClientInterface.php +++ b/src/OpenConext/EngineBlockBundle/Pdp/PdpClientInterface.php @@ -22,5 +22,5 @@ interface PdpClientInterface { - public function requestInterruptDecisionFor(Request $request) : PolicyDecision; + public function requestDecisionFor(Request $request) : PolicyDecision; } diff --git a/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php new file mode 100644 index 0000000000..359140584c --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php @@ -0,0 +1,70 @@ +msg = $jsonData['msg']; + $response->nonce = $jsonData['nonce'] ?? null; + + if (is_array($jsonData['attributes'])) { + $response->attributes = $jsonData['attributes']; + } else { + $response->attributes = []; + } + + return $response; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php new file mode 100644 index 0000000000..a26502730a --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php @@ -0,0 +1,75 @@ +userId = $userId; + $request->continueUrl = $continueUrl; + $request->serviceId = $serviceId; + $request->issuerId = $issuerId; + + return $request; + } + + public function jsonSerialize() : array + { + return [ + 'user_id' => $this->userId, + 'continue_url' => $this->continueUrl, + 'service_id' => $this->serviceId, + 'issuer_id' => $this->issuerId + ]; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Sbs/Dto/EntitlementsRequest.php b/src/OpenConext/EngineBlockBundle/Sbs/Dto/EntitlementsRequest.php new file mode 100644 index 0000000000..0231b1f082 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Sbs/Dto/EntitlementsRequest.php @@ -0,0 +1,48 @@ +nonce = $nonce; + + return $request; + } + + public function jsonSerialize() : array + { + return [ + 'nonce' => $this->nonce, + ]; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponse.php b/src/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponse.php new file mode 100644 index 0000000000..5cce69f5fb --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponse.php @@ -0,0 +1,44 @@ +attributes = $jsonData['attributes']; + + return $response; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php new file mode 100644 index 0000000000..956aa87668 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php @@ -0,0 +1,79 @@ +allowedAttributeNames = $allowedAttributeNames; + } + + public function mergeAttributes(array $samlAttributes, array $sbsAttributes): array + { + $this->assertOnlyAllowedSbsAttributes($sbsAttributes); + + foreach ($sbsAttributes as $key => $value) { + if (!isset($samlAttributes[$key])) { + $samlAttributes[$key] = $value; + continue; + } + + if (is_array($value) && is_array($samlAttributes[$key])) { + // Merge and remove duplicates if both values are arrays + $samlAttributes[$key] = array_unique(array_merge($samlAttributes[$key], $value)); + continue; + } + + $samlAttributes[$key] = $value; + } + + return $samlAttributes; + } + + /** + * @SuppressWarnings(PHPMD.UnusedLocalVariable) $value is never used in the foreach + */ + private function assertOnlyAllowedSbsAttributes(array $sbsAttributes): void + { + $invalidKeys = []; + + foreach ($sbsAttributes as $key => $value) { + if (!in_array($key, $this->allowedAttributeNames, true)) { + $invalidKeys[] = $key; + } + } + + if (empty($invalidKeys)) { + return; + } + + throw new InvalidSbsResponseException( + sprintf('Attributes "%s" is not allowed to be overwritten by SBS.', implode(', ', $invalidKeys)) + ); + } +} diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php index 5882be74a2..39cdc76426 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php @@ -19,12 +19,11 @@ namespace OpenConext\EngineBlockBundle\Sbs; use OpenConext\EngineBlock\Http\HttpClient; -use OpenConext\EngineBlockBundle\Pdp\Dto\Request; -use OpenConext\EngineBlockBundle\Pdp\Dto\Response; -use OpenConext\EngineBlockBundle\Pdp\PdpClientInterface; -use OpenConext\EngineBlockBundle\Pdp\PolicyDecision; +use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; +use OpenConext\EngineBlockBundle\Sbs\Dto\EntitlementsRequest; +use OpenConext\EngineBlockBundle\Sbs\Dto\AuthzRequest; -final class SbsClient implements PdpClientInterface +final class SbsClient implements SbsClientInterface { /** * @var HttpClient @@ -39,45 +38,89 @@ final class SbsClient implements PdpClientInterface */ private $entitlementsLocation; + /** + * @var string + */ + private $apiToken; + + /** + * @var string + */ + private $sbsBaseUrl; + + /** + * @var string + */ + private $authzLocation; + /** + * @var bool + */ + private $verifyPeer; + public function __construct( HttpClient $httpClient, + string $sbsBaseUrl, + string $authzLocation, string $interruptLocation, - string $entitlementsLocation + string $entitlementsLocation, + string $apiToken, + bool $verifyPeer ) { - $this->httpClient = $httpClient; + $this->httpClient = $httpClient; + $this->sbsBaseUrl = $sbsBaseUrl; + $this->authzLocation = $authzLocation; $this->interruptLocation = $interruptLocation; $this->entitlementsLocation = $entitlementsLocation; + $this->apiToken = $apiToken; + $this->verifyPeer = $verifyPeer; } - public function requestInterruptDecisionFor(Request $request) : PolicyDecision + public function authz(AuthzRequest $request): AuthzResponse { $jsonData = $this->httpClient->post( json_encode($request), - $this->interruptLocation, + $this->authzLocation, [], - [ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ] + $this->requestHeaders(), + $this->verifyPeer ); - $response = Response::fromData($jsonData); - return PolicyDecision::fromResponse($response); + if (!is_array($jsonData)) { + throw new InvalidSbsResponseException('Received non-array from SBS server'); + } + + return AuthzResponse::fromData($jsonData); } - public function requestEntitlementsFor(Request $request) : PolicyDecision + + public function requestEntitlementsFor(EntitlementsRequest $request): EntitlementsResponse { $jsonData = $this->httpClient->post( json_encode($request), $this->entitlementsLocation, [], - [ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ] + $this->requestHeaders(), + $this->verifyPeer ); - $response = Response::fromData($jsonData); - return PolicyDecision::fromResponse($response); + if (!is_array($jsonData)) { + throw new InvalidSbsResponseException('Received non-array from SBS server'); + } + + return EntitlementsResponse::fromData($jsonData); + } + + private function requestHeaders(): array + { + return [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => $this->apiToken, + ]; + } + + public function getInterruptLocationLink(string $nonce): string + { + return $this->sbsBaseUrl . $this->interruptLocation . "?nonce=$nonce"; } } diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php index 193ce016dc..cd216783a0 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php @@ -18,10 +18,19 @@ namespace OpenConext\EngineBlockBundle\Sbs; -use OpenConext\EngineBlockBundle\Pdp\Dto\Request; -use OpenConext\EngineBlockBundle\Pdp\PolicyDecision; +use OpenConext\EngineBlockBundle\Sbs\Dto\EntitlementsRequest; +use OpenConext\EngineBlockBundle\Sbs\Dto\AuthzRequest; interface SbsClientInterface { - public function requestDecisionFor(Request $request) : PolicyDecision; + public const INTERRUPT = 'interrupt'; + public const AUTHORIZED = 'authorized'; + + public const VALID_MESSAGES = [self::INTERRUPT, self::AUTHORIZED]; + + public function getInterruptLocationLink(string $nonce); + + public function requestEntitlementsFor(EntitlementsRequest $request) : EntitlementsResponse; + + public function authz(AuthzRequest $request) : AuthzResponse; } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php new file mode 100644 index 0000000000..0389f716c4 --- /dev/null +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php @@ -0,0 +1,83 @@ +sbsClientStateManager = $sbsClientStateManager; + $this->dataStore = $dataStore; + } + + /** + * The endpoint Engine calls to see if the user is 'known' in SBS + */ + public function authzAction(Request $request): JsonResponse + { + $this->dataStore->save(json_decode($request->getContent(), true)); + return new JsonResponse($this->sbsClientStateManager->getPreparedAuthzResponse()); + } + + /** + * The endpoint the browser is redirected to if the user is 'unknown' in SBS + */ + public function interruptAction(Request $request): Response + { + $storedData = $this->dataStore->load(); + $returnUrl = $storedData['continue_url']; + + // url contains the ID=, so the session is preserved + return new Response(sprintf( + '
', + $returnUrl + )); + } + + /** + * The endpoint called by Engine to fetch the entitlements after the browser has made a trip to the interrupt action + * and has returned to the continue_url + */ + public function entitlementsAction() + { + return new JsonResponse([ + 'attributes' => $this->sbsClientStateManager->getPreparedEntitlementsResponse() + ]); + } +} diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php index 05dc879894..df4370332a 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php @@ -23,11 +23,14 @@ use DOMDocument; use DOMElement; use DOMXPath; +use InvalidArgumentException; +use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\DataStore\AbstractDataStore; use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\FunctionalTestingAttributeAggregationClient; use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\FunctionalTestingAuthenticationLoopGuard; use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\FunctionalTestingFeatureConfiguration; use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\FunctionalTestingPdpClient; +use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\SbsClientStateManager; use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\ServiceRegistryFixture; use OpenConext\EngineBlockFunctionalTestingBundle\Mock\EntityRegistry; use OpenConext\EngineBlockFunctionalTestingBundle\Mock\MockIdentityProvider; @@ -116,6 +119,10 @@ class EngineBlockContext extends AbstractSubContext * @var string */ private $currentRequestId = ''; + /** + * @var SbsClientStateManager + */ + private $sbsClientStateManager; private AbstractDataStore $dataStore; @@ -141,6 +148,7 @@ public function __construct( FunctionalTestingAuthenticationLoopGuard $authenticationLoopGuard, FunctionalTestingAttributeAggregationClient $attributeAggregationClient, AbstractDataStore $authGuardDataStore, + SbsClientStateManager $sbsClientStateManager ) { $this->serviceRegistryFixture = $serviceRegistry; $this->engineBlock = $engineBlock; @@ -151,6 +159,7 @@ public function __construct( $this->authenticationLoopGuard = $authenticationLoopGuard; $this->attributeAggregationClient = $attributeAggregationClient; $this->dataStore = $authGuardDataStore; + $this->sbsClientStateManager = $sbsClientStateManager; } /** @@ -192,6 +201,16 @@ public function iPassThroughEngineblock() $mink->pressButton('Submit'); } + /** + * @Given /^I pass through SBS/ + */ + public function iPassThroughSBS() + { + $mink = $this->getMinkContext(); + + $mink->pressButton('Submit'); + } + /** * @Given /^EngineBlock raises an unexpected error$/ */ @@ -746,6 +765,46 @@ public function aaReturnsAttributes(TableNode $attributes) } } + /** + * @Given /^the sbs server will trigger the "([^"]*)" authz flow when called$/ + */ + public function primeAuthzResponse(string $msg): void + { + if ($msg === 'error') { + $this->sbsClientStateManager->prepareAuthzResponse('error'); + return; + } + + if (!in_array($msg, SbsClientInterface::VALID_MESSAGES)) { + throw new InvalidArgumentException("$msg is not a valid message type"); + } + $this->sbsClientStateManager->prepareAuthzResponse($msg); + } + + /** + * @Given /^the sbs server will trigger the 'authorized' authz flow and will return invalid attributes$/ + */ + public function authzWillReturnInvalidAttributes(): void + { + $this->sbsClientStateManager->prepareAuthzResponse(SbsClientInterface::AUTHORIZED, ['foo' => ['bar' => 'baz']]); + } + + /** + * @Given /^the sbs server will return valid entitlements$/ + */ + public function entitlementsWillReturnValidAttributes(): void + { + $this->sbsClientStateManager->prepareEntitlementsResponse($this->sbsClientStateManager->getValidMockAttributes()); + } + + /** + * @Given /^the sbs server will return invalid entitlements$/ + */ + public function entitlementsWillReturnInvalidAttributes(): void + { + $this->sbsClientStateManager->prepareEntitlementsResponse(['foo' => ['bar' => 'baz']]); + } + /** * @Given /^I should see ART code "([^"]*)"$/ */ diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/MockSpContext.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/MockSpContext.php index ef5fb57cb4..7252f5a6af 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/MockSpContext.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/MockSpContext.php @@ -159,6 +159,18 @@ public function spDoesNotRequireConsent($spName) ->save(); } + /** + * @Given /^the SP "([^"]*)" requires SRAM collaboration$/ + * @param string $spName + */ + public function theSpRequiresSbsCollaboration(string $spName) + { + $sp = $this->mockSpRegistry->get($spName); + + $this->serviceRegistryFixture->setSpCollabEnabled($sp->entityId()); + $this->serviceRegistryFixture->save(); + } + /** * @When /^I log in at "([^"]*)"$/ */ diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/Discoveries.feature b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Discoveries.feature similarity index 100% rename from src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/Discoveries.feature rename to src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Discoveries.feature diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature new file mode 100644 index 0000000000..c787f70b94 --- /dev/null +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature @@ -0,0 +1,93 @@ +Feature: + In order to support SBS integration + As EngineBlock + I want to support SBS checks and merge attributes + + Background: + Given an EngineBlock instance on "dev.openconext.local" + And no registered SPs + And no registered Idps + And an Identity Provider named "SSO-IdP" + And a Service Provider named "SSO-SP" + + Scenario: If the SBS authz check returns 'interrupt', the browser is redirected to SBS + Given the SP "SSO-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the "interrupt" authz flow when called + And the sbs server will return valid entitlements + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + Then the url should match "/functional-testing/interrupt" + And I pass through SBS + Then the url should match "/authentication/idp/process-sraminterrupt" + And the response should contain "Review your information that will be shared." + And the response should contain "test_user@test.sram.surf.nl" + And the response should contain "Proceed to SSO-SP" + When I give my consent + And I pass through EngineBlock + Then the url should match "/functional-testing/SSO-SP/acs" + Then the response should contain "sshkey" + Then the response should contain "ssh_key1" + + Scenario: If the SBS authz check returns 'authorized', the attributes are merged, and the browser is not redirected. + Given the SP "SSO-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the "authorized" authz flow when called + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + Then the url should match "/authentication/sp/consume-assertion" + And the response should contain "Review your information that will be shared." + And the response should contain "test_user@test.sram.surf.nl" + And the response should contain "Proceed to SSO-SP" + When I give my consent + And I pass through EngineBlock + Then the url should match "/functional-testing/SSO-SP/acs" + Then the response should contain "sshkey" + Then the response should contain "ssh_key1" + + Scenario: If the SBS authz check returns an invalid response, the flow is halted. + Given the SP "SSO-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the "error" authz flow when called + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + Then the url should match "/feedback/unknown-error" + And the response should contain "Logging in has failed" + + Scenario: If the SBS authz check returns an 'interrupt' response, and the entitlements call to sbs returns an invalid response + Given the SP "SSO-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the "interrupt" authz flow when called + And the sbs server will return invalid entitlements + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + Then the url should match "/functional-testing/interrupt" + And I pass through SBS + And the response should contain "Logging in has failed" + + Scenario: If the authz call returns unknown attributes, the flow is halted + Given the SP "SSO-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the 'authorized' authz flow and will return invalid attributes + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + Then the url should match "/feedback/unknown-error" + And the response should contain "Logging in has failed" + + Scenario: If the sbs flow is active, other filters like PDP are still executed + Given SP "SSO-SP" requires a policy enforcement decision + And pdp gives an IdP specific deny response for "SSO-IdP" + Given the SP "SSO-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the "interrupt" authz flow when called + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + And I should see "Error - Access denied" + And I should see "Message from your organisation:" + And I should see "Students of SSO-IdP do not have access to this resource" diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/FunctionalTestingPdpClient.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/FunctionalTestingPdpClient.php index f6eef3c111..43e9cc868b 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/FunctionalTestingPdpClient.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/FunctionalTestingPdpClient.php @@ -51,7 +51,7 @@ public function __construct(AbstractDataStore $dataStore) /** * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function requestInterruptDecisionFor(Request $request) : PolicyDecision + public function requestDecisionFor(Request $request) : PolicyDecision { $pdpResponse = new Response(); diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php new file mode 100644 index 0000000000..5b56c60265 --- /dev/null +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php @@ -0,0 +1,108 @@ +dataStore = $dataStore; + } + + public function prepareAuthzResponse(string $msg, ?array $attributes = null): void + { + if ($msg === SbsClientInterface::INTERRUPT) { + $this->authz = [ + 'msg' => SbsClientInterface::INTERRUPT, + 'nonce' => 'my-nonce', + ]; + } elseif ($msg === SbsClientInterface::AUTHORIZED) { + $this->authz = [ + 'msg' => SbsClientInterface::AUTHORIZED, + 'attributes' => $attributes ?? $this->getValidMockAttributes(), + ]; + } elseif ($msg === 'error') { + $this->authz = [ + 'msg' => 'gibberish', + 'nonce' => 'my-nonce', + ]; + } else { + throw new InvalidArgumentException(sprintf('"%s" is not a valid authz message.', $msg)); + } + + $this->save(); + } + + public function getPreparedAuthzResponse(): array + { + return $this->dataStore->load()['authz']; + } + + /** + * @return array[] + */ + public function getValidMockAttributes(): array + { + return [ + "eduPersonEntitlement" => ["user_aff1@test.sram.surf.nl", "user_aff2@test.sram.surf.nl"], + "eduPersonPrincipalName" => ["test_user@test.sram.surf.nl"], + "uid" => ["test_user"], + "sshkey" => ["ssh_key1", "ssh_key2"] + ]; + } + + public function prepareEntitlementsResponse(array $entitlements): void + { + $this->entitlements = $entitlements; + $this->save(); + } + + public function getPreparedEntitlementsResponse(): array + { + return $this->dataStore->load()['entitlements']; + } + + private function save() + { + $this->dataStore->save([ + 'authz' => $this->authz, + 'entitlements' => $this->entitlements, + ]); + } +} diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php index 994ee97d29..5937cdc117 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/ServiceRegistryFixture.php @@ -260,6 +260,13 @@ public function setSpEntityNoConsent($entityId) return $this; } + public function setSpCollabEnabled($entityId) + { + $this->setCoin($this->getServiceProvider($entityId), 'collabEnabled', true); + + return $this; + } + public function setSpEntityWantsSignature($entityId) { $this->getServiceProvider($entityId)->requestsMustBeSigned = true; diff --git a/tests/behat.yml b/tests/behat.yml index 7df80fd6c4..956d53a62a 100644 --- a/tests/behat.yml +++ b/tests/behat.yml @@ -19,6 +19,7 @@ default: pdpClient: '@engineblock.functional_testing.fixture.pdp_client' authenticationLoopGuard: '@engineblock.functional_testing.fixture.authentication_loop_guard' attributeAggregationClient: '@engineblock.functional_testing.fixture.attribute_aggregation_client' + sbsClientStateManager: '@engineblock.functional_testing.fixture.sbs_client_state_manager' - OpenConext\EngineBlockFunctionalTestingBundle\Features\Context\MockIdpContext: serviceRegistryFixture: '@engineblock.functional_testing.fixture.service_registry' engineBlock: '@engineblock.functional_testing.service.engine_block' @@ -42,7 +43,7 @@ default: serviceRegistryFixture: '@engineblock.functional_testing.fixture.service_registry' - OpenConext\EngineBlockFunctionalTestingBundle\Features\Context\TranslationContext: mockTranslator: '@engineblock.functional_testing.mock.translator' - - OpenConext\EngineBlockFunctionalTestingBundle\Features\Context\MinkContext + - OpenConext\EngineBlockFunctionalTestingBundle\Features\Context\MinkContext: functional: mink_session: chrome mink_javascript_session: chrome diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/EnforcePolicyTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/EnforcePolicyTest.php index 59eb90e73b..cf7dbfa2a9 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/EnforcePolicyTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/EnforcePolicyTest.php @@ -159,7 +159,7 @@ private function mockRequest(): EngineBlock_Saml2_AuthnRequestAnnotationDecorato private function mockPdpClientWithException(Throwable $exception): void { $pdpClient = Mockery::mock(PdpClientInterface::class); - $pdpClient->expects('requestInterruptDecisionFor')->andThrow($exception); + $pdpClient->expects('requestDecisionFor')->andThrow($exception); /** @var EngineBlock_Application_TestDiContainer $container */ $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); @@ -181,4 +181,13 @@ private function mockIdentityProviderWithoutPolicyEnforcement(): IdentityProvide $idp->shouldReceive('getCoins->policyEnforcementDecisionRequired')->andReturn(false); return $idp; } + protected function tearDown(): void + { + /** @var EngineBlock_Application_TestDiContainer $container */ + $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); + $container->setPdpClient(null); + + parent::tearDown(); + } + } diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMTestFilterTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMTestFilterTest.php index 8c70bf9501..4e0a9fadc2 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMTestFilterTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMTestFilterTest.php @@ -17,14 +17,18 @@ */ use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; -use OpenConext\EngineBlock\Http\Exception\UnreadableResourceException; use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; -use OpenConext\EngineBlock\Metadata\MetadataRepository\MetadataRepositoryInterface; -use OpenConext\EngineBlockBundle\Pdp\PdpClientInterface; +use OpenConext\EngineBlockBundle\Configuration\Feature; +use OpenConext\EngineBlockBundle\Configuration\FeatureConfiguration; +use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; +use OpenConext\EngineBlockBundle\Sbs\Dto\AuthzRequest; +use OpenConext\EngineBlockBundle\Sbs\AuthzResponse; +use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; use PHPUnit\Framework\TestCase; use SAML2\Assertion; use SAML2\AuthnRequest; +use SAML2\Response; class EngineBlock_Test_Corto_Filter_Command_SramTestFilterTest extends TestCase { @@ -32,69 +36,217 @@ class EngineBlock_Test_Corto_Filter_Command_SramTestFilterTest extends TestCase public function testItDoesNothingIfFeatureFlagNotEnabled(): void { - $this->fail('TODO: Implement me'); + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); + + $request = $this->mockRequest(); + $sramFilter->setRequest($request); + + $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => new Feature('eb.feature_enable_sram_interrupt', false)])); + + $sramFilter->execute(); + $this->assertEmpty($sramFilter->getResponseAttributes()); } public function testItDoesNothingIfSpDoesNotHaveCollabEnabled(): void { - $this->fail('TODO: Implement me'); + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); + + $request = $this->mockRequest(); + $sramFilter->setRequest($request); + + $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => new Feature('eb.feature_enable_sram_interrupt', true)])); + + /** @var \Mockery\Mock $sbsClient */ + $sbsClient = $this->mockSbsClient(); + $sbsClient->shouldNotReceive('authz'); + + $sp = $this->mockServiceProvider('spEntityId'); + $sp->expects('getCoins->collabEnabled')->andReturn(false); + + $sramFilter->setServiceProvider($sp); + + $sramFilter->execute(); + $this->assertEmpty($sramFilter->getResponseAttributes()); } - public function testItCallsSram(): void + public function testItAddsNonceWhenMessageInterrupt(): void { - $this->fail('TODO: Implement me'); + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); + + $initialAttributes = ['urn:mace:dir:attribute-def:uid' => ['userIdValue']]; + $sramFilter->setResponseAttributes($initialAttributes); + + $server = Mockery::mock(EngineBlock_Corto_ProxyServer::class); + $server->expects('getUrl')->andReturn('https://example.org'); + + $sramFilter->setProxyServer($server); + + $request = $this->mockRequest(); + $sramFilter->setRequest($request); + + $sramFilter->setResponse(new EngineBlock_Saml2_ResponseAnnotationDecorator(new Response())); + + $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => new Feature('eb.feature_enable_sram_interrupt', true)])); + + /** @var \Mockery\Mock $sbsClient */ + $sbsClient = $this->mockSbsClient(); + + $response = new AuthzResponse(); + $response->msg = 'interrupt'; + $response->nonce = 'hash123'; + $response->attributes = ['dummy' => 'attributes']; + + $expectedRequest = new AuthzRequest(); + $expectedRequest->userId = 'userIdValue'; + $expectedRequest->continueUrl = 'https://example.org?ID='; + $expectedRequest->serviceId = 'spEntityId'; + $expectedRequest->issuerId = 'idpEntityId'; + + $sbsClient->shouldReceive('authz') + ->withArgs(function ($args) use ($expectedRequest) { + + return $args->userId === $expectedRequest->userId + && strpos($args->continueUrl, $expectedRequest->continueUrl) === 0 + && $args->serviceId === $expectedRequest->serviceId + && $args->issuerId === $expectedRequest->issuerId; + }) + ->andReturn($response); + + /** @var \Mockery\Mock|ServiceProvider $sp */ + $sp = $this->mockServiceProvider('spEntityId'); + $sp->expects('getCoins->collabEnabled')->andReturn(true); + $sramFilter->setServiceProvider($sp); + + /** @var \Mockery\Mock|IdentityProvider $sp */ + $idp = $this->mockIdentityProvider('idpEntityId'); + $sramFilter->setIdentityProvider($idp); + + $sramFilter->execute(); + $this->assertSame($initialAttributes, $sramFilter->getResponseAttributes()); + $this->assertSame('hash123', $sramFilter->getResponse()->getSRAMInterruptNonce()); } public function testItAddsSramAttributesOnStatusAuthorized(): void { - $this->fail('TODO: Implement me'); - } + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); - public function testItAdds() - { - $this->fail('TODO: Implement me'); + $initialAttributes = ['urn:mace:dir:attribute-def:uid' => ['userIdValue']]; + $sramFilter->setResponseAttributes($initialAttributes); + + $server = Mockery::mock(EngineBlock_Corto_ProxyServer::class); + $server->expects('getUrl')->andReturn('https://example.org'); + + $sramFilter->setProxyServer($server); + + $request = $this->mockRequest(); + $sramFilter->setRequest($request); + + $sramFilter->setResponse(new EngineBlock_Saml2_ResponseAnnotationDecorator(new Response())); + + $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => new Feature('eb.feature_enable_sram_interrupt', true)])); + + /** @var \Mockery\Mock $sbsClient */ + $sbsClient = $this->mockSbsClient(); + + $response = new AuthzResponse(); + $response->msg = 'authorized'; + $response->nonce = 'hash123'; + $response->attributes = ['eduPersonEntitlement' => 'attributes', 'uid' => ['more' => ['attributes' => 'attributeValues']]]; + + $expectedRequest = new AuthzRequest(); + $expectedRequest->userId = 'userIdValue'; + $expectedRequest->continueUrl = 'https://example.org?ID='; + $expectedRequest->serviceId = 'spEntityId'; + $expectedRequest->issuerId = 'idpEntityId'; + + $sbsClient->shouldReceive('authz') + ->withArgs(function ($args) use ($expectedRequest) { + + return $args->userId === $expectedRequest->userId + && strpos($args->continueUrl, $expectedRequest->continueUrl) === 0 + && $args->serviceId === $expectedRequest->serviceId + && $args->issuerId === $expectedRequest->issuerId; + }) + ->andReturn($response); + + /** @var \Mockery\Mock|ServiceProvider $sp */ + $sp = $this->mockServiceProvider('spEntityId'); + $sp->expects('getCoins->collabEnabled')->andReturn(true); + $sramFilter->setServiceProvider($sp); + + /** @var \Mockery\Mock|IdentityProvider $sp */ + $idp = $this->mockIdentityProvider('idpEntityId'); + $sramFilter->setIdentityProvider($idp); + + + $expectedAttributes = [ + 'urn:mace:dir:attribute-def:uid' => ['userIdValue'], + 'eduPersonEntitlement' => 'attributes', + 'uid' => [ + 'more' => + ['attributes' => 'attributeValues'] + ] + ]; + + $sramFilter->execute(); + $this->assertSame($expectedAttributes, $sramFilter->getResponseAttributes()); + $this->assertSame('', $sramFilter->getResponse()->getSRAMInterruptNonce()); } public function testThrowsEngineBlockExceptionIfPolicyCannotBeChecked() { -// $this->expectException('EngineBlock_Exception_PdpCheckFailed'); -// $this->expectExceptionMessage('Policy Enforcement Point: Could not perform PDP check: Resource could not be read (status code "503")'); -// -// $this->mockPdpClientWithException(new UnreadableResourceException('Resource could not be read (status code "503")')); -// -// $policy = new EngineBlock_Corto_Filter_Command_EnforcePolicy(); -// -// $request = $this->mockRequest(); -// $policy->setRequest($request); -// -// $repo = Mockery::mock(MetadataRepositoryInterface::class); -// $server = Mockery::mock(EngineBlock_Corto_ProxyServer::class); -// $server->expects('getRepository')->andReturn($repo); -// -// $sp = $this->mockServiceProvider(); -// -// $policy->setServiceProvider($sp); -// $policy->setProxyServer($server); -// $policy->setResponseAttributes([]); -// -// $policy->setCollabPersonId('foo'); -// -// $idp = Mockery::mock(IdentityProvider::class); -// $idp->entityId = 'bar'; -// $policy->setIdentityProvider($idp); -// -// $policy->execute(); + $this->expectException(EngineBlock_Exception_SbsCheckFailed::class); + $this->expectExceptionMessage('The SBS server could not be queried: Server could not be reached.'); + + $sbsClient = $this->mockSbsClient(); + $sbsClient->expects('authz')->andThrows(new InvalidSbsResponseException('Server could not be reached.')); + + + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); + + $initialAttributes = ['urn:mace:dir:attribute-def:uid' => ['userIdValue']]; + $sramFilter->setResponseAttributes($initialAttributes); + + $server = Mockery::mock(EngineBlock_Corto_ProxyServer::class); + $server->expects('getUrl')->andReturn('https://example.org'); + + $sramFilter->setProxyServer($server); + + $request = $this->mockRequest(); + $sramFilter->setRequest($request); + + $sramFilter->setResponse(new EngineBlock_Saml2_ResponseAnnotationDecorator(new Response())); + + $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => new Feature('eb.feature_enable_sram_interrupt', true)])); + + /** @var \Mockery\Mock|ServiceProvider $sp */ + $sp = $this->mockServiceProvider('spEntityId'); + $sp->expects('getCoins->collabEnabled')->andReturn(true); + $sramFilter->setServiceProvider($sp); + + /** @var \Mockery\Mock|IdentityProvider $sp */ + $idp = $this->mockIdentityProvider('idpEntityId'); + $sramFilter->setIdentityProvider($idp); + + $sramFilter->execute(); } - private function mockServiceProvider(): ServiceProvider + private function mockServiceProvider(string $entityId): ServiceProvider { $sp = Mockery::mock(ServiceProvider::class); - $sp->entityId = 'bar'; + $sp->entityId = $entityId; $sp->shouldReceive('getCoins->isTrustedProxy')->andReturn(false); $sp->shouldReceive('getCoins->policyEnforcementDecisionRequired')->andReturn(true); return $sp; } + private function mockIdentityProvider(string $entityId): IdentityProvider + { + $idp = Mockery::mock(IdentityProvider::class); + $idp->entityId = $entityId; + return $idp; + } + private function mockRequest(): EngineBlock_Saml2_AuthnRequestAnnotationDecorator { $assertion = new Assertion(); @@ -104,14 +256,33 @@ private function mockRequest(): EngineBlock_Saml2_AuthnRequestAnnotationDecorato return new EngineBlock_Saml2_AuthnRequestAnnotationDecorator($request); } -// private function mockPdpClientWithException(Throwable $exception): void -// { -// $pdpClient = Mockery::mock(PdpClientInterface::class); -// $pdpClient->expects('requestDecisionFor')->andThrow($exception); -// -// /** @var EngineBlock_Application_TestDiContainer $container */ -// $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); -// $container->setPdpClient($pdpClient); -// } + private function mockSbsClient() + { + $sbsClient = Mockery::mock(SbsClientInterface::class); + + /** @var EngineBlock_Application_TestDiContainer $container */ + $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); + $container->setSbsClient($sbsClient); + + return $sbsClient; + } + + private function mockFeatureConfiguration(FeatureConfiguration $featureConfiguration) + { + /** @var EngineBlock_Application_TestDiContainer $container */ + $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); + $container->setFeatureConfiguration($featureConfiguration); + } + + protected function tearDown(): void + { + $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); + $container->setSbsClient(null); + + $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); + $container->setFeatureConfiguration(null); + + parent::tearDown(); + } } diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php index 914c58d752..c03324f094 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php @@ -172,5 +172,6 @@ public function testIsAllowedWhenCollabEnabledCoinIsTrueEvenWhenNotAllowed() $verifier->setIdentityProvider(new IdentityProvider('OpenConext')); $verifier->setServiceProvider($sp); $verifier->execute(); + $this->expectNotToPerformAssertions(); } } diff --git a/tests/unit/OpenConext/EngineBlockBundle/Pdp/PdpClientTest.php b/tests/unit/OpenConext/EngineBlockBundle/Pdp/PdpClientTest.php index af5ec938d4..c84ce4d82d 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Pdp/PdpClientTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Pdp/PdpClientTest.php @@ -47,7 +47,7 @@ public function a_pdp_client_gives_policy_decisions_based_on_pdp_responses_to_pd $guzzle = new Client(['handler' => $mockHandler]); $pdpClient = new PdpClient(new HttpClient($guzzle), '/pdp/api/decide/policy'); - $policyDecision = $pdpClient->requestInterruptDecisionFor($pdpRequest); + $policyDecision = $pdpClient->requestDecisionFor($pdpRequest); $this->assertInstanceOf(PolicyDecision::class, $policyDecision); } diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php new file mode 100644 index 0000000000..c849d648c4 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php @@ -0,0 +1,109 @@ + SbsClientInterface::AUTHORIZED, + 'attributes' => ['role' => 'admin'] + ]; + + $response = AuthzResponse::fromData($jsonData); + + $this->assertInstanceOf(AuthzResponse::class, $response); + $this->assertEquals(SbsClientInterface::AUTHORIZED, $response->msg); + $this->assertEquals(['role' => 'admin'], $response->attributes); + $this->assertNull($response->nonce); + } + + public function testFromDataValidInterruptResponse(): void + { + $jsonData = [ + 'msg' => SbsClientInterface::INTERRUPT, + 'nonce' => 'random_nonce' + ]; + + $response = AuthzResponse::fromData($jsonData); + + $this->assertInstanceOf(AuthzResponse::class, $response); + $this->assertEquals(SbsClientInterface::INTERRUPT, $response->msg); + $this->assertEquals('random_nonce', $response->nonce); + $this->assertEmpty($response->attributes); + } + + public function testFromDataMissingMsgThrowsException(): void + { + $this->expectException(InvalidSbsResponseException::class); + $this->expectExceptionMessage('Key: "msg" was not found in the SBS response'); + + AuthzResponse::fromData([]); + } + + public function testFromDataInvalidMsgThrowsException(): void + { + $this->expectException(InvalidSbsResponseException::class); + $this->expectExceptionMessage('Msg: "INVALID" is not a valid message'); + + AuthzResponse::fromData(['msg' => 'INVALID']); + } + + public function testFromDataInterruptWithoutNonceThrowsException(): void + { + $this->expectException(InvalidSbsResponseException::class); + $this->expectExceptionMessage('Key: "nonce" was not found in the SBS response'); + + AuthzResponse::fromData(['msg' => SbsClientInterface::INTERRUPT]); + } + + public function testFromDataAuthorizedWithoutAttributesThrowsException(): void + { + $this->expectException(InvalidSbsResponseException::class); + $this->expectExceptionMessage('Key: "attributes" was not found in the SBS response'); + + AuthzResponse::fromData(['msg' => SbsClientInterface::AUTHORIZED]); + } + + public function testFromDataAttributesNotArrayDefaultsToEmpty(): void + { + $jsonData = [ + 'msg' => SbsClientInterface::AUTHORIZED, + 'attributes' => 'invalid_type' + ]; + + $response = AuthzResponse::fromData($jsonData); + + $this->assertInstanceOf(AuthzResponse::class, $response); + $this->assertEquals(SbsClientInterface::AUTHORIZED, $response->msg); + $this->assertEmpty($response->attributes); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponseTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponseTest.php new file mode 100644 index 0000000000..839be3b32d --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponseTest.php @@ -0,0 +1,54 @@ + ['key1' => 'value1', 'key2' => 'value2']]; + + $response = EntitlementsResponse::fromData($jsonData); + + $this->assertInstanceOf(EntitlementsResponse::class, $response); + $this->assertEquals($jsonData['attributes'], $response->attributes); + } + + public function testFromDataMissingAttributes() + { + $this->expectException(InvalidSbsResponseException::class); + $this->expectExceptionMessage('Key: Attributes was not found in the SBS entitlements response'); + + $jsonData = ['someOtherKey' => []]; + EntitlementsResponse::fromData($jsonData); + } + + public function testFromDataAttributesNotArray() + { + $this->expectException(InvalidSbsResponseException::class); + $this->expectExceptionMessage('Key: Attributes was not found in the SBS entitlements response'); + + $jsonData = ['attributes' => 'not_an_array']; + EntitlementsResponse::fromData($jsonData); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php new file mode 100644 index 0000000000..6831a24b76 --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php @@ -0,0 +1,95 @@ + '1234', + "eduPersonEntitlement" => ["user_aff1@test.nl"], + "eduPersonPrincipalName" => ["test_user@test.nl"], + "uid" => ["test_user"], + "original" => ['bar', 'soap'], + "myString" => 'foobar', + ]; + + $sbsAttributes = [ + "uuid" => '5678', + "eduPersonEntitlement" => ["user_aff2@test.nl"], + "eduPersonPrincipalName" => ["test_user@test.nl"], + "sshkey" => ["ssh_key1", "ssh_key2"] + ]; + + $expectedResult = [ + "uuid" => '5678', + "eduPersonEntitlement" => ["user_aff1@test.nl", "user_aff2@test.nl"], + "eduPersonPrincipalName" => ["test_user@test.nl"], + "uid" => ["test_user"], + "sshkey" => ["ssh_key1", "ssh_key2"], + "original" => ['bar', 'soap'], + "myString" => 'foobar', + ]; + + $this->assertEquals($expectedResult, $merger->mergeAttributes($samlAttributes, $sbsAttributes)); + } + + public function testMergeAttributesWithInvalidKeysThrowsException(): void + { + $this->expectException(InvalidSbsResponseException::class); + $this->expectExceptionMessage('Attributes "role" is not allowed to be overwritten by SBS.'); + + $allowedAttributes = ['email', 'name']; + $merger = new SbsAttributeMerger($allowedAttributes); + + $samlAttributes = [ + 'email' => ['user@example.com'], + 'role' => ['admin'] + ]; + + $sbsAttributes = [ + 'role' => ['user'] + ]; + + $merger->mergeAttributes($samlAttributes, $sbsAttributes); + } + + public function testMergeAttributesWithEmptySbsAttributes(): void + { + $allowedAttributes = ['email', 'name']; + $merger = new SbsAttributeMerger($allowedAttributes); + + $samlAttributes = [ + 'email' => ['user@example.com'], + 'role' => ['admin'] + ]; + + $sbsAttributes = []; + + $this->assertEquals($samlAttributes, $merger->mergeAttributes($samlAttributes, $sbsAttributes)); + } +} diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php new file mode 100644 index 0000000000..2119a5994a --- /dev/null +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php @@ -0,0 +1,99 @@ +guzzleMock = $this->createMock(ClientInterface::class); + $this->httpClient = $this->createMock(HttpClient::class); + + $this->sbsClient = new SbsClient( + $this->httpClient, + 'https://sbs.example.com', + '/authz', + '/interrupt', + '/entitlements', + 'Bearer test_token', + true + ); + } + + public function testAuthz(): void + { + $requestMock = $this->createMock(AuthzRequest::class); + $jsonResponse = ['msg' => 'interrupt', 'nonce' => 'hash']; + + $this->httpClient->expects($this->once()) + ->method('post') + ->with( + $this->anything(), + '/authz', + [], + [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer test_token', + ] + ) + ->willReturn($jsonResponse); + + $authzResponse = $this->sbsClient->authz($requestMock); + + $this->assertInstanceOf(AuthzResponse::class, $authzResponse); + } + + public function testRequestEntitlementsFor(): void + { + $requestMock = $this->createMock(EntitlementsRequest::class); + $jsonResponse = ['attributes' => ['name' => 'value']]; + + $this->httpClient->expects($this->once()) + ->method('post') + ->with( + $this->anything(), + '/entitlements', + [], + [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer test_token', + ] + ) + ->willReturn($jsonResponse); + + $entitlementsResponse = $this->sbsClient->requestEntitlementsFor($requestMock); + + $this->assertInstanceOf(EntitlementsResponse::class, $entitlementsResponse); + } +} From 0d4a41ef7eb7b14535817698a705741181c5a5cf Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Tue, 25 Mar 2025 11:18:32 +0100 Subject: [PATCH 05/38] Several fixes --- .../EngineBlock/Application/DiContainer.php | 2 +- ...TestFilter.php => SRAMInterruptFilter.php} | 2 +- library/EngineBlock/Corto/Filter/Input.php | 2 +- .../Module/Service/AssertionConsumer.php | 21 ++-- .../Corto/Module/Service/SRAMInterrupt.php | 8 +- sbs-stub/requirements.txt | 1 + sbs-stub/sbs.py | 110 ++++++++++++++++++ sbs-stub/start | 2 + sbs-stub/templates/interrupt.j2 | 9 ++ .../Service/ProcessingStateHelper.php | 2 +- .../Sbs/SbsAttributeMerger.php | 22 ++-- .../EngineBlockBundle/Sbs/SbsClient.php | 5 +- ...erTest.php => SRAMInterruptFilterTest.php} | 12 +- 13 files changed, 163 insertions(+), 35 deletions(-) rename library/EngineBlock/Corto/Filter/Command/{SRAMTestFilter.php => SRAMInterruptFilter.php} (97%) create mode 100644 sbs-stub/requirements.txt create mode 100755 sbs-stub/sbs.py create mode 100755 sbs-stub/start create mode 100644 sbs-stub/templates/interrupt.j2 rename tests/library/EngineBlock/Test/Corto/Filter/Command/{SRAMTestFilterTest.php => SRAMInterruptFilterTest.php} (95%) diff --git a/library/EngineBlock/Application/DiContainer.php b/library/EngineBlock/Application/DiContainer.php index 6d288395b7..556d4055f6 100644 --- a/library/EngineBlock/Application/DiContainer.php +++ b/library/EngineBlock/Application/DiContainer.php @@ -312,7 +312,7 @@ protected function getSymfonyContainer() public function getSbsAttributeMerger(): SbsAttributeMerger { - return $this->container->get('engineblack.sbs.attribute_merger'); + return $this->container->get('engineblock.sbs.attribute_merger'); } public function getSbsClient(): \OpenConext\EngineBlockBundle\Sbs\SbsClientInterface diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php similarity index 97% rename from library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php rename to library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index a5196cd82d..bc7d598f30 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMTestFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -21,7 +21,7 @@ * limitations under the License. */ -class EngineBlock_Corto_Filter_Command_SRAMTestFilter extends EngineBlock_Corto_Filter_Command_Abstract +class EngineBlock_Corto_Filter_Command_SRAMInterruptFilter extends EngineBlock_Corto_Filter_Command_Abstract implements EngineBlock_Corto_Filter_Command_ResponseAttributesModificationInterface { /** diff --git a/library/EngineBlock/Corto/Filter/Input.php b/library/EngineBlock/Corto/Filter/Input.php index f4f40b6254..3cf124911f 100644 --- a/library/EngineBlock/Corto/Filter/Input.php +++ b/library/EngineBlock/Corto/Filter/Input.php @@ -90,7 +90,7 @@ public function getCommands() $diContainer->getAttributeAggregationClient() ), - new EngineBlock_Corto_Filter_Command_SRAMTestFilter(), + new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(), // Check if the Policy Decision Point needs to be consulted for this request new EngineBlock_Corto_Filter_Command_EnforcePolicy(), diff --git a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php index ea42b90170..7aa1888e23 100644 --- a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php @@ -171,8 +171,16 @@ public function serve($serviceName, Request $httpRequest) $originalAssertions = clone $receivedResponse->getAssertions()[0]; $this->_server->filterInputAssertionAttributes($receivedResponse, $receivedRequest); + // Add the consent step + $currentProcessStep = $this->_processingStateHelper->addStep( + $receivedRequest->getId(), + ProcessingStateHelperInterface::STEP_CONSENT, + $this->getEngineSpRole($this->_server), + $receivedResponse + ); + // Send SRAM Interrupt call - if ($receivedResponse->getSRAMInterruptNonce() !== "") { + if ("" != $receivedResponse->getSRAMInterruptNonce()) { $log->info('Handle SRAM Interrupt callout'); // Add the SRAM step @@ -185,15 +193,9 @@ public function serve($serviceName, Request $httpRequest) // Redirect to SRAM $this->_server->sendSRAMInterruptRequest($receivedResponse, $receivedRequest); - } - // Add the consent step - $currentProcessStep = $this->_processingStateHelper->addStep( - $receivedRequest->getId(), - ProcessingStateHelperInterface::STEP_CONSENT, - $this->getEngineSpRole($this->_server), - $receivedResponse - ); + return; + } // When dealing with an SP that acts as a trusted proxy, we should use the proxying SP and not the proxy itself. if ($sp->getCoins()->isTrustedProxy()) { @@ -232,6 +234,7 @@ public function serve($serviceName, Request $httpRequest) $sp->getCoins()->isStepupForceAuthn(), $originalAssertions ); + } /** diff --git a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php index 325e5de47e..eb8b744a5c 100644 --- a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php +++ b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php @@ -49,7 +49,7 @@ class EngineBlock_Corto_Module_Service_SRAMInterrupt /** * @var SbsAttributeMerger */ - private $sbsAttributeMerger; + private $_sbsAttributeMerger; public function __construct( @@ -64,12 +64,12 @@ public function __construct( $this->_authenticationStateHelper = $stateHelper; $this->_processingStateHelper = $processingStateHelper; $this->_stepupGatewayCallOutHelper = $stepupGatewayCallOutHelper; - $this->sbsAttributeMerger = $sbsAttributeMerger; + $this->_sbsAttributeMerger = $sbsAttributeMerger; } /** * route that receives the user when they get back from their SBS interrupt, - * fetches the attribues from SBS, + * fetches the attributes from SBS, * and resumes the AuthN flow. * * @param $serviceName @@ -97,7 +97,7 @@ public function serve($serviceName, Request $httpRequest) $interruptResponse = $this->getSbsClient()->requestEntitlementsFor($request); if (!empty($interruptResponse->attributes)) { - $attributes = $this->sbsAttributeMerger->mergeAttributes($attributes, $interruptResponse->attributes); + $attributes = $this->_sbsAttributeMerger->mergeAttributes($attributes, $interruptResponse->attributes); $receivedResponse->getAssertion()->setAttributes($attributes); } diff --git a/sbs-stub/requirements.txt b/sbs-stub/requirements.txt new file mode 100644 index 0000000000..7e1060246f --- /dev/null +++ b/sbs-stub/requirements.txt @@ -0,0 +1 @@ +flask diff --git a/sbs-stub/sbs.py b/sbs-stub/sbs.py new file mode 100755 index 0000000000..cf85489920 --- /dev/null +++ b/sbs-stub/sbs.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +import json +import logging +import secrets + +from flask import Flask, Response, request, render_template + +logging.getLogger().setLevel(logging.DEBUG) +logging.getLogger('flask_pyoidc').setLevel(logging.ERROR) +logging.getLogger('oic').setLevel(logging.ERROR) +logging.getLogger('jwkest').setLevel(logging.ERROR) +logging.getLogger('urllib3').setLevel(logging.ERROR) +logging.getLogger('werkzeug').setLevel(logging.ERROR) + +app = Flask(__name__, template_folder='templates', static_folder='static') + +nonces = {} + + +def debug(request): + for header in request.headers: + logging.debug(header) + logging.debug(f'request.args: {request.args}') + logging.debug(f'request.data: {request.data}') + logging.debug(f'request.form: {request.form}') + logging.debug(f'request.json: {request.json}') + + +@app.route('/', defaults={'path': ''}) +@app.route('/') +def catch_all(path): + logging.debug(f'-> {path}') + debug(request) + response = Response(status=200) + return response + + +@app.route('/api/users/authz_eb', methods=['POST']) +def authz(): + logging.debug('-> /api/users/authz_eb') + debug(request) + + uid = request.json.get('user_id') + continue_url = request.json.get('continue_url') + service_entity_id = request.json.get('service_id') + issuer_id = request.json.get('issuer_id') + + nonce = secrets.token_urlsafe() + nonces[nonce] = (uid, continue_url, service_entity_id, issuer_id) + + response = Response(status=200) + body = { + # 'msg': 'interrupt', + 'msg': 'authorized', + 'nonce': nonce, + 'attributes': { + 'urn:mace:dir:attribute-def:eduPersonEntitlement': [ + uid, + nonce, + 'urn:foobar' + ], + 'urn:mace:dir:attribute-def:uid': ['SBS-uid'], + 'urn:oid:1.3.6.1.4.1.24552.500.1.1.1.13': ['someKey'], + } + } + + logging.debug(f'<- {body}') + response.data = json.dumps(body) + + return response + + +@app.route('/api/users/interrupt', methods=['GET']) +def interrupt(): + logging.debug('-> /api/users/interrupt') + nonce = request.args.get('nonce') + (uid, continue_url, service_entity_id, issuer_id) = nonces.get(nonce, ('unknown', '/', '/', '')) + response = render_template('interrupt.j2', uid=uid, + service_entity_id=service_entity_id, issuer_id=issuer_id, url=continue_url) + + return response + + +@app.route('/api/users/attributes', methods=['POST']) +def attributes(): + logging.debug('-> /api/users/attributes') + debug(request) + + nonce = request.json.get('nonce') + (uid, _, _, _) = nonces.pop(nonce) + + response = Response(status=200) + body = { + 'attributes': { + 'urn:mace:dir:attribute-def:eduPersonEntitlement': [ + uid, + nonce, + 'urn:foobar', + ] + } + } + + logging.debug(f'<- {body}') + response.data = json.dumps(body) + + return response + + +if __name__ == "__main__": + app.run(host='0.0.0.0', port=12345, debug=True) diff --git a/sbs-stub/start b/sbs-stub/start new file mode 100755 index 0000000000..d0a61dad17 --- /dev/null +++ b/sbs-stub/start @@ -0,0 +1,2 @@ +#!/bin/sh +./venv/bin/python sbs.py diff --git a/sbs-stub/templates/interrupt.j2 b/sbs-stub/templates/interrupt.j2 new file mode 100644 index 0000000000..cd9a506668 --- /dev/null +++ b/sbs-stub/templates/interrupt.j2 @@ -0,0 +1,9 @@ + + +

Hello {{uid}}!!

+

Coming from {{issuer_id}}

+

Going to {{service_entity_id}}

+

Accept AUP

+

Continue

+ + diff --git a/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php b/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php index 02589eed9b..30173bc9aa 100644 --- a/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php +++ b/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php @@ -74,7 +74,7 @@ public function getStepByRequestId($requestId, $name) { $processing = $this->session->get(self::SESSION_KEY); if (empty($processing)) { - throw new EngineBlock_Corto_Module_Services_SessionLostException('Session lost after consent'); + throw new EngineBlock_Corto_Module_Services_SessionLostException('Session lost'); } if (!isset($processing[$requestId])) { throw new EngineBlock_Corto_Module_Services_SessionLostException( diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php index 956aa87668..962a5cd161 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php @@ -18,6 +18,7 @@ namespace OpenConext\EngineBlockBundle\Sbs; +use EngineBlock_ApplicationSingleton; use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; class SbsAttributeMerger @@ -35,9 +36,9 @@ public function __construct(array $allowedAttributeNames) public function mergeAttributes(array $samlAttributes, array $sbsAttributes): array { - $this->assertOnlyAllowedSbsAttributes($sbsAttributes); + $validAttributes = $this->validSbsAttributes($sbsAttributes); - foreach ($sbsAttributes as $key => $value) { + foreach ($validAttributes as $key => $value) { if (!isset($samlAttributes[$key])) { $samlAttributes[$key] = $value; continue; @@ -58,22 +59,25 @@ public function mergeAttributes(array $samlAttributes, array $sbsAttributes): ar /** * @SuppressWarnings(PHPMD.UnusedLocalVariable) $value is never used in the foreach */ - private function assertOnlyAllowedSbsAttributes(array $sbsAttributes): void + private function validSbsAttributes(array $sbsAttributes): array { + $validAttributes = []; $invalidKeys = []; foreach ($sbsAttributes as $key => $value) { - if (!in_array($key, $this->allowedAttributeNames, true)) { + if (in_array($key, $this->allowedAttributeNames, true)) { + $validAttributes[$key] = $sbsAttributes[$key]; + } else { $invalidKeys[] = $key; } } - if (empty($invalidKeys)) { - return; + if (!empty($invalidKeys)) { + $application = EngineBlock_ApplicationSingleton::getInstance(); + $log = $application->getLogInstance(); + $log->warning(sprintf('Attributes "%s" is not allowed to be overwritten by SBS.', implode(', ', $invalidKeys))); } - throw new InvalidSbsResponseException( - sprintf('Attributes "%s" is not allowed to be overwritten by SBS.', implode(', ', $invalidKeys)) - ); + return $validAttributes; } } diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php index 39cdc76426..877dc85779 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php @@ -63,7 +63,6 @@ public function __construct( string $sbsBaseUrl, string $authzLocation, string $interruptLocation, - string $entitlementsLocation, string $apiToken, bool $verifyPeer ) { @@ -71,7 +70,6 @@ public function __construct( $this->sbsBaseUrl = $sbsBaseUrl; $this->authzLocation = $authzLocation; $this->interruptLocation = $interruptLocation; - $this->entitlementsLocation = $entitlementsLocation; $this->apiToken = $apiToken; $this->verifyPeer = $verifyPeer; } @@ -93,11 +91,12 @@ public function authz(AuthzRequest $request): AuthzResponse return AuthzResponse::fromData($jsonData); } + // Entitlements use authzLocation !! public function requestEntitlementsFor(EntitlementsRequest $request): EntitlementsResponse { $jsonData = $this->httpClient->post( json_encode($request), - $this->entitlementsLocation, + $this->authzLocation, [], $this->requestHeaders(), $this->verifyPeer diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMTestFilterTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php similarity index 95% rename from tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMTestFilterTest.php rename to tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php index 4e0a9fadc2..fff546a3f8 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMTestFilterTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php @@ -30,13 +30,13 @@ use SAML2\AuthnRequest; use SAML2\Response; -class EngineBlock_Test_Corto_Filter_Command_SramTestFilterTest extends TestCase +class EngineBlock_Test_Corto_Filter_Command_SramInterruptFilterTest extends TestCase { use MockeryPHPUnitIntegration; public function testItDoesNothingIfFeatureFlagNotEnabled(): void { - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); $request = $this->mockRequest(); $sramFilter->setRequest($request); @@ -49,7 +49,7 @@ public function testItDoesNothingIfFeatureFlagNotEnabled(): void public function testItDoesNothingIfSpDoesNotHaveCollabEnabled(): void { - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); $request = $this->mockRequest(); $sramFilter->setRequest($request); @@ -71,7 +71,7 @@ public function testItDoesNothingIfSpDoesNotHaveCollabEnabled(): void public function testItAddsNonceWhenMessageInterrupt(): void { - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); $initialAttributes = ['urn:mace:dir:attribute-def:uid' => ['userIdValue']]; $sramFilter->setResponseAttributes($initialAttributes); @@ -128,7 +128,7 @@ public function testItAddsNonceWhenMessageInterrupt(): void public function testItAddsSramAttributesOnStatusAuthorized(): void { - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); $initialAttributes = ['urn:mace:dir:attribute-def:uid' => ['userIdValue']]; $sramFilter->setResponseAttributes($initialAttributes); @@ -202,7 +202,7 @@ public function testThrowsEngineBlockExceptionIfPolicyCannotBeChecked() $sbsClient->expects('authz')->andThrows(new InvalidSbsResponseException('Server could not be reached.')); - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMTestFilter(); + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); $initialAttributes = ['urn:mace:dir:attribute-def:uid' => ['userIdValue']]; $sramFilter->setResponseAttributes($initialAttributes); From df8de3ab18fc0013bc9e0d04fce4902fdc66b563 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Tue, 25 Mar 2025 11:27:10 +0100 Subject: [PATCH 06/38] WIP --- config/packages/parameters.yml.dist | 1 - .../EngineBlockBundle/Sbs/SbsClient.php | 6 ++--- .../Controllers/SbsController.php | 2 +- .../Features/Context/EngineBlockContext.php | 2 +- .../Module/Service/ProcessConsentTest.php | 2 +- .../Test/Corto/ProxyServerTest.php | 24 +++++++++++++++++++ .../Sbs/SbsAttributeMergerTest.php | 10 ++++---- .../EngineBlockBundle/Sbs/SbsClientTest.php | 3 +-- 8 files changed, 36 insertions(+), 14 deletions(-) diff --git a/config/packages/parameters.yml.dist b/config/packages/parameters.yml.dist index 13abc4e307..56a2a54c6e 100644 --- a/config/packages/parameters.yml.dist +++ b/config/packages/parameters.yml.dist @@ -318,7 +318,6 @@ parameters: sram.base_url: 'https://engine.dev.openconext.local/functional-testing/' sram.authz_location: authz sram.interrupt_location: interrupt - sram.entitlements_location: entitlements sram.verify_peer: false sram.allowed_attributes: - eduPersonEntitlement diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php index 877dc85779..4abb39221a 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php @@ -29,14 +29,11 @@ final class SbsClient implements SbsClientInterface * @var HttpClient */ private $httpClient; + /** * @var string */ private $interruptLocation; - /** - * @var string - */ - private $entitlementsLocation; /** * @var string @@ -52,6 +49,7 @@ final class SbsClient implements SbsClientInterface * @var string */ private $authzLocation; + /** * @var bool */ diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php index 0389f716c4..7b7f5d4cfe 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php @@ -65,7 +65,7 @@ public function interruptAction(Request $request): Response // url contains the ID=, so the session is preserved return new Response(sprintf( - '
', + 'Continue', $returnUrl )); } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php index df4370332a..4b9b129423 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php @@ -208,7 +208,7 @@ public function iPassThroughSBS() { $mink = $this->getMinkContext(); - $mink->pressButton('Submit'); + $mink->clickLink('Continue'); } /** diff --git a/tests/library/EngineBlock/Test/Corto/Module/Service/ProcessConsentTest.php b/tests/library/EngineBlock/Test/Corto/Module/Service/ProcessConsentTest.php index a7e53b6da1..18095329ff 100644 --- a/tests/library/EngineBlock/Test/Corto/Module/Service/ProcessConsentTest.php +++ b/tests/library/EngineBlock/Test/Corto/Module/Service/ProcessConsentTest.php @@ -96,7 +96,7 @@ public function setUp(): void public function testSessionLostExceptionIfNoSession() { $this->expectException(EngineBlock_Corto_Module_Services_SessionLostException::class); - $this->expectExceptionMessage('Session lost after consent'); + $this->expectExceptionMessage('Session lost'); $sessionData = [ 'Processing' => [], diff --git a/tests/library/EngineBlock/Test/Corto/ProxyServerTest.php b/tests/library/EngineBlock/Test/Corto/ProxyServerTest.php index f1a7add364..84a991c65c 100644 --- a/tests/library/EngineBlock/Test/Corto/ProxyServerTest.php +++ b/tests/library/EngineBlock/Test/Corto/ProxyServerTest.php @@ -48,6 +48,30 @@ public function testDefaultNameIDPolicy() $this->assertSame(['AllowCreate' => true], $nameIdPolicy); } + public function testAllowCreateIsSet() + { + $proxyServer = $this->factoryProxyServer(); + + $originalRequest = $this->factoryOriginalRequest(); + $identityProvider = $proxyServer->getRepository()->fetchIdentityProviderByEntityId('testIdp'); + /** @var AuthnRequest $enhancedRequest */ + $enhancedRequest = EngineBlock_Saml2_AuthnRequestFactory::createFromRequest( + $originalRequest, + $identityProvider, + $proxyServer + ); + + $nameIdPolicy = $enhancedRequest->getNameIdPolicy(); + + $this->assertContains( + 'AllowCreate', + array_keys($nameIdPolicy), + 'The NameIDPolicy should contain the key "AllowCreate"', + false, + true + ); + } + public function testNameIDFormatIsSetFromRemoteMetaData() { $proxyServer = $this->factoryProxyServer(); diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php index 6831a24b76..b8b7a436fc 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php @@ -60,9 +60,6 @@ public function testMergeAttributesSuccessfully(): void public function testMergeAttributesWithInvalidKeysThrowsException(): void { - $this->expectException(InvalidSbsResponseException::class); - $this->expectExceptionMessage('Attributes "role" is not allowed to be overwritten by SBS.'); - $allowedAttributes = ['email', 'name']; $merger = new SbsAttributeMerger($allowedAttributes); @@ -75,7 +72,12 @@ public function testMergeAttributesWithInvalidKeysThrowsException(): void 'role' => ['user'] ]; - $merger->mergeAttributes($samlAttributes, $sbsAttributes); + $expectedResult = [ + 'email' => ['user@example.com'], + 'role' => ['admin'] + ]; + + $this->assertEquals($expectedResult, $merger->mergeAttributes($samlAttributes, $sbsAttributes)); } public function testMergeAttributesWithEmptySbsAttributes(): void diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php index 2119a5994a..6c8b3eb223 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php @@ -43,7 +43,6 @@ protected function setUp(): void 'https://sbs.example.com', '/authz', '/interrupt', - '/entitlements', 'Bearer test_token', true ); @@ -82,7 +81,7 @@ public function testRequestEntitlementsFor(): void ->method('post') ->with( $this->anything(), - '/entitlements', + '/authz', [], [ 'Content-Type' => 'application/json', From 1b0868422baeaee40695bfa2f182aac5450a4653 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Wed, 26 Mar 2025 13:12:55 +0100 Subject: [PATCH 07/38] Adjust SBS flow integration test to updated specs Prior to this change, the test still assumed the entitlements call was used. This change adjusts the test to prime the sbs functional test endpoint for multiple calls on the authz endpoint instead. --- src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php | 4 ++-- .../EngineBlockBundle/Sbs/SbsClientInterface.php | 2 +- .../Features/SbsFlowIntegration.feature | 13 ++++++++----- .../EngineBlockBundle/Sbs/SbsClientTest.php | 7 +++++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php index 4abb39221a..1b2cf971df 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php @@ -90,7 +90,7 @@ public function authz(AuthzRequest $request): AuthzResponse } // Entitlements use authzLocation !! - public function requestEntitlementsFor(EntitlementsRequest $request): EntitlementsResponse + public function requestEntitlementsFor(EntitlementsRequest $request): AuthzResponse { $jsonData = $this->httpClient->post( json_encode($request), @@ -104,7 +104,7 @@ public function requestEntitlementsFor(EntitlementsRequest $request): Entitlemen throw new InvalidSbsResponseException('Received non-array from SBS server'); } - return EntitlementsResponse::fromData($jsonData); + return AuthzResponse::fromData($jsonData); } private function requestHeaders(): array diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php index cd216783a0..509f2a754f 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php @@ -30,7 +30,7 @@ interface SbsClientInterface public function getInterruptLocationLink(string $nonce); - public function requestEntitlementsFor(EntitlementsRequest $request) : EntitlementsResponse; + public function requestEntitlementsFor(EntitlementsRequest $request) : AuthzResponse; public function authz(AuthzRequest $request) : AuthzResponse; } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature index c787f70b94..a395a53b4d 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature @@ -14,11 +14,12 @@ Feature: Given the SP "SSO-SP" requires SRAM collaboration And feature "eb.feature_enable_sram_interrupt" is enabled And the sbs server will trigger the "interrupt" authz flow when called - And the sbs server will return valid entitlements +# And the sbs server will return valid entitlements ## @TODO Remove this endpoint call altogether? When I log in at "SSO-SP" And I pass through EngineBlock And I pass through the IdP Then the url should match "/functional-testing/interrupt" + Given the sbs server will trigger the "authorized" authz flow when called And I pass through SBS Then the url should match "/authentication/idp/process-sraminterrupt" And the response should contain "Review your information that will be shared." @@ -61,23 +62,25 @@ Feature: Given the SP "SSO-SP" requires SRAM collaboration And feature "eb.feature_enable_sram_interrupt" is enabled And the sbs server will trigger the "interrupt" authz flow when called - And the sbs server will return invalid entitlements +# And the sbs server will return invalid entitlements ## @TODO remove entitlements call? When I log in at "SSO-SP" And I pass through EngineBlock And I pass through the IdP Then the url should match "/functional-testing/interrupt" + And the sbs server will trigger the "error" authz flow when called And I pass through SBS And the response should contain "Logging in has failed" - Scenario: If the authz call returns unknown attributes, the flow is halted + Scenario: If the authz call returns unknown attributes, the unknown attributes are ignored Given the SP "SSO-SP" requires SRAM collaboration And feature "eb.feature_enable_sram_interrupt" is enabled And the sbs server will trigger the 'authorized' authz flow and will return invalid attributes When I log in at "SSO-SP" And I pass through EngineBlock And I pass through the IdP - Then the url should match "/feedback/unknown-error" - And the response should contain "Logging in has failed" + Then the url should match "/authentication/sp/consume-assertion" + And the response should not contain "foo" + And the response should not contain "baz" Scenario: If the sbs flow is active, other filters like PDP are still executed Given SP "SSO-SP" requires a policy enforcement decision diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php index 6c8b3eb223..b3a3b7fef3 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php @@ -75,7 +75,10 @@ public function testAuthz(): void public function testRequestEntitlementsFor(): void { $requestMock = $this->createMock(EntitlementsRequest::class); - $jsonResponse = ['attributes' => ['name' => 'value']]; + $jsonResponse = [ + 'msg' => 'authorized', + 'attributes' => ['name' => 'value'] + ]; $this->httpClient->expects($this->once()) ->method('post') @@ -93,6 +96,6 @@ public function testRequestEntitlementsFor(): void $entitlementsResponse = $this->sbsClient->requestEntitlementsFor($requestMock); - $this->assertInstanceOf(EntitlementsResponse::class, $entitlementsResponse); + $this->assertInstanceOf(AuthzResponse::class, $entitlementsResponse); } } From 8cdadf97dbdbeefab45800aa4f4a5a4f3571760a Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Wed, 26 Mar 2025 15:36:59 +0100 Subject: [PATCH 08/38] Add SBS message logging --- .../EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php | 5 ++++- src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index bc7d598f30..06f95f7fbd 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -39,6 +39,8 @@ public function getResponse() public function execute(): void { + $log = EngineBlock_ApplicationSingleton::getLog(); + if (!$this->getFeatureConfiguration()->isEnabled('eb.feature_enable_sram_interrupt')) { return; } @@ -53,13 +55,14 @@ public function execute(): void $interruptResponse = $this->getSbsClient()->authz($request); if ($interruptResponse->msg === 'interrupt') { + $log->notice("SBS: " . $interruptResponse->message); $this->_response->setSRAMInterruptNonce($interruptResponse->nonce); } elseif ($interruptResponse->msg === 'authorized' && !empty($interruptResponse->attributes)) { $this->_responseAttributes = $this->getSbsAttributeMerger()->mergeAttributes($this->_responseAttributes, $interruptResponse->attributes); } else { throw new InvalidSbsResponseException(sprintf('Invalid SBS response received: %s', $interruptResponse->msg)); } - }catch (Throwable $e){ + } catch (Throwable $e){ throw new EngineBlock_Exception_SbsCheckFailed('The SBS server could not be queried: ' . $e->getMessage()); } } diff --git a/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php index 359140584c..d17fea5e6e 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php @@ -58,6 +58,7 @@ public static function fromData(array $jsonData) : AuthzResponse $response = new self; $response->msg = $jsonData['msg']; $response->nonce = $jsonData['nonce'] ?? null; + $response->message = $jsonData['message'] ?? null; if (is_array($jsonData['attributes'])) { $response->attributes = $jsonData['attributes']; From 10d6a2a2868edfda2b37a649afccb5ddd2a68a1d Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Fri, 28 Mar 2025 16:31:32 +0100 Subject: [PATCH 09/38] Use collabPersonId for sbs user_id --- .../EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index 06f95f7fbd..dc23f4942a 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -91,7 +91,8 @@ private function buildRequest(): AuthzRequest $attributes = $this->getResponseAttributes(); $id = $this->_request->getId(); - $user_id = $attributes['urn:mace:dir:attribute-def:uid'][0]; + // $user_id = $attributes['urn:mace:dir:attribute-def:uid'][0]; + $user_id = $this->_collabPersonId; $continue_url = $this->_server->getUrl('SRAMInterruptService', '') . "?ID=$id"; $service_id = $this->_serviceProvider->entityId; $issuer_id = $this->_identityProvider->entityId; From a2b62f9193f17cc4c016ffb41574a4d262c68f97 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Mon, 31 Mar 2025 11:52:06 +0200 Subject: [PATCH 10/38] Add SHO and EPPN to SBS authz call --- .../Corto/Filter/Command/SRAMInterruptFilter.php | 3 ++- .../EngineBlockBundle/Sbs/Dto/AuthzRequest.php | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index dc23f4942a..49a74e4f95 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -91,14 +91,15 @@ private function buildRequest(): AuthzRequest $attributes = $this->getResponseAttributes(); $id = $this->_request->getId(); - // $user_id = $attributes['urn:mace:dir:attribute-def:uid'][0]; $user_id = $this->_collabPersonId; + $eppn = $attributes['urn:mace:dir:attribute-def:eduPersonPrincipalName'][0] ?? ""; $continue_url = $this->_server->getUrl('SRAMInterruptService', '') . "?ID=$id"; $service_id = $this->_serviceProvider->entityId; $issuer_id = $this->_identityProvider->entityId; return AuthzRequest::create( $user_id, + $eppn, $continue_url, $service_id, $issuer_id diff --git a/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php index a26502730a..85461004d6 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php @@ -28,6 +28,11 @@ class AuthzRequest implements JsonSerializable */ public $userId; + /** + * @var string + */ + public $eduPersonPrincipalName; + /** * @var string */ @@ -45,17 +50,21 @@ class AuthzRequest implements JsonSerializable public static function create( string $userId, + string $eppn, string $continueUrl, string $serviceId, string $issuerId ) : AuthzRequest { Assertion::string($userId, 'The userId must be a string.'); + Assertion::string($eppn, 'The eduPersonPrincipalName must be a string.'); Assertion::string($continueUrl, 'The continueUrl must be a string.'); Assertion::string($serviceId, 'The serviceId must be a string.'); Assertion::string($issuerId, 'The issuerId must be a string.'); $request = new self(); $request->userId = $userId; + $request->schacHomeOrganization = $sho; + $request->eduPersonPrincipalName = $eppn; $request->continueUrl = $continueUrl; $request->serviceId = $serviceId; $request->issuerId = $issuerId; @@ -67,6 +76,7 @@ public function jsonSerialize() : array { return [ 'user_id' => $this->userId, + 'eppn' => $this->eduPersonPrincipalName, 'continue_url' => $this->continueUrl, 'service_id' => $this->serviceId, 'issuer_id' => $this->issuerId From 1924a8633bd3bcabdf649099544e57654b3cfc6e Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Mon, 31 Mar 2025 13:34:24 +0200 Subject: [PATCH 11/38] Add message to sbs authz response --- sbs-stub/sbs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sbs-stub/sbs.py b/sbs-stub/sbs.py index cf85489920..bf5e4e2871 100755 --- a/sbs-stub/sbs.py +++ b/sbs-stub/sbs.py @@ -50,9 +50,10 @@ def authz(): response = Response(status=200) body = { - # 'msg': 'interrupt', - 'msg': 'authorized', + 'msg': 'interrupt', + # 'msg': 'authorized', 'nonce': nonce, + 'message': 'Foobar message', 'attributes': { 'urn:mace:dir:attribute-def:eduPersonEntitlement': [ uid, From c127771d065953f161116ce1e3f64dbc18c307a1 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Mon, 7 Apr 2025 15:06:29 +0200 Subject: [PATCH 12/38] Fix tests --- .../Filter/Command/SRAMInterruptFilter.php | 2 +- .../Sbs/Dto/AuthzRequest.php | 1 - .../Sbs/SbsClientInterface.php | 5 +++-- .../Command/SRAMInterruptFilterTest.php | 21 +++++++++++++++---- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index 49a74e4f95..a9d88da255 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -91,7 +91,7 @@ private function buildRequest(): AuthzRequest $attributes = $this->getResponseAttributes(); $id = $this->_request->getId(); - $user_id = $this->_collabPersonId; + $user_id = $this->_collabPersonId ?? ""; $eppn = $attributes['urn:mace:dir:attribute-def:eduPersonPrincipalName'][0] ?? ""; $continue_url = $this->_server->getUrl('SRAMInterruptService', '') . "?ID=$id"; $service_id = $this->_serviceProvider->entityId; diff --git a/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php index 85461004d6..766f94fa21 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php @@ -63,7 +63,6 @@ public static function create( $request = new self(); $request->userId = $userId; - $request->schacHomeOrganization = $sho; $request->eduPersonPrincipalName = $eppn; $request->continueUrl = $continueUrl; $request->serviceId = $serviceId; diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php index 509f2a754f..9c09f32674 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php @@ -25,12 +25,13 @@ interface SbsClientInterface { public const INTERRUPT = 'interrupt'; public const AUTHORIZED = 'authorized'; + public const ERROR = 'error'; public const VALID_MESSAGES = [self::INTERRUPT, self::AUTHORIZED]; - public function getInterruptLocationLink(string $nonce); + public function authz(AuthzRequest $request) : AuthzResponse; public function requestEntitlementsFor(EntitlementsRequest $request) : AuthzResponse; - public function authz(AuthzRequest $request) : AuthzResponse; + public function getInterruptLocationLink(string $nonce); } diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php index fff546a3f8..5f7d4bb5e6 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php @@ -94,10 +94,13 @@ public function testItAddsNonceWhenMessageInterrupt(): void $response = new AuthzResponse(); $response->msg = 'interrupt'; $response->nonce = 'hash123'; - $response->attributes = ['dummy' => 'attributes']; + $response->attributes = [ + 'dummy' => 'attributes', + ]; $expectedRequest = new AuthzRequest(); - $expectedRequest->userId = 'userIdValue'; + $expectedRequest->userId = ''; + $expectedRequest->eduPersonPrincipalName = ''; $expectedRequest->continueUrl = 'https://example.org?ID='; $expectedRequest->serviceId = 'spEntityId'; $expectedRequest->issuerId = 'idpEntityId'; @@ -106,6 +109,7 @@ public function testItAddsNonceWhenMessageInterrupt(): void ->withArgs(function ($args) use ($expectedRequest) { return $args->userId === $expectedRequest->userId + && $args->eduPersonPrincipalName === $expectedRequest->eduPersonPrincipalName && strpos($args->continueUrl, $expectedRequest->continueUrl) === 0 && $args->serviceId === $expectedRequest->serviceId && $args->issuerId === $expectedRequest->issuerId; @@ -151,10 +155,18 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void $response = new AuthzResponse(); $response->msg = 'authorized'; $response->nonce = 'hash123'; - $response->attributes = ['eduPersonEntitlement' => 'attributes', 'uid' => ['more' => ['attributes' => 'attributeValues']]]; + $response->attributes = [ + 'eduPersonEntitlement' => 'attributes', + 'uid' => [ + 'more' => [ + 'attributes' => 'attributeValues' + ] + ] + ]; $expectedRequest = new AuthzRequest(); - $expectedRequest->userId = 'userIdValue'; + $expectedRequest->userId = ''; + $expectedRequest->eduPersonPrincipalName = ''; $expectedRequest->continueUrl = 'https://example.org?ID='; $expectedRequest->serviceId = 'spEntityId'; $expectedRequest->issuerId = 'idpEntityId'; @@ -163,6 +175,7 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void ->withArgs(function ($args) use ($expectedRequest) { return $args->userId === $expectedRequest->userId + && $args->eduPersonPrincipalName === $expectedRequest->eduPersonPrincipalName && strpos($args->continueUrl, $expectedRequest->continueUrl) === 0 && $args->serviceId === $expectedRequest->serviceId && $args->issuerId === $expectedRequest->issuerId; From e5374fe165eea0b46d66c30acb6c55f859cf1358 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Wed, 9 Apr 2025 09:22:11 +0200 Subject: [PATCH 13/38] Reinstate attributes endpoint --- config/packages/parameters.yml.dist | 1 + sbs-stub/sbs.py | 5 +++-- .../EngineBlockBundle/Sbs/AuthzResponse.php | 4 ++++ src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php | 13 ++++++++++--- .../EngineBlockBundle/Sbs/SbsClientInterface.php | 4 ++-- .../EngineBlockBundle/Sbs/SbsClientTest.php | 3 ++- 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/config/packages/parameters.yml.dist b/config/packages/parameters.yml.dist index 56a2a54c6e..e0bc4001fd 100644 --- a/config/packages/parameters.yml.dist +++ b/config/packages/parameters.yml.dist @@ -317,6 +317,7 @@ parameters: sram.api_token: xxx sram.base_url: 'https://engine.dev.openconext.local/functional-testing/' sram.authz_location: authz + sram.attributes_location: authz sram.interrupt_location: interrupt sram.verify_peer: false sram.allowed_attributes: diff --git a/sbs-stub/sbs.py b/sbs-stub/sbs.py index bf5e4e2871..848457d465 100755 --- a/sbs-stub/sbs.py +++ b/sbs-stub/sbs.py @@ -52,6 +52,7 @@ def authz(): body = { 'msg': 'interrupt', # 'msg': 'authorized', + # 'msg': 'error', 'nonce': nonce, 'message': 'Foobar message', 'attributes': { @@ -82,9 +83,9 @@ def interrupt(): return response -@app.route('/api/users/attributes', methods=['POST']) +@app.route('/api/users/attributes_eb', methods=['POST']) def attributes(): - logging.debug('-> /api/users/attributes') + logging.debug('-> /api/users/attributes_eb') debug(request) nonce = request.json.get('nonce') diff --git a/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php index d17fea5e6e..d42b16a717 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php @@ -55,6 +55,10 @@ public static function fromData(array $jsonData) : AuthzResponse throw new InvalidSbsResponseException('Key: "attributes" was not found in the SBS response'); } + if (($jsonData['msg'] === SbsClientInterface::ERROR)) { + throw new InvalidSbsResponseException('SBS returned an error: ' . $jsonData['message']); + } + $response = new self; $response->msg = $jsonData['msg']; $response->nonce = $jsonData['nonce'] ?? null; diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php index 1b2cf971df..aba6a3a7e2 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php @@ -50,6 +50,11 @@ final class SbsClient implements SbsClientInterface */ private $authzLocation; + /** + * @var string + */ + private $attributesLocation; + /** * @var bool */ @@ -60,6 +65,7 @@ public function __construct( HttpClient $httpClient, string $sbsBaseUrl, string $authzLocation, + string $attributesLocation, string $interruptLocation, string $apiToken, bool $verifyPeer @@ -67,6 +73,7 @@ public function __construct( $this->httpClient = $httpClient; $this->sbsBaseUrl = $sbsBaseUrl; $this->authzLocation = $authzLocation; + $this->attributesLocation = $attributesLocation; $this->interruptLocation = $interruptLocation; $this->apiToken = $apiToken; $this->verifyPeer = $verifyPeer; @@ -90,11 +97,11 @@ public function authz(AuthzRequest $request): AuthzResponse } // Entitlements use authzLocation !! - public function requestEntitlementsFor(EntitlementsRequest $request): AuthzResponse + public function requestEntitlementsFor(EntitlementsRequest $request): EntitlementsResponse { $jsonData = $this->httpClient->post( json_encode($request), - $this->authzLocation, + $this->attributesLocation, [], $this->requestHeaders(), $this->verifyPeer @@ -104,7 +111,7 @@ public function requestEntitlementsFor(EntitlementsRequest $request): AuthzRespo throw new InvalidSbsResponseException('Received non-array from SBS server'); } - return AuthzResponse::fromData($jsonData); + return EntitlementsResponse::fromData($jsonData); } private function requestHeaders(): array diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php index 9c09f32674..2f15c5ad4f 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php @@ -27,11 +27,11 @@ interface SbsClientInterface public const AUTHORIZED = 'authorized'; public const ERROR = 'error'; - public const VALID_MESSAGES = [self::INTERRUPT, self::AUTHORIZED]; + public const VALID_MESSAGES = [self::INTERRUPT, self::AUTHORIZED, self::ERROR]; public function authz(AuthzRequest $request) : AuthzResponse; - public function requestEntitlementsFor(EntitlementsRequest $request) : AuthzResponse; + public function requestEntitlementsFor(EntitlementsRequest $request) : EntitlementsResponse; public function getInterruptLocationLink(string $nonce); } diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php index b3a3b7fef3..6c4c9640dd 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php @@ -42,6 +42,7 @@ protected function setUp(): void $this->httpClient, 'https://sbs.example.com', '/authz', + '/authz', '/interrupt', 'Bearer test_token', true @@ -96,6 +97,6 @@ public function testRequestEntitlementsFor(): void $entitlementsResponse = $this->sbsClient->requestEntitlementsFor($requestMock); - $this->assertInstanceOf(AuthzResponse::class, $entitlementsResponse); + $this->assertInstanceOf(EntitlementsResponse::class, $entitlementsResponse); } } From e911fa840cf92f552a229198bbd15430bf5a2184 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Fri, 11 Apr 2025 11:31:12 +0200 Subject: [PATCH 14/38] Rename entitlements to attributes --- .../functional_testing/functional_testing.yml | 6 +++--- .../Corto/Module/Service/SRAMInterrupt.php | 6 +++--- ...ementsResponse.php => AttributesResponse.php} | 8 ++++---- ...tlementsRequest.php => AttributesRequest.php} | 4 ++-- .../EngineBlockBundle/Sbs/SbsClient.php | 8 ++++---- .../EngineBlockBundle/Sbs/SbsClientInterface.php | 4 ++-- .../Controllers/SbsController.php | 6 +++--- .../Features/Context/EngineBlockContext.php | 12 ++++++------ .../Features/SbsFlowIntegration.feature | 6 +++--- .../Fixtures/SbsClientStateManager.php | 12 ++++++------ ...sponseTest.php => AttributesResponseTest.php} | 16 ++++++++-------- .../EngineBlockBundle/Sbs/AuthzResponseTest.php | 4 ++-- .../EngineBlockBundle/Sbs/SbsClientTest.php | 12 ++++++------ 13 files changed, 52 insertions(+), 52 deletions(-) rename src/OpenConext/EngineBlockBundle/Sbs/{EntitlementsResponse.php => AttributesResponse.php} (84%) rename src/OpenConext/EngineBlockBundle/Sbs/Dto/{EntitlementsRequest.php => AttributesRequest.php} (93%) rename tests/unit/OpenConext/EngineBlockBundle/Sbs/{EntitlementsResponseTest.php => AttributesResponseTest.php} (74%) diff --git a/config/routes/functional_testing/functional_testing.yml b/config/routes/functional_testing/functional_testing.yml index bbf4c4d814..0ef35f93e5 100644 --- a/config/routes/functional_testing/functional_testing.yml +++ b/config/routes/functional_testing/functional_testing.yml @@ -80,7 +80,7 @@ functional_testing_sram_interrupt: defaults: _controller: engineblock.functional_test.controller.sbs::interruptAction -functional_testing_sram_entitlements: - path: "/entitlements" +functional_testing_sram_attributes: + path: "/attributes" defaults: - _controller: engineblock.functional_test.controller.sbs::entitlementsAction + _controller: engineblock.functional_test.controller.sbs::attributesAction diff --git a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php index eb8b744a5c..0760dfb3b1 100644 --- a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php +++ b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php @@ -19,7 +19,7 @@ use OpenConext\EngineBlock\Service\AuthenticationStateHelperInterface; use OpenConext\EngineBlock\Service\ProcessingStateHelperInterface; use OpenConext\EngineBlock\Stepup\StepupGatewayCallOutHelper; -use OpenConext\EngineBlockBundle\Sbs\Dto\EntitlementsRequest; +use OpenConext\EngineBlockBundle\Sbs\Dto\AttributesRequest; use OpenConext\EngineBlockBundle\Sbs\SbsAttributeMerger; use Symfony\Component\HttpFoundation\Request; @@ -93,8 +93,8 @@ public function serve($serviceName, Request $httpRequest) $attributes = $receivedResponse->getAssertion()->getAttributes(); $nonce = $receivedResponse->getSRAMInterruptNonce(); - $request = EntitlementsRequest::create($nonce); - $interruptResponse = $this->getSbsClient()->requestEntitlementsFor($request); + $request = AttributesRequest::create($nonce); + $interruptResponse = $this->getSbsClient()->requestAttributesFor($request); if (!empty($interruptResponse->attributes)) { $attributes = $this->_sbsAttributeMerger->mergeAttributes($attributes, $interruptResponse->attributes); diff --git a/src/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponse.php b/src/OpenConext/EngineBlockBundle/Sbs/AttributesResponse.php similarity index 84% rename from src/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponse.php rename to src/OpenConext/EngineBlockBundle/Sbs/AttributesResponse.php index 5cce69f5fb..12e92aaf2b 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponse.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/AttributesResponse.php @@ -20,20 +20,20 @@ use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; -class EntitlementsResponse +class AttributesResponse { /** * @var array */ public $attributes; - public static function fromData(array $jsonData) : EntitlementsResponse + public static function fromData(array $jsonData) : AttributesResponse { if (!isset($jsonData['attributes'])) { - throw new InvalidSbsResponseException('Key: Attributes was not found in the SBS entitlements response'); + throw new InvalidSbsResponseException('Key: Attributes was not found in the SBS attributes response'); } if (!is_array($jsonData['attributes'])) { - throw new InvalidSbsResponseException('Key: Attributes was not found in the SBS entitlements response'); + throw new InvalidSbsResponseException('Key: Attributes was not an array in the SBS attributes response'); } $response = new self; diff --git a/src/OpenConext/EngineBlockBundle/Sbs/Dto/EntitlementsRequest.php b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AttributesRequest.php similarity index 93% rename from src/OpenConext/EngineBlockBundle/Sbs/Dto/EntitlementsRequest.php rename to src/OpenConext/EngineBlockBundle/Sbs/Dto/AttributesRequest.php index 0231b1f082..567988cde6 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/Dto/EntitlementsRequest.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AttributesRequest.php @@ -21,7 +21,7 @@ use JsonSerializable; use OpenConext\EngineBlock\Assert\Assertion; -class EntitlementsRequest implements JsonSerializable +class AttributesRequest implements JsonSerializable { /** * @var string @@ -30,7 +30,7 @@ class EntitlementsRequest implements JsonSerializable public static function create( string $nonce - ) : EntitlementsRequest { + ) : AttributesRequest { Assertion::string($nonce, 'The nonce must be a string.'); $request = new self(); diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php index aba6a3a7e2..87c51dc87a 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php @@ -20,7 +20,7 @@ use OpenConext\EngineBlock\Http\HttpClient; use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; -use OpenConext\EngineBlockBundle\Sbs\Dto\EntitlementsRequest; +use OpenConext\EngineBlockBundle\Sbs\Dto\AttributesRequest; use OpenConext\EngineBlockBundle\Sbs\Dto\AuthzRequest; final class SbsClient implements SbsClientInterface @@ -96,8 +96,8 @@ public function authz(AuthzRequest $request): AuthzResponse return AuthzResponse::fromData($jsonData); } - // Entitlements use authzLocation !! - public function requestEntitlementsFor(EntitlementsRequest $request): EntitlementsResponse + // Attributes use authzLocation !! + public function requestAttributesFor(AttributesRequest $request): AttributesResponse { $jsonData = $this->httpClient->post( json_encode($request), @@ -111,7 +111,7 @@ public function requestEntitlementsFor(EntitlementsRequest $request): Entitlemen throw new InvalidSbsResponseException('Received non-array from SBS server'); } - return EntitlementsResponse::fromData($jsonData); + return AttributesResponse::fromData($jsonData); } private function requestHeaders(): array diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php index 2f15c5ad4f..6ecc1ca01c 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php @@ -18,7 +18,7 @@ namespace OpenConext\EngineBlockBundle\Sbs; -use OpenConext\EngineBlockBundle\Sbs\Dto\EntitlementsRequest; +use OpenConext\EngineBlockBundle\Sbs\Dto\AttributesRequest; use OpenConext\EngineBlockBundle\Sbs\Dto\AuthzRequest; interface SbsClientInterface @@ -31,7 +31,7 @@ interface SbsClientInterface public function authz(AuthzRequest $request) : AuthzResponse; - public function requestEntitlementsFor(EntitlementsRequest $request) : EntitlementsResponse; + public function requestAttributesFor(AttributesRequest $request) : AttributesResponse; public function getInterruptLocationLink(string $nonce); } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php index 7b7f5d4cfe..02be36d57f 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php @@ -71,13 +71,13 @@ public function interruptAction(Request $request): Response } /** - * The endpoint called by Engine to fetch the entitlements after the browser has made a trip to the interrupt action + * The endpoint called by Engine to fetch the attributes after the browser has made a trip to the interrupt action * and has returned to the continue_url */ - public function entitlementsAction() + public function attributesAction() { return new JsonResponse([ - 'attributes' => $this->sbsClientStateManager->getPreparedEntitlementsResponse() + 'attributes' => $this->sbsClientStateManager->getPreparedAttributesResponse() ]); } } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php index 4b9b129423..82f473253c 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php @@ -790,19 +790,19 @@ public function authzWillReturnInvalidAttributes(): void } /** - * @Given /^the sbs server will return valid entitlements$/ + * @Given /^the sbs server will return valid attributes/ */ - public function entitlementsWillReturnValidAttributes(): void + public function attributesWillReturnValidAttributes(): void { - $this->sbsClientStateManager->prepareEntitlementsResponse($this->sbsClientStateManager->getValidMockAttributes()); + $this->sbsClientStateManager->prepareAttributesResponse($this->sbsClientStateManager->getValidMockAttributes()); } /** - * @Given /^the sbs server will return invalid entitlements$/ + * @Given /^the sbs server will return invalid attributes/ */ - public function entitlementsWillReturnInvalidAttributes(): void + public function attributesWillReturnInvalidAttributes(): void { - $this->sbsClientStateManager->prepareEntitlementsResponse(['foo' => ['bar' => 'baz']]); + $this->sbsClientStateManager->prepareAttributesResponse(['foo' => ['bar' => 'baz']]); } /** diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature index a395a53b4d..d105c43ed8 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature @@ -14,7 +14,7 @@ Feature: Given the SP "SSO-SP" requires SRAM collaboration And feature "eb.feature_enable_sram_interrupt" is enabled And the sbs server will trigger the "interrupt" authz flow when called -# And the sbs server will return valid entitlements ## @TODO Remove this endpoint call altogether? +# And the sbs server will return valid attributes ## @TODO Remove this endpoint call altogether? When I log in at "SSO-SP" And I pass through EngineBlock And I pass through the IdP @@ -58,11 +58,11 @@ Feature: Then the url should match "/feedback/unknown-error" And the response should contain "Logging in has failed" - Scenario: If the SBS authz check returns an 'interrupt' response, and the entitlements call to sbs returns an invalid response + Scenario: If the SBS authz check returns an 'interrupt' response, and the attributes call to sbs returns an invalid response Given the SP "SSO-SP" requires SRAM collaboration And feature "eb.feature_enable_sram_interrupt" is enabled And the sbs server will trigger the "interrupt" authz flow when called -# And the sbs server will return invalid entitlements ## @TODO remove entitlements call? +# And the sbs server will return invalid attributes ## @TODO remove attributes call? When I log in at "SSO-SP" And I pass through EngineBlock And I pass through the IdP diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php index 5b56c60265..0363b69327 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php @@ -37,7 +37,7 @@ class SbsClientStateManager /** * @var array */ - private $entitlements = []; + private $attributes = []; public function __construct( AbstractDataStore $dataStore @@ -87,22 +87,22 @@ public function getValidMockAttributes(): array ]; } - public function prepareEntitlementsResponse(array $entitlements): void + public function prepareAttributesResponse(array $attributes): void { - $this->entitlements = $entitlements; + $this->attributes = $attributes; $this->save(); } - public function getPreparedEntitlementsResponse(): array + public function getPreparedAttributesResponse(): array { - return $this->dataStore->load()['entitlements']; + return $this->dataStore->load()['attributes']; } private function save() { $this->dataStore->save([ 'authz' => $this->authz, - 'entitlements' => $this->entitlements, + 'attributes' => $this->attributes, ]); } } diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponseTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/AttributesResponseTest.php similarity index 74% rename from tests/unit/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponseTest.php rename to tests/unit/OpenConext/EngineBlockBundle/Sbs/AttributesResponseTest.php index 839be3b32d..8f50d759d9 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Sbs/EntitlementsResponseTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/AttributesResponseTest.php @@ -19,36 +19,36 @@ namespace OpenConext\EngineBlockBundle\Tests; use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; -use OpenConext\EngineBlockBundle\Sbs\EntitlementsResponse; +use OpenConext\EngineBlockBundle\Sbs\AttributesResponse; use PHPUnit\Framework\TestCase; -class EntitlementsResponseTest extends TestCase +class AttributesResponseTest extends TestCase { public function testFromDataValidAttributes() { $jsonData = ['attributes' => ['key1' => 'value1', 'key2' => 'value2']]; - $response = EntitlementsResponse::fromData($jsonData); + $response = AttributesResponse::fromData($jsonData); - $this->assertInstanceOf(EntitlementsResponse::class, $response); + $this->assertInstanceOf(AttributesResponse::class, $response); $this->assertEquals($jsonData['attributes'], $response->attributes); } public function testFromDataMissingAttributes() { $this->expectException(InvalidSbsResponseException::class); - $this->expectExceptionMessage('Key: Attributes was not found in the SBS entitlements response'); + $this->expectExceptionMessage('Key: Attributes was not found in the SBS attributes response'); $jsonData = ['someOtherKey' => []]; - EntitlementsResponse::fromData($jsonData); + AttributesResponse::fromData($jsonData); } public function testFromDataAttributesNotArray() { $this->expectException(InvalidSbsResponseException::class); - $this->expectExceptionMessage('Key: Attributes was not found in the SBS entitlements response'); + $this->expectExceptionMessage('Key: Attributes was not an array in the SBS attributes response'); $jsonData = ['attributes' => 'not_an_array']; - EntitlementsResponse::fromData($jsonData); + AttributesResponse::fromData($jsonData); } } diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php index c849d648c4..2bfe86913a 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php @@ -23,8 +23,8 @@ use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; use OpenConext\EngineBlockBundle\Sbs\AuthzResponse; use OpenConext\EngineBlockBundle\Sbs\Dto\AuthzRequest; -use OpenConext\EngineBlockBundle\Sbs\Dto\EntitlementsRequest; -use OpenConext\EngineBlockBundle\Sbs\EntitlementsResponse; +use OpenConext\EngineBlockBundle\Sbs\Dto\AttributesRequest; +use OpenConext\EngineBlockBundle\Sbs\AttributesResponse; use OpenConext\EngineBlockBundle\Sbs\SbsClient; use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; use PHPUnit\Framework\TestCase; diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php index 6c4c9640dd..153921bcfe 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php @@ -22,8 +22,8 @@ use OpenConext\EngineBlock\Http\HttpClient; use OpenConext\EngineBlockBundle\Sbs\AuthzResponse; use OpenConext\EngineBlockBundle\Sbs\Dto\AuthzRequest; -use OpenConext\EngineBlockBundle\Sbs\Dto\EntitlementsRequest; -use OpenConext\EngineBlockBundle\Sbs\EntitlementsResponse; +use OpenConext\EngineBlockBundle\Sbs\Dto\AttributesRequest; +use OpenConext\EngineBlockBundle\Sbs\AttributesResponse; use OpenConext\EngineBlockBundle\Sbs\SbsClient; use PHPUnit\Framework\TestCase; @@ -73,9 +73,9 @@ public function testAuthz(): void $this->assertInstanceOf(AuthzResponse::class, $authzResponse); } - public function testRequestEntitlementsFor(): void + public function testRequestAttributesFor(): void { - $requestMock = $this->createMock(EntitlementsRequest::class); + $requestMock = $this->createMock(AttributesRequest::class); $jsonResponse = [ 'msg' => 'authorized', 'attributes' => ['name' => 'value'] @@ -95,8 +95,8 @@ public function testRequestEntitlementsFor(): void ) ->willReturn($jsonResponse); - $entitlementsResponse = $this->sbsClient->requestEntitlementsFor($requestMock); + $attributesResponse = $this->sbsClient->requestAttributesFor($requestMock); - $this->assertInstanceOf(EntitlementsResponse::class, $entitlementsResponse); + $this->assertInstanceOf(AttributesResponse::class, $attributesResponse); } } From 5658873865aac3ae9b58d1b8a50b8ad3289e0b89 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Mon, 14 Apr 2025 14:22:57 +0200 Subject: [PATCH 15/38] Better SBS interrupt reason logging --- .../EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index a9d88da255..ed336fe23c 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -55,7 +55,7 @@ public function execute(): void $interruptResponse = $this->getSbsClient()->authz($request); if ($interruptResponse->msg === 'interrupt') { - $log->notice("SBS: " . $interruptResponse->message); + $log->info("SBS interrupt reason: " . $interruptResponse->message); $this->_response->setSRAMInterruptNonce($interruptResponse->nonce); } elseif ($interruptResponse->msg === 'authorized' && !empty($interruptResponse->attributes)) { $this->_responseAttributes = $this->getSbsAttributeMerger()->mergeAttributes($this->_responseAttributes, $interruptResponse->attributes); From 9ad17463e46128952ab8470548a9de58ab64ba4d Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Mon, 14 Apr 2025 14:52:57 +0200 Subject: [PATCH 16/38] Reinstate attributes endpoint test --- config/packages/parameters.yml.dist | 10 +++++----- .../Controllers/SbsController.php | 4 +--- .../Features/Context/EngineBlockContext.php | 4 ++-- .../Features/SbsFlowIntegration.feature | 9 ++++----- .../Fixtures/SbsClientStateManager.php | 17 +++++++++-------- .../Filter/Command/SRAMInterruptFilterTest.php | 14 +++----------- 6 files changed, 24 insertions(+), 34 deletions(-) diff --git a/config/packages/parameters.yml.dist b/config/packages/parameters.yml.dist index e0bc4001fd..95142eadb4 100644 --- a/config/packages/parameters.yml.dist +++ b/config/packages/parameters.yml.dist @@ -317,11 +317,11 @@ parameters: sram.api_token: xxx sram.base_url: 'https://engine.dev.openconext.local/functional-testing/' sram.authz_location: authz - sram.attributes_location: authz + sram.attributes_location: attributes sram.interrupt_location: interrupt sram.verify_peer: false sram.allowed_attributes: - - eduPersonEntitlement - - eduPersonPrincipalName - - uid - - sshkey + - 'urn:mace:dir:attribute-def:eduPersonEntitlement' + - 'urn:mace:dir:attribute-def:eduPersonPrincipalName' + - 'urn:mace:dir:attribute-def:uid' + - 'urn:oid:1.3.6.1.4.1.24552.500.1.1.1.13' diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php index 02be36d57f..dd6145c262 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php @@ -76,8 +76,6 @@ public function interruptAction(Request $request): Response */ public function attributesAction() { - return new JsonResponse([ - 'attributes' => $this->sbsClientStateManager->getPreparedAttributesResponse() - ]); + return new JsonResponse($this->sbsClientStateManager->getPreparedAttributesResponse()); } } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php index 82f473253c..5c087b4866 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php @@ -786,7 +786,7 @@ public function primeAuthzResponse(string $msg): void */ public function authzWillReturnInvalidAttributes(): void { - $this->sbsClientStateManager->prepareAuthzResponse(SbsClientInterface::AUTHORIZED, ['foo' => ['bar' => 'baz']]); + $this->sbsClientStateManager->prepareAuthzResponse(SbsClientInterface::AUTHORIZED, ['attributes' => ['foo' => ['bar' => 'baz']]]); } /** @@ -802,7 +802,7 @@ public function attributesWillReturnValidAttributes(): void */ public function attributesWillReturnInvalidAttributes(): void { - $this->sbsClientStateManager->prepareAttributesResponse(['foo' => ['bar' => 'baz']]); + $this->sbsClientStateManager->prepareAttributesResponse(['msg' => 'error', 'message' => 'something went wrong']); } /** diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature index d105c43ed8..03f00edbd5 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature @@ -14,12 +14,11 @@ Feature: Given the SP "SSO-SP" requires SRAM collaboration And feature "eb.feature_enable_sram_interrupt" is enabled And the sbs server will trigger the "interrupt" authz flow when called -# And the sbs server will return valid attributes ## @TODO Remove this endpoint call altogether? + And the sbs server will return valid attributes When I log in at "SSO-SP" And I pass through EngineBlock And I pass through the IdP Then the url should match "/functional-testing/interrupt" - Given the sbs server will trigger the "authorized" authz flow when called And I pass through SBS Then the url should match "/authentication/idp/process-sraminterrupt" And the response should contain "Review your information that will be shared." @@ -28,8 +27,8 @@ Feature: When I give my consent And I pass through EngineBlock Then the url should match "/functional-testing/SSO-SP/acs" - Then the response should contain "sshkey" Then the response should contain "ssh_key1" + And the response should contain "ssh_key2" Scenario: If the SBS authz check returns 'authorized', the attributes are merged, and the browser is not redirected. Given the SP "SSO-SP" requires SRAM collaboration @@ -45,8 +44,8 @@ Feature: When I give my consent And I pass through EngineBlock Then the url should match "/functional-testing/SSO-SP/acs" - Then the response should contain "sshkey" Then the response should contain "ssh_key1" + And the response should contain "ssh_key2" Scenario: If the SBS authz check returns an invalid response, the flow is halted. Given the SP "SSO-SP" requires SRAM collaboration @@ -62,7 +61,7 @@ Feature: Given the SP "SSO-SP" requires SRAM collaboration And feature "eb.feature_enable_sram_interrupt" is enabled And the sbs server will trigger the "interrupt" authz flow when called -# And the sbs server will return invalid attributes ## @TODO remove attributes call? + And the sbs server will return invalid attributes When I log in at "SSO-SP" And I pass through EngineBlock And I pass through the IdP diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php index 0363b69327..768dd8bbc5 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php @@ -55,12 +55,11 @@ public function prepareAuthzResponse(string $msg, ?array $attributes = null): vo } elseif ($msg === SbsClientInterface::AUTHORIZED) { $this->authz = [ 'msg' => SbsClientInterface::AUTHORIZED, - 'attributes' => $attributes ?? $this->getValidMockAttributes(), ]; - } elseif ($msg === 'error') { + $this->authz += $attributes ?? $this->getValidMockAttributes(); + } elseif ($msg === SbsClientInterface::ERROR) { $this->authz = [ - 'msg' => 'gibberish', - 'nonce' => 'my-nonce', + 'msg' => SbsClientInterface::ERROR, ]; } else { throw new InvalidArgumentException(sprintf('"%s" is not a valid authz message.', $msg)); @@ -80,10 +79,12 @@ public function getPreparedAuthzResponse(): array public function getValidMockAttributes(): array { return [ - "eduPersonEntitlement" => ["user_aff1@test.sram.surf.nl", "user_aff2@test.sram.surf.nl"], - "eduPersonPrincipalName" => ["test_user@test.sram.surf.nl"], - "uid" => ["test_user"], - "sshkey" => ["ssh_key1", "ssh_key2"] + "attributes" => [ + "urn:mace:dir:attribute-def:eduPersonEntitlement" => ["user_aff1@test.sram.surf.nl", "user_aff2@test.sram.surf.nl"], + "urn:mace:dir:attribute-def:eduPersonPrincipalName" => ["test_user@test.sram.surf.nl"], + "urn:mace:dir:attribute-def:uid" => ["test_user"], + "urn:oid:1.3.6.1.4.1.24552.500.1.1.1.13" => ["ssh_key1", "ssh_key2"], + ], ]; } diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php index 5f7d4bb5e6..3ec0a6032a 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php @@ -156,12 +156,8 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void $response->msg = 'authorized'; $response->nonce = 'hash123'; $response->attributes = [ - 'eduPersonEntitlement' => 'attributes', - 'uid' => [ - 'more' => [ - 'attributes' => 'attributeValues' - ] - ] + 'urn:mace:dir:attribute-def:uid' => ['userIdValue'], + 'urn:mace:dir:attribute-def:eduPersonEntitlement' => 'attributes', ]; $expectedRequest = new AuthzRequest(); @@ -194,11 +190,7 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void $expectedAttributes = [ 'urn:mace:dir:attribute-def:uid' => ['userIdValue'], - 'eduPersonEntitlement' => 'attributes', - 'uid' => [ - 'more' => - ['attributes' => 'attributeValues'] - ] + 'urn:mace:dir:attribute-def:eduPersonEntitlement' => 'attributes', ]; $sramFilter->execute(); From dca981587a374cec1eeb043146889d7fb92da2e2 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Thu, 17 Apr 2025 16:56:13 +0200 Subject: [PATCH 17/38] Refactoring attempt --- .../EngineBlock/Application/DiContainer.php | 3 +- library/EngineBlock/Corto/Filter/Input.php | 2 + .../Corto/Module/Service/SRAMInterrupt.php | 49 +------------------ library/EngineBlock/Corto/ProxyServer.php | 10 +--- 4 files changed, 8 insertions(+), 56 deletions(-) diff --git a/library/EngineBlock/Application/DiContainer.php b/library/EngineBlock/Application/DiContainer.php index 556d4055f6..85367ae01a 100644 --- a/library/EngineBlock/Application/DiContainer.php +++ b/library/EngineBlock/Application/DiContainer.php @@ -27,6 +27,7 @@ use OpenConext\EngineBlock\Stepup\StepupGatewayCallOutHelper; use OpenConext\EngineBlock\Validator\AllowedSchemeValidator; use OpenConext\EngineBlockBundle\Sbs\SbsAttributeMerger; +use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface; use Symfony\Component\Mailer\MailerInterface; use Twig\Environment; @@ -315,7 +316,7 @@ public function getSbsAttributeMerger(): SbsAttributeMerger return $this->container->get('engineblock.sbs.attribute_merger'); } - public function getSbsClient(): \OpenConext\EngineBlockBundle\Sbs\SbsClientInterface + public function getSbsClient(): SbsClientInterface { return $this->container->get('engineblock.sbs.sbs_client'); } diff --git a/library/EngineBlock/Corto/Filter/Input.php b/library/EngineBlock/Corto/Filter/Input.php index 3cf124911f..4db94f46f4 100644 --- a/library/EngineBlock/Corto/Filter/Input.php +++ b/library/EngineBlock/Corto/Filter/Input.php @@ -90,6 +90,8 @@ public function getCommands() $diContainer->getAttributeAggregationClient() ), + // Check if we need to callout to SRAM to enforce AUP's + // Add SRAM attributes if not new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(), // Check if the Policy Decision Point needs to be consulted for this request diff --git a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php index 0760dfb3b1..fc9698d36f 100644 --- a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php +++ b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php @@ -105,56 +105,11 @@ public function serve($serviceName, Request $httpRequest) * Continue to Consent/StepUp */ - // Flush log if SP or IdP has additional logging enabled - $issuer = $receivedResponse->getIssuer() ? $receivedResponse->getIssuer()->getValue() : ''; - $idp = $this->_server->getRepository()->fetchIdentityProviderByEntityId($issuer); - - if ($receivedRequest->isDebugRequest()) { - $sp = $this->_server->getEngineSpRole($this->_server); - } else { - $issuer = $receivedRequest->getIssuer() ? $receivedRequest->getIssuer()->getValue() : ''; - $sp = $this->_server->getRepository()->fetchServiceProviderByEntityId($issuer); - } - - // When dealing with an SP that acts as a trusted proxy, we should use the proxying SP and not the proxy itself. - if ($sp->getCoins()->isTrustedProxy()) { - // Overwrite the trusted proxy SP instance with that of the SP that uses the trusted proxy. - $sp = $this->_server->findOriginalServiceProvider($receivedRequest, $this->_server->getLogger()); - } - - $pdpLoas = $receivedResponse->getPdpRequestedLoas(); - $loaRepository = $application->getDiContainer()->getLoaRepository(); - $authnRequestLoas = $receivedRequest->getStepupObligations($loaRepository->getStepUpLoas()); - - $shouldUseStepup = $this->_stepupGatewayCallOutHelper->shouldUseStepup($idp, $sp, $authnRequestLoas, $pdpLoas); - - // Goto consent if no Stepup authentication is needed - if (!$shouldUseStepup) { - $this->_server->sendConsentAuthenticationRequest($receivedResponse, $receivedRequest, $nextProcessStep->getRole(), $this->_authenticationStateHelper->getAuthenticationState()); + if ($this->_server->handleConsentAuthenticationCallout($receivedResponse, $receivedRequest)) { return; } - $this->_server->getLogger()->info('Handle Stepup authentication callout'); - - // Add Stepup authentication step - $currentProcessStep = $this->_processingStateHelper->addStep( - $receivedRequest->getId(), - ProcessingStateHelperInterface::STEP_STEPUP, - $application->getDiContainer()->getStepupIdentityProvider($this->_server), - $receivedResponse - ); - - // Get mapped AuthnClassRef and get NameId - $nameId = clone $receivedResponse->getNameId(); - $authnClassRef = $this->_stepupGatewayCallOutHelper->getStepupLoa($idp, $sp, $authnRequestLoas, $pdpLoas); - - $this->_server->sendStepupAuthenticationRequest( - $receivedRequest, - $currentProcessStep->getRole(), - $authnClassRef, - $nameId, - $sp->getCoins()->isStepupForceAuthn() - ); + $this->_server->handleStepupAuthenticationCallout($receivedResponse, $receivedRequest); } private function getSbsClient() diff --git a/library/EngineBlock/Corto/ProxyServer.php b/library/EngineBlock/Corto/ProxyServer.php index 26db1d7559..5e6fe09a30 100644 --- a/library/EngineBlock/Corto/ProxyServer.php +++ b/library/EngineBlock/Corto/ProxyServer.php @@ -27,8 +27,10 @@ use OpenConext\EngineBlock\Metadata\Service; use OpenConext\EngineBlock\Metadata\TransparentMfaEntity; use OpenConext\EngineBlock\Stepup\StepupGsspUserAttributeExtension; +use OpenConext\EngineBlock\Metadata\X509\KeyPairFactory; use OpenConext\EngineBlockBundle\Authentication\AuthenticationState; use OpenConext\EngineBlockBundle\Exception\UnknownKeyIdException; +use OpenConext\EngineBlock\Service\ProcessingStateHelperInterface; use OpenConext\Value\Saml\Entity; use OpenConext\Value\Saml\EntityId; use OpenConext\Value\Saml\EntityType; @@ -577,14 +579,6 @@ function sendConsentAuthenticationRequest( $this->_server->getBindingsModule()->send($newResponse, $serviceProvider); } - function sendSRAMInterruptRequest($response, $request) { - $nonce = $response->getSRAMInterruptNonce(); - - $sbsClient = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getSbsClient(); - $redirect_url = $sbsClient->getInterruptLocationLink($nonce); - $this->redirect($redirect_url, ''); - } - //////// RESPONSE HANDLING //////// public function createProxyCountExceededResponse(EngineBlock_Saml2_AuthnRequestAnnotationDecorator $request) From 6992b69fd3b0e824183d5321b3f7a2cc7db66d45 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Tue, 22 Apr 2025 08:51:55 +0200 Subject: [PATCH 18/38] SRAM enabled false by default --- config/packages/parameters.yml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/packages/parameters.yml.dist b/config/packages/parameters.yml.dist index 95142eadb4..cfc3509c5c 100644 --- a/config/packages/parameters.yml.dist +++ b/config/packages/parameters.yml.dist @@ -223,7 +223,7 @@ parameters: feature_enable_idp_initiated_flow: true feature_stepup_sfo_override_engine_entityid: false feature_stepup_send_user_attributes: false - feature_enable_sram_interrupt: true + feature_enable_sram_interrupt: false ########################################################################################## ## PROFILE SETTINGS From 73a154aef04448a05a22cf0956dc23758cb7782c Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Tue, 6 May 2025 14:34:18 +0200 Subject: [PATCH 19/38] Fix SRAM RP entityId behind oidcng trusted proxy --- .../Filter/Command/SRAMInterruptFilter.php | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index ed336fe23c..8d7a226f08 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -45,12 +45,26 @@ public function execute(): void return; } - if ($this->_serviceProvider->getCoins()->collabEnabled() === false) { + $serviceProvider = EngineBlock_SamlHelper::findRequesterServiceProvider( + $this->_serviceProvider, + $this->_request, + $this->_server->getRepository(), + $this->logger + ); + + if (!$serviceProvider) { + $serviceProvider = $this->_serviceProvider; + } + + if ($serviceProvider->getCoins()->collabEnabled() === false) { + $log->notice("No SBS interrupt for serviceProvider: " . $serviceProvider->entityId); return; } + $log->notice("SBS interrupt for serviceProvider: " . $serviceProvider->entityId); + try { - $request = $this->buildRequest(); + $request = $this->buildRequest($serviceProvider); $interruptResponse = $this->getSbsClient()->authz($request); @@ -86,7 +100,7 @@ private function getSbsAttributeMerger(): SbsAttributeMerger * @return AuthzRequest * @throws EngineBlock_Corto_ProxyServer_Exception */ - private function buildRequest(): AuthzRequest + private function buildRequest($serviceProvider): AuthzRequest { $attributes = $this->getResponseAttributes(); $id = $this->_request->getId(); @@ -94,7 +108,7 @@ private function buildRequest(): AuthzRequest $user_id = $this->_collabPersonId ?? ""; $eppn = $attributes['urn:mace:dir:attribute-def:eduPersonPrincipalName'][0] ?? ""; $continue_url = $this->_server->getUrl('SRAMInterruptService', '') . "?ID=$id"; - $service_id = $this->_serviceProvider->entityId; + $service_id = $serviceProvider->entityId; $issuer_id = $this->_identityProvider->entityId; return AuthzRequest::create( From be531d65fd25670d5ca2d7e093e5efaa3c94f509 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Tue, 6 May 2025 15:35:22 +0200 Subject: [PATCH 20/38] Fix SRAMInterruptFilterTest --- docker/docker-compose.yml | 2 +- .../Command/SRAMInterruptFilterTest.php | 33 ++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5c8f9bd3b8..3890d0d2b1 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -62,5 +62,5 @@ services: - ../theme:/theme volumes: - eb-mysql-data: + # eb-mysql-data: eb-mysql-test-data: diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php index 3ec0a6032a..42e7de295a 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php @@ -19,6 +19,7 @@ use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; +use OpenConext\EngineBlock\Metadata\MetadataRepository\MetadataRepositoryInterface; use OpenConext\EngineBlockBundle\Configuration\Feature; use OpenConext\EngineBlockBundle\Configuration\FeatureConfiguration; use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; @@ -34,6 +35,25 @@ class EngineBlock_Test_Corto_Filter_Command_SramInterruptFilterTest extends Test { use MockeryPHPUnitIntegration; + /** + * @var ServiceProvider + */ + private $sp; + + /** + * @var MetadataRepositoryInterface + */ + private $repository; + + public function setUp(): void + { + $this->sp = new ServiceProvider('SP'); + + $this->repository = Mockery::mock(MetadataRepositoryInterface::class); + $this->repository->shouldReceive('findServiceProviderByEntityId') + ->andReturn($this->sp); + } + public function testItDoesNothingIfFeatureFlagNotEnabled(): void { $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); @@ -51,6 +71,12 @@ public function testItDoesNothingIfSpDoesNotHaveCollabEnabled(): void { $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); + $server = Mockery::mock(EngineBlock_Corto_ProxyServer::class); + $server->shouldReceive('getRepository') + ->andReturn($this->repository); + + $sramFilter->setProxyServer($server); + $request = $this->mockRequest(); $sramFilter->setRequest($request); @@ -78,6 +104,8 @@ public function testItAddsNonceWhenMessageInterrupt(): void $server = Mockery::mock(EngineBlock_Corto_ProxyServer::class); $server->expects('getUrl')->andReturn('https://example.org'); + $server->shouldReceive('getRepository') + ->andReturn($this->repository); $sramFilter->setProxyServer($server); @@ -139,6 +167,8 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void $server = Mockery::mock(EngineBlock_Corto_ProxyServer::class); $server->expects('getUrl')->andReturn('https://example.org'); + $server->shouldReceive('getRepository') + ->andReturn($this->repository); $sramFilter->setProxyServer($server); @@ -206,7 +236,6 @@ public function testThrowsEngineBlockExceptionIfPolicyCannotBeChecked() $sbsClient = $this->mockSbsClient(); $sbsClient->expects('authz')->andThrows(new InvalidSbsResponseException('Server could not be reached.')); - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); $initialAttributes = ['urn:mace:dir:attribute-def:uid' => ['userIdValue']]; @@ -214,6 +243,8 @@ public function testThrowsEngineBlockExceptionIfPolicyCannotBeChecked() $server = Mockery::mock(EngineBlock_Corto_ProxyServer::class); $server->expects('getUrl')->andReturn('https://example.org'); + $server->shouldReceive('getRepository') + ->andReturn($this->repository); $sramFilter->setProxyServer($server); From 0812f8380ec68322029f2dda9593c4f5bb671411 Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Mon, 12 May 2025 11:27:16 +0200 Subject: [PATCH 21/38] Improve SBS response debugging --- src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php index 87c51dc87a..d9737c5b74 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php @@ -90,7 +90,7 @@ public function authz(AuthzRequest $request): AuthzResponse ); if (!is_array($jsonData)) { - throw new InvalidSbsResponseException('Received non-array from SBS server'); + throw new InvalidSbsResponseException('Received non-array from SBS server: ' . var_export($jsonData, true)); } return AuthzResponse::fromData($jsonData); @@ -108,7 +108,7 @@ public function requestAttributesFor(AttributesRequest $request): AttributesResp ); if (!is_array($jsonData)) { - throw new InvalidSbsResponseException('Received non-array from SBS server'); + throw new InvalidSbsResponseException('Received non-array from SBS server: ' . var_export($jsonData, true)); } return AttributesResponse::fromData($jsonData); From 3e51a7f741e49854b5d7c73a06f7509c7d988b2e Mon Sep 17 00:00:00 2001 From: Martin van Es Date: Mon, 12 May 2025 11:27:32 +0200 Subject: [PATCH 22/38] Don't restart test mariadb container --- docker/docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3890d0d2b1..7e729e0aa2 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -2,7 +2,6 @@ services: mariadb: image: mariadb:10.6 - restart: always container_name: eb-db-test environment: MYSQL_ROOT_PASSWORD: "root" From a6f6d904bb8790c38709aeba1e29f0110b0d6399 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Tue, 21 Oct 2025 14:19:53 +0200 Subject: [PATCH 23/38] Fixes after rebase --- config/packages/engineblock_features.yaml | 1 + config/services/ci/controllers.yml | 6 +++ config/services/services.yml | 30 +++++++++++++++ config/services_ci.yaml | 4 ++ .../Filter/Command/SRAMInterruptFilter.php | 2 +- .../Saml2/ResponseAnnotationDecorator.php | 1 - .../Configuration/FeatureConfiguration.php | 2 +- .../EngineBlockBundle/Sbs/AuthzResponse.php | 8 +++- .../Controllers/SbsController.php | 4 +- .../Features/Context/EngineBlockContext.php | 1 + .../Command/SRAMInterruptFilterTest.php | 38 ++++++++++--------- 11 files changed, 73 insertions(+), 24 deletions(-) diff --git a/config/packages/engineblock_features.yaml b/config/packages/engineblock_features.yaml index 467d8c1908..8e7ed423c1 100644 --- a/config/packages/engineblock_features.yaml +++ b/config/packages/engineblock_features.yaml @@ -15,3 +15,4 @@ parameters: eb.feature_enable_idp_initiated_flow: "%feature_enable_idp_initiated_flow%" eb.stepup.sfo.override_engine_entityid: "%feature_stepup_sfo_override_engine_entityid%" eb.stepup.send_user_attributes: "%feature_stepup_send_user_attributes%" + eb.feature_enable_sram_interrupt: "%feature_enable_sram_interrupt%" diff --git a/config/services/ci/controllers.yml b/config/services/ci/controllers.yml index dfc1a56123..27a303cbc7 100644 --- a/config/services/ci/controllers.yml +++ b/config/services/ci/controllers.yml @@ -50,3 +50,9 @@ services: - '@OpenConext\EngineBlock\Validator\UnsolicitedSsoRequestValidator' - '@OpenConext\EngineBlock\Service\AuthenticationStateHelper' - '@engineblock.functional_testing.fixture.features' + + engineblock.functional_test.controller.sbs: + class: OpenConext\EngineBlockFunctionalTestingBundle\Controllers\SbsController + arguments: + - '@engineblock.functional_testing.fixture.sbs_client_state_manager' + - '@engineblock.functional_testing.data_store.sbs_server_state' diff --git a/config/services/services.yml b/config/services/services.yml index 9896af82a0..a5afac948b 100644 --- a/config/services/services.yml +++ b/config/services/services.yml @@ -342,3 +342,33 @@ services: symfony.mailer: public: true alias: mailer + + engineblock.sbs.sbs_client: + class: OpenConext\EngineBlockBundle\Sbs\SbsClient + arguments: + - "@engineblock.sbs.http_client" + - "%sram.base_url%" + - "%sram.authz_location%" + - "%sram.attributes_location%" + - "%sram.interrupt_location%" + - "%sram.api_token%" + - "%sram.verify_peer%" + + engineblock.sbs.http_client: + class: OpenConext\EngineBlock\Http\HttpClient + arguments: + - "@engineblock.sbs.guzzle_http_client" + + engineblock.sbs.guzzle_http_client: + class: GuzzleHttp\Client + arguments: + - base_uri: "%sram.base_url%/" + options: + headers: + Authentication: "%sram.api_token%" + timeout: "%http_client.timeout%" + + engineblock.sbs.attribute_merger: + class: OpenConext\EngineBlockBundle\Sbs\SbsAttributeMerger + arguments: + - "%sram.allowed_attributes%" diff --git a/config/services_ci.yaml b/config/services_ci.yaml index c2b9a59d0c..74d2666f86 100644 --- a/config/services_ci.yaml +++ b/config/services_ci.yaml @@ -67,3 +67,7 @@ services: OpenConext\EngineBlockFunctionalTestingBundle\Features\Context\MinkContext: tags: ['fob.context'] + + OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\SbsClientStateManager: + arguments: + - "@engineblock.functional_testing.data_store.sbs_client_state_mananger" diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index 8d7a226f08..ab0f568814 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -49,7 +49,7 @@ public function execute(): void $this->_serviceProvider, $this->_request, $this->_server->getRepository(), - $this->logger + $log, ); if (!$serviceProvider) { diff --git a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php index 956812ffbf..2272650e4c 100644 --- a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php +++ b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php @@ -347,7 +347,6 @@ public function __sleep() '_serializableSspMessageXml', '_serializableRelayState', 'SRAMInterruptNonce', - 'SRAMInterrupt', ]; } diff --git a/src/OpenConext/EngineBlockBundle/Configuration/FeatureConfiguration.php b/src/OpenConext/EngineBlockBundle/Configuration/FeatureConfiguration.php index dfd001b185..f632968c50 100644 --- a/src/OpenConext/EngineBlockBundle/Configuration/FeatureConfiguration.php +++ b/src/OpenConext/EngineBlockBundle/Configuration/FeatureConfiguration.php @@ -29,7 +29,7 @@ class FeatureConfiguration implements FeatureConfigurationInterface private $features; /** - * @param Feature[] $features indexed by feature key + * @param array $features indexed by feature key */ public function __construct(array $features) { diff --git a/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php index d42b16a717..7df7788da3 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php @@ -37,6 +37,12 @@ final class AuthzResponse */ public $attributes; + public ?string $message; + + private function __construct() + { + } + public static function fromData(array $jsonData) : AuthzResponse { if (!isset($jsonData['msg'])) { @@ -64,7 +70,7 @@ public static function fromData(array $jsonData) : AuthzResponse $response->nonce = $jsonData['nonce'] ?? null; $response->message = $jsonData['message'] ?? null; - if (is_array($jsonData['attributes'])) { + if (isset($jsonData['attributes']) && is_array($jsonData['attributes'])) { $response->attributes = $jsonData['attributes']; } else { $response->attributes = []; diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php index dd6145c262..65608d716b 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php @@ -20,12 +20,12 @@ use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\DataStore\AbstractDataStore; use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\SbsClientStateManager; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -class SbsController extends Controller +class SbsController extends AbstractController { /** diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php index 5c087b4866..5dc4822e0f 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php @@ -47,6 +47,7 @@ * @SuppressWarnings(PHPMD.TooManyMethods) Both set up and tasks can be a lot... * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Due to all integration specific features * @SuppressWarnings(PHPMD.ExcessivePublicCount) Both set up and tasks can be a lot... + * @SuppressWarnings(PHPMD.TooManyFields) Both set up and tasks can be a lot... */ class EngineBlockContext extends AbstractSubContext { diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php index 42e7de295a..451453007b 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php @@ -61,7 +61,7 @@ public function testItDoesNothingIfFeatureFlagNotEnabled(): void $request = $this->mockRequest(); $sramFilter->setRequest($request); - $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => new Feature('eb.feature_enable_sram_interrupt', false)])); + $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => false])); $sramFilter->execute(); $this->assertEmpty($sramFilter->getResponseAttributes()); @@ -80,7 +80,7 @@ public function testItDoesNothingIfSpDoesNotHaveCollabEnabled(): void $request = $this->mockRequest(); $sramFilter->setRequest($request); - $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => new Feature('eb.feature_enable_sram_interrupt', true)])); + $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true])); /** @var \Mockery\Mock $sbsClient */ $sbsClient = $this->mockSbsClient(); @@ -114,17 +114,18 @@ public function testItAddsNonceWhenMessageInterrupt(): void $sramFilter->setResponse(new EngineBlock_Saml2_ResponseAnnotationDecorator(new Response())); - $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => new Feature('eb.feature_enable_sram_interrupt', true)])); + $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true])); /** @var \Mockery\Mock $sbsClient */ $sbsClient = $this->mockSbsClient(); - $response = new AuthzResponse(); - $response->msg = 'interrupt'; - $response->nonce = 'hash123'; - $response->attributes = [ - 'dummy' => 'attributes', - ]; + $response = AuthzResponse::fromData([ + 'msg' => 'interrupt', + 'nonce' => 'hash123', + 'attributes' => [ + 'dummy' => 'attributes', + ] + ]); $expectedRequest = new AuthzRequest(); $expectedRequest->userId = ''; @@ -177,18 +178,19 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void $sramFilter->setResponse(new EngineBlock_Saml2_ResponseAnnotationDecorator(new Response())); - $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => new Feature('eb.feature_enable_sram_interrupt', true)])); + $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true])); /** @var \Mockery\Mock $sbsClient */ $sbsClient = $this->mockSbsClient(); - $response = new AuthzResponse(); - $response->msg = 'authorized'; - $response->nonce = 'hash123'; - $response->attributes = [ - 'urn:mace:dir:attribute-def:uid' => ['userIdValue'], - 'urn:mace:dir:attribute-def:eduPersonEntitlement' => 'attributes', - ]; + $response = AuthzResponse::fromData([ + 'msg' => 'authorized', + 'nonce' => 'hash123', + 'attributes' => [ + 'urn:mace:dir:attribute-def:uid' => ['userIdValue'], + 'urn:mace:dir:attribute-def:eduPersonEntitlement' => 'attributes', + ], + ]); $expectedRequest = new AuthzRequest(); $expectedRequest->userId = ''; @@ -253,7 +255,7 @@ public function testThrowsEngineBlockExceptionIfPolicyCannotBeChecked() $sramFilter->setResponse(new EngineBlock_Saml2_ResponseAnnotationDecorator(new Response())); - $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => new Feature('eb.feature_enable_sram_interrupt', true)])); + $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true])); /** @var \Mockery\Mock|ServiceProvider $sp */ $sp = $this->mockServiceProvider('spEntityId'); From f6bd380e659534d02e8dd89657be54bea0d79d33 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Thu, 23 Oct 2025 16:34:28 +0200 Subject: [PATCH 24/38] WIP - Handle mock idp/sp serialization --- .../EngineBlockBundle/Sbs/SbsClient.php | 13 +++- .../Mock/MockIdentityProvider.php | 60 +++++++++++++++++++ .../Mock/MockServiceProvider.php | 57 ++++++++++++++++++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php index d9737c5b74..a0989e0b63 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php @@ -83,7 +83,7 @@ public function authz(AuthzRequest $request): AuthzResponse { $jsonData = $this->httpClient->post( json_encode($request), - $this->authzLocation, + $this->buildUrl($this->authzLocation), [], $this->requestHeaders(), $this->verifyPeer @@ -101,7 +101,7 @@ public function requestAttributesFor(AttributesRequest $request): AttributesResp { $jsonData = $this->httpClient->post( json_encode($request), - $this->attributesLocation, + $this->buildUrl($this->attributesLocation), [], $this->requestHeaders(), $this->verifyPeer @@ -125,6 +125,13 @@ private function requestHeaders(): array public function getInterruptLocationLink(string $nonce): string { - return $this->sbsBaseUrl . $this->interruptLocation . "?nonce=$nonce"; + return $this->buildUrl($this->interruptLocation) . "?nonce=$nonce"; + } + + private function buildUrl(string $path): string + { + $baseUrl = rtrim($this->sbsBaseUrl, '/'); + $path = '/' . ltrim($path, '/'); + return $baseUrl . $path; } } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php index a21d74ef76..edb1f6d30f 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php @@ -23,6 +23,8 @@ use RobRichards\XMLSecLibs\XMLSecurityKey; use RuntimeException; use SAML2\Constants; +use SAML2\DOMDocumentFactory; +use SAML2\Response as SAMLResponse; use SAML2\XML\md\IDPSSODescriptor; /** @@ -347,4 +349,62 @@ protected function getRoleClass() { return IDPSSODescriptor::class; } + + /** + * Handle serialization of the MockIdentityProvider. + * Convert the SAMLResponse (which contains non-serializable DOMDocument) to XML string. + * + * @return array + */ + public function __sleep() + { + $role = $this->getSsoRole(); + $extensions = $role->getExtensions(); + + // Convert SAMLResponse to XML if it exists + if (isset($extensions['SAMLResponse']) && $extensions['SAMLResponse'] instanceof SAMLResponse) { + $samlResponse = $extensions['SAMLResponse']; + $xml = $samlResponse->toUnsignedXML()->ownerDocument->saveXML(); + + // Store the XML temporarily in the extensions + $extensions['_SAMLResponseXML'] = $xml; + unset($extensions['SAMLResponse']); + $role->setExtensions($extensions); + } + + return ['name', 'descriptor', 'sendAssertions', 'turnBackTime', 'fromTheFuture']; + } + + /** + * Handle deserialization of the MockIdentityProvider. + * Reconstruct the SAMLResponse from the stored XML string. + */ + public function __wakeup() + { + $role = $this->getSsoRole(); + $extensions = $role->getExtensions(); + + // Reconstruct SAMLResponse from XML if it was serialized + if (isset($extensions['_SAMLResponseXML'])) { + $xml = $extensions['_SAMLResponseXML']; + + // Parse the XML to get the DOMElement + $document = DOMDocumentFactory::fromString($xml); + $messageDomElement = $document->getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'Response')->item(0); + + if ($messageDomElement) { + // Create a custom Response instance by passing the DOMElement to the constructor + // This properly initializes all the parent class properties + $samlResponse = new Response($messageDomElement); + + // DO NOT set the XML string - let the Response object generate signed XML dynamically + // when toXml() is called with the signature keys that will be set by ResponseFactory + + // Restore it to the extensions + unset($extensions['_SAMLResponseXML']); + $extensions['SAMLResponse'] = $samlResponse; + $role->setExtensions($extensions); + } + } + } } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockServiceProvider.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockServiceProvider.php index 91c18fdffe..4286fb4947 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockServiceProvider.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockServiceProvider.php @@ -20,6 +20,7 @@ use OpenConext\EngineBlockFunctionalTestingBundle\Saml2\AuthnRequest; use SAML2\AuthnRequest as SAMLAuthnRequest; +use SAML2\DOMDocumentFactory; use SAML2\XML\md\SPSSODescriptor; /** @@ -180,4 +181,60 @@ public function setAuthnContextClassRef($classRef) ) ); } + + /** + * Handle serialization of the MockServiceProvider. + * Convert the SAMLRequest (which contains non-serializable DOMDocument) to XML string. + * + * @return array + */ + public function __sleep() + { + $extensions = $this->descriptor->getExtensions(); + + // Convert SAMLRequest to XML if it exists + if (isset($extensions['SAMLRequest']) && $extensions['SAMLRequest'] instanceof SAMLAuthnRequest) { + $samlRequest = $extensions['SAMLRequest']; + $xml = $samlRequest->toUnsignedXML()->ownerDocument->saveXML(); + + // Store the XML temporarily in the extensions + $extensions['_SAMLRequestXML'] = $xml; + unset($extensions['SAMLRequest']); + $this->descriptor->setExtensions($extensions); + } + + return ['name', 'descriptor']; + } + + /** + * Handle deserialization of the MockServiceProvider. + * Reconstruct the SAMLRequest from the stored XML string. + */ + public function __wakeup() + { + $extensions = $this->descriptor->getExtensions(); + + // Reconstruct SAMLRequest from XML if it was serialized + if (isset($extensions['_SAMLRequestXML'])) { + $xml = $extensions['_SAMLRequestXML']; + + // Parse the XML to get the DOMElement + $document = DOMDocumentFactory::fromString($xml); + $messageDomElement = $document->getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'AuthnRequest')->item(0); + + if ($messageDomElement) { + // Create a custom AuthnRequest instance by passing the DOMElement to the constructor + // This properly initializes all the parent class properties + $samlRequest = new AuthnRequest($messageDomElement); + + // DO NOT set the XML string - let the AuthnRequest object generate signed XML dynamically + // when toXml() is called with the signature keys if signing is configured + + // Restore it to the extensions + unset($extensions['_SAMLRequestXML']); + $extensions['SAMLRequest'] = $samlRequest; + $this->descriptor->setExtensions($extensions); + } + } + } } From 94a1088fa48c6afc92b477ef542e2f7ec89f9af8 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Mon, 27 Oct 2025 16:15:19 +0100 Subject: [PATCH 25/38] wip - fix sram --- config/packages/parameters.yml.dist | 8 +- docker/docker-compose.yml | 1 - .../EngineBlock/Application/DiContainer.php | 3 + .../EngineBlock/Application/ErrorHandler.php | 1 - .../Application/TestDiContainer.php | 16 ++ .../Filter/Command/SRAMInterruptFilter.php | 1 + .../Module/Service/AssertionConsumer.php | 66 +------- .../Corto/Module/Service/SRAMInterrupt.php | 18 ++- library/EngineBlock/Corto/ProxyServer.php | 152 ++++++++++++++++++ .../Controller/IdentityProviderController.php | 3 +- .../EngineBlockBundle/Sbs/AuthzResponse.php | 27 ++-- .../Sbs/SbsAttributeMerger.php | 40 ++++- .../Features/SbsFlowIntegration.feature | 37 +---- .../Mock/MockIdentityProvider.php | 1 + .../Command/SRAMInterruptFilterTest.php | 22 ++- .../Sbs/SbsAttributeMergerTest.php | 26 ++- .../EngineBlockBundle/Sbs/SbsClientTest.php | 6 +- 17 files changed, 295 insertions(+), 133 deletions(-) diff --git a/config/packages/parameters.yml.dist b/config/packages/parameters.yml.dist index cfc3509c5c..eb354d623a 100644 --- a/config/packages/parameters.yml.dist +++ b/config/packages/parameters.yml.dist @@ -321,7 +321,7 @@ parameters: sram.interrupt_location: interrupt sram.verify_peer: false sram.allowed_attributes: - - 'urn:mace:dir:attribute-def:eduPersonEntitlement' - - 'urn:mace:dir:attribute-def:eduPersonPrincipalName' - - 'urn:mace:dir:attribute-def:uid' - - 'urn:oid:1.3.6.1.4.1.24552.500.1.1.1.13' + 'urn:mace:dir:attribute-def:eduPersonEntitlement': 'xs:string' + 'urn:mace:dir:attribute-def:eduPersonPrincipalName': 'xs:string' + 'urn:mace:dir:attribute-def:uid': 'xs:string' + 'urn:oid:1.3.6.1.4.1.24552.500.1.1.1.13': 'xs:string' diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7e729e0aa2..959903ab61 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -61,5 +61,4 @@ services: - ../theme:/theme volumes: - # eb-mysql-data: eb-mysql-test-data: diff --git a/library/EngineBlock/Application/DiContainer.php b/library/EngineBlock/Application/DiContainer.php index 85367ae01a..adc6cdba90 100644 --- a/library/EngineBlock/Application/DiContainer.php +++ b/library/EngineBlock/Application/DiContainer.php @@ -311,6 +311,9 @@ protected function getSymfonyContainer() return $this->container; } + /** + * @TODO Johan, can we use proper DI? + */ public function getSbsAttributeMerger(): SbsAttributeMerger { return $this->container->get('engineblock.sbs.attribute_merger'); diff --git a/library/EngineBlock/Application/ErrorHandler.php b/library/EngineBlock/Application/ErrorHandler.php index d33eb4cdbd..83c0be8c3b 100644 --- a/library/EngineBlock/Application/ErrorHandler.php +++ b/library/EngineBlock/Application/ErrorHandler.php @@ -62,7 +62,6 @@ public function exception(Throwable $e) foreach ($this->_exitHandlers as $exitHandler) { $exitHandler($e); } - throw $e; $this->_application->reportError($e); diff --git a/library/EngineBlock/Application/TestDiContainer.php b/library/EngineBlock/Application/TestDiContainer.php index df6e4c430c..39f491b7fd 100644 --- a/library/EngineBlock/Application/TestDiContainer.php +++ b/library/EngineBlock/Application/TestDiContainer.php @@ -20,6 +20,7 @@ use OpenConext\EngineBlockBundle\Configuration\FeatureConfigurationInterface; use OpenConext\EngineBlockBundle\Pdp\PdpClientInterface; use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; +use OpenConext\EngineBlockBundle\Sbs\SbsAttributeMerger; /** * Creates mocked versions of dependencies for unit testing @@ -41,6 +42,11 @@ class EngineBlock_Application_TestDiContainer extends EngineBlock_Application_Di */ private $featureConfiguration; + /** + * @var SbsAttributeMerger|null + */ + private $sbsAttributeMerger; + public function getXmlConverter(): EngineBlock_Corto_XmlToArray { return Phake::mock('EngineBlock_Corto_XmlToArray'); @@ -70,6 +76,16 @@ public function setSbsClient(?SbsClientInterface $sbsClient) { $this->sbsClient = $sbsClient; } + public function setSbsAttributeMerger(?SbsAttributeMerger $sbsAttributeMerger) + { + $this->sbsAttributeMerger = $sbsAttributeMerger; + } + + public function getSbsAttributeMerger(): SbsAttributeMerger + { + return $this->sbsAttributeMerger ?? parent::getSbsAttributeMerger(); + } + public function getSbsClient(): SbsClientInterface { diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index ab0f568814..07786bd4a5 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -72,6 +72,7 @@ public function execute(): void $log->info("SBS interrupt reason: " . $interruptResponse->message); $this->_response->setSRAMInterruptNonce($interruptResponse->nonce); } elseif ($interruptResponse->msg === 'authorized' && !empty($interruptResponse->attributes)) { + // @TODO JOHAN hier ook types? Nee? $this->_responseAttributes = $this->getSbsAttributeMerger()->mergeAttributes($this->_responseAttributes, $interruptResponse->attributes); } else { throw new InvalidSbsResponseException(sprintf('Invalid SBS response received: %s', $interruptResponse->msg)); diff --git a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php index 7aa1888e23..31ef02193c 100644 --- a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php @@ -164,78 +164,26 @@ public function serve($serviceName, Request $httpRequest) } // Keep track of what IDP was used for this SP. This way the user does - // not have to go trough the WAYF again when logging into this service + // not have to go through the WAYF again when logging into this service // or another service. EngineBlock_Corto_Model_Response_Cache::rememberIdp($receivedRequest, $receivedResponse); $originalAssertions = clone $receivedResponse->getAssertions()[0]; $this->_server->filterInputAssertionAttributes($receivedResponse, $receivedRequest); - // Add the consent step - $currentProcessStep = $this->_processingStateHelper->addStep( - $receivedRequest->getId(), - ProcessingStateHelperInterface::STEP_CONSENT, - $this->getEngineSpRole($this->_server), - $receivedResponse - ); - - // Send SRAM Interrupt call - if ("" != $receivedResponse->getSRAMInterruptNonce()) { - $log->info('Handle SRAM Interrupt callout'); - - // Add the SRAM step - $this->_processingStateHelper->addStep( - $receivedRequest->getId(), - ProcessingStateHelperInterface::STEP_SRAM, - $this->getEngineSpRole($this->_server), - $receivedResponse - ); - - // Redirect to SRAM - $this->_server->sendSRAMInterruptRequest($receivedResponse, $receivedRequest); + if ($this->_server->handleSRAMInterruptCallout($receivedResponse, $receivedRequest)) { return; } - // When dealing with an SP that acts as a trusted proxy, we should use the proxying SP and not the proxy itself. - if ($sp->getCoins()->isTrustedProxy()) { - // Overwrite the trusted proxy SP instance with that of the SP that uses the trusted proxy. - $sp = $this->_server->findOriginalServiceProvider($receivedRequest, $log); - } - - $pdpLoas = $receivedResponse->getPdpRequestedLoas(); - $loaRepository = $application->getDiContainer()->getLoaRepository(); - $authnRequestLoas = $receivedRequest->getStepupObligations($loaRepository->getStepUpLoas()); - // Goto consent if no Stepup authentication is needed - if (!$this->_stepupGatewayCallOutHelper->shouldUseStepup($idp, $sp, $authnRequestLoas, $pdpLoas)) { - $this->_server->sendConsentAuthenticationRequest($receivedResponse, $receivedRequest, $currentProcessStep->getRole(), $this->getAuthenticationState()); + // Handle Consent authentication callout + if ($this->_server->handleConsentAuthenticationCallout($receivedResponse, $receivedRequest)) { return; } - $log->info('Handle Stepup authentication callout'); - - // Add Stepup authentication step - $currentProcessStep = $this->_processingStateHelper->addStep( - $receivedRequest->getId(), - ProcessingStateHelperInterface::STEP_STEPUP, - $application->getDiContainer()->getStepupIdentityProvider($this->_server), - $receivedResponse - ); - - // Get mapped AuthnClassRef and get NameId - $nameId = clone $receivedResponse->getNameId(); - $authnClassRef = $this->_stepupGatewayCallOutHelper->getStepupLoa($idp, $sp, $authnRequestLoas, $pdpLoas); - - $this->_server->sendStepupAuthenticationRequest( - $receivedRequest, - $currentProcessStep->getRole(), - $authnClassRef, - $nameId, - $sp->getCoins()->isStepupForceAuthn(), - $originalAssertions - ); - - } + // Handle Stepup authentication callout + $this->_server->handleStepupAuthenticationCallout($receivedResponse, $receivedRequest, $originalAssertions); + } /** * @return AuthenticationState diff --git a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php index fc9698d36f..a5d3281be5 100644 --- a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php +++ b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php @@ -57,7 +57,7 @@ public function __construct( AuthenticationStateHelperInterface $stateHelper, ProcessingStateHelperInterface $processingStateHelper, StepupGatewayCallOutHelper $stepupGatewayCallOutHelper, - SbsAttributeMerger $sbsAttributeMerger + SbsAttributeMerger $sbsAttributeMerger, ) { $this->_server = $server; @@ -69,7 +69,6 @@ public function __construct( /** * route that receives the user when they get back from their SBS interrupt, - * fetches the attributes from SBS, * and resumes the AuthN flow. * * @param $serviceName @@ -89,6 +88,7 @@ public function serve($serviceName, Request $httpRequest) $receivedResponse = $nextProcessStep->getResponse(); $receivedRequest = $this->_server->getReceivedRequestFromResponse($receivedResponse); + $originalAssertion = clone $receivedResponse->getAssertions()[0]; $attributes = $receivedResponse->getAssertion()->getAttributes(); $nonce = $receivedResponse->getSRAMInterruptNonce(); @@ -99,17 +99,29 @@ public function serve($serviceName, Request $httpRequest) if (!empty($interruptResponse->attributes)) { $attributes = $this->_sbsAttributeMerger->mergeAttributes($attributes, $interruptResponse->attributes); $receivedResponse->getAssertion()->setAttributes($attributes); + + // Get the value types for the merged attributes and set them on the assertion + $mergedAttributeTypes = $this->_sbsAttributeMerger->getMergedAttributeTypes(); + if (!empty($mergedAttributeTypes)) { + // Get existing attribute value types and merge with the new ones + $existingTypes = $receivedResponse->getAssertion()->getAttributesValueTypes(); + $updatedTypes = array_merge($existingTypes, $mergedAttributeTypes); + $receivedResponse->getAssertion()->setAttributesValueTypes($updatedTypes); + } } /* * Continue to Consent/StepUp */ + // @TODO JOHAN, HIER gaat het fout + // dit is andersom! if ($this->_server->handleConsentAuthenticationCallout($receivedResponse, $receivedRequest)) { return; } - $this->_server->handleStepupAuthenticationCallout($receivedResponse, $receivedRequest); + // @TODO JOHAN: Klopt de $originalAssertion hier? + $this->_server->handleStepupAuthenticationCallout($receivedResponse, $receivedRequest, $originalAssertion); } private function getSbsClient() diff --git a/library/EngineBlock/Corto/ProxyServer.php b/library/EngineBlock/Corto/ProxyServer.php index 5e6fe09a30..7feae09b9d 100644 --- a/library/EngineBlock/Corto/ProxyServer.php +++ b/library/EngineBlock/Corto/ProxyServer.php @@ -145,11 +145,14 @@ class EngineBlock_Corto_ProxyServer * @var Environment */ private $twig; + private EngineBlock_Application_DiContainer $_diContainer; public function __construct(Environment $twig) { $this->_server = $this; $this->twig = $twig; + // @TODO: JOHAN + $this->_diContainer = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); } //////// GETTERS / SETTERS ///////// @@ -556,6 +559,155 @@ public function sendStepupAuthenticationRequest( $this->getBindingsModule()->send($ebRequest, $identityProvider, true); } + // @TODO JOHAN: Heeft geen dependencies op ProxyServer, buiten redirect, Buiten Corto plaatsen? + function sendSRAMInterruptRequest($response): void + { + $nonce = $response->getSRAMInterruptNonce(); + + $sbsClient = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getSbsClient(); + $redirect_url = $sbsClient->getInterruptLocationLink($nonce); + $this->redirect($redirect_url, ''); + } + + function handleSRAMInterruptCallout( + $receivedResponse, + $receivedRequest + ) { + $logger = $this->getLogger(); + $logger->info('Handle SRAM interrupt callout'); + + if ($receivedResponse->getSRAMInterruptNonce() !== '') { + + // Add the SRAM step + $this->_diContainer->getProcessingStateHelper()->addStep( + $receivedRequest->getId(), + ProcessingStateHelperInterface::STEP_SRAM, + $this->getEngineSpRole(), + $receivedResponse + ); + + // Redirect to SRAM + $this->sendSRAMInterruptRequest($receivedResponse); + + return true; + } + + return false; + } + function handleStepupAuthenticationCallout( + $receivedResponse, + $receivedRequest, + $originalAssertion, + ): void { + $logger = $this->getLogger(); + $logger->info('Handle Stepup authentication callout'); + + // Add Stepup authentication step + $currentProcessStep = $this->_diContainer->getProcessingStateHelper()->addStep( + $receivedRequest->getId(), + ProcessingStateHelperInterface::STEP_STEPUP, + $this->_diContainer->getStepupIdentityProvider($this), + $receivedResponse + ); + + if ($receivedRequest->isDebugRequest()) { + $sp = $this->getEngineSpRole(); + } else { + $issuer = $receivedRequest->getIssuer() ? $receivedRequest->getIssuer()->getValue() : ''; + $sp = $this->getRepository()->fetchServiceProviderByEntityId($issuer); + } + + $issuer = $receivedResponse->getIssuer() ? $receivedResponse->getIssuer()->getValue() : ''; + $idp = $this->getRepository()->fetchIdentityProviderByEntityId($issuer); + + // When dealing with an SP that acts as a trusted proxy, we should use the proxying SP and not the proxy itself. + if ($sp->getCoins()->isTrustedProxy()) { + // Overwrite the trusted proxy SP instance with that of the SP that uses the trusted proxy. + $sp = $this->findOriginalServiceProvider($receivedRequest, $logger); + } + + $pdpLoas = $receivedResponse->getPdpRequestedLoas(); + $loaRepository = $this->_diContainer->getLoaRepository(); + $authnRequestLoas = $receivedRequest->getStepupObligations($loaRepository->getStepUpLoas()); + + // Get mapped AuthnClassRef and get NameId + $nameId = clone $receivedResponse->getNameId(); + $authnClassRef = $this->_diContainer->getStepupGatewayCallOutHelper()->getStepupLoa($idp, $sp, $authnRequestLoas, $pdpLoas); + + $this->sendStepupAuthenticationRequest( + $receivedRequest, + $currentProcessStep->getRole(), + $authnClassRef, + $nameId, + $sp->getCoins()->isStepupForceAuthn(), + $originalAssertion, + ); + } + + /** + * @return ServiceProvider + * // @TODO: JOHAN + */ + public function getEngineSpRole() + { + $keyId = $this->getKeyId(); + if (!$keyId) { + $keyId = KeyPairFactory::DEFAULT_KEY_PAIR_IDENTIFIER; + } + + $serviceProvider = $this->_diContainer->getServiceProviderFactory()->createEngineBlockEntityFrom($keyId); + return ServiceProvider::fromServiceProviderEntity($serviceProvider); + } + + // @TODO: JOHAN + function handleConsentAuthenticationCallout( + $receivedResponse, + $receivedRequest + // $currentProcessStep + ) { + $logger = $this->getLogger(); + $logger->info('Handle Consent authentication callout'); + + // Add the consent step + $currentProcessStep = $this->_diContainer->getProcessingStateHelper()->addStep( + $receivedRequest->getId(), + ProcessingStateHelperInterface::STEP_CONSENT, + $this->getEngineSpRole(), + $receivedResponse + ); + + $issuer = $receivedResponse->getIssuer() ? $receivedResponse->getIssuer()->getValue() : ''; + $idp = $this->getRepository()->fetchIdentityProviderByEntityId($issuer); + + if ($receivedRequest->isDebugRequest()) { + $sp = $this->getEngineSpRole(); + } else { + $issuer = $receivedRequest->getIssuer() ? $receivedRequest->getIssuer()->getValue() : ''; + $sp = $this->getRepository()->fetchServiceProviderByEntityId($issuer); + } + + // When dealing with an SP that acts as a trusted proxy, we should use the proxying SP and not the proxy itself. + if ($sp->getCoins()->isTrustedProxy()) { + // Overwrite the trusted proxy SP instance with that of the SP that uses the trusted proxy. + $sp = $this->_server->findOriginalServiceProvider($receivedRequest, $this->getLogger()); + } + + $pdpLoas = $receivedResponse->getPdpRequestedLoas(); + $loaRepository = $this->_diContainer->getLoaRepository(); + $authnRequestLoas = $receivedRequest->getStepupObligations($loaRepository->getStepUpLoas()); + + $shouldUseStepup = $this->_diContainer->getStepupGatewayCallOutHelper()->shouldUseStepup($idp, $sp, $authnRequestLoas, $pdpLoas); + + // Goto consent if no Stepup authentication is needed + if (!$shouldUseStepup) { + $this->sendConsentAuthenticationRequest($receivedResponse, $receivedRequest, $currentProcessStep->getRole(), $this->_diContainer->getAuthenticationStateHelper()->getAuthenticationState()); + return true; + } + + // + return false; + } + function sendConsentAuthenticationRequest( EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, EngineBlock_Saml2_AuthnRequestAnnotationDecorator $receivedRequest, diff --git a/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php b/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php index 66557c0499..4b47f404f6 100644 --- a/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php +++ b/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php @@ -216,7 +216,7 @@ public function processConsentAction() * @return Response * @throws \EngineBlock_Exception * - * @Route("/authentication/idp/requestAccess", name="authentication_idp_request_access", methods={"GET"}) + * @Route("/authentication/idp/process-sraminterrupt", name="authentication_idp_process_sraminterrupt", methods={"GET"}) */ public function processSRAMInterrupt(Request $request) { @@ -230,6 +230,7 @@ public function processSRAMInterrupt(Request $request) * @param Request $request * @return Response * @throws \EngineBlock_Exception + * @Route("/authentication/idp/requestAccess", name="authentication_idp_request_access", methods={"GET"}) */ public function requestAccessAction(Request $request) { diff --git a/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php index 7df7788da3..08dfd38f5a 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php @@ -39,8 +39,17 @@ final class AuthzResponse public ?string $message; - private function __construct() + private function __construct(array $jsonData) { + $this->msg = $jsonData['msg']; + $this->nonce = $jsonData['nonce'] ?? null; + $this->message = $jsonData['message'] ?? null; + + if (isset($jsonData['attributes']) && is_array($jsonData['attributes'])) { + $this->attributes = $jsonData['attributes']; + } else { + $this->attributes = []; + } } public static function fromData(array $jsonData) : AuthzResponse @@ -62,20 +71,10 @@ public static function fromData(array $jsonData) : AuthzResponse } if (($jsonData['msg'] === SbsClientInterface::ERROR)) { - throw new InvalidSbsResponseException('SBS returned an error: ' . $jsonData['message']); - } - - $response = new self; - $response->msg = $jsonData['msg']; - $response->nonce = $jsonData['nonce'] ?? null; - $response->message = $jsonData['message'] ?? null; - - if (isset($jsonData['attributes']) && is_array($jsonData['attributes'])) { - $response->attributes = $jsonData['attributes']; - } else { - $response->attributes = []; + $error = $jsonData['message'] ?? 'unknown message'; + throw new InvalidSbsResponseException('SBS returned an error: ' . $error); } - return $response; + return new self($jsonData); } } diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php index 962a5cd161..64401b65df 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php @@ -19,26 +19,42 @@ namespace OpenConext\EngineBlockBundle\Sbs; use EngineBlock_ApplicationSingleton; -use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; class SbsAttributeMerger { /** - * @var array + * @var array Map of attribute names to their value types (e.g., 'name' => 'xs:string') */ - private $allowedAttributeNames; + private $allowedAttributeTypes; - public function __construct(array $allowedAttributeNames) + /** + * @var array Tracks the value types of merged attributes + */ + private $mergedAttributeTypes = []; + + public function __construct(array $allowedAttributeTypes) { - $this->allowedAttributeNames = $allowedAttributeNames; + foreach ($allowedAttributeTypes as $key => $value) { + assert(is_string($key), 'All keys in allowedAttributeTypes must be strings'); + assert(is_string($value), 'All values in allowedAttributeTypes must be strings'); + } + $this->allowedAttributeTypes = $allowedAttributeTypes; } public function mergeAttributes(array $samlAttributes, array $sbsAttributes): array { + // Reset merged attribute types for this merge operation + $this->mergedAttributeTypes = []; + $validAttributes = $this->validSbsAttributes($sbsAttributes); foreach ($validAttributes as $key => $value) { + // Track the value type for this merged attribute + if (isset($this->allowedAttributeTypes[$key])) { + $this->mergedAttributeTypes[$key] = $this->allowedAttributeTypes[$key]; + } + if (!isset($samlAttributes[$key])) { $samlAttributes[$key] = $value; continue; @@ -56,6 +72,16 @@ public function mergeAttributes(array $samlAttributes, array $sbsAttributes): ar return $samlAttributes; } + /** + * Get the value types for attributes that were merged in the last mergeAttributes() call + * + * @return array Map of attribute names to their value types + */ + public function getMergedAttributeTypes(): array + { + return $this->mergedAttributeTypes; + } + /** * @SuppressWarnings(PHPMD.UnusedLocalVariable) $value is never used in the foreach */ @@ -65,8 +91,8 @@ private function validSbsAttributes(array $sbsAttributes): array $invalidKeys = []; foreach ($sbsAttributes as $key => $value) { - if (in_array($key, $this->allowedAttributeNames, true)) { - $validAttributes[$key] = $sbsAttributes[$key]; + if (array_key_exists($key, $this->allowedAttributeTypes)) { + $validAttributes[$key] = $value; } else { $invalidKeys[] = $key; } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature index 03f00edbd5..13d19e2731 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature @@ -57,39 +57,4 @@ Feature: Then the url should match "/feedback/unknown-error" And the response should contain "Logging in has failed" - Scenario: If the SBS authz check returns an 'interrupt' response, and the attributes call to sbs returns an invalid response - Given the SP "SSO-SP" requires SRAM collaboration - And feature "eb.feature_enable_sram_interrupt" is enabled - And the sbs server will trigger the "interrupt" authz flow when called - And the sbs server will return invalid attributes - When I log in at "SSO-SP" - And I pass through EngineBlock - And I pass through the IdP - Then the url should match "/functional-testing/interrupt" - And the sbs server will trigger the "error" authz flow when called - And I pass through SBS - And the response should contain "Logging in has failed" - - Scenario: If the authz call returns unknown attributes, the unknown attributes are ignored - Given the SP "SSO-SP" requires SRAM collaboration - And feature "eb.feature_enable_sram_interrupt" is enabled - And the sbs server will trigger the 'authorized' authz flow and will return invalid attributes - When I log in at "SSO-SP" - And I pass through EngineBlock - And I pass through the IdP - Then the url should match "/authentication/sp/consume-assertion" - And the response should not contain "foo" - And the response should not contain "baz" - - Scenario: If the sbs flow is active, other filters like PDP are still executed - Given SP "SSO-SP" requires a policy enforcement decision - And pdp gives an IdP specific deny response for "SSO-IdP" - Given the SP "SSO-SP" requires SRAM collaboration - And feature "eb.feature_enable_sram_interrupt" is enabled - And the sbs server will trigger the "interrupt" authz flow when called - When I log in at "SSO-SP" - And I pass through EngineBlock - And I pass through the IdP - And I should see "Error - Access denied" - And I should see "Message from your organisation:" - And I should see "Students of SSO-IdP do not have access to this resource" + #// hebben we stepup getest? diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php index edb1f6d30f..b9e781b1c7 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php @@ -29,6 +29,7 @@ /** * @SuppressWarnings(PHPMD.TooManyPublicMethods) Allows for better control + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class MockIdentityProvider extends AbstractMockEntityRole { diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php index 451453007b..774ee2a821 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php @@ -26,6 +26,7 @@ use OpenConext\EngineBlockBundle\Sbs\Dto\AuthzRequest; use OpenConext\EngineBlockBundle\Sbs\AuthzResponse; use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; +use OpenConext\EngineBlockBundle\Sbs\SbsAttributeMerger; use PHPUnit\Framework\TestCase; use SAML2\Assertion; use SAML2\AuthnRequest; @@ -178,6 +179,12 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void $sramFilter->setResponse(new EngineBlock_Saml2_ResponseAnnotationDecorator(new Response())); + + // Mock the SbsAttributeMerger with allowed attributes + $this->mockSbsAttributeMerger([ + 'urn:mace:dir:attribute-def:uid' => 'xs:string', + 'urn:mace:dir:attribute-def:eduPersonEntitlement' => 'xs:string', + ]); $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true])); /** @var \Mockery\Mock $sbsClient */ @@ -204,7 +211,7 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void return $args->userId === $expectedRequest->userId && $args->eduPersonPrincipalName === $expectedRequest->eduPersonPrincipalName - && strpos($args->continueUrl, $expectedRequest->continueUrl) === 0 + && str_starts_with($args->continueUrl, $expectedRequest->continueUrl) && $args->serviceId === $expectedRequest->serviceId && $args->issuerId === $expectedRequest->issuerId; }) @@ -305,11 +312,23 @@ private function mockSbsClient() return $sbsClient; } + private function mockSbsAttributeMerger(array $allowedAttributeTypes) + { + $sbsAttributeMerger = new SbsAttributeMerger($allowedAttributeTypes); + + /** @var EngineBlock_Application_TestDiContainer $container */ + $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); + $container->setSbsAttributeMerger($sbsAttributeMerger); + } + private function mockFeatureConfiguration(FeatureConfiguration $featureConfiguration) { /** @var EngineBlock_Application_TestDiContainer $container */ $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); $container->setFeatureConfiguration($featureConfiguration); + + $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); + $container->setSbsAttributeMerger(null); } protected function tearDown(): void @@ -319,6 +338,7 @@ protected function tearDown(): void $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); $container->setFeatureConfiguration(null); + $container->setSbsAttributeMerger(null); parent::tearDown(); } diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php index b8b7a436fc..a51d07e954 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php @@ -26,7 +26,12 @@ class SbsAttributeMergerTest extends TestCase { public function testMergeAttributesSuccessfully(): void { - $allowedAttributes = ['eduPersonEntitlement', 'eduPersonPrincipalName', 'uuid', 'sshkey']; + $allowedAttributes = [ + 'eduPersonEntitlement' => 'xs:string', + 'eduPersonPrincipalName' => 'xs:string', + 'uuid' => 'xs:string', + 'sshkey' => 'xs:string' + ]; $merger = new SbsAttributeMerger($allowedAttributes); $samlAttributes = [ @@ -56,11 +61,23 @@ public function testMergeAttributesSuccessfully(): void ]; $this->assertEquals($expectedResult, $merger->mergeAttributes($samlAttributes, $sbsAttributes)); + + // Verify that merged attribute types are tracked + $expectedTypes = [ + 'uuid' => 'xs:string', + 'eduPersonEntitlement' => 'xs:string', + 'eduPersonPrincipalName' => 'xs:string', + 'sshkey' => 'xs:string' + ]; + $this->assertEquals($expectedTypes, $merger->getMergedAttributeTypes()); } public function testMergeAttributesWithInvalidKeysThrowsException(): void { - $allowedAttributes = ['email', 'name']; + $allowedAttributes = [ + 'email' => 'xs:string', + 'name' => 'xs:string' + ]; $merger = new SbsAttributeMerger($allowedAttributes); $samlAttributes = [ @@ -82,7 +99,10 @@ public function testMergeAttributesWithInvalidKeysThrowsException(): void public function testMergeAttributesWithEmptySbsAttributes(): void { - $allowedAttributes = ['email', 'name']; + $allowedAttributes = [ + 'email' => 'xs:string', + 'name' => 'xs:string' + ]; $merger = new SbsAttributeMerger($allowedAttributes); $samlAttributes = [ diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php index 153921bcfe..4c0fd087dc 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsClientTest.php @@ -40,7 +40,7 @@ protected function setUp(): void $this->sbsClient = new SbsClient( $this->httpClient, - 'https://sbs.example.com', + 'https://sbs.example.com/', '/authz', '/authz', '/interrupt', @@ -58,7 +58,7 @@ public function testAuthz(): void ->method('post') ->with( $this->anything(), - '/authz', + 'https://sbs.example.com/authz', [], [ 'Content-Type' => 'application/json', @@ -85,7 +85,7 @@ public function testRequestAttributesFor(): void ->method('post') ->with( $this->anything(), - '/authz', + 'https://sbs.example.com/authz', [], [ 'Content-Type' => 'application/json', From 9c61e0a4dffda29f2beaef65a71e902f8fdcaf67 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Tue, 28 Oct 2025 12:01:09 +0100 Subject: [PATCH 26/38] Fix deserialization of mock idp/sp --- .../Fixtures/DataStore/AbstractDataStore.php | 22 +++++++++++++++++++ .../Mock/EntityRegistry.php | 2 +- .../Mock/MockIdentityProvider.php | 10 +++++++-- .../Mock/MockServiceProvider.php | 10 +++++++-- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/DataStore/AbstractDataStore.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/DataStore/AbstractDataStore.php index aee892ce3f..8d56b1d1ed 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/DataStore/AbstractDataStore.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/DataStore/AbstractDataStore.php @@ -38,6 +38,11 @@ public function __construct($filePath) $this->filePath = basename($filePath); $adapter = new Local($directory); $this->fileSystem = new Filesystem($adapter); + + if (!$this->fileSystem->fileExists($this->filePath)) { + $this->save([]); + $this->ensureWwwCanWrite($filePath); + } } /** @@ -72,9 +77,26 @@ public function load($default = []) public function save($data) { $this->fileSystem->write($this->filePath, $this->encode($data)); + + // Because the serialization of IdP / SP mocks is destructive, use the restored data after saving. + // If this data is not used, the destructed object remains in memory. + $fileContents = $this->fileSystem->read($this->filePath); + + return $this->decode($fileContents); } abstract protected function encode($data); abstract protected function decode($data); + + /** + * The db is created during the behat process, which runs as root. + * But requests from the webserver should also be able to update the state. + */ + private function ensureWwwCanWrite(string $absolutePath): void + { + chown($absolutePath, 'www-data'); + chgrp($absolutePath, 'www-data'); + chmod($absolutePath, 0664); + } } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/EntityRegistry.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/EntityRegistry.php index a1a2552099..6bd0a0ac86 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/EntityRegistry.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/EntityRegistry.php @@ -67,7 +67,7 @@ public function clear() public function save() { - $this->dataStore->save($this->parameters); + $this->parameters = $this->dataStore->save($this->parameters); return $this; } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php index b9e781b1c7..5757f52ed4 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockIdentityProvider.php @@ -367,8 +367,9 @@ public function __sleep() $samlResponse = $extensions['SAMLResponse']; $xml = $samlResponse->toUnsignedXML()->ownerDocument->saveXML(); - // Store the XML temporarily in the extensions + // Store the XML and RelayState temporarily in the extensions $extensions['_SAMLResponseXML'] = $xml; + $extensions['_SAMLResponseRelayState'] = $samlResponse->getRelayState(); unset($extensions['SAMLResponse']); $role->setExtensions($extensions); } @@ -398,11 +399,16 @@ public function __wakeup() // This properly initializes all the parent class properties $samlResponse = new Response($messageDomElement); + // Restore RelayState if it was stored + if (isset($extensions['_SAMLResponseRelayState']) && $extensions['_SAMLResponseRelayState'] !== null) { + $samlResponse->setRelayState($extensions['_SAMLResponseRelayState']); + } + // DO NOT set the XML string - let the Response object generate signed XML dynamically // when toXml() is called with the signature keys that will be set by ResponseFactory // Restore it to the extensions - unset($extensions['_SAMLResponseXML']); + unset($extensions['_SAMLResponseXML'], $extensions['_SAMLResponseRelayState']); $extensions['SAMLResponse'] = $samlResponse; $role->setExtensions($extensions); } diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockServiceProvider.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockServiceProvider.php index 4286fb4947..aa157a4c39 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockServiceProvider.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Mock/MockServiceProvider.php @@ -197,8 +197,9 @@ public function __sleep() $samlRequest = $extensions['SAMLRequest']; $xml = $samlRequest->toUnsignedXML()->ownerDocument->saveXML(); - // Store the XML temporarily in the extensions + // Store the XML and RelayState temporarily in the extensions $extensions['_SAMLRequestXML'] = $xml; + $extensions['_SAMLRequestRelayState'] = $samlRequest->getRelayState(); unset($extensions['SAMLRequest']); $this->descriptor->setExtensions($extensions); } @@ -227,11 +228,16 @@ public function __wakeup() // This properly initializes all the parent class properties $samlRequest = new AuthnRequest($messageDomElement); + // Restore RelayState if it was stored + if (isset($extensions['_SAMLRequestRelayState']) && $extensions['_SAMLRequestRelayState'] !== null) { + $samlRequest->setRelayState($extensions['_SAMLRequestRelayState']); + } + // DO NOT set the XML string - let the AuthnRequest object generate signed XML dynamically // when toXml() is called with the signature keys if signing is configured // Restore it to the extensions - unset($extensions['_SAMLRequestXML']); + unset($extensions['_SAMLRequestXML'], $extensions['_SAMLRequestRelayState']); $extensions['SAMLRequest'] = $samlRequest; $this->descriptor->setExtensions($extensions); } From e1fbb0716721a32fcf72d86ffc4c30670e5c13b8 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Tue, 28 Oct 2025 14:50:33 +0100 Subject: [PATCH 27/38] Rework test, di, enum --- .../EngineBlock/Application/DiContainer.php | 3 - .../Application/TestDiContainer.php | 49 ------- .../Filter/Command/SRAMInterruptFilter.php | 85 ++++++------ library/EngineBlock/Corto/Filter/Input.php | 7 +- .../Saml2/ResponseAnnotationDecorator.php | 3 - .../EngineBlockBundle/Sbs/AuthzResponse.php | 45 ++---- .../Sbs/Dto/AuthzRequest.php | 50 ++----- src/OpenConext/EngineBlockBundle/Sbs/Msg.php | 38 +++++ .../Sbs/SbsClientInterface.php | 6 - .../Controllers/SbsController.php | 10 +- .../Features/Context/EngineBlockContext.php | 15 +- .../Fixtures/SbsClientStateManager.php | 18 +-- .../Command/SRAMInterruptFilterTest.php | 131 +++++++----------- .../Test/Corto/ProxyServerTest.php | 2 - .../Sbs/AuthzResponseTest.php | 29 ++-- 15 files changed, 192 insertions(+), 299 deletions(-) create mode 100644 src/OpenConext/EngineBlockBundle/Sbs/Msg.php diff --git a/library/EngineBlock/Application/DiContainer.php b/library/EngineBlock/Application/DiContainer.php index adc6cdba90..85367ae01a 100644 --- a/library/EngineBlock/Application/DiContainer.php +++ b/library/EngineBlock/Application/DiContainer.php @@ -311,9 +311,6 @@ protected function getSymfonyContainer() return $this->container; } - /** - * @TODO Johan, can we use proper DI? - */ public function getSbsAttributeMerger(): SbsAttributeMerger { return $this->container->get('engineblock.sbs.attribute_merger'); diff --git a/library/EngineBlock/Application/TestDiContainer.php b/library/EngineBlock/Application/TestDiContainer.php index 39f491b7fd..68d39f0c35 100644 --- a/library/EngineBlock/Application/TestDiContainer.php +++ b/library/EngineBlock/Application/TestDiContainer.php @@ -16,11 +16,7 @@ * limitations under the License. */ -use OpenConext\EngineBlock\Stepup\StepupEndpoint; -use OpenConext\EngineBlockBundle\Configuration\FeatureConfigurationInterface; use OpenConext\EngineBlockBundle\Pdp\PdpClientInterface; -use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; -use OpenConext\EngineBlockBundle\Sbs\SbsAttributeMerger; /** * Creates mocked versions of dependencies for unit testing @@ -32,21 +28,6 @@ class EngineBlock_Application_TestDiContainer extends EngineBlock_Application_Di */ private $pdpClient; - /** - * @var SbsClientInterface|null - */ - private $sbsClient; - - /** - * @var FeatureConfigurationInterface|null - */ - private $featureConfiguration; - - /** - * @var SbsAttributeMerger|null - */ - private $sbsAttributeMerger; - public function getXmlConverter(): EngineBlock_Corto_XmlToArray { return Phake::mock('EngineBlock_Corto_XmlToArray'); @@ -72,36 +53,6 @@ public function setPdpClient(?PdpClientInterface $pdpClient) $this->pdpClient = $pdpClient; } - public function setSbsClient(?SbsClientInterface $sbsClient) - { - $this->sbsClient = $sbsClient; - } - public function setSbsAttributeMerger(?SbsAttributeMerger $sbsAttributeMerger) - { - $this->sbsAttributeMerger = $sbsAttributeMerger; - } - - public function getSbsAttributeMerger(): SbsAttributeMerger - { - return $this->sbsAttributeMerger ?? parent::getSbsAttributeMerger(); - } - - - public function getSbsClient(): SbsClientInterface - { - return $this->sbsClient ?? parent::getSbsClient(); - } - - public function setFeatureConfiguration(?FeatureConfigurationInterface $featureConfiguration) - { - $this->featureConfiguration = $featureConfiguration; - } - - public function getFeatureConfiguration(): FeatureConfigurationInterface - { - return $this->featureConfiguration ?? parent::getFeatureConfiguration(); - } - public function getConsentFactory(): EngineBlock_Corto_Model_Consent_Factory { $consentFactoryMock = Phake::mock('EngineBlock_Corto_Model_Consent_Factory'); diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index 07786bd4a5..f7b64b622f 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -1,10 +1,5 @@ getFeatureConfiguration()->isEnabled('eb.feature_enable_sram_interrupt')) { + if (!$this->featureConfiguration->isEnabled('eb.feature_enable_sram_interrupt')) { return; } @@ -49,59 +61,49 @@ public function execute(): void $this->_serviceProvider, $this->_request, $this->_server->getRepository(), - $log, + $this->logger, ); - if (!$serviceProvider) { + if ($serviceProvider === null) { $serviceProvider = $this->_serviceProvider; } if ($serviceProvider->getCoins()->collabEnabled() === false) { - $log->notice("No SBS interrupt for serviceProvider: " . $serviceProvider->entityId); + $this->logger->notice("No SBS interrupt for serviceProvider: " . $serviceProvider->entityId); + return; } - $log->notice("SBS interrupt for serviceProvider: " . $serviceProvider->entityId); + $this->logger->notice("SBS interrupt for serviceProvider: " . $serviceProvider->entityId); try { $request = $this->buildRequest($serviceProvider); - $interruptResponse = $this->getSbsClient()->authz($request); + $interruptResponse = $this->sbsClient->authz($request); - if ($interruptResponse->msg === 'interrupt') { - $log->info("SBS interrupt reason: " . $interruptResponse->message); + if ($interruptResponse->msg === Msg::Interrupt) { + $this->logger->info("SBS interrupt reason: " . $interruptResponse->message); $this->_response->setSRAMInterruptNonce($interruptResponse->nonce); - } elseif ($interruptResponse->msg === 'authorized' && !empty($interruptResponse->attributes)) { + + return; + } + + if ($interruptResponse->msg === Msg::Authorized && !empty($interruptResponse->attributes)) { // @TODO JOHAN hier ook types? Nee? - $this->_responseAttributes = $this->getSbsAttributeMerger()->mergeAttributes($this->_responseAttributes, $interruptResponse->attributes); - } else { - throw new InvalidSbsResponseException(sprintf('Invalid SBS response received: %s', $interruptResponse->msg)); + $this->_responseAttributes = $this->attributeMerger->mergeAttributes($this->_responseAttributes, $interruptResponse->attributes); + + return; } + + $this->logger->error(sprintf('SBS Authz returned error: %s', $interruptResponse->message)); + + throw new InvalidSbsResponseException('SBS Authz returned error.'); } catch (Throwable $e){ throw new EngineBlock_Exception_SbsCheckFailed('The SBS server could not be queried: ' . $e->getMessage()); } } - private function getSbsClient() - { - return EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getSbsClient(); - } - - private function getFeatureConfiguration(): FeatureConfigurationInterface - { - return EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getFeatureConfiguration(); - } - - private function getSbsAttributeMerger(): SbsAttributeMerger - { - return EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getSbsAttributeMerger(); - } - - /** - * @return AuthzRequest - * @throws EngineBlock_Corto_ProxyServer_Exception - */ - private function buildRequest($serviceProvider): AuthzRequest + private function buildRequest(ServiceProvider $serviceProvider): AuthzRequest { $attributes = $this->getResponseAttributes(); $id = $this->_request->getId(); @@ -112,7 +114,8 @@ private function buildRequest($serviceProvider): AuthzRequest $service_id = $serviceProvider->entityId; $issuer_id = $this->_identityProvider->entityId; - return AuthzRequest::create( + + return new AuthzRequest( $user_id, $eppn, $continue_url, diff --git a/library/EngineBlock/Corto/Filter/Input.php b/library/EngineBlock/Corto/Filter/Input.php index 4db94f46f4..b7b4631cd4 100644 --- a/library/EngineBlock/Corto/Filter/Input.php +++ b/library/EngineBlock/Corto/Filter/Input.php @@ -92,7 +92,12 @@ public function getCommands() // Check if we need to callout to SRAM to enforce AUP's // Add SRAM attributes if not - new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(), + new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + $diContainer->getSbsClient(), + $diContainer->getFeatureConfiguration(), + $diContainer->getSbsAttributeMerger(), + $logger, + ), // Check if the Policy Decision Point needs to be consulted for this request new EngineBlock_Corto_Filter_Command_EnforcePolicy(), diff --git a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php index 2272650e4c..1b918b8e25 100644 --- a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php +++ b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php @@ -371,9 +371,6 @@ public function __wakeup() } } - /** - * @param string - */ public function setSRAMInterruptNonce(string $SRAMInterruptNonce): void { $this->SRAMInterruptNonce = $SRAMInterruptNonce; diff --git a/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php index 08dfd38f5a..fc6dee0e1d 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/AuthzResponse.php @@ -20,30 +20,18 @@ use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; -final class AuthzResponse +final readonly class AuthzResponse { - /** - * @var string - */ - public $msg; + public Msg $msg; + public string|null $nonce; + public array $attributes; + public string $message; - /** - * @var $nonce - */ - public $nonce; - - /** - * @var array - */ - public $attributes; - - public ?string $message; - - private function __construct(array $jsonData) + private function __construct(Msg $msg, array $jsonData) { - $this->msg = $jsonData['msg']; + $this->msg = $msg; $this->nonce = $jsonData['nonce'] ?? null; - $this->message = $jsonData['message'] ?? null; + $this->message = $jsonData['message'] ?? ''; if (isset($jsonData['attributes']) && is_array($jsonData['attributes'])) { $this->attributes = $jsonData['attributes']; @@ -58,23 +46,20 @@ public static function fromData(array $jsonData) : AuthzResponse throw new InvalidSbsResponseException('Key: "msg" was not found in the SBS response'); } - if (!in_array($jsonData['msg'], SbsClientInterface::VALID_MESSAGES, true)) { - throw new InvalidSbsResponseException(sprintf('Msg: "%s" is not a valid message', $jsonData['msg'])); + try { + $msg = Msg::from($jsonData['msg']); + } catch (\ValueError $e) { + throw new InvalidSbsResponseException(sprintf('"%s" is not a valid msg', $jsonData['msg'])); } - if (($jsonData['msg'] === SbsClientInterface::INTERRUPT) && !isset($jsonData['nonce'])) { + if ($msg === Msg::Interrupt && !isset($jsonData['nonce'])) { throw new InvalidSbsResponseException('Key: "nonce" was not found in the SBS response'); } - if (($jsonData['msg'] === SbsClientInterface::AUTHORIZED) && !isset($jsonData['attributes'])) { + if ($msg === Msg::Authorized && !isset($jsonData['attributes'])) { throw new InvalidSbsResponseException('Key: "attributes" was not found in the SBS response'); } - if (($jsonData['msg'] === SbsClientInterface::ERROR)) { - $error = $jsonData['message'] ?? 'unknown message'; - throw new InvalidSbsResponseException('SBS returned an error: ' . $error); - } - - return new self($jsonData); + return new self($msg, $jsonData); } } diff --git a/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php index 766f94fa21..d8a403ed07 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AuthzRequest.php @@ -23,52 +23,18 @@ class AuthzRequest implements JsonSerializable { - /** - * @var string - */ - public $userId; - - /** - * @var string - */ - public $eduPersonPrincipalName; - - /** - * @var string - */ - public $continueUrl; - - /** - * @var string - */ - public $serviceId; - - /** - * @var string - */ - public $issuerId; - - public static function create( - string $userId, - string $eppn, - string $continueUrl, - string $serviceId, - string $issuerId - ) : AuthzRequest { + public function __construct( + public readonly string $userId, + public readonly string $eduPersonPrincipalName, + public readonly string $continueUrl, + public readonly string $serviceId, + public readonly string $issuerId + ) { Assertion::string($userId, 'The userId must be a string.'); - Assertion::string($eppn, 'The eduPersonPrincipalName must be a string.'); + Assertion::string($eduPersonPrincipalName, 'The eduPersonPrincipalName must be a string.'); Assertion::string($continueUrl, 'The continueUrl must be a string.'); Assertion::string($serviceId, 'The serviceId must be a string.'); Assertion::string($issuerId, 'The issuerId must be a string.'); - - $request = new self(); - $request->userId = $userId; - $request->eduPersonPrincipalName = $eppn; - $request->continueUrl = $continueUrl; - $request->serviceId = $serviceId; - $request->issuerId = $issuerId; - - return $request; } public function jsonSerialize() : array diff --git a/src/OpenConext/EngineBlockBundle/Sbs/Msg.php b/src/OpenConext/EngineBlockBundle/Sbs/Msg.php new file mode 100644 index 0000000000..4d4b10c423 --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Sbs/Msg.php @@ -0,0 +1,38 @@ + self::Interrupt, + 'authorized' => self::Authorized, + 'error' => self::Error, + default => throw new InvalidArgumentException('Invalid SBS msg: ' . $string), + }; + } +} diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php index 6ecc1ca01c..0f85dc791a 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClientInterface.php @@ -23,12 +23,6 @@ interface SbsClientInterface { - public const INTERRUPT = 'interrupt'; - public const AUTHORIZED = 'authorized'; - public const ERROR = 'error'; - - public const VALID_MESSAGES = [self::INTERRUPT, self::AUTHORIZED, self::ERROR]; - public function authz(AuthzRequest $request) : AuthzResponse; public function requestAttributesFor(AttributesRequest $request) : AttributesResponse; diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php index 65608d716b..6e291ec0ac 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Controllers/SbsController.php @@ -25,6 +25,12 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +/** + * SBS is a membership management portal. + * It allows user to define their own collaborations (rich groups), invite new users to those groups and give this collaboration access to services. + * + * This controller acts as an SBS mock. + */ class SbsController extends AbstractController { @@ -71,8 +77,8 @@ public function interruptAction(Request $request): Response } /** - * The endpoint called by Engine to fetch the attributes after the browser has made a trip to the interrupt action - * and has returned to the continue_url + * The endpoint called by Engine to fetch the attributes from SBS, to be merged into the SAML request + * This gets called after the interrupt, or when the user is already authorized in SBS. */ public function attributesAction() { diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php index 5dc4822e0f..e8013cf25e 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/EngineBlockContext.php @@ -23,8 +23,7 @@ use DOMDocument; use DOMElement; use DOMXPath; -use InvalidArgumentException; -use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; +use OpenConext\EngineBlockBundle\Sbs\Msg; use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\DataStore\AbstractDataStore; use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\FunctionalTestingAttributeAggregationClient; use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\FunctionalTestingAuthenticationLoopGuard; @@ -769,16 +768,14 @@ public function aaReturnsAttributes(TableNode $attributes) /** * @Given /^the sbs server will trigger the "([^"]*)" authz flow when called$/ */ - public function primeAuthzResponse(string $msg): void + public function primeAuthzResponse(string $msgString): void { - if ($msg === 'error') { - $this->sbsClientStateManager->prepareAuthzResponse('error'); + $msg = Msg::fromString($msgString); + if ($msg === Msg::Error) { + $this->sbsClientStateManager->prepareAuthzResponse(Msg::Error); return; } - if (!in_array($msg, SbsClientInterface::VALID_MESSAGES)) { - throw new InvalidArgumentException("$msg is not a valid message type"); - } $this->sbsClientStateManager->prepareAuthzResponse($msg); } @@ -787,7 +784,7 @@ public function primeAuthzResponse(string $msg): void */ public function authzWillReturnInvalidAttributes(): void { - $this->sbsClientStateManager->prepareAuthzResponse(SbsClientInterface::AUTHORIZED, ['attributes' => ['foo' => ['bar' => 'baz']]]); + $this->sbsClientStateManager->prepareAuthzResponse(Msg::Authorized, ['attributes' => ['foo' => ['bar' => 'baz']]]); } /** diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php index 768dd8bbc5..6633b715aa 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Fixtures/SbsClientStateManager.php @@ -19,6 +19,8 @@ namespace OpenConext\EngineBlockFunctionalTestingBundle\Fixtures; use InvalidArgumentException; +use OpenConext\EngineBlockBundle\Sbs\AuthzResponse; +use OpenConext\EngineBlockBundle\Sbs\Msg; use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; use OpenConext\EngineBlockFunctionalTestingBundle\Fixtures\DataStore\AbstractDataStore; @@ -45,24 +47,24 @@ public function __construct( $this->dataStore = $dataStore; } - public function prepareAuthzResponse(string $msg, ?array $attributes = null): void + public function prepareAuthzResponse(Msg $msg, ?array $attributes = null): void { - if ($msg === SbsClientInterface::INTERRUPT) { + if ($msg === Msg::Interrupt) { $this->authz = [ - 'msg' => SbsClientInterface::INTERRUPT, + 'msg' => $msg->value, 'nonce' => 'my-nonce', ]; - } elseif ($msg === SbsClientInterface::AUTHORIZED) { + } elseif ($msg === Msg::Authorized) { $this->authz = [ - 'msg' => SbsClientInterface::AUTHORIZED, + 'msg' => $msg->value, ]; $this->authz += $attributes ?? $this->getValidMockAttributes(); - } elseif ($msg === SbsClientInterface::ERROR) { + } elseif ($msg === Msg::Error) { $this->authz = [ - 'msg' => SbsClientInterface::ERROR, + 'msg' => $msg->value, ]; } else { - throw new InvalidArgumentException(sprintf('"%s" is not a valid authz message.', $msg)); + throw new InvalidArgumentException(sprintf('"%s" is not a valid authz message.', $msg->value)); } $this->save(); diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php index 774ee2a821..e344b54927 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php @@ -20,7 +20,6 @@ use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; use OpenConext\EngineBlock\Metadata\MetadataRepository\MetadataRepositoryInterface; -use OpenConext\EngineBlockBundle\Configuration\Feature; use OpenConext\EngineBlockBundle\Configuration\FeatureConfiguration; use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; use OpenConext\EngineBlockBundle\Sbs\Dto\AuthzRequest; @@ -28,6 +27,7 @@ use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; use OpenConext\EngineBlockBundle\Sbs\SbsAttributeMerger; use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; use SAML2\Assertion; use SAML2\AuthnRequest; use SAML2\Response; @@ -57,20 +57,32 @@ public function setUp(): void public function testItDoesNothingIfFeatureFlagNotEnabled(): void { - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); + $sbsClient = Mockery::mock(SbsClientInterface::class); + + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + $sbsClient, + new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => false]), + new SbsAttributeMerger([]), + new NullLogger(), + ); $request = $this->mockRequest(); $sramFilter->setRequest($request); - $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => false])); - $sramFilter->execute(); $this->assertEmpty($sramFilter->getResponseAttributes()); } public function testItDoesNothingIfSpDoesNotHaveCollabEnabled(): void { - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); + $sbsClient = Mockery::mock(SbsClientInterface::class); + + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + $sbsClient, + new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true]), + new SbsAttributeMerger([]), + new NullLogger(), + ); $server = Mockery::mock(EngineBlock_Corto_ProxyServer::class); $server->shouldReceive('getRepository') @@ -81,10 +93,6 @@ public function testItDoesNothingIfSpDoesNotHaveCollabEnabled(): void $request = $this->mockRequest(); $sramFilter->setRequest($request); - $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true])); - - /** @var \Mockery\Mock $sbsClient */ - $sbsClient = $this->mockSbsClient(); $sbsClient->shouldNotReceive('authz'); $sp = $this->mockServiceProvider('spEntityId'); @@ -98,7 +106,14 @@ public function testItDoesNothingIfSpDoesNotHaveCollabEnabled(): void public function testItAddsNonceWhenMessageInterrupt(): void { - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); + $sbsClient = Mockery::mock(SbsClientInterface::class); + + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + $sbsClient, + new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true]), + new SbsAttributeMerger([]), + new NullLogger(), + ); $initialAttributes = ['urn:mace:dir:attribute-def:uid' => ['userIdValue']]; $sramFilter->setResponseAttributes($initialAttributes); @@ -115,11 +130,6 @@ public function testItAddsNonceWhenMessageInterrupt(): void $sramFilter->setResponse(new EngineBlock_Saml2_ResponseAnnotationDecorator(new Response())); - $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true])); - - /** @var \Mockery\Mock $sbsClient */ - $sbsClient = $this->mockSbsClient(); - $response = AuthzResponse::fromData([ 'msg' => 'interrupt', 'nonce' => 'hash123', @@ -128,12 +138,7 @@ public function testItAddsNonceWhenMessageInterrupt(): void ] ]); - $expectedRequest = new AuthzRequest(); - $expectedRequest->userId = ''; - $expectedRequest->eduPersonPrincipalName = ''; - $expectedRequest->continueUrl = 'https://example.org?ID='; - $expectedRequest->serviceId = 'spEntityId'; - $expectedRequest->issuerId = 'idpEntityId'; + $expectedRequest = new AuthzRequest('', '', 'https://example.org?ID=', 'spEntityId', 'idpEntityId'); $sbsClient->shouldReceive('authz') ->withArgs(function ($args) use ($expectedRequest) { @@ -162,7 +167,19 @@ public function testItAddsNonceWhenMessageInterrupt(): void public function testItAddsSramAttributesOnStatusAuthorized(): void { - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); + $sbsClient = Mockery::mock(SbsClientInterface::class); + + $attributeMerger = new SbsAttributeMerger([ + 'urn:mace:dir:attribute-def:uid' => 'xs:string', + 'urn:mace:dir:attribute-def:eduPersonEntitlement' => 'xs:string', + ]); + + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + $sbsClient, + new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true]), + $attributeMerger, + new NullLogger() + ); $initialAttributes = ['urn:mace:dir:attribute-def:uid' => ['userIdValue']]; $sramFilter->setResponseAttributes($initialAttributes); @@ -180,16 +197,6 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void $sramFilter->setResponse(new EngineBlock_Saml2_ResponseAnnotationDecorator(new Response())); - // Mock the SbsAttributeMerger with allowed attributes - $this->mockSbsAttributeMerger([ - 'urn:mace:dir:attribute-def:uid' => 'xs:string', - 'urn:mace:dir:attribute-def:eduPersonEntitlement' => 'xs:string', - ]); - $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true])); - - /** @var \Mockery\Mock $sbsClient */ - $sbsClient = $this->mockSbsClient(); - $response = AuthzResponse::fromData([ 'msg' => 'authorized', 'nonce' => 'hash123', @@ -199,12 +206,7 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void ], ]); - $expectedRequest = new AuthzRequest(); - $expectedRequest->userId = ''; - $expectedRequest->eduPersonPrincipalName = ''; - $expectedRequest->continueUrl = 'https://example.org?ID='; - $expectedRequest->serviceId = 'spEntityId'; - $expectedRequest->issuerId = 'idpEntityId'; + $expectedRequest = new AuthzRequest('', '', 'https://example.org?ID=', 'spEntityId', 'idpEntityId'); $sbsClient->shouldReceive('authz') ->withArgs(function ($args) use ($expectedRequest) { @@ -242,10 +244,15 @@ public function testThrowsEngineBlockExceptionIfPolicyCannotBeChecked() $this->expectException(EngineBlock_Exception_SbsCheckFailed::class); $this->expectExceptionMessage('The SBS server could not be queried: Server could not be reached.'); - $sbsClient = $this->mockSbsClient(); + $sbsClient = Mockery::mock(SbsClientInterface::class); $sbsClient->expects('authz')->andThrows(new InvalidSbsResponseException('Server could not be reached.')); - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter(); + $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + $sbsClient, + new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true]), + new SbsAttributeMerger([]), + new NullLogger(), + ); $initialAttributes = ['urn:mace:dir:attribute-def:uid' => ['userIdValue']]; $sramFilter->setResponseAttributes($initialAttributes); @@ -262,7 +269,6 @@ public function testThrowsEngineBlockExceptionIfPolicyCannotBeChecked() $sramFilter->setResponse(new EngineBlock_Saml2_ResponseAnnotationDecorator(new Response())); - $this->mockFeatureConfiguration(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true])); /** @var \Mockery\Mock|ServiceProvider $sp */ $sp = $this->mockServiceProvider('spEntityId'); @@ -300,47 +306,4 @@ private function mockRequest(): EngineBlock_Saml2_AuthnRequestAnnotationDecorato $response->setAssertions(array($assertion)); return new EngineBlock_Saml2_AuthnRequestAnnotationDecorator($request); } - - private function mockSbsClient() - { - $sbsClient = Mockery::mock(SbsClientInterface::class); - - /** @var EngineBlock_Application_TestDiContainer $container */ - $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); - $container->setSbsClient($sbsClient); - - return $sbsClient; - } - - private function mockSbsAttributeMerger(array $allowedAttributeTypes) - { - $sbsAttributeMerger = new SbsAttributeMerger($allowedAttributeTypes); - - /** @var EngineBlock_Application_TestDiContainer $container */ - $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); - $container->setSbsAttributeMerger($sbsAttributeMerger); - } - - private function mockFeatureConfiguration(FeatureConfiguration $featureConfiguration) - { - /** @var EngineBlock_Application_TestDiContainer $container */ - $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); - $container->setFeatureConfiguration($featureConfiguration); - - $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); - $container->setSbsAttributeMerger(null); - } - - protected function tearDown(): void - { - $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); - $container->setSbsClient(null); - - $container = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); - $container->setFeatureConfiguration(null); - $container->setSbsAttributeMerger(null); - - parent::tearDown(); - } - } diff --git a/tests/library/EngineBlock/Test/Corto/ProxyServerTest.php b/tests/library/EngineBlock/Test/Corto/ProxyServerTest.php index 84a991c65c..9249f288b2 100644 --- a/tests/library/EngineBlock/Test/Corto/ProxyServerTest.php +++ b/tests/library/EngineBlock/Test/Corto/ProxyServerTest.php @@ -67,8 +67,6 @@ public function testAllowCreateIsSet() 'AllowCreate', array_keys($nameIdPolicy), 'The NameIDPolicy should contain the key "AllowCreate"', - false, - true ); } diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php index 2bfe86913a..f668e31e11 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/AuthzResponseTest.php @@ -18,15 +18,9 @@ namespace OpenConext\EngineBlockBundle\Tests; -use GuzzleHttp\ClientInterface; -use OpenConext\EngineBlock\Http\HttpClient; use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; use OpenConext\EngineBlockBundle\Sbs\AuthzResponse; -use OpenConext\EngineBlockBundle\Sbs\Dto\AuthzRequest; -use OpenConext\EngineBlockBundle\Sbs\Dto\AttributesRequest; -use OpenConext\EngineBlockBundle\Sbs\AttributesResponse; -use OpenConext\EngineBlockBundle\Sbs\SbsClient; -use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; +use OpenConext\EngineBlockBundle\Sbs\Msg; use PHPUnit\Framework\TestCase; class AuthzResponseTest extends TestCase @@ -34,14 +28,13 @@ class AuthzResponseTest extends TestCase public function testFromDataValidAuthorizedResponse(): void { $jsonData = [ - 'msg' => SbsClientInterface::AUTHORIZED, + 'msg' => Msg::Authorized->value, 'attributes' => ['role' => 'admin'] ]; $response = AuthzResponse::fromData($jsonData); - $this->assertInstanceOf(AuthzResponse::class, $response); - $this->assertEquals(SbsClientInterface::AUTHORIZED, $response->msg); + $this->assertEquals(Msg::Authorized, $response->msg); $this->assertEquals(['role' => 'admin'], $response->attributes); $this->assertNull($response->nonce); } @@ -49,14 +42,13 @@ public function testFromDataValidAuthorizedResponse(): void public function testFromDataValidInterruptResponse(): void { $jsonData = [ - 'msg' => SbsClientInterface::INTERRUPT, + 'msg' => Msg::Interrupt->value, 'nonce' => 'random_nonce' ]; $response = AuthzResponse::fromData($jsonData); - $this->assertInstanceOf(AuthzResponse::class, $response); - $this->assertEquals(SbsClientInterface::INTERRUPT, $response->msg); + $this->assertEquals(Msg::Interrupt, $response->msg); $this->assertEquals('random_nonce', $response->nonce); $this->assertEmpty($response->attributes); } @@ -72,7 +64,7 @@ public function testFromDataMissingMsgThrowsException(): void public function testFromDataInvalidMsgThrowsException(): void { $this->expectException(InvalidSbsResponseException::class); - $this->expectExceptionMessage('Msg: "INVALID" is not a valid message'); + $this->expectExceptionMessage('"INVALID" is not a valid msg'); AuthzResponse::fromData(['msg' => 'INVALID']); } @@ -82,7 +74,7 @@ public function testFromDataInterruptWithoutNonceThrowsException(): void $this->expectException(InvalidSbsResponseException::class); $this->expectExceptionMessage('Key: "nonce" was not found in the SBS response'); - AuthzResponse::fromData(['msg' => SbsClientInterface::INTERRUPT]); + AuthzResponse::fromData(['msg' => Msg::Interrupt->value]); } public function testFromDataAuthorizedWithoutAttributesThrowsException(): void @@ -90,20 +82,19 @@ public function testFromDataAuthorizedWithoutAttributesThrowsException(): void $this->expectException(InvalidSbsResponseException::class); $this->expectExceptionMessage('Key: "attributes" was not found in the SBS response'); - AuthzResponse::fromData(['msg' => SbsClientInterface::AUTHORIZED]); + AuthzResponse::fromData(['msg' => Msg::Authorized->value]); } public function testFromDataAttributesNotArrayDefaultsToEmpty(): void { $jsonData = [ - 'msg' => SbsClientInterface::AUTHORIZED, + 'msg' => Msg::Authorized->value, 'attributes' => 'invalid_type' ]; $response = AuthzResponse::fromData($jsonData); - $this->assertInstanceOf(AuthzResponse::class, $response); - $this->assertEquals(SbsClientInterface::AUTHORIZED, $response->msg); + $this->assertEquals(Msg::Authorized, $response->msg); $this->assertEmpty($response->attributes); } } From 82d755c126beae70f39eaba1f9339fee8c503134 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Tue, 28 Oct 2025 16:17:53 +0100 Subject: [PATCH 28/38] rework sbs flow --- .../Command/ValidateAllowedConnection.php | 2 +- library/EngineBlock/Corto/Module/Bindings.php | 2 +- .../Module/Service/AssertionConsumer.php | 19 ++- .../Corto/Module/Service/SRAMInterrupt.php | 59 +++------ library/EngineBlock/Corto/Module/Services.php | 2 - library/EngineBlock/Corto/ProxyServer.php | 115 +++++++++--------- src/OpenConext/EngineBlock/Metadata/Coins.php | 4 +- 7 files changed, 91 insertions(+), 112 deletions(-) diff --git a/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php b/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php index ec03586f70..c2e1c60a7c 100644 --- a/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php +++ b/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php @@ -49,7 +49,7 @@ public function execute() } } - private function sbsFlowActive(ServiceProvider $sp) + private function sbsFlowActive(ServiceProvider $sp): bool { return $sp->getCoins()->collabEnabled(); } diff --git a/library/EngineBlock/Corto/Module/Bindings.php b/library/EngineBlock/Corto/Module/Bindings.php index 1a47e8aefb..0c035c57e3 100644 --- a/library/EngineBlock/Corto/Module/Bindings.php +++ b/library/EngineBlock/Corto/Module/Bindings.php @@ -615,7 +615,7 @@ public function send( EngineBlock_Saml2_MessageAnnotationDecorator $message, AbstractRole $remoteEntity, bool $useDefaultKey = false - ) { + ): void { $bindingUrn = $message->getDeliverByBinding(); $sspMessage = $message->getSspMessage(); diff --git a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php index 31ef02193c..8541640df7 100644 --- a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php @@ -91,6 +91,10 @@ public function serve($serviceName, Request $httpRequest) $application = EngineBlock_ApplicationSingleton::getInstance(); $log = $application->getLogInstance(); + if(!$receivedRequest instanceof EngineBlock_Saml2_AuthnRequestAnnotationDecorator){ + throw new RuntimeException('Request cannot be empty at this stage'); + } + if ($receivedResponse->isTransparentErrorResponse()) { $log->info('Response contains an error response status code, SP is configured with transparent_authn_context.'); $response = $this->_server->createTransparentErrorResponse($receivedRequest, $receivedResponse); @@ -172,17 +176,22 @@ public function serve($serviceName, Request $httpRequest) $this->_server->filterInputAssertionAttributes($receivedResponse, $receivedRequest); - if ($this->_server->handleSRAMInterruptCallout($receivedResponse, $receivedRequest)) { + $log->info('Handle SRAM interrupt callout'); + if($this->_server->shouldPerformSramCallout($receivedResponse) === true){ + $this->_server->handleSRAMInterruptCallout($receivedResponse, $receivedRequest); + return; } - // Handle Consent authentication callout - if ($this->_server->handleConsentAuthenticationCallout($receivedResponse, $receivedRequest)) { + $this->_server->addConsentProcessStep($receivedResponse, $receivedRequest); + + if($this->_server->shouldUseStepup($receivedResponse, $receivedRequest)){ + $this->_server->handleStepupAuthenticationCallout($receivedResponse, $receivedRequest, $originalAssertions); + return; } - // Handle Stepup authentication callout - $this->_server->handleStepupAuthenticationCallout($receivedResponse, $receivedRequest, $originalAssertions); + $this->_server->handleConsentAuthenticationCallout($receivedResponse, $receivedRequest); } /** diff --git a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php index a5d3281be5..a31d4bcfde 100644 --- a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php +++ b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php @@ -16,9 +16,7 @@ * limitations under the License. */ -use OpenConext\EngineBlock\Service\AuthenticationStateHelperInterface; use OpenConext\EngineBlock\Service\ProcessingStateHelperInterface; -use OpenConext\EngineBlock\Stepup\StepupGatewayCallOutHelper; use OpenConext\EngineBlockBundle\Sbs\Dto\AttributesRequest; use OpenConext\EngineBlockBundle\Sbs\SbsAttributeMerger; use Symfony\Component\HttpFoundation\Request; @@ -26,59 +24,30 @@ class EngineBlock_Corto_Module_Service_SRAMInterrupt implements EngineBlock_Corto_Module_Service_ServiceInterface { - /** - * @var EngineBlock_Corto_ProxyServer - */ - protected $_server; + private EngineBlock_Corto_ProxyServer $_server; - /** - * @var AuthenticationStateHelperInterface - */ - private $_authenticationStateHelper; + private ProcessingStateHelperInterface $_processingStateHelper; - /** - * @var ProcessingStateHelperInterface - */ - private $_processingStateHelper; - - /** - * @var StepupGatewayCallOutHelper - */ - private $_stepupGatewayCallOutHelper; - - /** - * @var SbsAttributeMerger - */ - private $_sbsAttributeMerger; + private SbsAttributeMerger $_sbsAttributeMerger; public function __construct( EngineBlock_Corto_ProxyServer $server, - AuthenticationStateHelperInterface $stateHelper, ProcessingStateHelperInterface $processingStateHelper, - StepupGatewayCallOutHelper $stepupGatewayCallOutHelper, SbsAttributeMerger $sbsAttributeMerger, ) { $this->_server = $server; - $this->_authenticationStateHelper = $stateHelper; $this->_processingStateHelper = $processingStateHelper; - $this->_stepupGatewayCallOutHelper = $stepupGatewayCallOutHelper; $this->_sbsAttributeMerger = $sbsAttributeMerger; } /** * route that receives the user when they get back from their SBS interrupt, * and resumes the AuthN flow. - * - * @param $serviceName - * @param Request $httpRequest */ - public function serve($serviceName, Request $httpRequest) + public function serve($serviceName, Request $httpRequest): void { - $application = EngineBlock_ApplicationSingleton::getInstance(); - - // Get active request $id = $httpRequest->get('ID'); $nextProcessStep = $this->_processingStateHelper->getStepByRequestId( @@ -90,6 +59,10 @@ public function serve($serviceName, Request $httpRequest) $receivedRequest = $this->_server->getReceivedRequestFromResponse($receivedResponse); $originalAssertion = clone $receivedResponse->getAssertions()[0]; + if(!$receivedRequest instanceof EngineBlock_Saml2_AuthnRequestAnnotationDecorator){ + throw new RuntimeException('Request cannot be empty at this stage'); + } + $attributes = $receivedResponse->getAssertion()->getAttributes(); $nonce = $receivedResponse->getSRAMInterruptNonce(); @@ -100,28 +73,24 @@ public function serve($serviceName, Request $httpRequest) $attributes = $this->_sbsAttributeMerger->mergeAttributes($attributes, $interruptResponse->attributes); $receivedResponse->getAssertion()->setAttributes($attributes); - // Get the value types for the merged attributes and set them on the assertion $mergedAttributeTypes = $this->_sbsAttributeMerger->getMergedAttributeTypes(); if (!empty($mergedAttributeTypes)) { - // Get existing attribute value types and merge with the new ones $existingTypes = $receivedResponse->getAssertion()->getAttributesValueTypes(); $updatedTypes = array_merge($existingTypes, $mergedAttributeTypes); $receivedResponse->getAssertion()->setAttributesValueTypes($updatedTypes); } } - /* - * Continue to Consent/StepUp - */ - // @TODO JOHAN, HIER gaat het fout - // dit is andersom! - if ($this->_server->handleConsentAuthenticationCallout($receivedResponse, $receivedRequest)) { + $this->_server->addConsentProcessStep($receivedResponse, $receivedRequest); + + if($this->_server->shouldUseStepup($receivedResponse, $receivedRequest)){ + $this->_server->handleStepupAuthenticationCallout($receivedResponse, $receivedRequest, $originalAssertion); + return; } - // @TODO JOHAN: Klopt de $originalAssertion hier? - $this->_server->handleStepupAuthenticationCallout($receivedResponse, $receivedRequest, $originalAssertion); + $this->_server->handleConsentAuthenticationCallout($receivedResponse, $receivedRequest); } private function getSbsClient() diff --git a/library/EngineBlock/Corto/Module/Services.php b/library/EngineBlock/Corto/Module/Services.php index 279415ccb0..d9c2e4ce72 100644 --- a/library/EngineBlock/Corto/Module/Services.php +++ b/library/EngineBlock/Corto/Module/Services.php @@ -99,9 +99,7 @@ private function factoryService($className, EngineBlock_Corto_ProxyServer $serve case EngineBlock_Corto_Module_Service_SRAMInterrupt::class : return new EngineBlock_Corto_Module_Service_SRAMInterrupt( $server, - $diContainer->getAuthenticationStateHelper(), $diContainer->getProcessingStateHelper(), - $diContainer->getStepupGatewayCallOutHelper(), $diContainer->getSbsAttributeMerger() ); case EngineBlock_Corto_Module_Service_AssertionConsumer::class : diff --git a/library/EngineBlock/Corto/ProxyServer.php b/library/EngineBlock/Corto/ProxyServer.php index 7feae09b9d..e74161d076 100644 --- a/library/EngineBlock/Corto/ProxyServer.php +++ b/library/EngineBlock/Corto/ProxyServer.php @@ -482,7 +482,7 @@ public function sendStepupAuthenticationRequest( NameID $nameId, bool $isForceAuthn, Assertion $originalAssertion - ) { + ): void { $ebRequest = EngineBlock_Saml2_AuthnRequestFactory::createFromRequest( $spRequest, $identityProvider, @@ -569,34 +569,32 @@ function sendSRAMInterruptRequest($response): void $this->redirect($redirect_url, ''); } - function handleSRAMInterruptCallout( - $receivedResponse, - $receivedRequest - ) { - $logger = $this->getLogger(); - $logger->info('Handle SRAM interrupt callout'); - - if ($receivedResponse->getSRAMInterruptNonce() !== '') { - - // Add the SRAM step - $this->_diContainer->getProcessingStateHelper()->addStep( - $receivedRequest->getId(), - ProcessingStateHelperInterface::STEP_SRAM, - $this->getEngineSpRole(), - $receivedResponse - ); - - // Redirect to SRAM - $this->sendSRAMInterruptRequest($receivedResponse); + public function shouldPerformSramCallout( + EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, + ): bool + { + return $receivedResponse->getSRAMInterruptNonce() !== ''; + } - return true; - } + public function handleSRAMInterruptCallout( + EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, + EngineBlock_Saml2_AuthnRequestAnnotationDecorator $receivedRequest, + ): void { + // Add the SRAM step + $this->_diContainer->getProcessingStateHelper()->addStep( + $receivedRequest->getId(), + ProcessingStateHelperInterface::STEP_SRAM, + $this->getEngineSpRole(), + $receivedResponse + ); - return false; + // Redirect to SRAM + $this->sendSRAMInterruptRequest($receivedResponse); } - function handleStepupAuthenticationCallout( - $receivedResponse, - $receivedRequest, + + public function handleStepupAuthenticationCallout( + EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, + EngineBlock_Saml2_AuthnRequestAnnotationDecorator $receivedRequest, $originalAssertion, ): void { $logger = $this->getLogger(); @@ -644,11 +642,7 @@ function handleStepupAuthenticationCallout( ); } - /** - * @return ServiceProvider - * // @TODO: JOHAN - */ - public function getEngineSpRole() + public function getEngineSpRole(): ServiceProvider { $keyId = $this->getKeyId(); if (!$keyId) { @@ -659,23 +653,11 @@ public function getEngineSpRole() return ServiceProvider::fromServiceProviderEntity($serviceProvider); } - // @TODO: JOHAN - function handleConsentAuthenticationCallout( - $receivedResponse, - $receivedRequest - // $currentProcessStep - ) { - $logger = $this->getLogger(); - $logger->info('Handle Consent authentication callout'); - - // Add the consent step - $currentProcessStep = $this->_diContainer->getProcessingStateHelper()->addStep( - $receivedRequest->getId(), - ProcessingStateHelperInterface::STEP_CONSENT, - $this->getEngineSpRole(), - $receivedResponse - ); - + public function shouldUseStepup( + EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, + EngineBlock_Saml2_AuthnRequestAnnotationDecorator $receivedRequest, + ): bool + { $issuer = $receivedResponse->getIssuer() ? $receivedResponse->getIssuer()->getValue() : ''; $idp = $this->getRepository()->fetchIdentityProviderByEntityId($issuer); @@ -696,16 +678,37 @@ function handleConsentAuthenticationCallout( $loaRepository = $this->_diContainer->getLoaRepository(); $authnRequestLoas = $receivedRequest->getStepupObligations($loaRepository->getStepUpLoas()); - $shouldUseStepup = $this->_diContainer->getStepupGatewayCallOutHelper()->shouldUseStepup($idp, $sp, $authnRequestLoas, $pdpLoas); + return $this->_diContainer->getStepupGatewayCallOutHelper()->shouldUseStepup($idp, $sp, $authnRequestLoas, $pdpLoas); + } - // Goto consent if no Stepup authentication is needed - if (!$shouldUseStepup) { - $this->sendConsentAuthenticationRequest($receivedResponse, $receivedRequest, $currentProcessStep->getRole(), $this->_diContainer->getAuthenticationStateHelper()->getAuthenticationState()); - return true; - } + public function handleConsentAuthenticationCallout( + EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, + EngineBlock_Saml2_AuthnRequestAnnotationDecorator $receivedRequest + ): void { + $logger = $this->getLogger(); + $logger->info('Handle Consent authentication callout'); - // - return false; + + $this->sendConsentAuthenticationRequest( + $receivedResponse, + $receivedRequest, + $this->getEngineSpRole(), + $this->_diContainer->getAuthenticationStateHelper()->getAuthenticationState(), + ); + } + + public function addConsentProcessStep( + EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, + EngineBlock_Saml2_AuthnRequestAnnotationDecorator $receivedRequest, + ) + { + // Add the consent step + $this->_diContainer->getProcessingStateHelper()->addStep( + $receivedRequest->getId(), + ProcessingStateHelperInterface::STEP_CONSENT, + $this->getEngineSpRole(), + $receivedResponse + ); } function sendConsentAuthenticationRequest( diff --git a/src/OpenConext/EngineBlock/Metadata/Coins.php b/src/OpenConext/EngineBlock/Metadata/Coins.php index c9d3005d54..e5d9e53f75 100644 --- a/src/OpenConext/EngineBlock/Metadata/Coins.php +++ b/src/OpenConext/EngineBlock/Metadata/Coins.php @@ -240,9 +240,9 @@ public function signatureMethod() return $this->getValue('signatureMethod', XMLSecurityKey::RSA_SHA256); } - public function collabEnabled() + public function collabEnabled(): bool { - return $this->getValue('collabEnabled', false); + return $this->getValue('collabEnabled', false) === true; } public function mfaEntities(): MfaEntityCollection From 11ff3d998be521c8b9d8ce91fdf050c300b95d30 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Wed, 29 Oct 2025 09:12:03 +0100 Subject: [PATCH 29/38] make sram filter handle attribute types correctly --- config/packages/parameters.yml.dist | 8 ++-- .../Filter/Command/SRAMInterruptFilter.php | 12 +++++- .../Corto/Module/Service/SRAMInterrupt.php | 9 +---- .../Sbs/SbsAttributeMerger.php | 38 +++---------------- tests/behat.yml | 2 +- .../Command/SRAMInterruptFilterTest.php | 4 +- .../Sbs/SbsAttributeMergerTest.php | 26 ++++--------- 7 files changed, 33 insertions(+), 66 deletions(-) diff --git a/config/packages/parameters.yml.dist b/config/packages/parameters.yml.dist index eb354d623a..cfc3509c5c 100644 --- a/config/packages/parameters.yml.dist +++ b/config/packages/parameters.yml.dist @@ -321,7 +321,7 @@ parameters: sram.interrupt_location: interrupt sram.verify_peer: false sram.allowed_attributes: - 'urn:mace:dir:attribute-def:eduPersonEntitlement': 'xs:string' - 'urn:mace:dir:attribute-def:eduPersonPrincipalName': 'xs:string' - 'urn:mace:dir:attribute-def:uid': 'xs:string' - 'urn:oid:1.3.6.1.4.1.24552.500.1.1.1.13': 'xs:string' + - 'urn:mace:dir:attribute-def:eduPersonEntitlement' + - 'urn:mace:dir:attribute-def:eduPersonPrincipalName' + - 'urn:mace:dir:attribute-def:uid' + - 'urn:oid:1.3.6.1.4.1.24552.500.1.1.1.13' diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index f7b64b622f..f012107b45 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -26,7 +26,7 @@ use Psr\Log\LoggerInterface; class EngineBlock_Corto_Filter_Command_SRAMInterruptFilter extends EngineBlock_Corto_Filter_Command_Abstract - implements EngineBlock_Corto_Filter_Command_ResponseAttributesModificationInterface + implements EngineBlock_Corto_Filter_Command_ResponseAttributesModificationInterface, EngineBlock_Corto_Filter_Command_ResponseAttributeValueTypesModificationInterface { public function __construct( @@ -89,7 +89,6 @@ public function execute(): void } if ($interruptResponse->msg === Msg::Authorized && !empty($interruptResponse->attributes)) { - // @TODO JOHAN hier ook types? Nee? $this->_responseAttributes = $this->attributeMerger->mergeAttributes($this->_responseAttributes, $interruptResponse->attributes); return; @@ -123,4 +122,13 @@ private function buildRequest(ServiceProvider $serviceProvider): AuthzRequest $issuer_id ); } + + public function getResponseAttributeValueTypes(): array + { + /** + * Since we do not know the types of the attributes received from SRAM, they are reset here, like in AttributeManipulations + */ + + return []; + } } diff --git a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php index a31d4bcfde..8d68bd0fb3 100644 --- a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php +++ b/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php @@ -73,15 +73,10 @@ public function serve($serviceName, Request $httpRequest): void $attributes = $this->_sbsAttributeMerger->mergeAttributes($attributes, $interruptResponse->attributes); $receivedResponse->getAssertion()->setAttributes($attributes); - $mergedAttributeTypes = $this->_sbsAttributeMerger->getMergedAttributeTypes(); - if (!empty($mergedAttributeTypes)) { - $existingTypes = $receivedResponse->getAssertion()->getAttributesValueTypes(); - $updatedTypes = array_merge($existingTypes, $mergedAttributeTypes); - $receivedResponse->getAssertion()->setAttributesValueTypes($updatedTypes); - } + // After updating the attributes, reset the types, so SAML2 will set them + $receivedResponse->getAssertion()->setAttributesValueTypes([]); } - $this->_server->addConsentProcessStep($receivedResponse, $receivedRequest); if($this->_server->shouldUseStepup($receivedResponse, $receivedRequest)){ diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php index 64401b65df..dc8e66b6fb 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMerger.php @@ -19,42 +19,26 @@ namespace OpenConext\EngineBlockBundle\Sbs; use EngineBlock_ApplicationSingleton; +use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; class SbsAttributeMerger { /** - * @var array Map of attribute names to their value types (e.g., 'name' => 'xs:string') + * @var array */ - private $allowedAttributeTypes; + private array $allowedAttributeNames; - /** - * @var array Tracks the value types of merged attributes - */ - private $mergedAttributeTypes = []; - - public function __construct(array $allowedAttributeTypes) + public function __construct(array $allowedAttributeNames) { - foreach ($allowedAttributeTypes as $key => $value) { - assert(is_string($key), 'All keys in allowedAttributeTypes must be strings'); - assert(is_string($value), 'All values in allowedAttributeTypes must be strings'); - } - $this->allowedAttributeTypes = $allowedAttributeTypes; + $this->allowedAttributeNames = $allowedAttributeNames; } public function mergeAttributes(array $samlAttributes, array $sbsAttributes): array { - // Reset merged attribute types for this merge operation - $this->mergedAttributeTypes = []; - $validAttributes = $this->validSbsAttributes($sbsAttributes); foreach ($validAttributes as $key => $value) { - // Track the value type for this merged attribute - if (isset($this->allowedAttributeTypes[$key])) { - $this->mergedAttributeTypes[$key] = $this->allowedAttributeTypes[$key]; - } - if (!isset($samlAttributes[$key])) { $samlAttributes[$key] = $value; continue; @@ -72,16 +56,6 @@ public function mergeAttributes(array $samlAttributes, array $sbsAttributes): ar return $samlAttributes; } - /** - * Get the value types for attributes that were merged in the last mergeAttributes() call - * - * @return array Map of attribute names to their value types - */ - public function getMergedAttributeTypes(): array - { - return $this->mergedAttributeTypes; - } - /** * @SuppressWarnings(PHPMD.UnusedLocalVariable) $value is never used in the foreach */ @@ -91,7 +65,7 @@ private function validSbsAttributes(array $sbsAttributes): array $invalidKeys = []; foreach ($sbsAttributes as $key => $value) { - if (array_key_exists($key, $this->allowedAttributeTypes)) { + if (in_array($key, $this->allowedAttributeNames, true)) { $validAttributes[$key] = $value; } else { $invalidKeys[] = $key; diff --git a/tests/behat.yml b/tests/behat.yml index 956d53a62a..aa9ecbf600 100644 --- a/tests/behat.yml +++ b/tests/behat.yml @@ -42,7 +42,7 @@ default: mockIdpRegistry: '@engineblock.mock_entities.idp_registry' serviceRegistryFixture: '@engineblock.functional_testing.fixture.service_registry' - OpenConext\EngineBlockFunctionalTestingBundle\Features\Context\TranslationContext: - mockTranslator: '@engineblock.functional_testing.mock.translator' + mockTranslator: '@engineblock.functional_testing.mock.translator' - OpenConext\EngineBlockFunctionalTestingBundle\Features\Context\MinkContext: functional: mink_session: chrome diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php index e344b54927..22c34baa19 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php @@ -170,8 +170,8 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void $sbsClient = Mockery::mock(SbsClientInterface::class); $attributeMerger = new SbsAttributeMerger([ - 'urn:mace:dir:attribute-def:uid' => 'xs:string', - 'urn:mace:dir:attribute-def:eduPersonEntitlement' => 'xs:string', + 'urn:mace:dir:attribute-def:uid', + 'urn:mace:dir:attribute-def:eduPersonEntitlement', ]); $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( diff --git a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php index a51d07e954..d7fbb2ea08 100644 --- a/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php +++ b/tests/unit/OpenConext/EngineBlockBundle/Sbs/SbsAttributeMergerTest.php @@ -18,7 +18,6 @@ namespace OpenConext\EngineBlockBundle\Tests; -use OpenConext\EngineBlockBundle\Exception\InvalidSbsResponseException; use OpenConext\EngineBlockBundle\Sbs\SbsAttributeMerger; use PHPUnit\Framework\TestCase; @@ -27,10 +26,10 @@ class SbsAttributeMergerTest extends TestCase public function testMergeAttributesSuccessfully(): void { $allowedAttributes = [ - 'eduPersonEntitlement' => 'xs:string', - 'eduPersonPrincipalName' => 'xs:string', - 'uuid' => 'xs:string', - 'sshkey' => 'xs:string' + 'eduPersonEntitlement', + 'eduPersonPrincipalName', + 'uuid', + 'sshkey' ]; $merger = new SbsAttributeMerger($allowedAttributes); @@ -61,22 +60,13 @@ public function testMergeAttributesSuccessfully(): void ]; $this->assertEquals($expectedResult, $merger->mergeAttributes($samlAttributes, $sbsAttributes)); - - // Verify that merged attribute types are tracked - $expectedTypes = [ - 'uuid' => 'xs:string', - 'eduPersonEntitlement' => 'xs:string', - 'eduPersonPrincipalName' => 'xs:string', - 'sshkey' => 'xs:string' - ]; - $this->assertEquals($expectedTypes, $merger->getMergedAttributeTypes()); } public function testMergeAttributesWithInvalidKeysThrowsException(): void { $allowedAttributes = [ - 'email' => 'xs:string', - 'name' => 'xs:string' + 'email', + 'name' ]; $merger = new SbsAttributeMerger($allowedAttributes); @@ -100,8 +90,8 @@ public function testMergeAttributesWithInvalidKeysThrowsException(): void public function testMergeAttributesWithEmptySbsAttributes(): void { $allowedAttributes = [ - 'email' => 'xs:string', - 'name' => 'xs:string' + 'email', + 'name' ]; $merger = new SbsAttributeMerger($allowedAttributes); From b8b40f01336cd798545a071777be64e02699d59e Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Wed, 29 Oct 2025 09:44:00 +0100 Subject: [PATCH 30/38] cleanup --- library/EngineBlock/Corto/ProxyServer.php | 5 +- .../Saml2/ResponseAnnotationDecorator.php | 1 - sbs-stub/requirements.txt | 1 - sbs-stub/sbs.py | 112 ------------------ sbs-stub/start | 2 - sbs-stub/templates/interrupt.j2 | 9 -- 6 files changed, 2 insertions(+), 128 deletions(-) delete mode 100644 sbs-stub/requirements.txt delete mode 100755 sbs-stub/sbs.py delete mode 100755 sbs-stub/start delete mode 100644 sbs-stub/templates/interrupt.j2 diff --git a/library/EngineBlock/Corto/ProxyServer.php b/library/EngineBlock/Corto/ProxyServer.php index e74161d076..e4f1ea931d 100644 --- a/library/EngineBlock/Corto/ProxyServer.php +++ b/library/EngineBlock/Corto/ProxyServer.php @@ -151,7 +151,7 @@ public function __construct(Environment $twig) { $this->_server = $this; $this->twig = $twig; - // @TODO: JOHAN + $this->_diContainer = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer(); } @@ -559,8 +559,7 @@ public function sendStepupAuthenticationRequest( $this->getBindingsModule()->send($ebRequest, $identityProvider, true); } - // @TODO JOHAN: Heeft geen dependencies op ProxyServer, buiten redirect, Buiten Corto plaatsen? - function sendSRAMInterruptRequest($response): void + private function sendSRAMInterruptRequest($response): void { $nonce = $response->getSRAMInterruptNonce(); diff --git a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php index 1b918b8e25..6791ebf570 100644 --- a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php +++ b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php @@ -93,7 +93,6 @@ class EngineBlock_Saml2_ResponseAnnotationDecorator extends EngineBlock_Saml2_Me */ private ?string $_serializableRelayState = null; - /** @TODO JOHAN: Make nice */ protected string $SRAMInterruptNonce = ""; /** diff --git a/sbs-stub/requirements.txt b/sbs-stub/requirements.txt deleted file mode 100644 index 7e1060246f..0000000000 --- a/sbs-stub/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -flask diff --git a/sbs-stub/sbs.py b/sbs-stub/sbs.py deleted file mode 100755 index 848457d465..0000000000 --- a/sbs-stub/sbs.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python -import json -import logging -import secrets - -from flask import Flask, Response, request, render_template - -logging.getLogger().setLevel(logging.DEBUG) -logging.getLogger('flask_pyoidc').setLevel(logging.ERROR) -logging.getLogger('oic').setLevel(logging.ERROR) -logging.getLogger('jwkest').setLevel(logging.ERROR) -logging.getLogger('urllib3').setLevel(logging.ERROR) -logging.getLogger('werkzeug').setLevel(logging.ERROR) - -app = Flask(__name__, template_folder='templates', static_folder='static') - -nonces = {} - - -def debug(request): - for header in request.headers: - logging.debug(header) - logging.debug(f'request.args: {request.args}') - logging.debug(f'request.data: {request.data}') - logging.debug(f'request.form: {request.form}') - logging.debug(f'request.json: {request.json}') - - -@app.route('/', defaults={'path': ''}) -@app.route('/') -def catch_all(path): - logging.debug(f'-> {path}') - debug(request) - response = Response(status=200) - return response - - -@app.route('/api/users/authz_eb', methods=['POST']) -def authz(): - logging.debug('-> /api/users/authz_eb') - debug(request) - - uid = request.json.get('user_id') - continue_url = request.json.get('continue_url') - service_entity_id = request.json.get('service_id') - issuer_id = request.json.get('issuer_id') - - nonce = secrets.token_urlsafe() - nonces[nonce] = (uid, continue_url, service_entity_id, issuer_id) - - response = Response(status=200) - body = { - 'msg': 'interrupt', - # 'msg': 'authorized', - # 'msg': 'error', - 'nonce': nonce, - 'message': 'Foobar message', - 'attributes': { - 'urn:mace:dir:attribute-def:eduPersonEntitlement': [ - uid, - nonce, - 'urn:foobar' - ], - 'urn:mace:dir:attribute-def:uid': ['SBS-uid'], - 'urn:oid:1.3.6.1.4.1.24552.500.1.1.1.13': ['someKey'], - } - } - - logging.debug(f'<- {body}') - response.data = json.dumps(body) - - return response - - -@app.route('/api/users/interrupt', methods=['GET']) -def interrupt(): - logging.debug('-> /api/users/interrupt') - nonce = request.args.get('nonce') - (uid, continue_url, service_entity_id, issuer_id) = nonces.get(nonce, ('unknown', '/', '/', '')) - response = render_template('interrupt.j2', uid=uid, - service_entity_id=service_entity_id, issuer_id=issuer_id, url=continue_url) - - return response - - -@app.route('/api/users/attributes_eb', methods=['POST']) -def attributes(): - logging.debug('-> /api/users/attributes_eb') - debug(request) - - nonce = request.json.get('nonce') - (uid, _, _, _) = nonces.pop(nonce) - - response = Response(status=200) - body = { - 'attributes': { - 'urn:mace:dir:attribute-def:eduPersonEntitlement': [ - uid, - nonce, - 'urn:foobar', - ] - } - } - - logging.debug(f'<- {body}') - response.data = json.dumps(body) - - return response - - -if __name__ == "__main__": - app.run(host='0.0.0.0', port=12345, debug=True) diff --git a/sbs-stub/start b/sbs-stub/start deleted file mode 100755 index d0a61dad17..0000000000 --- a/sbs-stub/start +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -./venv/bin/python sbs.py diff --git a/sbs-stub/templates/interrupt.j2 b/sbs-stub/templates/interrupt.j2 deleted file mode 100644 index cd9a506668..0000000000 --- a/sbs-stub/templates/interrupt.j2 +++ /dev/null @@ -1,9 +0,0 @@ - - -

Hello {{uid}}!!

-

Coming from {{issuer_id}}

-

Going to {{service_entity_id}}

-

Accept AUP

-

Continue

- - From 2adb678a8117095d813dfa9bc2065552adcc3dc4 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Thu, 30 Oct 2025 10:48:25 +0100 Subject: [PATCH 31/38] Add scenario's that test if SBS integrates well with other EB features --- .../Features/SbsFlowIntegration.feature | 125 +++++++++++++++++- 1 file changed, 120 insertions(+), 5 deletions(-) diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature index 13d19e2731..83f7be7a4c 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature @@ -5,10 +5,10 @@ Feature: Background: Given an EngineBlock instance on "dev.openconext.local" - And no registered SPs - And no registered Idps - And an Identity Provider named "SSO-IdP" - And a Service Provider named "SSO-SP" + And no registered SPs + And no registered Idps + And an Identity Provider named "SSO-IdP" + And a Service Provider named "SSO-SP" Scenario: If the SBS authz check returns 'interrupt', the browser is redirected to SBS Given the SP "SSO-SP" requires SRAM collaboration @@ -57,4 +57,119 @@ Feature: Then the url should match "/feedback/unknown-error" And the response should contain "Logging in has failed" - #// hebben we stepup getest? + Scenario: Stepup authentication combined with SBS 'authorized' flow merges attributes and shows them on consent screen + Given the SP "SSO-SP" requires Stepup LoA "http://dev.openconext.local/assurance/loa2" + And the SP "SSO-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the "authorized" authz flow when called + And the sbs server will return valid attributes + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + And Stepup will successfully verify a user + Then the url should match "/authentication/stepup/consume-assertion" + And the response should contain "Review your information that will be shared." + And the response should contain "test_user@test.sram.surf.nl" + And the response should contain "ssh_key1" + And the response should contain "ssh_key2" + And the response should contain "Proceed to SSO-SP" + When I give my consent + And I pass through EngineBlock + Then the url should match "/functional-testing/SSO-SP/acs" + And the response should contain "ssh_key1" + And the response should contain "ssh_key2" + + Scenario: Stepup authentication combined with SBS 'interrupt' flow redirects to SBS then shows merged attributes on consent + Given the SP "SSO-SP" requires Stepup LoA "http://dev.openconext.local/assurance/loa2" + And the SP "SSO-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the "interrupt" authz flow when called + And the sbs server will return valid attributes + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + Then the url should match "/functional-testing/interrupt" + And I pass through SBS + And Stepup will successfully verify a user + Then the url should match "/authentication/stepup/consume-assertion" + And the response should contain "Review your information that will be shared." + And the response should contain "test_user@test.sram.surf.nl" + And the response should contain "ssh_key1" + And the response should contain "ssh_key2" + And the response should contain "Proceed to SSO-SP" + When I give my consent + And I pass through EngineBlock + Then the url should match "/functional-testing/SSO-SP/acs" + And the response should contain "ssh_key1" + And the response should contain "ssh_key2" + + Scenario: SBS flow is skipped when feature is disabled + Given the SP "SSO-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is disabled + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + Then the url should match "/authentication/sp/consume-assertion" + And the response should contain "Review your information that will be shared." + And the response should not contain "test_user@test.sram.surf.nl" + And the response should not contain "ssh_key1" + When I give my consent + And I pass through EngineBlock + Then the url should match "/functional-testing/SSO-SP/acs" + And the response should not contain "ssh_key1" + + Scenario: SBS flow is skipped when SP does not require SRAM collaboration + Given feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the "authorized" authz flow when called + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + Then the url should match "/authentication/sp/consume-assertion" + And the response should contain "Review your information that will be shared." + And the response should not contain "test_user@test.sram.surf.nl" + And the response should not contain "ssh_key1" + When I give my consent + And I pass through EngineBlock + Then the url should match "/functional-testing/SSO-SP/acs" + And the response should not contain "ssh_key1" + + Scenario: SBS 'authorized' flow works with trusted proxy + Given an Identity Provider named "Trusted-IdP" + And a Service Provider named "Proxy-SP" + And a Service Provider named "End-SP" + And the SP "End-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the "authorized" authz flow when called + And the sbs server will return valid attributes + And SP "Proxy-SP" is authenticating for SP "End-SP" + And SP "Proxy-SP" is a trusted proxy + And SP "Proxy-SP" signs its requests + When I log in at "Proxy-SP" + And I select "Trusted-IdP" on the WAYF + And I pass through EngineBlock + And I pass through the IdP + Then the response should contain "Review your information that will be shared." + And the response should contain "test_user@test.sram.surf.nl" + And the response should contain "ssh_key1" + When I give my consent + And I pass through EngineBlock + Then the url should match "/functional-testing/Proxy-SP/acs" + And the response should contain "ssh_key1" + And the response should contain "ssh_key2" + + Scenario: SBS attributes respect attribute release policy + Given the SP "SSO-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the "authorized" authz flow when called + And the sbs server will return valid attributes + And SP "SSO-SP" allows no attributes + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + Then the response should contain "Review your information that will be shared." + And the response should not contain "ssh_key1" + And the response should not contain "test_user@test.sram.surf.nl" + When I give my consent + And I pass through EngineBlock + Then the url should match "/functional-testing/SSO-SP/acs" + And the response should not contain "ssh_key1" From 111a20f1aba530bae7f60a43bf634db72ce81673 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Thu, 30 Oct 2025 15:10:56 +0100 Subject: [PATCH 32/38] Small Rework --- ci/qa-config/rector.php | 1 + .../Filter/Command/SRAMInterruptFilter.php | 3 +- .../Corto/Module/Service/SRAMInterrupt.php | 2 +- .../Sbs/Dto/AttributesRequest.php | 17 +---- .../EngineBlockBundle/Sbs/SbsClient.php | 67 ++++--------------- 5 files changed, 19 insertions(+), 71 deletions(-) diff --git a/ci/qa-config/rector.php b/ci/qa-config/rector.php index 687ba884ee..ffe582ce30 100644 --- a/ci/qa-config/rector.php +++ b/ci/qa-config/rector.php @@ -33,4 +33,5 @@ \Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector::class, \Rector\Php81\Rector\Property\ReadOnlyPropertyRector::class, \Rector\DeadCode\Rector\StaticCall\RemoveParentCallWithoutParentRector::class, + \Rector\Php82\Rector\Class_\ReadOnlyClassRector::class, ]); diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index f012107b45..84bb4f3b98 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -1,7 +1,7 @@ getAssertion()->getAttributes(); $nonce = $receivedResponse->getSRAMInterruptNonce(); - $request = AttributesRequest::create($nonce); + $request = new AttributesRequest($nonce); $interruptResponse = $this->getSbsClient()->requestAttributesFor($request); if (!empty($interruptResponse->attributes)) { diff --git a/src/OpenConext/EngineBlockBundle/Sbs/Dto/AttributesRequest.php b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AttributesRequest.php index 567988cde6..290849f75c 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/Dto/AttributesRequest.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/Dto/AttributesRequest.php @@ -23,23 +23,12 @@ class AttributesRequest implements JsonSerializable { - /** - * @var string - */ - public $nonce; - - public static function create( - string $nonce - ) : AttributesRequest { + public function __construct(public readonly string $nonce) + { Assertion::string($nonce, 'The nonce must be a string.'); - - $request = new self(); - $request->nonce = $nonce; - - return $request; } - public function jsonSerialize() : array + public function jsonSerialize(): array { return [ 'nonce' => $this->nonce, diff --git a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php index a0989e0b63..d51ea930c2 100644 --- a/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php +++ b/src/OpenConext/EngineBlockBundle/Sbs/SbsClient.php @@ -25,64 +25,22 @@ final class SbsClient implements SbsClientInterface { - /** - * @var HttpClient - */ - private $httpClient; - - /** - * @var string - */ - private $interruptLocation; - - /** - * @var string - */ - private $apiToken; - - /** - * @var string - */ - private $sbsBaseUrl; - - /** - * @var string - */ - private $authzLocation; - - /** - * @var string - */ - private $attributesLocation; - - /** - * @var bool - */ - private $verifyPeer; - public function __construct( - HttpClient $httpClient, - string $sbsBaseUrl, - string $authzLocation, - string $attributesLocation, - string $interruptLocation, - string $apiToken, - bool $verifyPeer + private readonly HttpClient $httpClient, + private readonly string $sbsBaseUrl, + private readonly string $authzLocation, + private readonly string $attributesLocation, + private readonly string $interruptLocation, + private readonly string $apiToken, + private readonly bool $verifyPeer ) { - $this->httpClient = $httpClient; - $this->sbsBaseUrl = $sbsBaseUrl; - $this->authzLocation = $authzLocation; - $this->attributesLocation = $attributesLocation; - $this->interruptLocation = $interruptLocation; - $this->apiToken = $apiToken; - $this->verifyPeer = $verifyPeer; } public function authz(AuthzRequest $request): AuthzResponse { $jsonData = $this->httpClient->post( - json_encode($request), + json_encode($request, JSON_THROW_ON_ERROR), $this->buildUrl($this->authzLocation), [], $this->requestHeaders(), @@ -90,17 +48,16 @@ public function authz(AuthzRequest $request): AuthzResponse ); if (!is_array($jsonData)) { - throw new InvalidSbsResponseException('Received non-array from SBS server: ' . var_export($jsonData, true)); + throw new InvalidSbsResponseException('Received non-array from SBS server: ' . get_debug_type($jsonData)); } return AuthzResponse::fromData($jsonData); } - // Attributes use authzLocation !! public function requestAttributesFor(AttributesRequest $request): AttributesResponse { $jsonData = $this->httpClient->post( - json_encode($request), + json_encode($request, JSON_THROW_ON_ERROR), $this->buildUrl($this->attributesLocation), [], $this->requestHeaders(), @@ -108,7 +65,7 @@ public function requestAttributesFor(AttributesRequest $request): AttributesResp ); if (!is_array($jsonData)) { - throw new InvalidSbsResponseException('Received non-array from SBS server: ' . var_export($jsonData, true)); + throw new InvalidSbsResponseException('Received non-array from SBS server: ' . get_debug_type($jsonData)); } return AttributesResponse::fromData($jsonData); @@ -125,7 +82,7 @@ private function requestHeaders(): array public function getInterruptLocationLink(string $nonce): string { - return $this->buildUrl($this->interruptLocation) . "?nonce=$nonce"; + return $this->buildUrl($this->interruptLocation) . "?nonce=" . $nonce; } private function buildUrl(string $path): string From ab034aa929dcdef0dda45ce04b26d98a36e75f13 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Thu, 30 Oct 2025 15:47:12 +0100 Subject: [PATCH 33/38] Small Rework --- .../Corto/Filter/Command/SRAMInterruptFilter.php | 16 ++++++++-------- .../Filter/Command/ValidateAllowedConnection.php | 2 ++ .../Corto/Module/Service/AssertionConsumer.php | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php index 84bb4f3b98..82b0b0874d 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php @@ -108,19 +108,19 @@ private function buildRequest(ServiceProvider $serviceProvider): AuthzRequest $attributes = $this->getResponseAttributes(); $id = $this->_request->getId(); - $user_id = $this->_collabPersonId ?? ""; + $userId = $this->_collabPersonId ?? ""; $eppn = $attributes['urn:mace:dir:attribute-def:eduPersonPrincipalName'][0] ?? ""; - $continue_url = $this->_server->getUrl('SRAMInterruptService', '') . "?ID=$id"; - $service_id = $serviceProvider->entityId; - $issuer_id = $this->_identityProvider->entityId; + $continueUrl = $this->_server->getUrl('SRAMInterruptService', '') . "?ID=$id"; + $serviceId = $serviceProvider->entityId; + $issuerId = $this->_identityProvider->entityId; return new AuthzRequest( - $user_id, + $userId, $eppn, - $continue_url, - $service_id, - $issuer_id + $continueUrl, + $serviceId, + $issuerId ); } diff --git a/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php b/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php index c2e1c60a7c..43f9c27df1 100644 --- a/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php +++ b/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php @@ -27,6 +27,8 @@ public function execute() { $sp = $this->_serviceProvider; + // As part of the SBS integration, the Trusted proxy check should not be executed if the `collabEnabled` coin is enabled for the SP + // and the `feature_enable_sram_interrupt` is enabled. if ($this->sbsFlowActive($sp)) { return; } diff --git a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php index 8541640df7..4fd02a780b 100644 --- a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php @@ -176,8 +176,8 @@ public function serve($serviceName, Request $httpRequest) $this->_server->filterInputAssertionAttributes($receivedResponse, $receivedRequest); - $log->info('Handle SRAM interrupt callout'); if($this->_server->shouldPerformSramCallout($receivedResponse) === true){ + $log->info('Handle SRAM interrupt callout'); $this->_server->handleSRAMInterruptCallout($receivedResponse, $receivedRequest); return; From 418e411f03ff6f6e856254d6959178551cc66211 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Thu, 30 Oct 2025 16:09:55 +0100 Subject: [PATCH 34/38] Do not skip the ValidateAllowedConnection filter if SRAM feature flag is disabled. --- .../Command/ValidateAllowedConnection.php | 9 +++ library/EngineBlock/Corto/Filter/Input.php | 4 +- .../Command/ValidateAllowedConnectionTest.php | 75 +++++-------------- 3 files changed, 29 insertions(+), 59 deletions(-) diff --git a/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php b/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php index 43f9c27df1..d5aed296f2 100644 --- a/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php +++ b/library/EngineBlock/Corto/Filter/Command/ValidateAllowedConnection.php @@ -17,12 +17,17 @@ */ use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; +use OpenConext\EngineBlockBundle\Configuration\FeatureConfigurationInterface; /** * Validate if the IDP sending this response is allowed to connect to the SP that made the request. **/ class EngineBlock_Corto_Filter_Command_ValidateAllowedConnection extends EngineBlock_Corto_Filter_Command_Abstract { + public function __construct(private readonly FeatureConfigurationInterface $featureConfiguration) + { + } + public function execute() { $sp = $this->_serviceProvider; @@ -53,6 +58,10 @@ public function execute() private function sbsFlowActive(ServiceProvider $sp): bool { + if ($this->featureConfiguration->isEnabled('eb.feature_enable_sram_interrupt') === false) { + return false; + } + return $sp->getCoins()->collabEnabled(); } } diff --git a/library/EngineBlock/Corto/Filter/Input.php b/library/EngineBlock/Corto/Filter/Input.php index b7b4631cd4..105a8d06bf 100644 --- a/library/EngineBlock/Corto/Filter/Input.php +++ b/library/EngineBlock/Corto/Filter/Input.php @@ -71,7 +71,7 @@ public function getCommands() new EngineBlock_Corto_Filter_Command_VerifyShibMdScopingAllowsSubjectId($logger, $blockUsersOnViolation), // Check whether this IdP is allowed to send a response to the destination SP - new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection(), + new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection($featureConfiguration), // Require valid UID and SchacHomeOrganization new EngineBlock_Corto_Filter_Command_ValidateRequiredAttributes(), @@ -94,7 +94,7 @@ public function getCommands() // Add SRAM attributes if not new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( $diContainer->getSbsClient(), - $diContainer->getFeatureConfiguration(), + $featureConfiguration, $diContainer->getSbsAttributeMerger(), $logger, ), diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php index c03324f094..996020bdf2 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/ValidateAllowedConnectionTest.php @@ -22,6 +22,7 @@ use Monolog\Logger; use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider; use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; +use OpenConext\EngineBlockBundle\Configuration\FeatureConfiguration; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\TestCase; use SAML2\Assertion; @@ -64,7 +65,7 @@ public function testItShouldRunInNormalConditions() { $this->expectNotToPerformAssertions(); - $verifier = new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection(); + $verifier = new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => false])); $verifier->setResponse($this->response); $sp = new ServiceProvider('FoobarSP'); $sp->allowAll = true; @@ -73,10 +74,21 @@ public function testItShouldRunInNormalConditions() $verifier->execute(); } + public function testItShouldReturnNullIfSramIsEnabled() + { + $verifier = new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true])); + $verifier->setResponse($this->response); + $sp = new ServiceProvider('FoobarSP'); + $sp->allowAll = true; + $verifier->setIdentityProvider(new IdentityProvider('OpenConext')); + $verifier->setServiceProvider($sp); + $this->assertNull($verifier->execute()); + } + #[DoesNotPerformAssertions] public function testItShouldRunInNormalConditionsWithTrustedProxy() { - $verifier = new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection(); + $verifier = new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => false])); $verifier->setResponse($this->response); $sp = m::mock(ServiceProvider::class); $sp->shouldReceive('isAllowed')->andReturn(true); @@ -96,7 +108,7 @@ public function testItShouldRunInNormalConditionsWithTrustedProxy() public function testNotAllowed() { - $verifier = new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection(); + $verifier = new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => false])); $verifier->setResponse($this->response); $sp = new ServiceProvider('FoobarSP'); $sp->allowAll = false; @@ -109,63 +121,12 @@ public function testNotAllowed() public function testIsAllowedWhenCollabEnabledCoinIsTrueEvenWhenNotAllowed() { - $verifier = new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection(); + $verifier = new EngineBlock_Corto_Filter_Command_ValidateAllowedConnection(new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true])); $verifier->setResponse($this->response); - /** - * @TODO Use PHP8 named parameters to pass collabEnabled: true, all other params are default. - */ $sp = new ServiceProvider( - 'FoobarSP', - null, - null, - null, - null, - null, - false, - [], - [], - '', - '', - '', - false, - '', - '', - '', - '', - '', - '', - null, - '', - '', - '', - null, - [], - false, - '', - '', - [], - false, - [], - false, - null, - true, - false, - false, - null, - false, - false, - false, - false, - '', - null, - null, - null, - null, - null, - null, - false, - true + entityId: 'FoobarSP', + collabEnabled: true ); $sp->allowAll = false; From 150c3979cc7231cbf757445898dd6bc074efb781 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Mon, 3 Nov 2025 09:34:11 +0100 Subject: [PATCH 35/38] Rename SRAM > Sram to match EB conventions --- library/EngineBlock/Corto/Adapter.php | 4 ++-- ...terruptFilter.php => SramInterruptFilter.php} | 6 +++--- library/EngineBlock/Corto/Filter/Input.php | 2 +- .../Corto/Module/Service/AssertionConsumer.php | 2 +- .../{SRAMInterrupt.php => SramInterrupt.php} | 4 ++-- library/EngineBlock/Corto/Module/Services.php | 4 ++-- library/EngineBlock/Corto/ProxyServer.php | 16 ++++++++-------- .../Saml2/ResponseAnnotationDecorator.php | 12 ++++++------ ...php => InvalidSramConfigurationException.php} | 2 +- .../Controller/IdentityProviderController.php | 4 ++-- ...ilterTest.php => SramInterruptFilterTest.php} | 14 +++++++------- 11 files changed, 35 insertions(+), 35 deletions(-) rename library/EngineBlock/Corto/Filter/Command/{SRAMInterruptFilter.php => SramInterruptFilter.php} (95%) rename library/EngineBlock/Corto/Module/Service/{SRAMInterrupt.php => SramInterrupt.php} (96%) rename src/OpenConext/EngineBlock/Exception/{InvalidSRAMConfigurationException.php => InvalidSramConfigurationException.php} (92%) rename tests/library/EngineBlock/Test/Corto/Filter/Command/{SRAMInterruptFilterTest.php => SramInterruptFilterTest.php} (96%) diff --git a/library/EngineBlock/Corto/Adapter.php b/library/EngineBlock/Corto/Adapter.php index a61627a54f..64e6812886 100644 --- a/library/EngineBlock/Corto/Adapter.php +++ b/library/EngineBlock/Corto/Adapter.php @@ -127,9 +127,9 @@ public function processWayf() $this->_callCortoServiceUri('continueToIdp'); } - public function processSRAMInterrupt() + public function processSramInterrupt() { - $this->_callCortoServiceUri('SRAMInterruptService'); + $this->_callCortoServiceUri('SramInterruptService'); } public function processConsent() diff --git a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php b/library/EngineBlock/Corto/Filter/Command/SramInterruptFilter.php similarity index 95% rename from library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php rename to library/EngineBlock/Corto/Filter/Command/SramInterruptFilter.php index 82b0b0874d..6a4de6b0d8 100644 --- a/library/EngineBlock/Corto/Filter/Command/SRAMInterruptFilter.php +++ b/library/EngineBlock/Corto/Filter/Command/SramInterruptFilter.php @@ -26,7 +26,7 @@ use OpenConext\EngineBlockBundle\Sbs\SbsClientInterface; use Psr\Log\LoggerInterface; -class EngineBlock_Corto_Filter_Command_SRAMInterruptFilter extends EngineBlock_Corto_Filter_Command_Abstract +class EngineBlock_Corto_Filter_Command_SramInterruptFilter extends EngineBlock_Corto_Filter_Command_Abstract implements EngineBlock_Corto_Filter_Command_ResponseAttributesModificationInterface, EngineBlock_Corto_Filter_Command_ResponseAttributeValueTypesModificationInterface { @@ -84,7 +84,7 @@ public function execute(): void if ($interruptResponse->msg === Msg::Interrupt) { $this->logger->info("SBS interrupt reason: " . $interruptResponse->message); - $this->_response->setSRAMInterruptNonce($interruptResponse->nonce); + $this->_response->setSramInterruptNonce($interruptResponse->nonce); return; } @@ -110,7 +110,7 @@ private function buildRequest(ServiceProvider $serviceProvider): AuthzRequest $userId = $this->_collabPersonId ?? ""; $eppn = $attributes['urn:mace:dir:attribute-def:eduPersonPrincipalName'][0] ?? ""; - $continueUrl = $this->_server->getUrl('SRAMInterruptService', '') . "?ID=$id"; + $continueUrl = $this->_server->getUrl('SramInterruptService', '') . "?ID=$id"; $serviceId = $serviceProvider->entityId; $issuerId = $this->_identityProvider->entityId; diff --git a/library/EngineBlock/Corto/Filter/Input.php b/library/EngineBlock/Corto/Filter/Input.php index 105a8d06bf..7c2d098219 100644 --- a/library/EngineBlock/Corto/Filter/Input.php +++ b/library/EngineBlock/Corto/Filter/Input.php @@ -92,7 +92,7 @@ public function getCommands() // Check if we need to callout to SRAM to enforce AUP's // Add SRAM attributes if not - new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + new EngineBlock_Corto_Filter_Command_SramInterruptFilter( $diContainer->getSbsClient(), $featureConfiguration, $diContainer->getSbsAttributeMerger(), diff --git a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php index 4fd02a780b..c5c6e193f9 100644 --- a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php @@ -178,7 +178,7 @@ public function serve($serviceName, Request $httpRequest) if($this->_server->shouldPerformSramCallout($receivedResponse) === true){ $log->info('Handle SRAM interrupt callout'); - $this->_server->handleSRAMInterruptCallout($receivedResponse, $receivedRequest); + $this->_server->handleSramInterruptCallout($receivedResponse, $receivedRequest); return; } diff --git a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php b/library/EngineBlock/Corto/Module/Service/SramInterrupt.php similarity index 96% rename from library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php rename to library/EngineBlock/Corto/Module/Service/SramInterrupt.php index f27387a575..06b304019c 100644 --- a/library/EngineBlock/Corto/Module/Service/SRAMInterrupt.php +++ b/library/EngineBlock/Corto/Module/Service/SramInterrupt.php @@ -21,7 +21,7 @@ use OpenConext\EngineBlockBundle\Sbs\SbsAttributeMerger; use Symfony\Component\HttpFoundation\Request; -class EngineBlock_Corto_Module_Service_SRAMInterrupt +class EngineBlock_Corto_Module_Service_SramInterrupt implements EngineBlock_Corto_Module_Service_ServiceInterface { private EngineBlock_Corto_ProxyServer $_server; @@ -64,7 +64,7 @@ public function serve($serviceName, Request $httpRequest): void } $attributes = $receivedResponse->getAssertion()->getAttributes(); - $nonce = $receivedResponse->getSRAMInterruptNonce(); + $nonce = $receivedResponse->getSramInterruptNonce(); $request = new AttributesRequest($nonce); $interruptResponse = $this->getSbsClient()->requestAttributesFor($request); diff --git a/library/EngineBlock/Corto/Module/Services.php b/library/EngineBlock/Corto/Module/Services.php index d9c2e4ce72..80459ab183 100644 --- a/library/EngineBlock/Corto/Module/Services.php +++ b/library/EngineBlock/Corto/Module/Services.php @@ -96,8 +96,8 @@ private function factoryService($className, EngineBlock_Corto_ProxyServer $serve $diContainer->getAuthenticationStateHelper(), $diContainer->getProcessingStateHelper() ); - case EngineBlock_Corto_Module_Service_SRAMInterrupt::class : - return new EngineBlock_Corto_Module_Service_SRAMInterrupt( + case EngineBlock_Corto_Module_Service_SramInterrupt::class : + return new EngineBlock_Corto_Module_Service_SramInterrupt( $server, $diContainer->getProcessingStateHelper(), $diContainer->getSbsAttributeMerger() diff --git a/library/EngineBlock/Corto/ProxyServer.php b/library/EngineBlock/Corto/ProxyServer.php index e4f1ea931d..0bbc1f2934 100644 --- a/library/EngineBlock/Corto/ProxyServer.php +++ b/library/EngineBlock/Corto/ProxyServer.php @@ -77,7 +77,7 @@ class EngineBlock_Corto_ProxyServer 'spMetadataService' => '/authentication/sp/metadata', 'stepupMetadataService' => '/authentication/stepup/metadata', 'singleLogoutService' => '/logout', - 'SRAMInterruptService' => '/authentication/idp/process-sraminterrupt' + 'SramInterruptService' => '/authentication/idp/process-sraminterrupt' ); // Todo: Make this mapping obsolete by updating all proxyserver getUrl callers. If they would reference the correct @@ -97,7 +97,7 @@ class EngineBlock_Corto_ProxyServer 'spMetadataService' => 'metadata_sp', 'stepupMetadataService' => 'metadata_stepup', 'singleLogoutService' => 'authentication_logout', - 'SRAMInterruptService' => 'authentication_idp_process_sraminterrupt' + 'SramInterruptService' => 'authentication_idp_process_sraminterrupt' ); protected $_servicesNotNeedingSession = array( @@ -559,11 +559,11 @@ public function sendStepupAuthenticationRequest( $this->getBindingsModule()->send($ebRequest, $identityProvider, true); } - private function sendSRAMInterruptRequest($response): void + private function sendSramInterruptRequest($response): void { - $nonce = $response->getSRAMInterruptNonce(); + $nonce = $response->getSramInterruptNonce(); - $sbsClient = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getSbsClient(); + $sbsClient = $this->_diContainer->getSbsClient(); $redirect_url = $sbsClient->getInterruptLocationLink($nonce); $this->redirect($redirect_url, ''); } @@ -572,10 +572,10 @@ public function shouldPerformSramCallout( EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, ): bool { - return $receivedResponse->getSRAMInterruptNonce() !== ''; + return $receivedResponse->getSramInterruptNonce() !== ''; } - public function handleSRAMInterruptCallout( + public function handleSramInterruptCallout( EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, EngineBlock_Saml2_AuthnRequestAnnotationDecorator $receivedRequest, ): void { @@ -588,7 +588,7 @@ public function handleSRAMInterruptCallout( ); // Redirect to SRAM - $this->sendSRAMInterruptRequest($receivedResponse); + $this->sendSramInterruptRequest($receivedResponse); } public function handleStepupAuthenticationCallout( diff --git a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php index 6791ebf570..5025b84d7b 100644 --- a/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php +++ b/library/EngineBlock/Saml2/ResponseAnnotationDecorator.php @@ -93,7 +93,7 @@ class EngineBlock_Saml2_ResponseAnnotationDecorator extends EngineBlock_Saml2_Me */ private ?string $_serializableRelayState = null; - protected string $SRAMInterruptNonce = ""; + protected string $SramInterruptNonce = ""; /** * @param Response $response @@ -345,7 +345,7 @@ public function __sleep() 'isTransparentErrorResponse', '_serializableSspMessageXml', '_serializableRelayState', - 'SRAMInterruptNonce', + 'SramInterruptNonce', ]; } @@ -370,14 +370,14 @@ public function __wakeup() } } - public function setSRAMInterruptNonce(string $SRAMInterruptNonce): void + public function setSramInterruptNonce(string $SramInterruptNonce): void { - $this->SRAMInterruptNonce = $SRAMInterruptNonce; + $this->SramInterruptNonce = $SramInterruptNonce; } - public function getSRAMInterruptNonce(): string + public function getSramInterruptNonce(): string { - return $this->SRAMInterruptNonce; + return $this->SramInterruptNonce; } } diff --git a/src/OpenConext/EngineBlock/Exception/InvalidSRAMConfigurationException.php b/src/OpenConext/EngineBlock/Exception/InvalidSramConfigurationException.php similarity index 92% rename from src/OpenConext/EngineBlock/Exception/InvalidSRAMConfigurationException.php rename to src/OpenConext/EngineBlock/Exception/InvalidSramConfigurationException.php index 7a3d1ae319..b687d039c3 100644 --- a/src/OpenConext/EngineBlock/Exception/InvalidSRAMConfigurationException.php +++ b/src/OpenConext/EngineBlock/Exception/InvalidSramConfigurationException.php @@ -20,7 +20,7 @@ use InvalidArgumentException as CoreInvalidArgumentException; -class InvalidSRAMConfigurationException extends CoreInvalidArgumentException implements Exception +class InvalidSramConfigurationException extends CoreInvalidArgumentException implements Exception { } diff --git a/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php b/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php index 4b47f404f6..3f88102fdb 100644 --- a/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php +++ b/src/OpenConext/EngineBlockBundle/Controller/IdentityProviderController.php @@ -218,10 +218,10 @@ public function processConsentAction() * * @Route("/authentication/idp/process-sraminterrupt", name="authentication_idp_process_sraminterrupt", methods={"GET"}) */ - public function processSRAMInterrupt(Request $request) + public function processSramInterrupt(Request $request) { $proxyServer = new EngineBlock_Corto_Adapter(); - $proxyServer->processSRAMInterrupt(); + $proxyServer->processSramInterrupt(); return ResponseFactory::fromEngineBlockResponse($this->engineBlockApplicationSingleton->getHttpResponse()); } diff --git a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php b/tests/library/EngineBlock/Test/Corto/Filter/Command/SramInterruptFilterTest.php similarity index 96% rename from tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php rename to tests/library/EngineBlock/Test/Corto/Filter/Command/SramInterruptFilterTest.php index 22c34baa19..d532e1e9be 100644 --- a/tests/library/EngineBlock/Test/Corto/Filter/Command/SRAMInterruptFilterTest.php +++ b/tests/library/EngineBlock/Test/Corto/Filter/Command/SramInterruptFilterTest.php @@ -59,7 +59,7 @@ public function testItDoesNothingIfFeatureFlagNotEnabled(): void { $sbsClient = Mockery::mock(SbsClientInterface::class); - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + $sramFilter = new EngineBlock_Corto_Filter_Command_SramInterruptFilter( $sbsClient, new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => false]), new SbsAttributeMerger([]), @@ -77,7 +77,7 @@ public function testItDoesNothingIfSpDoesNotHaveCollabEnabled(): void { $sbsClient = Mockery::mock(SbsClientInterface::class); - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + $sramFilter = new EngineBlock_Corto_Filter_Command_SramInterruptFilter( $sbsClient, new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true]), new SbsAttributeMerger([]), @@ -108,7 +108,7 @@ public function testItAddsNonceWhenMessageInterrupt(): void { $sbsClient = Mockery::mock(SbsClientInterface::class); - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + $sramFilter = new EngineBlock_Corto_Filter_Command_SramInterruptFilter( $sbsClient, new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true]), new SbsAttributeMerger([]), @@ -162,7 +162,7 @@ public function testItAddsNonceWhenMessageInterrupt(): void $sramFilter->execute(); $this->assertSame($initialAttributes, $sramFilter->getResponseAttributes()); - $this->assertSame('hash123', $sramFilter->getResponse()->getSRAMInterruptNonce()); + $this->assertSame('hash123', $sramFilter->getResponse()->getSramInterruptNonce()); } public function testItAddsSramAttributesOnStatusAuthorized(): void @@ -174,7 +174,7 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void 'urn:mace:dir:attribute-def:eduPersonEntitlement', ]); - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + $sramFilter = new EngineBlock_Corto_Filter_Command_SramInterruptFilter( $sbsClient, new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true]), $attributeMerger, @@ -236,7 +236,7 @@ public function testItAddsSramAttributesOnStatusAuthorized(): void $sramFilter->execute(); $this->assertSame($expectedAttributes, $sramFilter->getResponseAttributes()); - $this->assertSame('', $sramFilter->getResponse()->getSRAMInterruptNonce()); + $this->assertSame('', $sramFilter->getResponse()->getSramInterruptNonce()); } public function testThrowsEngineBlockExceptionIfPolicyCannotBeChecked() @@ -247,7 +247,7 @@ public function testThrowsEngineBlockExceptionIfPolicyCannotBeChecked() $sbsClient = Mockery::mock(SbsClientInterface::class); $sbsClient->expects('authz')->andThrows(new InvalidSbsResponseException('Server could not be reached.')); - $sramFilter = new EngineBlock_Corto_Filter_Command_SRAMInterruptFilter( + $sramFilter = new EngineBlock_Corto_Filter_Command_SramInterruptFilter( $sbsClient, new FeatureConfiguration(['eb.feature_enable_sram_interrupt' => true]), new SbsAttributeMerger([]), From 5796ee717251db8316b0af7b5d0e94d80130c13f Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Mon, 3 Nov 2025 09:38:07 +0100 Subject: [PATCH 36/38] Update copyright --- .../EngineBlock/Exception/InvalidSramConfigurationException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenConext/EngineBlock/Exception/InvalidSramConfigurationException.php b/src/OpenConext/EngineBlock/Exception/InvalidSramConfigurationException.php index b687d039c3..91ee2b8f82 100644 --- a/src/OpenConext/EngineBlock/Exception/InvalidSramConfigurationException.php +++ b/src/OpenConext/EngineBlock/Exception/InvalidSramConfigurationException.php @@ -1,7 +1,7 @@ Date: Mon, 3 Nov 2025 14:14:25 +0100 Subject: [PATCH 37/38] Ensure Stepup is performed before SRAM Prior to this change, stepup would be performed after the SRAM interrupt check. This change ensures steupup is performed first. The reason is that the SRAM callout could trigger a 2fa check in SBS. It would be weird to perform a 2fa stepup after that 2fa check. So doing the stepup first would seem better. --- .../Module/Service/AssertionConsumer.php | 12 ++++-- .../Corto/Module/Service/SramInterrupt.php | 6 --- .../Service/StepupAssertionConsumer.php | 13 +++++-- library/EngineBlock/Corto/ProxyServer.php | 37 +++++++++---------- .../Service/ProcessingStateHelper.php | 15 ++++++++ .../ProcessingStateHelperInterface.php | 2 + .../Features/Context/StepupContext.php | 15 +++++++- .../Features/SbsFlowIntegration.feature | 18 ++++++++- 8 files changed, 82 insertions(+), 36 deletions(-) diff --git a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php index c5c6e193f9..0220e3ef13 100644 --- a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php @@ -177,10 +177,7 @@ public function serve($serviceName, Request $httpRequest) if($this->_server->shouldPerformSramCallout($receivedResponse) === true){ - $log->info('Handle SRAM interrupt callout'); - $this->_server->handleSramInterruptCallout($receivedResponse, $receivedRequest); - - return; + $this->_server->addSramStep($receivedResponse, $receivedRequest); } $this->_server->addConsentProcessStep($receivedResponse, $receivedRequest); @@ -191,6 +188,13 @@ public function serve($serviceName, Request $httpRequest) return; } + if($this->_server->shouldPerformSramCallout($receivedResponse) === true){ + $log->info('Handle SRAM interrupt callout'); + $this->_server->handleSramInterruptCallout($receivedResponse); + + return; + } + $this->_server->handleConsentAuthenticationCallout($receivedResponse, $receivedRequest); } diff --git a/library/EngineBlock/Corto/Module/Service/SramInterrupt.php b/library/EngineBlock/Corto/Module/Service/SramInterrupt.php index 06b304019c..75a3b4fe13 100644 --- a/library/EngineBlock/Corto/Module/Service/SramInterrupt.php +++ b/library/EngineBlock/Corto/Module/Service/SramInterrupt.php @@ -79,12 +79,6 @@ public function serve($serviceName, Request $httpRequest): void $this->_server->addConsentProcessStep($receivedResponse, $receivedRequest); - if($this->_server->shouldUseStepup($receivedResponse, $receivedRequest)){ - $this->_server->handleStepupAuthenticationCallout($receivedResponse, $receivedRequest, $originalAssertion); - - return; - } - $this->_server->handleConsentAuthenticationCallout($receivedResponse, $receivedRequest); } diff --git a/library/EngineBlock/Corto/Module/Service/StepupAssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/StepupAssertionConsumer.php index 62b7d55d66..07b2c9100d 100644 --- a/library/EngineBlock/Corto/Module/Service/StepupAssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/StepupAssertionConsumer.php @@ -87,6 +87,10 @@ public function serve($serviceName, Request $httpRequest) $receivedResponse = $this->_server->getBindingsModule()->receiveResponse($serviceEntityId, $expectedDestination); $receivedRequest = $this->_server->getReceivedRequestFromResponse($receivedResponse); + if(!$receivedRequest instanceof EngineBlock_Saml2_AuthnRequestAnnotationDecorator){ + throw new RuntimeException('Request cannot be empty at this stage'); + } + $this->verifyReceivedLoa($receivedRequest, $receivedResponse, $log); // Update the AuthnContextClassRef to the loa returned @@ -136,15 +140,18 @@ public function serve($serviceName, Request $httpRequest) $this->verifyReceivedNameID($originalReceivedResponse, $receivedResponse); } + if ($this->_processingStateHelper->hasStepRequestById($receivedRequest->getId(), ProcessingStateHelperInterface::STEP_SRAM) === true) { + $this->_server->handleSramInterruptCallout($receivedResponse); + + return; + } + $nextProcessStep = $this->_processingStateHelper->getStepByRequestId( $receivedRequest->getId(), ProcessingStateHelperInterface::STEP_CONSENT ); - $this->_server->sendConsentAuthenticationRequest($originalReceivedResponse, $receivedRequest, $nextProcessStep->getRole(), $this->getAuthenticationState()); - - return; } /** diff --git a/library/EngineBlock/Corto/ProxyServer.php b/library/EngineBlock/Corto/ProxyServer.php index 0bbc1f2934..79e886157b 100644 --- a/library/EngineBlock/Corto/ProxyServer.php +++ b/library/EngineBlock/Corto/ProxyServer.php @@ -559,15 +559,6 @@ public function sendStepupAuthenticationRequest( $this->getBindingsModule()->send($ebRequest, $identityProvider, true); } - private function sendSramInterruptRequest($response): void - { - $nonce = $response->getSramInterruptNonce(); - - $sbsClient = $this->_diContainer->getSbsClient(); - $redirect_url = $sbsClient->getInterruptLocationLink($nonce); - $this->redirect($redirect_url, ''); - } - public function shouldPerformSramCallout( EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, ): bool @@ -577,18 +568,13 @@ public function shouldPerformSramCallout( public function handleSramInterruptCallout( EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, - EngineBlock_Saml2_AuthnRequestAnnotationDecorator $receivedRequest, ): void { - // Add the SRAM step - $this->_diContainer->getProcessingStateHelper()->addStep( - $receivedRequest->getId(), - ProcessingStateHelperInterface::STEP_SRAM, - $this->getEngineSpRole(), - $receivedResponse - ); - // Redirect to SRAM - $this->sendSramInterruptRequest($receivedResponse); + $nonce = $receivedResponse->getSramInterruptNonce(); + + $sbsClient = $this->_diContainer->getSbsClient(); + $redirect_url = $sbsClient->getInterruptLocationLink($nonce); + $this->redirect($redirect_url, ''); } public function handleStepupAuthenticationCallout( @@ -710,6 +696,19 @@ public function addConsentProcessStep( ); } + public function addSramStep( + EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, + EngineBlock_Saml2_AuthnRequestAnnotationDecorator $receivedRequest, + ): void { + // Add the SRAM step + $this->_diContainer->getProcessingStateHelper()->addStep( + $receivedRequest->getId(), + ProcessingStateHelperInterface::STEP_SRAM, + $this->getEngineSpRole(), + $receivedResponse + ); + } + function sendConsentAuthenticationRequest( EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, EngineBlock_Saml2_AuthnRequestAnnotationDecorator $receivedRequest, diff --git a/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php b/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php index 30173bc9aa..bf539609ff 100644 --- a/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php +++ b/src/OpenConext/EngineBlock/Service/ProcessingStateHelper.php @@ -90,6 +90,21 @@ public function getStepByRequestId($requestId, $name) return $processing[$requestId][$name]; } + public function hasStepRequestById(string $requestId, string $name): bool + { + $processing = $this->session->get(self::SESSION_KEY); + if (empty($processing)) { + return false; + } + if (!isset($processing[$requestId])) { + return false; + } + if (!isset($processing[$requestId][$name])) { + return false; + } + + return true; + } /** * @param string $name diff --git a/src/OpenConext/EngineBlock/Service/ProcessingStateHelperInterface.php b/src/OpenConext/EngineBlock/Service/ProcessingStateHelperInterface.php index a82cafe25c..0dae556ec2 100644 --- a/src/OpenConext/EngineBlock/Service/ProcessingStateHelperInterface.php +++ b/src/OpenConext/EngineBlock/Service/ProcessingStateHelperInterface.php @@ -54,6 +54,8 @@ public function addStep( */ public function getStepByRequestId($requestId, $name); + public function hasStepRequestById(string $requestId, string $name): bool; + /** * @param $name * @param $requestId diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/StepupContext.php b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/StepupContext.php index 560b24e0d8..882f49ea93 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/StepupContext.php +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/StepupContext.php @@ -23,6 +23,7 @@ use OpenConext\EngineBlockFunctionalTestingBundle\Mock\EntityRegistry; use OpenConext\EngineBlockFunctionalTestingBundle\Mock\MockIdentityProvider; use OpenConext\EngineBlockFunctionalTestingBundle\Mock\MockServiceProvider; +use RuntimeException; class StepupContext extends AbstractSubContext { @@ -66,9 +67,19 @@ public function __construct( */ public function stepupWillsSuccessfullyVerifyAUser() { - $mink = $this->getMinkContext(); + $page = $this->getMinkContext()->getSession()->getPage(); + $form = $page->find('css', 'form[action*="/authentication/stepup/consume-assertion"]'); + + if ($form === null) { + throw new RuntimeException('No form found for "/authentication/stepup/consume-assertion"'); + } + + $button = $form->find('css', 'input[type="submit"][value="Submit-success"]'); + if ($button === null) { + throw new RuntimeException('No form with submit button found for "/authentication/stepup/consume-assertion"'); + } - $mink->pressButton('Submit-success'); + $button->click(); } /** diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature index 83f7be7a4c..5474e1a777 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature @@ -88,10 +88,10 @@ Feature: When I log in at "SSO-SP" And I pass through EngineBlock And I pass through the IdP + And Stepup will successfully verify a user Then the url should match "/functional-testing/interrupt" And I pass through SBS - And Stepup will successfully verify a user - Then the url should match "/authentication/stepup/consume-assertion" + Then the url should match "/authentication/idp/process-sraminterrupt" And the response should contain "Review your information that will be shared." And the response should contain "test_user@test.sram.surf.nl" And the response should contain "ssh_key1" @@ -103,6 +103,20 @@ Feature: And the response should contain "ssh_key1" And the response should contain "ssh_key2" + Scenario: If no suitable stepup can be given, sbs interrupt is not executed + Given the SP "SSO-SP" requires Stepup LoA "http://dev.openconext.local/assurance/loa2" + And the SP "SSO-SP" requires SRAM collaboration + And feature "eb.feature_enable_sram_interrupt" is enabled + And the sbs server will trigger the "interrupt" authz flow when called + And the sbs server will return valid attributes + When I log in at "SSO-SP" + And I pass through EngineBlock + And I pass through the IdP + And Stepup will fail if the LoA can not be given + Then I should see "Error - No suitable token found" + And the url should match "/feedback/stepup-callout-unmet-loa" + And the response status code should be 400 + Scenario: SBS flow is skipped when feature is disabled Given the SP "SSO-SP" requires SRAM collaboration And feature "eb.feature_enable_sram_interrupt" is disabled From 9452e55e50753064947f5101b86045eafeabc6a7 Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Mon, 3 Nov 2025 14:42:41 +0100 Subject: [PATCH 38/38] Fixed & docs --- docs/filter_commands.md | 11 ++++++++++- .../Corto/Module/Service/AssertionConsumer.php | 1 - library/EngineBlock/Corto/ProxyServer.php | 2 +- .../Features/SbsFlowIntegration.feature | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/filter_commands.md b/docs/filter_commands.md index 188ac8ac08..3ba4328606 100644 --- a/docs/filter_commands.md +++ b/docs/filter_commands.md @@ -344,8 +344,17 @@ Uses: - EngineBlock_Saml2_AuthnRequestAnnotationDecorator ### SRAM test filter -When enabled and the SP has the collab_enabled coin, the SBS integration flow will be activated allowing SRAM integration. +SRAM integration. +In order to facilitate fine-grained access to SRAM, EB integrates with SRAM through the SBS service. +This process is only enabled if both the `feature_enable_sram_interrupt` feature flag is enabled and the `collabEnabled` coin of the SP is true. + +If enabled, the SramInterruptFilter will call SBS with the sessionId. +If the sessionId is known in SBS, EB will merge the attributes supplied by SBS into the Auth request. +IF the sessionId is unknown, later in the Consume Assertion process, the browser will be redirected to SBS, +which will redirect back to EB after a successful check. Then the attributes from SBS will be merged after all. + +See https://github.com/OpenConext/OpenConext-engineblock/issues/1804 for details. [input]: https://github.com/OpenConext/OpenConext-engineblock/tree/master/library/EngineBlock/Corto/Filter/Input.php diff --git a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php index 0220e3ef13..2d7bd959c2 100644 --- a/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php +++ b/library/EngineBlock/Corto/Module/Service/AssertionConsumer.php @@ -189,7 +189,6 @@ public function serve($serviceName, Request $httpRequest) } if($this->_server->shouldPerformSramCallout($receivedResponse) === true){ - $log->info('Handle SRAM interrupt callout'); $this->_server->handleSramInterruptCallout($receivedResponse); return; diff --git a/library/EngineBlock/Corto/ProxyServer.php b/library/EngineBlock/Corto/ProxyServer.php index 79e886157b..8ec67673a7 100644 --- a/library/EngineBlock/Corto/ProxyServer.php +++ b/library/EngineBlock/Corto/ProxyServer.php @@ -569,7 +569,7 @@ public function shouldPerformSramCallout( public function handleSramInterruptCallout( EngineBlock_Saml2_ResponseAnnotationDecorator $receivedResponse, ): void { - // Redirect to SRAM + $this->getLogger()->info('Handle SRAM interrupt callout'); $nonce = $receivedResponse->getSramInterruptNonce(); $sbsClient = $this->_diContainer->getSbsClient(); diff --git a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature index 5474e1a777..1e54654568 100644 --- a/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature +++ b/src/OpenConext/EngineBlockFunctionalTestingBundle/Features/SbsFlowIntegration.feature @@ -1,5 +1,5 @@ Feature: - In order to support SBS integration + In order to support SRAM integration As EngineBlock I want to support SBS checks and merge attributes