1+ <?php
2+ namespace App \Audit ;
3+ /**
4+ * Copyright 2025 OpenStack Foundation
5+ * Licensed under the Apache License, Version 2.0 (the "License");
6+ * you may not use this file except in compliance with the License.
7+ * You may obtain a copy of the License at
8+ * http://www.apache.org/licenses/LICENSE-2.0
9+ * Unless required by applicable law or agreed to in writing, software
10+ * distributed under the License is distributed on an "AS IS" BASIS,
11+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ * See the License for the specific language governing permissions and
13+ * limitations under the License.
14+ **/
15+
16+ use App \Audit \Interfaces \IAuditStrategy ;
17+ use App \Repositories \DoctrineUserRepository ;
18+ use Auth \Repositories \IUserRepository ;
19+ use Auth \User ;
20+ use Doctrine \ORM \Event \OnFlushEventArgs ;
21+ use Illuminate \Support \Facades \App ;
22+ use Illuminate \Support \Facades \Log ;
23+ use Illuminate \Support \Facades \Route ;
24+ use Illuminate \Http \Request ;
25+ use OAuth2 \IResourceServerContext ;
26+ /**
27+ * Class AuditEventListener
28+ * @package App\Audit
29+ */
30+ class AuditEventListener
31+ {
32+ private const ROUTE_METHOD_SEPARATOR = '| ' ;
33+
34+ public function onFlush (OnFlushEventArgs $ eventArgs ): void
35+ {
36+ if (app ()->environment ('testing ' )) {
37+ return ;
38+ }
39+ $ em = $ eventArgs ->getObjectManager ();
40+ $ uow = $ em ->getUnitOfWork ();
41+ // Strategy selection based on environment configuration
42+ $ strategy = $ this ->getAuditStrategy ($ em );
43+ if (!$ strategy ) {
44+ return ; // No audit strategy enabled
45+ }
46+
47+ $ ctx = $ this ->buildAuditContext ();
48+
49+ try {
50+ foreach ($ uow ->getScheduledEntityInsertions () as $ entity ) {
51+ $ strategy ->audit ($ entity , [], IAuditStrategy::EVENT_ENTITY_CREATION , $ ctx );
52+ }
53+
54+ foreach ($ uow ->getScheduledEntityUpdates () as $ entity ) {
55+ $ strategy ->audit ($ entity , $ uow ->getEntityChangeSet ($ entity ), IAuditStrategy::EVENT_ENTITY_UPDATE , $ ctx );
56+ }
57+
58+ foreach ($ uow ->getScheduledEntityDeletions () as $ entity ) {
59+ $ strategy ->audit ($ entity , [], IAuditStrategy::EVENT_ENTITY_DELETION , $ ctx );
60+ }
61+
62+ foreach ($ uow ->getScheduledCollectionUpdates () as $ col ) {
63+ $ strategy ->audit ($ col , [], IAuditStrategy::EVENT_COLLECTION_UPDATE , $ ctx );
64+ }
65+ } catch (\Exception $ e ) {
66+ Log::error ('Audit event listener failed ' , [
67+ 'error ' => $ e ->getMessage (),
68+ 'strategy_class ' => get_class ($ strategy ),
69+ 'trace ' => $ e ->getTraceAsString (),
70+ ]);
71+ }
72+ }
73+
74+ /**
75+ * Get the appropriate audit strategy based on environment configuration
76+ */
77+ private function getAuditStrategy ($ em ): ?IAuditStrategy
78+ {
79+ // Check if OTLP audit is enabled
80+ if (config ('opentelemetry.enabled ' , false )) {
81+ try {
82+ Log::debug ("AuditEventListener::getAuditStrategy strategy AuditLogOtlpStrategy " );
83+ return App::make (AuditLogOtlpStrategy::class);
84+ } catch (\Exception $ e ) {
85+ Log::warning ('Failed to create OTLP audit strategy, falling back to database ' , [
86+ 'error ' => $ e ->getMessage ()
87+ ]);
88+ }
89+ }
90+
91+ // Use database strategy (either as default or fallback)
92+ Log::debug ("AuditEventListener::getAuditStrategy strategy AuditLogStrategy " );
93+ return new AuditLogStrategy ($ em );
94+ }
95+
96+ private function buildAuditContext (): AuditContext
97+ {
98+ $ userId = app (IResourceServerContext::class)->getCurrentUserId ();
99+
100+ /**
101+ * @var User|null $user
102+ */
103+ $ user = $ userId ? app (IUserRepository::class)->getById ($ userId ) : null ;
104+
105+ $ defaultUiContext = [
106+ 'app ' => null ,
107+ 'flow ' => null
108+ ];
109+ $ uiContext = [
110+ ...$ defaultUiContext ,
111+ // ...app()->bound('ui.context') ? app('ui.context') : [],
112+ ];
113+
114+ $ req = request ();
115+ $ rawRoute = null ;
116+ // does not resolve the route when app is running in console mode
117+ if ($ req instanceof Request && !app ()->runningInConsole ()) {
118+ try {
119+ $ route = Route::getRoutes ()->match ($ req );
120+ $ method = $ route ->methods [0 ] ?? 'UNKNOWN ' ;
121+ $ rawRoute = $ method . self ::ROUTE_METHOD_SEPARATOR . $ route ->uri ;
122+ } catch (\Exception $ e ) {
123+ Log::warning ($ e );
124+ }
125+ }
126+
127+ return new AuditContext (
128+ userId: $ user ?->getId(),
129+ userEmail: $ user ?->getEmail(),
130+ userFirstName: $ user ?->getFirstName(),
131+ userLastName: $ user ?->getLastName(),
132+ uiApp: $ uiContext ['app ' ],
133+ uiFlow: $ uiContext ['flow ' ],
134+ route: $ req ?->path(),
135+ httpMethod: $ req ?->method(),
136+ clientIp: $ req ?->ip(),
137+ userAgent: $ req ?->userAgent(),
138+ rawRoute: $ rawRoute
139+ );
140+ }
141+ }
0 commit comments