diff --git a/app/Audit/AbstractAuditLogFormatter.php b/app/Audit/AbstractAuditLogFormatter.php index f0d053e2..c297e842 100644 --- a/app/Audit/AbstractAuditLogFormatter.php +++ b/app/Audit/AbstractAuditLogFormatter.php @@ -34,9 +34,7 @@ final public function setContext(AuditContext $ctx): void protected function getUserInfo(): string { - if (app()->runningInConsole()) { - return 'Worker Job'; - } + if (!$this->ctx) { return 'Unknown (unknown)'; } diff --git a/app/Audit/AuditEventListener.php b/app/Audit/AuditEventListener.php index aa41060c..deb241b9 100644 --- a/app/Audit/AuditEventListener.php +++ b/app/Audit/AuditEventListener.php @@ -26,6 +26,7 @@ use OAuth2\IResourceServerContext; use OAuth2\Models\IClient; use Services\OAuth2\ResourceServerContext; +use OpenTelemetry\API\Baggage\Baggage; /** * Class AuditEventListener @@ -99,6 +100,17 @@ private function getAuditStrategy($em): ?IAuditStrategy private function buildAuditContext(): AuditContext { + + if (app()->runningInConsole()) { + Log::debug('AuditEventListener::buildAuditContext - running in console, attempting to get baggage context'); + $contextFromBaggage = $this->buildContextFromBaggage(); + if ($contextFromBaggage) { + Log::debug('AuditEventListener::buildAuditContext - context successfully loaded from baggage'); + return $contextFromBaggage; + } + Log::debug('AuditEventListener::buildAuditContext - failed to load context from baggage, will use current request context'); + } + /*** * here we have 2 cases * 1. we are connecting to the IDP using an external APi ( under oauth2 ) so the @@ -157,4 +169,77 @@ private function buildAuditContext(): AuditContext rawRoute: $rawRoute ); } + + /** + * Rebuild audit context from OpenTelemetry Baggage (propagated from request to job) + */ + private function buildContextFromBaggage(): ?AuditContext + { + try { + $baggage = Baggage::getCurrent(); + + Log::debug('AuditEventListener::buildContextFromBaggage - baggage obtained', [ + 'baggage_class' => get_class($baggage), + ]); + + $userIdEntry = $baggage->getEntry('audit.userId'); + Log::debug('AuditEventListener::buildContextFromBaggage - userId entry', [ + 'entry_exists' => $userIdEntry !== null, + 'entry_class' => $userIdEntry ? get_class($userIdEntry) : 'null', + ]); + + $userId = $userIdEntry ? $userIdEntry->getValue() : null; + + Log::debug('AuditEventListener::buildContextFromBaggage - userId value', [ + 'userId' => $userId, + 'userId_type' => gettype($userId), + 'isEmpty' => empty($userId), + ]); + + if (!$userId) { + Log::debug('AuditEventListener: no userId in baggage'); + return null; + } + + $userEmail = $baggage->getEntry('audit.userEmail')?->getValue(); + $userFirstName = $baggage->getEntry('audit.userFirstName')?->getValue(); + $userLastName = $baggage->getEntry('audit.userLastName')?->getValue(); + $route = $baggage->getEntry('audit.route')?->getValue(); + $httpMethod = $baggage->getEntry('audit.httpMethod')?->getValue(); + $clientIp = $baggage->getEntry('audit.clientIp')?->getValue(); + $userAgent = $baggage->getEntry('audit.userAgent')?->getValue(); + + Log::debug('AuditEventListener::buildContextFromBaggage - extracted values', [ + 'userId' => $userId, + 'userEmail' => $userEmail, + 'userFirstName' => $userFirstName, + 'userLastName' => $userLastName, + 'route' => $route, + 'httpMethod' => $httpMethod, + 'clientIp' => $clientIp, + 'userAgent' => $userAgent, + ]); + + $auditContext = new AuditContext( + userId: (int)$userId > 0 ? (int)$userId : null, + userEmail: $userEmail, + userFirstName: $userFirstName, + userLastName: $userLastName, + route: $route, + httpMethod: $httpMethod, + clientIp: $clientIp, + userAgent: $userAgent, + ); + + Log::debug('AuditEventListener::buildContextFromBaggage - context created successfully'); + + return $auditContext; + } catch (\Exception $e) { + Log::debug('AuditEventListener: could not build context from baggage', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + return null; + } + } } \ No newline at end of file diff --git a/app/Listeners/CaptureJobAuditContextListener.php b/app/Listeners/CaptureJobAuditContextListener.php new file mode 100644 index 00000000..37d4a6ad --- /dev/null +++ b/app/Listeners/CaptureJobAuditContextListener.php @@ -0,0 +1,97 @@ +buildAuditContextFromCurrentRequest(); + + if (!$context) { + Log::warning('CaptureJobAuditContextListener: could not build audit context'); + return; + } + + $this->storeBaggageContext($context); + + Log::debug('CaptureJobAuditContextListener: audit context captured for job', [ + 'user_id' => $context->userId, + 'user_email' => $context->userEmail, + ]); + } catch (\Exception $e) { + Log::warning('CaptureJobAuditContextListener failed', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } + } + + private function buildAuditContextFromCurrentRequest(): ?AuditContext + { + $resource_server_context = app(IResourceServerContext::class); + $oauth2_current_client_id = $resource_server_context->getCurrentClientId(); + $userId = $oauth2_current_client_id = 1; + if (!empty($oauth2_current_client_id)) { + $userId = 1; + $user = $userId ? app(IUserRepository::class)->getById($userId) : null; + } else { + $user = Auth::user(); + } + if (!$user) { + return null; + } + + $req = request(); + + return new AuditContext( + userId: $user->getId(), + userEmail: $user->getEmail(), + userFirstName: $user->getFirstName(), + userLastName: $user->getLastName(), + route: $req?->path(), + httpMethod: $req?->method(), + clientIp: $req?->ip(), + userAgent: $req?->userAgent(), + ); + } + + /** + * Store the audit context in OpenTelemetry Baggage for queue propagation + */ + private function storeBaggageContext(AuditContext $context): void + { + try { + $baggage = Baggage::getCurrent() + ->toBuilder() + ->set('audit.userId', (string)($context->userId ?? '')) + ->set('audit.userEmail', $context->userEmail ?? '') + ->set('audit.userFirstName', $context->userFirstName ?? '') + ->set('audit.userLastName', $context->userLastName ?? '') + ->set('audit.route', $context->route ?? '') + ->set('audit.httpMethod', $context->httpMethod ?? '') + ->set('audit.clientIp', $context->clientIp ?? '') + ->set('audit.userAgent', $context->userAgent ?? '') + ->build(); + + $baggage->activate(); + + Log::debug('CaptureJobAuditContextListener: baggage context stored'); + } catch (\Exception $e) { + Log::warning('CaptureJobAuditContextListener: failed to store baggage', [ + 'error' => $e->getMessage(), + ]); + } + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index f89794d9..9ebea03a 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -57,6 +57,9 @@ final class EventServiceProvider extends ServiceProvider 'Illuminate\Auth\Events\Login' => [ 'App\Listeners\OnUserLogin', ], + \Illuminate\Queue\Events\JobQueued::class => [ + 'App\Listeners\CaptureJobAuditContextListener', + ], \SocialiteProviders\Manager\SocialiteWasCalled::class => [ // ... other providers 'SocialiteProviders\\Facebook\\FacebookExtendSocialite@handle',