Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions app/Audit/AbstractAuditLogFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)';
}
Expand Down
85 changes: 85 additions & 0 deletions app/Audit/AuditEventListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use OAuth2\IResourceServerContext;
use OAuth2\Models\IClient;
use Services\OAuth2\ResourceServerContext;
use OpenTelemetry\API\Baggage\Baggage;

/**
* Class AuditEventListener
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
}
97 changes: 97 additions & 0 deletions app/Listeners/CaptureJobAuditContextListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php
namespace App\Listeners;

use App\Audit\AuditContext;
use Illuminate\Queue\Events\JobQueued;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use OAuth2\IResourceServerContext;
use Auth\Repositories\IUserRepository;
use OpenTelemetry\API\Baggage\Baggage;

class CaptureJobAuditContextListener
{
/**
* Handle the event.
*/
public function handle(JobQueued $event): void
{
try {
$context = $this->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(),
]);
}
}
}
3 changes: 3 additions & 0 deletions app/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading