From a5539de73e45af0af77acdc5979ea462679935e1 Mon Sep 17 00:00:00 2001 From: Peter Havekes Date: Wed, 7 Oct 2026 11:23:25 +0200 Subject: [PATCH 1/3] Enrich log records with request context parameters --- .../Processor/RequestContextProcessor.php | 316 +++++++++++ .../ApiBundle/Resources/config/services.yml | 10 + .../Processor/RequestContextProcessorTest.php | 498 ++++++++++++++++++ 3 files changed, 824 insertions(+) create mode 100644 src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php create mode 100644 src/Surfnet/StepupMiddleware/ApiBundle/Tests/Monolog/Processor/RequestContextProcessorTest.php diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php b/src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php new file mode 100644 index 00000000..367fb5ba --- /dev/null +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php @@ -0,0 +1,316 @@ +requestStack->getCurrentRequest(); + + if (!$request instanceof Request) { + return $record; + } + + $context = $this->findRequestContext($request); + + foreach ($context as $key => $value) { + $record->extra[$key] = $value; + } + + return $record; + } + + /** + * @return array + */ + private function findRequestContext(Request $request): array + { + if ($request->attributes->has(self::CACHE_ATTRIBUTE)) { + /** @var array */ + return $request->attributes->get(self::CACHE_ATTRIBUTE); + } + + $context = $this->extractRequestContext($request); + $request->attributes->set(self::CACHE_ATTRIBUTE, $context); + + return $context; + } + + /** + * @return array + */ + private function extractRequestContext(Request $request): array + { + $jsonData = null; + $content = $request->getContent(); + if ($content !== '') { + $decoded = json_decode($content, true); + if (is_array($decoded)) { + $jsonData = $decoded; + } + } + + $context = []; + + $identityId = $this->extractIdentityId($request, $jsonData); + if ($identityId !== null) { + $context['identity_id'] = $identityId; + } + + $collabPersonId = $this->extractCollabPersonId($request, $jsonData); + if ($collabPersonId !== null) { + $context['collab_person_id'] = $collabPersonId; + } + + $secondFactorId = $this->extractSecondFactorId($request, $jsonData); + if ($secondFactorId !== null) { + $context['second_factor_id'] = $secondFactorId; + } + + $recoveryTokenId = $this->extractRecoveryTokenId($request, $jsonData); + if ($recoveryTokenId !== null) { + $context['recovery_token_id'] = $recoveryTokenId; + } + + $nameId = $this->extractNameId($request, $jsonData); + if ($nameId !== null) { + $context['name_id'] = $nameId; + } + + $institution = $this->extractInstitution($request, $jsonData); + if ($institution !== null) { + $context['institution'] = $institution; + } + + return $context; + } + + private function extractIdentityId(Request $request, ?array $jsonData): ?string + { + if ($request->attributes->get('_route') === 'identity' && $request->attributes->has('id')) { + $id = $this->stringify($request->attributes->get('id')); + if ($id !== null) { + return $id; + } + } + + $value = $this->extractFromRequestOrJson($request, ['identityId', 'identity_id'], $jsonData); + if ($value !== null) { + return $value; + } + + if ($jsonData !== null) { + if (isset($jsonData['command']['name']) + && is_string($jsonData['command']['name']) + && in_array($jsonData['command']['name'], ['Identity:CreateIdentity', 'Identity:UpdateIdentity'], true) + && isset($jsonData['command']['payload']['id']) + ) { + $id = $this->stringify($jsonData['command']['payload']['id']); + if ($id !== null) { + return $id; + } + } + } + + return null; + } + + private function extractCollabPersonId(Request $request, ?array $jsonData): ?string + { + return $this->extractFromRequestOrJson($request, ['collabPersonId', 'collab_person_id'], $jsonData); + } + + private function extractSecondFactorId(Request $request, ?array $jsonData): ?string + { + $route = (string) $request->attributes->get('_route'); + if (in_array($route, self::SECOND_FACTOR_ROUTES, true) && $request->attributes->has('id')) { + $id = $this->stringify($request->attributes->get('id')); + if ($id !== null) { + return $id; + } + } + + return $this->extractFromRequestOrJson( + $request, + ['secondFactorId', 'second_factor_id', 'authoringSecondFactorId', 'authoring_second_factor_id'], + $jsonData, + ); + } + + private function extractRecoveryTokenId(Request $request, ?array $jsonData): ?string + { + if ($request->attributes->get('_route') === 'recovery_token' && $request->attributes->has('id')) { + $id = $this->stringify($request->attributes->get('id')); + if ($id !== null) { + return $id; + } + } + + return $this->extractFromRequestOrJson( + $request, + ['recoveryTokenIdId', 'recoveryTokenId', 'recovery_token_id', 'recovery_token_id_id'], + $jsonData, + ); + } + + private function extractNameId(Request $request, ?array $jsonData): ?string + { + return $this->extractFromRequestOrJson($request, ['nameId', 'name_id', 'NameID'], $jsonData); + } + + private function extractInstitution(Request $request, ?array $jsonData): ?string + { + $value = $this->extractFromRequestOrJson( + $request, + ['institution', 'institutionName', 'institution_name', 'raInstitution', 'ra_institution'], + $jsonData, + ); + + if ($value !== null) { + return $value; + } + + if ($jsonData !== null && isset($jsonData['meta']) && is_array($jsonData['meta'])) { + foreach (['actor_institution', 'actorInstitution', 'institution'] as $key) { + if (isset($jsonData['meta'][$key])) { + $id = $this->stringify($jsonData['meta'][$key]); + if ($id !== null) { + return $id; + } + } + } + } + + return null; + } + + /** + * @param list $keys + */ + private function extractFromRequestOrJson(Request $request, array $keys, ?array $jsonData): ?string + { + foreach ($keys as $key) { + if ($request->attributes->has($key)) { + $val = $this->stringify($request->attributes->get($key)); + if ($val !== null) { + return $val; + } + } + } + + foreach ($keys as $key) { + if ($request->query->has($key)) { + $val = $this->stringify($request->query->get($key)); + if ($val !== null) { + return $val; + } + } + } + + foreach ($keys as $key) { + if ($request->request->has($key)) { + $val = $this->stringify($request->request->get($key)); + if ($val !== null) { + return $val; + } + } + } + + if ($jsonData !== null) { + foreach ($keys as $key) { + if (isset($jsonData[$key])) { + $val = $this->stringify($jsonData[$key]); + if ($val !== null) { + return $val; + } + } + } + + if (isset($jsonData['command']['payload']) && is_array($jsonData['command']['payload'])) { + foreach ($keys as $key) { + if (isset($jsonData['command']['payload'][$key])) { + $val = $this->stringify($jsonData['command']['payload'][$key]); + if ($val !== null) { + return $val; + } + } + } + } + + if (isset($jsonData['payload']) && is_array($jsonData['payload'])) { + foreach ($keys as $key) { + if (isset($jsonData['payload'][$key])) { + $val = $this->stringify($jsonData['payload'][$key]); + if ($val !== null) { + return $val; + } + } + } + } + } + + return null; + } + + private function stringify(mixed $value): ?string + { + if ($value === null) { + return null; + } + + if (is_string($value)) { + $trimmed = trim($value); + return $trimmed !== '' ? $trimmed : null; + } + + if (is_scalar($value) || $value instanceof Stringable) { + $trimmed = trim((string) $value); + return $trimmed !== '' ? $trimmed : null; + } + + return null; + } +} diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Resources/config/services.yml b/src/Surfnet/StepupMiddleware/ApiBundle/Resources/config/services.yml index 2cdb2765..7042c4fe 100644 --- a/src/Surfnet/StepupMiddleware/ApiBundle/Resources/config/services.yml +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Resources/config/services.yml @@ -136,6 +136,16 @@ services: tags: - { name: kernel.event_listener, event: kernel.exception } + # Monolog Processors + surfnet_stepup_middleware_api.monolog.request_context_processor: + class: Surfnet\StepupMiddleware\ApiBundle\Monolog\Processor\RequestContextProcessor + arguments: + - "@request_stack" + tags: + - { name: monolog.processor } + Surfnet\StepupMiddleware\ApiBundle\Monolog\Processor\RequestContextProcessor: + alias: surfnet_stepup_middleware_api.monolog.request_context_processor + # Security Entry Point surfnet_stepup_middleware_api.security.json_basic_auth_entry_point: class: Surfnet\StepupMiddleware\ApiBundle\Security\Http\EntryPoint\JsonBasicAuthenticationEntryPoint diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Tests/Monolog/Processor/RequestContextProcessorTest.php b/src/Surfnet/StepupMiddleware/ApiBundle/Tests/Monolog/Processor/RequestContextProcessorTest.php new file mode 100644 index 00000000..c7d8a459 --- /dev/null +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Tests/Monolog/Processor/RequestContextProcessorTest.php @@ -0,0 +1,498 @@ +createLogRecord(['existing' => 'data']); + $processed = $processor($record); + + $this->assertSame(['existing' => 'data'], $processed->extra); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_identity_id_from_route_attributes(): void + { + $request = new Request(); + $request->attributes->set('identityId', 'abc-123-route'); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('identity_id', $processed->extra); + $this->assertSame('abc-123-route', $processed->extra['identity_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_snake_case_identity_id_from_route_attributes(): void + { + $request = new Request(); + $request->attributes->set('identity_id', 'abc-123-snake-route'); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('identity_id', $processed->extra); + $this->assertSame('abc-123-snake-route', $processed->extra['identity_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_identity_id_from_identity_route_with_id_attribute(): void + { + $request = new Request(); + $request->attributes->set('_route', 'identity'); + $request->attributes->set('id', 'abc-123-identity-get'); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('identity_id', $processed->extra); + $this->assertSame('abc-123-identity-get', $processed->extra['identity_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_identity_id_from_query_parameters(): void + { + $request = new Request(query: ['identityId' => 'abc-123-query']); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('identity_id', $processed->extra); + $this->assertSame('abc-123-query', $processed->extra['identity_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_snake_case_identity_id_from_query_parameters(): void + { + $request = new Request(query: ['identity_id' => 'abc-123-snake-query']); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('identity_id', $processed->extra); + $this->assertSame('abc-123-snake-query', $processed->extra['identity_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_identity_id_from_request_body_post_parameters(): void + { + $request = new Request(request: ['identityId' => 'abc-123-body']); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('identity_id', $processed->extra); + $this->assertSame('abc-123-body', $processed->extra['identity_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_identity_id_from_command_json_payload(): void + { + $payload = [ + 'meta' => ['actor_id' => 'actor-uuid'], + 'command' => [ + 'name' => 'Identity:VetSecondFactor', + 'uuid' => 'command-uuid', + 'payload' => [ + 'identityId' => 'abc-123-command-payload', + ], + ], + ]; + + $request = new Request( + server: ['CONTENT_TYPE' => 'application/json'], + content: json_encode($payload), + ); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('identity_id', $processed->extra); + $this->assertSame('abc-123-command-payload', $processed->extra['identity_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_snake_case_identity_id_from_command_json_payload(): void + { + $payload = [ + 'meta' => ['actor_id' => 'actor-uuid'], + 'command' => [ + 'name' => 'Identity:VetSecondFactor', + 'uuid' => 'command-uuid', + 'payload' => [ + 'identity_id' => 'abc-123-command-snake-payload', + ], + ], + ]; + + $request = new Request( + server: ['CONTENT_TYPE' => 'application/json'], + content: json_encode($payload), + ); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('identity_id', $processed->extra); + $this->assertSame('abc-123-command-snake-payload', $processed->extra['identity_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_id_from_create_identity_command_json_payload(): void + { + $payload = [ + 'meta' => ['actor_id' => 'actor-uuid'], + 'command' => [ + 'name' => 'Identity:CreateIdentity', + 'uuid' => 'command-uuid', + 'payload' => [ + 'id' => 'abc-123-created-identity', + ], + ], + ]; + + $request = new Request( + server: ['CONTENT_TYPE' => 'application/json'], + content: json_encode($payload), + ); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('identity_id', $processed->extra); + $this->assertSame('abc-123-created-identity', $processed->extra['identity_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_identity_id_from_root_json_payload(): void + { + $payload = [ + 'identityId' => 'abc-123-root-json', + ]; + + $request = new Request( + server: ['CONTENT_TYPE' => 'application/json'], + content: json_encode($payload), + ); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('identity_id', $processed->extra); + $this->assertSame('abc-123-root-json', $processed->extra['identity_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_collab_person_id_from_route_and_query(): void + { + $request = new Request(); + $request->attributes->set('_route', 'deprovision'); + $request->attributes->set('collabPersonId', 'urn:collab:person:surfnet.nl:jdoe'); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('collab_person_id', $processed->extra); + $this->assertSame('urn:collab:person:surfnet.nl:jdoe', $processed->extra['collab_person_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_second_factor_id_from_route_id_query_and_payload(): void + { + $request = new Request(); + $request->attributes->set('_route', 'verified_second_factor'); + $request->attributes->set('id', 'sf-uuid-123'); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('second_factor_id', $processed->extra); + $this->assertSame('sf-uuid-123', $processed->extra['second_factor_id']); + + $payload = [ + 'command' => [ + 'name' => 'Identity:VetSecondFactor', + 'uuid' => 'cmd-1', + 'payload' => [ + 'secondFactorId' => 'sf-payload-456', + ], + ], + ]; + $request2 = new Request( + server: ['CONTENT_TYPE' => 'application/json'], + content: json_encode($payload), + ); + $requestStack2 = new RequestStack(); + $requestStack2->push($request2); + $processor2 = new RequestContextProcessor($requestStack2); + $processed2 = $processor2($this->createLogRecord()); + $this->assertSame('sf-payload-456', $processed2->extra['second_factor_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_recovery_token_id_from_route_id_and_recovery_token_id_id(): void + { + $request = new Request(); + $request->attributes->set('_route', 'recovery_token'); + $request->attributes->set('id', 'recovery-token-uuid-1'); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('recovery_token_id', $processed->extra); + $this->assertSame('recovery-token-uuid-1', $processed->extra['recovery_token_id']); + + $request2 = new Request(query: ['recoveryTokenIdId' => 'recovery-token-uuid-2']); + $requestStack2 = new RequestStack(); + $requestStack2->push($request2); + $processor2 = new RequestContextProcessor($requestStack2); + $processed2 = $processor2($this->createLogRecord()); + $this->assertSame('recovery-token-uuid-2', $processed2->extra['recovery_token_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_name_id_from_route_query_and_payload(): void + { + $request = new Request(); + $request->attributes->set('_route', 'identity_sraa_get'); + $request->attributes->set('nameId', 'urn:collab:person:surfnet.nl:nameid1'); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('name_id', $processed->extra); + $this->assertSame('urn:collab:person:surfnet.nl:nameid1', $processed->extra['name_id']); + + $request2 = new Request(query: ['NameID' => 'query-name-id']); + $requestStack2 = new RequestStack(); + $requestStack2->push($request2); + $processor2 = new RequestContextProcessor($requestStack2); + $processed2 = $processor2($this->createLogRecord()); + $this->assertSame('query-name-id', $processed2->extra['name_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_institution_from_query_route_and_meta(): void + { + $request = new Request(query: ['institution' => 'SURFnet']); + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayHasKey('institution', $processed->extra); + $this->assertSame('SURFnet', $processed->extra['institution']); + + $payload = [ + 'meta' => [ + 'actor_institution' => 'Institution-From-Meta', + ], + 'command' => [ + 'name' => 'Identity:SomeCommand', + 'uuid' => 'cmd-1', + 'payload' => [], + ], + ]; + $request2 = new Request( + server: ['CONTENT_TYPE' => 'application/json'], + content: json_encode($payload), + ); + $requestStack2 = new RequestStack(); + $requestStack2->push($request2); + $processor2 = new RequestContextProcessor($requestStack2); + $processed2 = $processor2($this->createLogRecord()); + $this->assertSame('Institution-From-Meta', $processed2->extra['institution']); + } + + #[Test] + #[Group('api-bundle')] + public function it_extracts_multiple_context_fields_simultaneously(): void + { + $payload = [ + 'meta' => [ + 'actor_institution' => 'SURFnet', + ], + 'command' => [ + 'name' => 'Identity:CreateIdentity', + 'uuid' => 'cmd-uuid', + 'payload' => [ + 'id' => 'identity-uuid-1', + 'name_id' => 'name-id-1', + 'institution' => 'SURFnet-Payload', + ], + ], + ]; + + $request = new Request( + query: ['secondFactorId' => 'sf-uuid-9'], + server: ['CONTENT_TYPE' => 'application/json'], + content: json_encode($payload), + ); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertSame('identity-uuid-1', $processed->extra['identity_id']); + $this->assertSame('name-id-1', $processed->extra['name_id']); + $this->assertSame('SURFnet-Payload', $processed->extra['institution']); + $this->assertSame('sf-uuid-9', $processed->extra['second_factor_id']); + } + + #[Test] + #[Group('api-bundle')] + public function it_does_not_modify_record_when_no_context_is_present(): void + { + $request = new Request(query: ['unrelated' => 'value']); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(['existing' => 'val']); + $processed = $processor($record); + + $this->assertSame(['existing' => 'val'], $processed->extra); + } + + #[Test] + #[Group('api-bundle')] + public function it_ignores_empty_identity_id(): void + { + $request = new Request(query: ['identityId' => ' ']); + + $requestStack = new RequestStack(); + $requestStack->push($request); + $processor = new RequestContextProcessor($requestStack); + + $record = $this->createLogRecord(); + $processed = $processor($record); + + $this->assertArrayNotHasKey('identity_id', $processed->extra); + } +} From 7932cf9059594be87ed327d4244c0b20696675d4 Mon Sep 17 00:00:00 2001 From: Peter Havekes Date: Wed, 7 Oct 2026 13:25:43 +0200 Subject: [PATCH 2/3] Reduce method and class complexity in RequestContextProcessor --- .../Processor/RequestContextProcessor.php | 279 +++++++----------- 1 file changed, 112 insertions(+), 167 deletions(-) diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php b/src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php index 367fb5ba..168de64e 100644 --- a/src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php @@ -24,10 +24,10 @@ use Stringable; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use function array_filter; use function in_array; use function is_array; use function is_scalar; -use function is_string; use function json_decode; use function trim; @@ -84,212 +84,165 @@ private function findRequestContext(Request $request): array */ private function extractRequestContext(Request $request): array { - $jsonData = null; - $content = $request->getContent(); - if ($content !== '') { - $decoded = json_decode($content, true); - if (is_array($decoded)) { - $jsonData = $decoded; - } - } - - $context = []; - - $identityId = $this->extractIdentityId($request, $jsonData); - if ($identityId !== null) { - $context['identity_id'] = $identityId; - } - - $collabPersonId = $this->extractCollabPersonId($request, $jsonData); - if ($collabPersonId !== null) { - $context['collab_person_id'] = $collabPersonId; - } - - $secondFactorId = $this->extractSecondFactorId($request, $jsonData); - if ($secondFactorId !== null) { - $context['second_factor_id'] = $secondFactorId; - } - - $recoveryTokenId = $this->extractRecoveryTokenId($request, $jsonData); - if ($recoveryTokenId !== null) { - $context['recovery_token_id'] = $recoveryTokenId; - } + $jsonData = $this->parseJsonContent($request->getContent()); + $sources = $this->collectSources($request, $jsonData); + + $fields = [ + 'identity_id' => $this->extractIdentityId($request, $sources, $jsonData), + 'collab_person_id' => $this->extractFromSources($sources, ['collabPersonId', 'collab_person_id']), + 'second_factor_id' => $this->extractSecondFactorId($request, $sources), + 'recovery_token_id' => $this->extractRecoveryTokenId($request, $sources), + 'name_id' => $this->extractFromSources($sources, ['nameId', 'name_id', 'NameID']), + 'institution' => $this->extractInstitution($sources, $jsonData), + ]; + + return array_filter($fields, static fn (?string $value): bool => $value !== null); + } - $nameId = $this->extractNameId($request, $jsonData); - if ($nameId !== null) { - $context['name_id'] = $nameId; + /** + * @return array|null + */ + private function parseJsonContent(string $content): ?array + { + if ($content === '') { + return null; } - $institution = $this->extractInstitution($request, $jsonData); - if ($institution !== null) { - $context['institution'] = $institution; - } + $decoded = json_decode($content, true); - return $context; + return is_array($decoded) ? $decoded : null; } - private function extractIdentityId(Request $request, ?array $jsonData): ?string + /** + * @param array|null $jsonData + * @return list> + */ + private function collectSources(Request $request, ?array $jsonData): array { - if ($request->attributes->get('_route') === 'identity' && $request->attributes->has('id')) { - $id = $this->stringify($request->attributes->get('id')); - if ($id !== null) { - return $id; - } + $sources = [ + $request->attributes->all(), + $request->query->all(), + $request->request->all(), + ]; + + if ($jsonData === null) { + return $sources; } - $value = $this->extractFromRequestOrJson($request, ['identityId', 'identity_id'], $jsonData); - if ($value !== null) { - return $value; + $sources[] = $jsonData; + + if (isset($jsonData['command']['payload']) && is_array($jsonData['command']['payload'])) { + $sources[] = $jsonData['command']['payload']; } - if ($jsonData !== null) { - if (isset($jsonData['command']['name']) - && is_string($jsonData['command']['name']) - && in_array($jsonData['command']['name'], ['Identity:CreateIdentity', 'Identity:UpdateIdentity'], true) - && isset($jsonData['command']['payload']['id']) - ) { - $id = $this->stringify($jsonData['command']['payload']['id']); - if ($id !== null) { - return $id; - } - } + if (isset($jsonData['payload']) && is_array($jsonData['payload'])) { + $sources[] = $jsonData['payload']; } - return null; + return $sources; } - private function extractCollabPersonId(Request $request, ?array $jsonData): ?string + /** + * @param list> $sources + * @param array|null $jsonData + */ + private function extractIdentityId(Request $request, array $sources, ?array $jsonData): ?string { - return $this->extractFromRequestOrJson($request, ['collabPersonId', 'collab_person_id'], $jsonData); + return $this->extractRouteId($request, ['identity']) + ?? $this->extractFromSources($sources, ['identityId', 'identity_id']) + ?? $this->extractIdentityCommandId($jsonData); } - private function extractSecondFactorId(Request $request, ?array $jsonData): ?string + /** + * @param list $allowedRoutes + */ + private function extractRouteId(Request $request, array $allowedRoutes): ?string { $route = (string) $request->attributes->get('_route'); - if (in_array($route, self::SECOND_FACTOR_ROUTES, true) && $request->attributes->has('id')) { - $id = $this->stringify($request->attributes->get('id')); - if ($id !== null) { - return $id; - } + if (in_array($route, $allowedRoutes, true)) { + return $this->stringify($request->attributes->get('id')); } - return $this->extractFromRequestOrJson( - $request, - ['secondFactorId', 'second_factor_id', 'authoringSecondFactorId', 'authoring_second_factor_id'], - $jsonData, - ); + return null; } - private function extractRecoveryTokenId(Request $request, ?array $jsonData): ?string + /** + * @param array|null $jsonData + */ + private function extractIdentityCommandId(?array $jsonData): ?string { - if ($request->attributes->get('_route') === 'recovery_token' && $request->attributes->has('id')) { - $id = $this->stringify($request->attributes->get('id')); - if ($id !== null) { - return $id; - } + $commandName = $jsonData['command']['name'] ?? null; + if (in_array($commandName, ['Identity:CreateIdentity', 'Identity:UpdateIdentity'], true)) { + return $this->stringify($jsonData['command']['payload']['id'] ?? null); } - return $this->extractFromRequestOrJson( - $request, - ['recoveryTokenIdId', 'recoveryTokenId', 'recovery_token_id', 'recovery_token_id_id'], - $jsonData, - ); + return null; } - private function extractNameId(Request $request, ?array $jsonData): ?string + /** + * @param list> $sources + */ + private function extractSecondFactorId(Request $request, array $sources): ?string { - return $this->extractFromRequestOrJson($request, ['nameId', 'name_id', 'NameID'], $jsonData); + return $this->extractRouteId($request, self::SECOND_FACTOR_ROUTES) + ?? $this->extractFromSources( + $sources, + ['secondFactorId', 'second_factor_id', 'authoringSecondFactorId', 'authoring_second_factor_id'], + ); } - private function extractInstitution(Request $request, ?array $jsonData): ?string + /** + * @param list> $sources + */ + private function extractRecoveryTokenId(Request $request, array $sources): ?string { - $value = $this->extractFromRequestOrJson( - $request, - ['institution', 'institutionName', 'institution_name', 'raInstitution', 'ra_institution'], - $jsonData, - ); + return $this->extractRouteId($request, ['recovery_token']) + ?? $this->extractFromSources( + $sources, + ['recoveryTokenIdId', 'recoveryTokenId', 'recovery_token_id', 'recovery_token_id_id'], + ); + } - if ($value !== null) { - return $value; - } + /** + * @param list> $sources + * @param array|null $jsonData + */ + private function extractInstitution(array $sources, ?array $jsonData): ?string + { + return $this->extractFromSources( + $sources, + ['institution', 'institutionName', 'institution_name', 'raInstitution', 'ra_institution'], + ) ?? $this->extractMetaInstitution($jsonData); + } - if ($jsonData !== null && isset($jsonData['meta']) && is_array($jsonData['meta'])) { - foreach (['actor_institution', 'actorInstitution', 'institution'] as $key) { - if (isset($jsonData['meta'][$key])) { - $id = $this->stringify($jsonData['meta'][$key]); - if ($id !== null) { - return $id; - } - } - } + /** + * @param array|null $jsonData + */ + private function extractMetaInstitution(?array $jsonData): ?string + { + $meta = $jsonData['meta'] ?? null; + if (!is_array($meta)) { + return null; } - return null; + return $this->extractFromSources([$meta], ['actor_institution', 'actorInstitution', 'institution']); } /** + * @param list> $sources * @param list $keys */ - private function extractFromRequestOrJson(Request $request, array $keys, ?array $jsonData): ?string + private function extractFromSources(array $sources, array $keys): ?string { - foreach ($keys as $key) { - if ($request->attributes->has($key)) { - $val = $this->stringify($request->attributes->get($key)); - if ($val !== null) { - return $val; - } - } - } - - foreach ($keys as $key) { - if ($request->query->has($key)) { - $val = $this->stringify($request->query->get($key)); - if ($val !== null) { - return $val; - } - } - } - - foreach ($keys as $key) { - if ($request->request->has($key)) { - $val = $this->stringify($request->request->get($key)); - if ($val !== null) { - return $val; - } - } - } - - if ($jsonData !== null) { + foreach ($sources as $source) { foreach ($keys as $key) { - if (isset($jsonData[$key])) { - $val = $this->stringify($jsonData[$key]); + if (isset($source[$key])) { + $val = $this->stringify($source[$key]); if ($val !== null) { return $val; } } } - - if (isset($jsonData['command']['payload']) && is_array($jsonData['command']['payload'])) { - foreach ($keys as $key) { - if (isset($jsonData['command']['payload'][$key])) { - $val = $this->stringify($jsonData['command']['payload'][$key]); - if ($val !== null) { - return $val; - } - } - } - } - - if (isset($jsonData['payload']) && is_array($jsonData['payload'])) { - foreach ($keys as $key) { - if (isset($jsonData['payload'][$key])) { - $val = $this->stringify($jsonData['payload'][$key]); - if ($val !== null) { - return $val; - } - } - } - } } return null; @@ -297,17 +250,9 @@ private function extractFromRequestOrJson(Request $request, array $keys, ?array private function stringify(mixed $value): ?string { - if ($value === null) { - return null; - } - - if (is_string($value)) { - $trimmed = trim($value); - return $trimmed !== '' ? $trimmed : null; - } - if (is_scalar($value) || $value instanceof Stringable) { $trimmed = trim((string) $value); + return $trimmed !== '' ? $trimmed : null; } From f2021c74466bbe0ca6228d539841172befc67647 Mon Sep 17 00:00:00 2001 From: Peter Havekes Date: Wed, 7 Oct 2026 17:27:05 +0200 Subject: [PATCH 3/3] Fix PHPStan type errors in RequestContextProcessor --- .../Processor/RequestContextProcessor.php | 59 ++++++++++++------- .../Processor/RequestContextProcessorTest.php | 19 +++--- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php b/src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php index 168de64e..5a5e0573 100644 --- a/src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Monolog/Processor/RequestContextProcessor.php @@ -28,6 +28,7 @@ use function in_array; use function is_array; use function is_scalar; +use function is_string; use function json_decode; use function trim; @@ -100,7 +101,7 @@ private function extractRequestContext(Request $request): array } /** - * @return array|null + * @return array|null */ private function parseJsonContent(string $content): ?array { @@ -114,8 +115,8 @@ private function parseJsonContent(string $content): ?array } /** - * @param array|null $jsonData - * @return list> + * @param array|null $jsonData + * @return list> */ private function collectSources(Request $request, ?array $jsonData): array { @@ -131,20 +132,22 @@ private function collectSources(Request $request, ?array $jsonData): array $sources[] = $jsonData; - if (isset($jsonData['command']['payload']) && is_array($jsonData['command']['payload'])) { - $sources[] = $jsonData['command']['payload']; + $command = $jsonData['command'] ?? null; + if (is_array($command) && isset($command['payload']) && is_array($command['payload'])) { + $sources[] = $command['payload']; } - if (isset($jsonData['payload']) && is_array($jsonData['payload'])) { - $sources[] = $jsonData['payload']; + $payload = $jsonData['payload'] ?? null; + if (is_array($payload)) { + $sources[] = $payload; } return $sources; } /** - * @param list> $sources - * @param array|null $jsonData + * @param list> $sources + * @param array|null $jsonData */ private function extractIdentityId(Request $request, array $sources, ?array $jsonData): ?string { @@ -158,8 +161,8 @@ private function extractIdentityId(Request $request, array $sources, ?array $jso */ private function extractRouteId(Request $request, array $allowedRoutes): ?string { - $route = (string) $request->attributes->get('_route'); - if (in_array($route, $allowedRoutes, true)) { + $route = $request->attributes->get('_route'); + if (is_string($route) && in_array($route, $allowedRoutes, true)) { return $this->stringify($request->attributes->get('id')); } @@ -167,20 +170,32 @@ private function extractRouteId(Request $request, array $allowedRoutes): ?string } /** - * @param array|null $jsonData + * @param array|null $jsonData */ private function extractIdentityCommandId(?array $jsonData): ?string { - $commandName = $jsonData['command']['name'] ?? null; + if ($jsonData === null) { + return null; + } + + $command = $jsonData['command'] ?? null; + if (!is_array($command)) { + return null; + } + + $commandName = $command['name'] ?? null; if (in_array($commandName, ['Identity:CreateIdentity', 'Identity:UpdateIdentity'], true)) { - return $this->stringify($jsonData['command']['payload']['id'] ?? null); + $payload = $command['payload'] ?? null; + if (is_array($payload)) { + return $this->stringify($payload['id'] ?? null); + } } return null; } /** - * @param list> $sources + * @param list> $sources */ private function extractSecondFactorId(Request $request, array $sources): ?string { @@ -192,7 +207,7 @@ private function extractSecondFactorId(Request $request, array $sources): ?strin } /** - * @param list> $sources + * @param list> $sources */ private function extractRecoveryTokenId(Request $request, array $sources): ?string { @@ -204,8 +219,8 @@ private function extractRecoveryTokenId(Request $request, array $sources): ?stri } /** - * @param list> $sources - * @param array|null $jsonData + * @param list> $sources + * @param array|null $jsonData */ private function extractInstitution(array $sources, ?array $jsonData): ?string { @@ -216,10 +231,14 @@ private function extractInstitution(array $sources, ?array $jsonData): ?string } /** - * @param array|null $jsonData + * @param array|null $jsonData */ private function extractMetaInstitution(?array $jsonData): ?string { + if ($jsonData === null) { + return null; + } + $meta = $jsonData['meta'] ?? null; if (!is_array($meta)) { return null; @@ -229,7 +248,7 @@ private function extractMetaInstitution(?array $jsonData): ?string } /** - * @param list> $sources + * @param list> $sources * @param list $keys */ private function extractFromSources(array $sources, array $keys): ?string diff --git a/src/Surfnet/StepupMiddleware/ApiBundle/Tests/Monolog/Processor/RequestContextProcessorTest.php b/src/Surfnet/StepupMiddleware/ApiBundle/Tests/Monolog/Processor/RequestContextProcessorTest.php index c7d8a459..7cc9f283 100644 --- a/src/Surfnet/StepupMiddleware/ApiBundle/Tests/Monolog/Processor/RequestContextProcessorTest.php +++ b/src/Surfnet/StepupMiddleware/ApiBundle/Tests/Monolog/Processor/RequestContextProcessorTest.php @@ -1,6 +1,6 @@ $extra + */ private function createLogRecord(array $extra = []): LogRecord { return new LogRecord( @@ -181,7 +184,7 @@ public function it_extracts_identity_id_from_command_json_payload(): void $request = new Request( server: ['CONTENT_TYPE' => 'application/json'], - content: json_encode($payload), + content: json_encode($payload, JSON_THROW_ON_ERROR), ); $requestStack = new RequestStack(); @@ -212,7 +215,7 @@ public function it_extracts_snake_case_identity_id_from_command_json_payload(): $request = new Request( server: ['CONTENT_TYPE' => 'application/json'], - content: json_encode($payload), + content: json_encode($payload, JSON_THROW_ON_ERROR), ); $requestStack = new RequestStack(); @@ -243,7 +246,7 @@ public function it_extracts_id_from_create_identity_command_json_payload(): void $request = new Request( server: ['CONTENT_TYPE' => 'application/json'], - content: json_encode($payload), + content: json_encode($payload, JSON_THROW_ON_ERROR), ); $requestStack = new RequestStack(); @@ -267,7 +270,7 @@ public function it_extracts_identity_id_from_root_json_payload(): void $request = new Request( server: ['CONTENT_TYPE' => 'application/json'], - content: json_encode($payload), + content: json_encode($payload, JSON_THROW_ON_ERROR), ); $requestStack = new RequestStack(); @@ -329,7 +332,7 @@ public function it_extracts_second_factor_id_from_route_id_query_and_payload(): ]; $request2 = new Request( server: ['CONTENT_TYPE' => 'application/json'], - content: json_encode($payload), + content: json_encode($payload, JSON_THROW_ON_ERROR), ); $requestStack2 = new RequestStack(); $requestStack2->push($request2); @@ -417,7 +420,7 @@ public function it_extracts_institution_from_query_route_and_meta(): void ]; $request2 = new Request( server: ['CONTENT_TYPE' => 'application/json'], - content: json_encode($payload), + content: json_encode($payload, JSON_THROW_ON_ERROR), ); $requestStack2 = new RequestStack(); $requestStack2->push($request2); @@ -448,7 +451,7 @@ public function it_extracts_multiple_context_fields_simultaneously(): void $request = new Request( query: ['secondFactorId' => 'sf-uuid-9'], server: ['CONTENT_TYPE' => 'application/json'], - content: json_encode($payload), + content: json_encode($payload, JSON_THROW_ON_ERROR), ); $requestStack = new RequestStack();