Skip to content
17 changes: 17 additions & 0 deletions app/Audit/AbstractAuditLogFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,22 @@ final public function setContext(AuditContext $ctx): void
$this->ctx = $ctx;
}

protected function getUserInfo(): string
{
if (!$this->ctx) {
return 'Unknown (unknown)';
}

$user_name = 'Unknown';
if ($this->ctx->userFirstName || $this->ctx->userLastName) {
$user_name = trim(sprintf("%s %s", $this->ctx->userFirstName ?? '', $this->ctx->userLastName ?? '')) ?: 'Unknown';
} elseif ($this->ctx->userEmail) {
$user_name = $this->ctx->userEmail;
}

$user_id = $this->ctx->userId ?? 'unknown';
return sprintf("%s (%s)", $user_name, $user_id);
}

abstract public function format($subject, array $change_set): ?string;
}
1 change: 1 addition & 0 deletions app/Audit/AuditContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function __construct(
public ?string $uiApp = null,
public ?string $uiFlow = null,
public ?string $route = null,
public ?string $rawRoute = null,
public ?string $httpMethod = null,
public ?string $clientIp = null,
public ?string $userAgent = null,
Expand Down
13 changes: 10 additions & 3 deletions app/Audit/AuditEventListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@
use Doctrine\ORM\Event\OnFlushEventArgs;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;

/**
* Class AuditEventListener
* @package App\Audit
*/
class AuditEventListener
{
private const ROUTE_METHOD_SEPARATOR = '|';

public function onFlush(OnFlushEventArgs $eventArgs): void
{
if (app()->environment('testing')){
if (app()->environment('testing')) {
return;
}
$em = $eventArgs->getObjectManager();
Expand Down Expand Up @@ -67,7 +69,7 @@ public function onFlush(OnFlushEventArgs $eventArgs): void
/**
* Get the appropriate audit strategy based on environment configuration
*/
private function getAuditStrategy($em)
private function getAuditStrategy($em): ?IAuditStrategy
{
// Check if OTLP audit is enabled
if (config('opentelemetry.enabled', false)) {
Expand Down Expand Up @@ -97,7 +99,11 @@ private function buildAuditContext(): AuditContext
//$ui = app()->bound('ui.context') ? app('ui.context') : [];

$req = request();


$route = Route::getRoutes()->match($req);
$method = $route->methods[0] ?? 'UNKNOWN';
$rawRoute = $method . self::ROUTE_METHOD_SEPARATOR . $route->uri;

return new AuditContext(
userId: $member?->getId(),
userEmail: $member?->getEmail(),
Expand All @@ -109,6 +115,7 @@ private function buildAuditContext(): AuditContext
httpMethod: $req?->method(),
clientIp: $req?->ip(),
userAgent: $req?->userAgent(),
rawRoute: $rawRoute
);
}
}
52 changes: 48 additions & 4 deletions app/Audit/AuditLogFormatterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,36 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt
if (count($subject) > 0) {
$child_entity = $subject[0];
}
if (is_null($child_entity) && count($subject->getSnapshot()) > 0) {
if (is_null($child_entity) && isset($subject->getSnapshot()[0]) && count($subject->getSnapshot()) > 0) {
$child_entity = $subject->getSnapshot()[0];
}
$child_entity_formatter = $child_entity != null ? ChildEntityFormatterFactory::build($child_entity) : null;
$formatter = new EntityCollectionUpdateAuditLogFormatter($child_entity_formatter);
break;
case IAuditStrategy::EVENT_ENTITY_CREATION:
$formatter = $this->getStrategyClass($subject, $eventType);
$formatter = $this->getFormatterByContext($subject, $eventType, $ctx);
if (is_null($formatter)) {
$formatter = $this->getStrategyClass($subject, $eventType);
}
if(is_null($formatter)) {
$formatter = new EntityCreationAuditLogFormatter();
}
break;
case IAuditStrategy::EVENT_ENTITY_DELETION:
$formatter = $this->getStrategyClass($subject, $eventType);
$formatter = $this->getFormatterByContext($subject, $eventType, $ctx);
if (is_null($formatter)) {
$formatter = $this->getStrategyClass($subject, $eventType);
}
if(is_null($formatter)) {
$child_entity_formatter = ChildEntityFormatterFactory::build($subject);
$formatter = new EntityDeletionAuditLogFormatter($child_entity_formatter);
}
break;
case IAuditStrategy::EVENT_ENTITY_UPDATE:
$formatter = $this->getStrategyClass($subject, $eventType);
$formatter = $this->getFormatterByContext($subject, $eventType, $ctx);
if (is_null($formatter)) {
$formatter = $this->getStrategyClass($subject, $eventType);
}
if(is_null($formatter)) {
$child_entity_formatter = ChildEntityFormatterFactory::build($subject);
$formatter = new EntityUpdateAuditLogFormatter($child_entity_formatter);
Expand All @@ -75,4 +84,39 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt
$formatter->setContext($ctx);
return $formatter;
}

private function getFormatterByContext(object $subject, string $event_type, AuditContext $ctx): ?IAuditLogFormatter
{
$class = get_class($subject);
$entity_config = $this->config['entities'][$class] ?? null;

if (!$entity_config || !isset($entity_config['strategies'])) {
return null;
}

foreach ($entity_config['strategies'] as $strategy) {
if (!$this->matchesStrategy($strategy, $ctx)) {
continue;
}

$formatter_class = $strategy['formatter'] ?? null;
return $formatter_class ? new $formatter_class($event_type) : null;
}

return null;
}

private function matchesStrategy(array $strategy, AuditContext $ctx): bool
{
if (isset($strategy['route']) && !$this->routeMatches($strategy['route'], $ctx->rawRoute)) {
return false;
}

return true;
}

private function routeMatches(string $route, string $actual_route): bool
{
return strcmp($actual_route, $route) === 0;
}
}
97 changes: 97 additions & 0 deletions app/Audit/ConcreteFormatters/FeaturedSpeakerAuditLogFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace App\Audit\ConcreteFormatters;

/**
* Copyright 2025 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use App\Audit\AbstractAuditLogFormatter;
use App\Audit\Interfaces\IAuditStrategy;
use App\Models\Foundation\Summit\Speakers\FeaturedSpeaker;
use Illuminate\Support\Facades\Log;

class FeaturedSpeakerAuditLogFormatter extends AbstractAuditLogFormatter
{
private string $event_type;

public function __construct(string $event_type)
{
$this->event_type = $event_type;
}

public function format($subject, array $change_set): ?string
{
if (!$subject instanceof FeaturedSpeaker) {
return null;
}

try {
$speaker = $subject->getSpeaker();
$speaker_email = $speaker ? ($speaker->getEmail() ?? 'unknown') : 'unknown';
$speaker_name = $speaker ? sprintf("%s %s", $speaker->getFirstName() ?? '', $speaker->getLastName() ?? '') : 'Unknown';
$speaker_name = trim($speaker_name) ?: $speaker_name;
$speaker_id = $speaker ? ($speaker->getId() ?? 'unknown') : 'unknown';

$summit = $subject->getSummit();
$summit_name = $summit ? ($summit->getName() ?? 'Unknown Summit') : 'Unknown Summit';

$order = $subject->getOrder();

switch ($this->event_type) {
case IAuditStrategy::EVENT_ENTITY_CREATION:
return sprintf(
"Speaker '%s' (%s) added as featured speaker for Summit '%s' with display order %d by user %s",
$speaker_name,
$speaker_id,
$summit_name,
$order,
$this->getUserInfo()
);

case IAuditStrategy::EVENT_ENTITY_UPDATE:
$changed_fields = [];

if (isset($change_set['Order'])) {
$old_order = $change_set['Order'][0] ?? 'unknown';
$new_order = $change_set['Order'][1] ?? 'unknown';
$changed_fields[] = sprintf("display_order %s → %s", $old_order, $new_order);
}
if (isset($change_set['PresentationSpeakerID'])) {
$changed_fields[] = "speaker";
}

$fields_str = !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties';
return sprintf(
"Featured speaker '%s' (%s) updated (%s changed) by user %s",
$speaker_name,
$speaker_id,
$fields_str,
$this->getUserInfo()
);

case IAuditStrategy::EVENT_ENTITY_DELETION:
return sprintf(
"Speaker '%s' (%s) removed from featured speakers list of Summit '%s' by user %s",
$speaker_name,
$speaker_id,
$summit_name,
$this->getUserInfo()
);
}
} catch (\Exception $ex) {
Log::warning("FeaturedSpeakerAuditLogFormatter error: " . $ex->getMessage());
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace App\Audit\ConcreteFormatters\PresentationFormatters;

/**
* Copyright 2025 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use App\Audit\AbstractAuditLogFormatter;
use App\Audit\Interfaces\IAuditStrategy;
use models\summit\Presentation;
use Illuminate\Support\Facades\Log;

abstract class BasePresentationAuditLogFormatter extends AbstractAuditLogFormatter
{
protected string $event_type;

public function __construct(string $event_type)
{
$this->event_type = $event_type;
}

protected function extractChangedFields(array $change_set): array
{
$changed_fields = [];
$old_status = null;
$new_status = null;

if (isset($change_set['Title'])) {
$changed_fields[] = "title";
}
if (isset($change_set['Abstract'])) {
$changed_fields[] = "abstract";
}
if (isset($change_set['ProblemAddressed'])) {
$changed_fields[] = "problem_addressed";
}
if (isset($change_set['AttendeesExpectedLearnt'])) {
$changed_fields[] = "attendees_expected_learnt";
}

if (isset($change_set['Status'])) {
$changed_fields[] = "status";
$old_status = $change_set['Status'][0] ?? null;
$new_status = $change_set['Status'][1] ?? null;
}
if (isset($change_set['CategoryID']) || isset($change_set['category'])) {
$changed_fields[] = "track";
}
if (isset($change_set['Published'])) {
$changed_fields[] = "published";
}
if (isset($change_set['SelectionPlanID'])) {
$changed_fields[] = "selection_plan";
}

return [
'fields' => !empty($changed_fields) ? implode(', ', $changed_fields) : 'properties',
'old_status' => $old_status,
'new_status' => $new_status,
];
}

protected function getPresentationData(Presentation $subject): array
{
$creator = $subject->getCreator();
$creator_name = $creator ? sprintf("%s %s", $creator->getFirstName() ?? '', $creator->getLastName() ?? '') : 'Unknown';
$creator_name = trim($creator_name) ?: 'Unknown';

$category = $subject->getCategory();
$category_name = $category ? $category->getTitle() : 'Unassigned Track';

$selection_plan = $subject->getSelectionPlan();
$plan_name = $selection_plan ? $selection_plan->getName() : 'Unknown Plan';

return [
'title' => $subject->getTitle() ?? 'Unknown Presentation',
'id' => $subject->getId() ?? 'unknown',
'creator_name' => $creator_name,
'category_name' => $category_name,
'plan_name' => $plan_name,
];
}

public function format($subject, array $change_set): ?string
{
if (!$subject instanceof Presentation) {
return null;
}

try {
$data = $this->getPresentationData($subject);

switch ($this->event_type) {
case IAuditStrategy::EVENT_ENTITY_CREATION:
return $this->formatCreation($data);

case IAuditStrategy::EVENT_ENTITY_UPDATE:
$extracted = $this->extractChangedFields($change_set);
return $this->formatUpdate($data, $extracted);

case IAuditStrategy::EVENT_ENTITY_DELETION:
return $this->formatDeletion($data);
}
} catch (\Exception $ex) {
Log::warning(static::class . " error: " . $ex->getMessage());
}

return null;
}

abstract protected function formatCreation(array $data): string;

abstract protected function formatUpdate(array $data, array $extracted): string;

abstract protected function formatDeletion(array $data): string;
}
Loading