fix(audit): harden module isolation and fix post-extraction drift
Fix 5 broken FQCN references (runtime errors) where core pages referenced non-existent MintyPHP\Service\Audit\AuditMetadataEnricher and SystemAuditService. Add AuditMetadataEnricherInterface with NullAuditMetadataEnricher fallback so deactivating the audit module no longer crashes core edit pages. Move audit search resources from core Search*Provider files into the module via a new AuditSearchResourceProvider implementing the existing SearchResourceProvider contract. Add module-specific purge permissions (audit.api.purge, audit.imports.purge) replacing core ABILITY_ADMIN_SETTINGS_UPDATE. Also fixes: session key prefix convention, hardcoded English string, $_SERVER superglobal fallback, and manifest schema drift (i18n_path, group in ui_slots). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,8 @@
|
|||||||
"order": { "type": "integer", "default": 100 },
|
"order": { "type": "integer", "default": 100 },
|
||||||
"details_storage": { "type": "string" },
|
"details_storage": { "type": "string" },
|
||||||
"details_open_active": { "type": "boolean" },
|
"details_open_active": { "type": "boolean" },
|
||||||
"base_url": { "type": "string" }
|
"base_url": { "type": "string" },
|
||||||
|
"group": { "type": "string", "description": "Sidebar nav group key for grouping related items under a details/summary section." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,6 +204,11 @@
|
|||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "Relative path to SQL migration scripts. Resolved against module basePath.",
|
"description": "Relative path to SQL migration scripts. Resolved against module basePath.",
|
||||||
"default": null
|
"default": null
|
||||||
|
},
|
||||||
|
"i18n_path": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"description": "Relative path to i18n translation files. Resolved against module basePath.",
|
||||||
|
"default": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ use MintyPHP\App\Container\Registrars\SettingsRegistrar;
|
|||||||
use MintyPHP\App\Container\Registrars\UserRegistrar;
|
use MintyPHP\App\Container\Registrars\UserRegistrar;
|
||||||
use MintyPHP\App\Module\ModuleEventDispatcher;
|
use MintyPHP\App\Module\ModuleEventDispatcher;
|
||||||
use MintyPHP\App\Module\ModuleRegistry;
|
use MintyPHP\App\Module\ModuleRegistry;
|
||||||
|
use MintyPHP\Service\Audit\AuditMetadataEnricherInterface;
|
||||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||||
use MintyPHP\Service\Audit\ImportAuditInterface;
|
use MintyPHP\Service\Audit\ImportAuditInterface;
|
||||||
|
use MintyPHP\Service\Audit\NullAuditMetadataEnricher;
|
||||||
use MintyPHP\Service\Audit\NullAuditRecorder;
|
use MintyPHP\Service\Audit\NullAuditRecorder;
|
||||||
use MintyPHP\Service\Audit\NullImportAudit;
|
use MintyPHP\Service\Audit\NullImportAudit;
|
||||||
use MintyPHP\Service\Audit\NullUserLifecycleAudit;
|
use MintyPHP\Service\Audit\NullUserLifecycleAudit;
|
||||||
@@ -82,5 +84,8 @@ if (!$container->has(UserLifecycleAuditInterface::class)) {
|
|||||||
if (!$container->has(ImportAuditInterface::class)) {
|
if (!$container->has(ImportAuditInterface::class)) {
|
||||||
$container->set(ImportAuditInterface::class, static fn (): ImportAuditInterface => new NullImportAudit());
|
$container->set(ImportAuditInterface::class, static fn (): ImportAuditInterface => new NullImportAudit());
|
||||||
}
|
}
|
||||||
|
if (!$container->has(AuditMetadataEnricherInterface::class)) {
|
||||||
|
$container->set(AuditMetadataEnricherInterface::class, static fn (): AuditMetadataEnricherInterface => new NullAuditMetadataEnricher());
|
||||||
|
}
|
||||||
|
|
||||||
return $container;
|
return $container;
|
||||||
|
|||||||
21
lib/Service/Audit/AuditMetadataEnricherInterface.php
Normal file
21
lib/Service/Audit/AuditMetadataEnricherInterface.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Service\Audit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core-side contract for enriching entity audit metadata (created_by, modified_by, etc.)
|
||||||
|
* with user display labels and UUIDs.
|
||||||
|
*
|
||||||
|
* Implemented by the audit module's AuditMetadataEnricher when the module is active.
|
||||||
|
* Falls back to NullAuditMetadataEnricher (no-op) when the audit module is disabled.
|
||||||
|
*/
|
||||||
|
interface AuditMetadataEnricherInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Resolve audit user IDs on an entity into display labels and UUIDs.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $entity The entity array (modified in place).
|
||||||
|
* @param list<string> $fields Audit field names to resolve (e.g. 'created_by', 'modified_by').
|
||||||
|
*/
|
||||||
|
public function enrich(array &$entity, array $fields = ['created_by', 'modified_by']): void;
|
||||||
|
}
|
||||||
14
lib/Service/Audit/NullAuditMetadataEnricher.php
Normal file
14
lib/Service/Audit/NullAuditMetadataEnricher.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Service\Audit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op metadata enricher used when the audit module is disabled.
|
||||||
|
*/
|
||||||
|
final class NullAuditMetadataEnricher implements AuditMetadataEnricherInterface
|
||||||
|
{
|
||||||
|
public function enrich(array &$entity, array $fields = ['created_by', 'modified_by']): void
|
||||||
|
{
|
||||||
|
// No-op: audit module is not active.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,28 +34,6 @@ class SearchItemMapperProvider
|
|||||||
$label = trim($label . ' — ' . strtoupper($status));
|
$label = trim($label . ' — ' . strtoupper($status));
|
||||||
}
|
}
|
||||||
$url = lurl('admin/mail-log/view/' . ($data['id'] ?? '')) . '?search=' . urlencode($query);
|
$url = lurl('admin/mail-log/view/' . ($data['id'] ?? '')) . '?search=' . urlencode($query);
|
||||||
} elseif ($key === 'api-audit') {
|
|
||||||
$method = strtoupper(trim((string) ($data['method'] ?? '')));
|
|
||||||
$path = trim((string) ($data['path'] ?? ''));
|
|
||||||
$statusCode = (int) ($data['status_code'] ?? 0);
|
|
||||||
$requestId = trim((string) ($data['request_id'] ?? ''));
|
|
||||||
$label = trim($method . ' ' . $path);
|
|
||||||
if ($statusCode > 0) {
|
|
||||||
$label = trim($label . ' — ' . $statusCode);
|
|
||||||
}
|
|
||||||
if ($label === '' && $requestId !== '') {
|
|
||||||
$label = $requestId;
|
|
||||||
}
|
|
||||||
$url = lurl('admin/api-audit/view/' . ($data['id'] ?? '')) . '?search=' . urlencode($query);
|
|
||||||
} elseif ($key === 'system-audit') {
|
|
||||||
$eventType = trim((string) ($data['event_type'] ?? ''));
|
|
||||||
$outcome = strtolower(trim((string) ($data['outcome'] ?? '')));
|
|
||||||
$requestId = trim((string) ($data['request_id'] ?? ''));
|
|
||||||
$label = $eventType !== '' ? $eventType : $requestId;
|
|
||||||
if ($outcome !== '') {
|
|
||||||
$label = trim($label . ' — ' . $outcome);
|
|
||||||
}
|
|
||||||
$url = lurl('admin/system-audit/view/' . ($data['id'] ?? '')) . '?search=' . urlencode($query);
|
|
||||||
} elseif ($key === 'pages') {
|
} elseif ($key === 'pages') {
|
||||||
$label = (string) ($data['slug'] ?? '');
|
$label = (string) ($data['slug'] ?? '');
|
||||||
$url = lurl('page/' . $label) . '?search=' . urlencode($query);
|
$url = lurl('page/' . $label) . '?search=' . urlencode($query);
|
||||||
@@ -106,24 +84,6 @@ class SearchItemMapperProvider
|
|||||||
$toEmail = trim((string) ($data['to_email'] ?? ''));
|
$toEmail = trim((string) ($data['to_email'] ?? ''));
|
||||||
$description = trim(($status !== '' ? strtoupper($status) : '') . ($toEmail !== '' ? ' — ' . $toEmail : ''));
|
$description = trim(($status !== '' ? strtoupper($status) : '') . ($toEmail !== '' ? ' — ' . $toEmail : ''));
|
||||||
$url = lurl('admin/mail-log/view/' . ($data['id'] ?? ''));
|
$url = lurl('admin/mail-log/view/' . ($data['id'] ?? ''));
|
||||||
} elseif ($key === 'api-audit') {
|
|
||||||
$method = strtoupper(trim((string) ($data['method'] ?? '')));
|
|
||||||
$path = trim((string) ($data['path'] ?? ''));
|
|
||||||
$statusCode = (int) ($data['status_code'] ?? 0);
|
|
||||||
$requestId = trim((string) ($data['request_id'] ?? ''));
|
|
||||||
$title = trim($method . ' ' . $path);
|
|
||||||
if ($title === '') {
|
|
||||||
$title = $requestId;
|
|
||||||
}
|
|
||||||
$description = trim(($statusCode > 0 ? (string) $statusCode : '') . ($requestId !== '' ? ' — ' . $requestId : ''));
|
|
||||||
$url = lurl('admin/api-audit/view/' . ($data['id'] ?? ''));
|
|
||||||
} elseif ($key === 'system-audit') {
|
|
||||||
$eventType = trim((string) ($data['event_type'] ?? ''));
|
|
||||||
$outcome = strtolower(trim((string) ($data['outcome'] ?? '')));
|
|
||||||
$requestId = trim((string) ($data['request_id'] ?? ''));
|
|
||||||
$title = $eventType !== '' ? $eventType : $requestId;
|
|
||||||
$description = trim(($outcome !== '' ? strtoupper($outcome) : '') . ($requestId !== '' ? ' — ' . $requestId : ''));
|
|
||||||
$url = lurl('admin/system-audit/view/' . ($data['id'] ?? ''));
|
|
||||||
} else {
|
} else {
|
||||||
$title = (string) ($data['description'] ?? '');
|
$title = (string) ($data['description'] ?? '');
|
||||||
$description = $label;
|
$description = $label;
|
||||||
|
|||||||
@@ -110,28 +110,6 @@ class SearchSqlResourceProvider
|
|||||||
'resultSql' => "select id, to_email, subject, status, created_at from mail_log where (to_email like ? escape '\\\\' or subject like ? escape '\\\\' or template like ? escape '\\\\' or provider_message_id like ? escape '\\\\') order by created_at desc",
|
'resultSql' => "select id, to_email, subject, status, created_at from mail_log where (to_email like ? escape '\\\\' or subject like ? escape '\\\\' or template like ? escape '\\\\' or provider_message_id like ? escape '\\\\') order by created_at desc",
|
||||||
'resultParams' => [$like, $like, $like, $like],
|
'resultParams' => [$like, $like, $like, $like],
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'key' => 'api-audit',
|
|
||||||
'label' => t('API audit'),
|
|
||||||
'permission' => 'api_audit.view',
|
|
||||||
'countSql' => "select count(*) from api_audit_log where (request_id like ? escape '\\\\' or path like ? escape '\\\\' or error_code like ? escape '\\\\' or ip like ? escape '\\\\')",
|
|
||||||
'countParams' => [$like, $like, $like, $like],
|
|
||||||
'previewSql' => "select id, request_id, method, path, status_code, created_at from api_audit_log where (request_id like ? escape '\\\\' or path like ? escape '\\\\' or error_code like ? escape '\\\\' or ip like ? escape '\\\\') order by created_at desc limit ?",
|
|
||||||
'previewParams' => [$like, $like, $like, $like],
|
|
||||||
'resultSql' => "select id, request_id, method, path, status_code, created_at from api_audit_log where (request_id like ? escape '\\\\' or path like ? escape '\\\\' or error_code like ? escape '\\\\' or ip like ? escape '\\\\') order by created_at desc",
|
|
||||||
'resultParams' => [$like, $like, $like, $like],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'key' => 'system-audit',
|
|
||||||
'label' => t('System audit'),
|
|
||||||
'permission' => 'system_audit.view',
|
|
||||||
'countSql' => "select count(*) from system_audit_log where (request_id like ? escape '\\\\' or event_type like ? escape '\\\\' or error_code like ? escape '\\\\' or path like ? escape '\\\\')",
|
|
||||||
'countParams' => [$like, $like, $like, $like],
|
|
||||||
'previewSql' => "select id, request_id, event_type, outcome, created_at from system_audit_log where (request_id like ? escape '\\\\' or event_type like ? escape '\\\\' or error_code like ? escape '\\\\' or path like ? escape '\\\\') order by created_at desc limit ?",
|
|
||||||
'previewParams' => [$like, $like, $like, $like],
|
|
||||||
'resultSql' => "select id, request_id, event_type, outcome, created_at from system_audit_log where (request_id like ? escape '\\\\' or event_type like ? escape '\\\\' or error_code like ? escape '\\\\' or path like ? escape '\\\\') order by created_at desc",
|
|
||||||
'resultParams' => [$like, $like, $like, $like],
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ class SearchUiMetaProvider
|
|||||||
'scheduled-jobs' => 'bi-calendar-check',
|
'scheduled-jobs' => 'bi-calendar-check',
|
||||||
'pages' => 'bi-file-text',
|
'pages' => 'bi-file-text',
|
||||||
'mail-log' => 'bi-envelope-paper',
|
'mail-log' => 'bi-envelope-paper',
|
||||||
'api-audit' => 'bi-shield-check',
|
|
||||||
'system-audit' => 'bi-journal-lock',
|
|
||||||
'docs' => 'bi-journal-text',
|
'docs' => 'bi-journal-text',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -27,8 +25,6 @@ class SearchUiMetaProvider
|
|||||||
'scheduled-jobs',
|
'scheduled-jobs',
|
||||||
'pages',
|
'pages',
|
||||||
'mail-log',
|
'mail-log',
|
||||||
'api-audit',
|
|
||||||
'system-audit',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public static function iconForKey(string $key): string
|
public static function iconForKey(string $key): string
|
||||||
@@ -49,8 +45,6 @@ class SearchUiMetaProvider
|
|||||||
'scheduled-jobs' => lurl('admin/scheduled-jobs') . '?search=' . $encoded,
|
'scheduled-jobs' => lurl('admin/scheduled-jobs') . '?search=' . $encoded,
|
||||||
'pages' => lurl(''),
|
'pages' => lurl(''),
|
||||||
'mail-log' => lurl('admin/mail-log') . '?search=' . $encoded,
|
'mail-log' => lurl('admin/mail-log') . '?search=' . $encoded,
|
||||||
'api-audit' => lurl('admin/api-audit') . '?search=' . $encoded,
|
|
||||||
'system-audit' => lurl('admin/system-audit') . '?search=' . $encoded,
|
|
||||||
default => lurl(''),
|
default => lurl(''),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,21 @@ use MintyPHP\Service\Access\PermissionService;
|
|||||||
final class AuditAuthorizationPolicy implements AuthorizationPolicyInterface
|
final class AuditAuthorizationPolicy implements AuthorizationPolicyInterface
|
||||||
{
|
{
|
||||||
public const ABILITY_API_AUDIT_VIEW = 'audit.api.view';
|
public const ABILITY_API_AUDIT_VIEW = 'audit.api.view';
|
||||||
|
public const ABILITY_API_AUDIT_PURGE = 'audit.api.purge';
|
||||||
public const ABILITY_SYSTEM_AUDIT_VIEW = 'audit.system.view';
|
public const ABILITY_SYSTEM_AUDIT_VIEW = 'audit.system.view';
|
||||||
public const ABILITY_SYSTEM_AUDIT_PURGE = 'audit.system.purge';
|
public const ABILITY_SYSTEM_AUDIT_PURGE = 'audit.system.purge';
|
||||||
public const ABILITY_IMPORTS_AUDIT_VIEW = 'audit.imports.view';
|
public const ABILITY_IMPORTS_AUDIT_VIEW = 'audit.imports.view';
|
||||||
|
public const ABILITY_IMPORTS_AUDIT_PURGE = 'audit.imports.purge';
|
||||||
public const ABILITY_USER_LIFECYCLE_VIEW = 'audit.user_lifecycle.view';
|
public const ABILITY_USER_LIFECYCLE_VIEW = 'audit.user_lifecycle.view';
|
||||||
public const ABILITY_USER_LIFECYCLE_RESTORE = 'audit.user_lifecycle.restore';
|
public const ABILITY_USER_LIFECYCLE_RESTORE = 'audit.user_lifecycle.restore';
|
||||||
|
|
||||||
private const ABILITY_PERMISSION_MAP = [
|
private const ABILITY_PERMISSION_MAP = [
|
||||||
self::ABILITY_API_AUDIT_VIEW => 'api_audit.view',
|
self::ABILITY_API_AUDIT_VIEW => 'api_audit.view',
|
||||||
|
self::ABILITY_API_AUDIT_PURGE => 'audit.api.purge',
|
||||||
self::ABILITY_SYSTEM_AUDIT_VIEW => 'system_audit.view',
|
self::ABILITY_SYSTEM_AUDIT_VIEW => 'system_audit.view',
|
||||||
self::ABILITY_SYSTEM_AUDIT_PURGE => 'system_audit.purge',
|
self::ABILITY_SYSTEM_AUDIT_PURGE => 'system_audit.purge',
|
||||||
self::ABILITY_IMPORTS_AUDIT_VIEW => 'imports.audit.view',
|
self::ABILITY_IMPORTS_AUDIT_VIEW => 'imports.audit.view',
|
||||||
|
self::ABILITY_IMPORTS_AUDIT_PURGE => 'audit.imports.purge',
|
||||||
self::ABILITY_USER_LIFECYCLE_VIEW => 'user_lifecycle_audit.view',
|
self::ABILITY_USER_LIFECYCLE_VIEW => 'user_lifecycle_audit.view',
|
||||||
self::ABILITY_USER_LIFECYCLE_RESTORE => 'users.lifecycle_restore',
|
self::ABILITY_USER_LIFECYCLE_RESTORE => 'users.lifecycle_restore',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -12,16 +12,17 @@ use MintyPHP\Module\Audit\Handler\SystemAuditPurgeJobHandler;
|
|||||||
use MintyPHP\Module\Audit\Handler\UserLifecycleAuditPurgeJobHandler;
|
use MintyPHP\Module\Audit\Handler\UserLifecycleAuditPurgeJobHandler;
|
||||||
use MintyPHP\Module\Audit\Http\ApiSystemAuditReporter;
|
use MintyPHP\Module\Audit\Http\ApiSystemAuditReporter;
|
||||||
use MintyPHP\Module\Audit\Providers\AuditLayoutProvider;
|
use MintyPHP\Module\Audit\Providers\AuditLayoutProvider;
|
||||||
use MintyPHP\Module\Audit\Service\AuditRepositoryFactory;
|
|
||||||
use MintyPHP\Module\Audit\Service\AuditServicesFactory;
|
|
||||||
use MintyPHP\Module\Audit\Service\ApiAuditService;
|
use MintyPHP\Module\Audit\Service\ApiAuditService;
|
||||||
use MintyPHP\Module\Audit\Service\AuditMetadataEnricher;
|
use MintyPHP\Module\Audit\Service\AuditMetadataEnricher;
|
||||||
|
use MintyPHP\Module\Audit\Service\AuditRepositoryFactory;
|
||||||
|
use MintyPHP\Module\Audit\Service\AuditServicesFactory;
|
||||||
use MintyPHP\Module\Audit\Service\FrontendTelemetryIngestService;
|
use MintyPHP\Module\Audit\Service\FrontendTelemetryIngestService;
|
||||||
use MintyPHP\Module\Audit\Service\ImportAuditService;
|
use MintyPHP\Module\Audit\Service\ImportAuditService;
|
||||||
use MintyPHP\Module\Audit\Service\SystemAuditService;
|
use MintyPHP\Module\Audit\Service\SystemAuditService;
|
||||||
use MintyPHP\Module\Audit\Service\UserLifecycleAuditService;
|
use MintyPHP\Module\Audit\Service\UserLifecycleAuditService;
|
||||||
use MintyPHP\Repository\User\UserReadRepository;
|
use MintyPHP\Repository\User\UserReadRepository;
|
||||||
use MintyPHP\Service\Access\PermissionService;
|
use MintyPHP\Service\Access\PermissionService;
|
||||||
|
use MintyPHP\Service\Audit\AuditMetadataEnricherInterface;
|
||||||
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||||
use MintyPHP\Service\Audit\ImportAuditInterface;
|
use MintyPHP\Service\Audit\ImportAuditInterface;
|
||||||
use MintyPHP\Service\Audit\UserLifecycleAuditInterface;
|
use MintyPHP\Service\Audit\UserLifecycleAuditInterface;
|
||||||
@@ -74,6 +75,7 @@ final class AuditContainerRegistrar implements ContainerRegistrar
|
|||||||
$container->set(AuditMetadataEnricher::class, static fn (AppContainer $c): AuditMetadataEnricher => new AuditMetadataEnricher(
|
$container->set(AuditMetadataEnricher::class, static fn (AppContainer $c): AuditMetadataEnricher => new AuditMetadataEnricher(
|
||||||
$c->get(UserReadRepository::class)
|
$c->get(UserReadRepository::class)
|
||||||
));
|
));
|
||||||
|
$container->set(AuditMetadataEnricherInterface::class, static fn (AppContainer $c): AuditMetadataEnricherInterface => $c->get(AuditMetadataEnricher::class));
|
||||||
|
|
||||||
// Authorization policy
|
// Authorization policy
|
||||||
$container->set(AuditAuthorizationPolicy::class, static fn (AppContainer $c): AuditAuthorizationPolicy => new AuditAuthorizationPolicy(
|
$container->set(AuditAuthorizationPolicy::class, static fn (AppContainer $c): AuditAuthorizationPolicy => new AuditAuthorizationPolicy(
|
||||||
|
|||||||
@@ -32,16 +32,12 @@ class ApiSystemAuditReporter
|
|||||||
RequestContext::ensureStarted();
|
RequestContext::ensureStarted();
|
||||||
$requestContext = RequestContext::context();
|
$requestContext = RequestContext::context();
|
||||||
|
|
||||||
$method = strtoupper(trim((string) ($requestContext['method'] ?? ($_SERVER['REQUEST_METHOD'] ?? 'GET'))));
|
$method = strtoupper(trim((string) ($requestContext['method'] ?? 'GET')));
|
||||||
if ($method === '') {
|
if ($method === '') {
|
||||||
$method = 'GET';
|
$method = 'GET';
|
||||||
}
|
}
|
||||||
|
|
||||||
$path = trim((string) ($requestContext['path'] ?? ''));
|
$path = trim((string) ($requestContext['path'] ?? ''));
|
||||||
if ($path === '') {
|
|
||||||
$fallbackPath = parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH);
|
|
||||||
$path = is_string($fallbackPath) ? trim($fallbackPath) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->context = [
|
$this->context = [
|
||||||
'started_at' => microtime(true),
|
'started_at' => microtime(true),
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Module\Audit\Providers;
|
||||||
|
|
||||||
|
use MintyPHP\App\Module\Contracts\SearchResourceProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides audit search resources (API audit, system audit) for the global search.
|
||||||
|
*/
|
||||||
|
final class AuditSearchResourceProvider implements SearchResourceProvider
|
||||||
|
{
|
||||||
|
public function resources(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'api-audit' => [
|
||||||
|
'label' => t('API audit'),
|
||||||
|
'permission' => 'api_audit.view',
|
||||||
|
'count_sql' => "select count(*) from api_audit_log where (request_id like ? escape '\\\\' or path like ? escape '\\\\' or error_code like ? escape '\\\\' or ip like ? escape '\\\\')",
|
||||||
|
'preview_sql' => "select id, request_id, method, path, status_code, created_at from api_audit_log where (request_id like ? escape '\\\\' or path like ? escape '\\\\' or error_code like ? escape '\\\\' or ip like ? escape '\\\\') order by created_at desc limit ?",
|
||||||
|
'result_sql' => "select id, request_id, method, path, status_code, created_at from api_audit_log where (request_id like ? escape '\\\\' or path like ? escape '\\\\' or error_code like ? escape '\\\\' or ip like ? escape '\\\\') order by created_at desc",
|
||||||
|
],
|
||||||
|
'system-audit' => [
|
||||||
|
'label' => t('System audit'),
|
||||||
|
'permission' => 'system_audit.view',
|
||||||
|
'count_sql' => "select count(*) from system_audit_log where (request_id like ? escape '\\\\' or event_type like ? escape '\\\\' or error_code like ? escape '\\\\' or path like ? escape '\\\\')",
|
||||||
|
'preview_sql' => "select id, request_id, event_type, outcome, created_at from system_audit_log where (request_id like ? escape '\\\\' or event_type like ? escape '\\\\' or error_code like ? escape '\\\\' or path like ? escape '\\\\') order by created_at desc limit ?",
|
||||||
|
'result_sql' => "select id, request_id, event_type, outcome, created_at from system_audit_log where (request_id like ? escape '\\\\' or event_type like ? escape '\\\\' or error_code like ? escape '\\\\' or path like ? escape '\\\\') order by created_at desc",
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mapResultItem(string $resourceKey, array $row): ?array
|
||||||
|
{
|
||||||
|
if ($resourceKey === 'api-audit') {
|
||||||
|
return $this->mapApiAuditItem($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($resourceKey === 'system-audit') {
|
||||||
|
return $this->mapSystemAuditItem($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listUrl(string $resourceKey, string $encodedSearch): string
|
||||||
|
{
|
||||||
|
return match ($resourceKey) {
|
||||||
|
'api-audit' => lurl('admin/api-audit') . '?search=' . $encodedSearch,
|
||||||
|
'system-audit' => lurl('admin/system-audit') . '?search=' . $encodedSearch,
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $row
|
||||||
|
* @return array{title: string, subtitle: string, url: string, icon: string}|null
|
||||||
|
*/
|
||||||
|
private function mapApiAuditItem(array $row): ?array
|
||||||
|
{
|
||||||
|
$method = strtoupper(trim((string) ($row['method'] ?? '')));
|
||||||
|
$path = trim((string) ($row['path'] ?? ''));
|
||||||
|
$statusCode = (int) ($row['status_code'] ?? 0);
|
||||||
|
$requestId = trim((string) ($row['request_id'] ?? ''));
|
||||||
|
|
||||||
|
$title = trim($method . ' ' . $path);
|
||||||
|
if ($title === '') {
|
||||||
|
$title = $requestId;
|
||||||
|
}
|
||||||
|
if ($title === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subtitle = trim(($statusCode > 0 ? (string) $statusCode : '') . ($requestId !== '' ? ' — ' . $requestId : ''));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $title,
|
||||||
|
'subtitle' => $subtitle,
|
||||||
|
'url' => lurl('admin/api-audit/view/' . ($row['id'] ?? '')),
|
||||||
|
'icon' => 'bi-shield-check',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $row
|
||||||
|
* @return array{title: string, subtitle: string, url: string, icon: string}|null
|
||||||
|
*/
|
||||||
|
private function mapSystemAuditItem(array $row): ?array
|
||||||
|
{
|
||||||
|
$eventType = trim((string) ($row['event_type'] ?? ''));
|
||||||
|
$outcome = strtolower(trim((string) ($row['outcome'] ?? '')));
|
||||||
|
$requestId = trim((string) ($row['request_id'] ?? ''));
|
||||||
|
|
||||||
|
$title = $eventType !== '' ? $eventType : $requestId;
|
||||||
|
if ($title === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subtitle = trim(($outcome !== '' ? strtoupper($outcome) : '') . ($requestId !== '' ? ' — ' . $requestId : ''));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $title,
|
||||||
|
'subtitle' => $subtitle,
|
||||||
|
'url' => lurl('admin/system-audit/view/' . ($row['id'] ?? '')),
|
||||||
|
'icon' => 'bi-journal-lock',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
namespace MintyPHP\Module\Audit\Service;
|
namespace MintyPHP\Module\Audit\Service;
|
||||||
|
|
||||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||||
|
use MintyPHP\Service\Audit\AuditMetadataEnricherInterface;
|
||||||
|
|
||||||
class AuditMetadataEnricher
|
class AuditMetadataEnricher implements AuditMetadataEnricherInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly UserReadRepositoryInterface $userReadRepository
|
private readonly UserReadRepositoryInterface $userReadRepository
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use MintyPHP\Service\Settings\SettingsFrontendTelemetryGateway;
|
|||||||
class FrontendTelemetryIngestService
|
class FrontendTelemetryIngestService
|
||||||
{
|
{
|
||||||
private const INGEST_RATE_SCOPE = 'frontend.telemetry.ingest';
|
private const INGEST_RATE_SCOPE = 'frontend.telemetry.ingest';
|
||||||
private const SESSION_STATE_KEY = 'frontend_telemetry_state';
|
private const SESSION_STATE_KEY = 'module.audit.frontend_telemetry_state';
|
||||||
|
|
||||||
private const INGEST_MAX_HITS = 80;
|
private const INGEST_MAX_HITS = 80;
|
||||||
private const INGEST_WINDOW_SECONDS = 300;
|
private const INGEST_WINDOW_SECONDS = 300;
|
||||||
|
|||||||
@@ -99,10 +99,18 @@ return [
|
|||||||
'key' => 'audit.system.purge',
|
'key' => 'audit.system.purge',
|
||||||
'description' => 'Purge system audit logs',
|
'description' => 'Purge system audit logs',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => 'audit.api.purge',
|
||||||
|
'description' => 'Purge API audit logs',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'key' => 'audit.imports.view',
|
'key' => 'audit.imports.view',
|
||||||
'description' => 'View import audit logs',
|
'description' => 'View import audit logs',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => 'audit.imports.purge',
|
||||||
|
'description' => 'Purge import audit logs',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'key' => 'audit.user_lifecycle.view',
|
'key' => 'audit.user_lifecycle.view',
|
||||||
'description' => 'View user lifecycle audit logs',
|
'description' => 'View user lifecycle audit logs',
|
||||||
@@ -178,7 +186,9 @@ return [
|
|||||||
'session_providers' => [],
|
'session_providers' => [],
|
||||||
|
|
||||||
'event_listeners' => [],
|
'event_listeners' => [],
|
||||||
'search_resources' => [],
|
'search_resources' => [
|
||||||
|
\MintyPHP\Module\Audit\Providers\AuditSearchResourceProvider::class,
|
||||||
|
],
|
||||||
'asset_groups' => [],
|
'asset_groups' => [],
|
||||||
'migrations_path' => 'db/updates',
|
'migrations_path' => 'db/updates',
|
||||||
'i18n_path' => 'i18n',
|
'i18n_path' => 'i18n',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use MintyPHP\Support\Flash;
|
|||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
Guard::requireLogin();
|
Guard::requireLogin();
|
||||||
Guard::requireAbility(\MintyPHP\Service\Access\SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE);
|
Guard::requireAbility(\MintyPHP\Module\Audit\AuditAuthorizationPolicy::ABILITY_API_AUDIT_PURGE);
|
||||||
|
|
||||||
if (strtoupper((string) (requestInput()->method())) !== 'POST') {
|
if (strtoupper((string) (requestInput()->method())) !== 'POST') {
|
||||||
Router::redirect('audit/api-audit');
|
Router::redirect('audit/api-audit');
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Guard::requireAbility(\MintyPHP\Module\Audit\AuditAuthorizationPolicy::ABILITY_A
|
|||||||
$auditId = (int) ($id ?? 0);
|
$auditId = (int) ($id ?? 0);
|
||||||
$auditLog = $auditId > 0 ? app(\MintyPHP\Module\Audit\Service\ApiAuditService::class)->find($auditId) : null;
|
$auditLog = $auditId > 0 ? app(\MintyPHP\Module\Audit\Service\ApiAuditService::class)->find($auditId) : null;
|
||||||
if (!$auditLog) {
|
if (!$auditLog) {
|
||||||
Flash::error('API audit entry not found', 'audit/api-audit', 'api_audit_not_found');
|
Flash::error(t('API audit entry not found'), 'audit/api-audit', 'api_audit_not_found');
|
||||||
Router::redirect('audit/api-audit');
|
Router::redirect('audit/api-audit');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use MintyPHP\Support\Flash;
|
|||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
Guard::requireLogin();
|
Guard::requireLogin();
|
||||||
Guard::requireAbility(\MintyPHP\Service\Access\SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE);
|
Guard::requireAbility(\MintyPHP\Module\Audit\AuditAuthorizationPolicy::ABILITY_IMPORTS_AUDIT_PURGE);
|
||||||
|
|
||||||
if (strtoupper((string) (requestInput()->method())) !== 'POST') {
|
if (strtoupper((string) (requestInput()->method())) !== 'POST') {
|
||||||
Router::redirect('audit/import-audit');
|
Router::redirect('audit/import-audit');
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Tests\Module\Audit\Providers;
|
||||||
|
|
||||||
|
use MintyPHP\Module\Audit\Providers\AuditSearchResourceProvider;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class AuditSearchResourceProviderTest extends TestCase
|
||||||
|
{
|
||||||
|
private AuditSearchResourceProvider $provider;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->provider = new AuditSearchResourceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResourcesReturnsBothAuditKeys(): void
|
||||||
|
{
|
||||||
|
$resources = $this->provider->resources();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('api-audit', $resources);
|
||||||
|
$this->assertArrayHasKey('system-audit', $resources);
|
||||||
|
$this->assertCount(2, $resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResourcesContainRequiredSqlKeys(): void
|
||||||
|
{
|
||||||
|
foreach ($this->provider->resources() as $key => $resource) {
|
||||||
|
$this->assertArrayHasKey('label', $resource, "Resource '{$key}' missing label");
|
||||||
|
$this->assertArrayHasKey('permission', $resource, "Resource '{$key}' missing permission");
|
||||||
|
$this->assertArrayHasKey('count_sql', $resource, "Resource '{$key}' missing count_sql");
|
||||||
|
$this->assertArrayHasKey('preview_sql', $resource, "Resource '{$key}' missing preview_sql");
|
||||||
|
$this->assertArrayHasKey('result_sql', $resource, "Resource '{$key}' missing result_sql");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResourcePermissionKeys(): void
|
||||||
|
{
|
||||||
|
$resources = $this->provider->resources();
|
||||||
|
|
||||||
|
$this->assertSame('api_audit.view', $resources['api-audit']['permission']);
|
||||||
|
$this->assertSame('system_audit.view', $resources['system-audit']['permission']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapApiAuditItem(): void
|
||||||
|
{
|
||||||
|
$row = [
|
||||||
|
'id' => 42,
|
||||||
|
'method' => 'POST',
|
||||||
|
'path' => '/api/v1/users',
|
||||||
|
'status_code' => 201,
|
||||||
|
'request_id' => 'req-abc',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->provider->mapResultItem('api-audit', $row);
|
||||||
|
|
||||||
|
$this->assertNotNull($result);
|
||||||
|
$this->assertSame('POST /api/v1/users', $result['title']);
|
||||||
|
$this->assertStringContains('201', $result['subtitle']);
|
||||||
|
$this->assertStringContains('req-abc', $result['subtitle']);
|
||||||
|
$this->assertSame('bi-shield-check', $result['icon']);
|
||||||
|
$this->assertStringContainsString('admin/api-audit/view/42', $result['url']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapApiAuditItemFallsBackToRequestId(): void
|
||||||
|
{
|
||||||
|
$row = ['id' => 1, 'method' => '', 'path' => '', 'status_code' => 0, 'request_id' => 'req-fallback'];
|
||||||
|
|
||||||
|
$result = $this->provider->mapResultItem('api-audit', $row);
|
||||||
|
|
||||||
|
$this->assertNotNull($result);
|
||||||
|
$this->assertSame('req-fallback', $result['title']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapApiAuditItemReturnsNullWhenEmpty(): void
|
||||||
|
{
|
||||||
|
$row = ['id' => 1, 'method' => '', 'path' => '', 'status_code' => 0, 'request_id' => ''];
|
||||||
|
|
||||||
|
$result = $this->provider->mapResultItem('api-audit', $row);
|
||||||
|
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapSystemAuditItem(): void
|
||||||
|
{
|
||||||
|
$row = [
|
||||||
|
'id' => 99,
|
||||||
|
'event_type' => 'admin.settings.update',
|
||||||
|
'outcome' => 'success',
|
||||||
|
'request_id' => 'req-xyz',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->provider->mapResultItem('system-audit', $row);
|
||||||
|
|
||||||
|
$this->assertNotNull($result);
|
||||||
|
$this->assertSame('admin.settings.update', $result['title']);
|
||||||
|
$this->assertStringContains('SUCCESS', $result['subtitle']);
|
||||||
|
$this->assertSame('bi-journal-lock', $result['icon']);
|
||||||
|
$this->assertStringContainsString('admin/system-audit/view/99', $result['url']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapSystemAuditItemFallsBackToRequestId(): void
|
||||||
|
{
|
||||||
|
$row = ['id' => 1, 'event_type' => '', 'outcome' => '', 'request_id' => 'req-fallback'];
|
||||||
|
|
||||||
|
$result = $this->provider->mapResultItem('system-audit', $row);
|
||||||
|
|
||||||
|
$this->assertNotNull($result);
|
||||||
|
$this->assertSame('req-fallback', $result['title']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapSystemAuditItemReturnsNullWhenEmpty(): void
|
||||||
|
{
|
||||||
|
$row = ['id' => 1, 'event_type' => '', 'outcome' => '', 'request_id' => ''];
|
||||||
|
|
||||||
|
$result = $this->provider->mapResultItem('system-audit', $row);
|
||||||
|
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapResultItemReturnsNullForUnknownKey(): void
|
||||||
|
{
|
||||||
|
$result = $this->provider->mapResultItem('unknown', ['id' => 1]);
|
||||||
|
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListUrlForApiAudit(): void
|
||||||
|
{
|
||||||
|
$url = $this->provider->listUrl('api-audit', 'test%20query');
|
||||||
|
|
||||||
|
$this->assertStringContainsString('admin/api-audit', $url);
|
||||||
|
$this->assertStringContainsString('search=test%20query', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListUrlForSystemAudit(): void
|
||||||
|
{
|
||||||
|
$url = $this->provider->listUrl('system-audit', 'test');
|
||||||
|
|
||||||
|
$this->assertStringContainsString('admin/system-audit', $url);
|
||||||
|
$this->assertStringContainsString('search=test', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListUrlReturnsEmptyForUnknownKey(): void
|
||||||
|
{
|
||||||
|
$url = $this->provider->listUrl('unknown', 'test');
|
||||||
|
|
||||||
|
$this->assertSame('', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to check string containment (mirrors assertStringContainsString for readability).
|
||||||
|
*/
|
||||||
|
private static function assertStringContains(string $needle, string $haystack): void
|
||||||
|
{
|
||||||
|
self::assertStringContainsString($needle, $haystack);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -210,12 +210,12 @@ class FrontendTelemetryIngestServiceTest extends TestCase
|
|||||||
$sessionStore->expects($this->once())->method('all')->willReturn(['user' => ['id' => 7]]);
|
$sessionStore->expects($this->once())->method('all')->willReturn(['user' => ['id' => 7]]);
|
||||||
$sessionStore->expects($this->once())
|
$sessionStore->expects($this->once())
|
||||||
->method('get')
|
->method('get')
|
||||||
->with('frontend_telemetry_state', [])
|
->with('module.audit.frontend_telemetry_state', [])
|
||||||
->willReturn([]);
|
->willReturn([]);
|
||||||
$sessionStore->expects($this->once())
|
$sessionStore->expects($this->once())
|
||||||
->method('set')
|
->method('set')
|
||||||
->with(
|
->with(
|
||||||
'frontend_telemetry_state',
|
'module.audit.frontend_telemetry_state',
|
||||||
$this->callback(static function (mixed $value): bool {
|
$this->callback(static function (mixed $value): bool {
|
||||||
if (!is_array($value)) {
|
if (!is_array($value)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ if (!$canViewPage) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
app(\MintyPHP\Service\Audit\AuditMetadataEnricher::class)->enrich($department);
|
app(\MintyPHP\Service\Audit\AuditMetadataEnricherInterface::class)->enrich($department);
|
||||||
|
|
||||||
$errorBag = formErrors();
|
$errorBag = formErrors();
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ $capabilities = $contextDecision->attribute('capabilities', []);
|
|||||||
$canUpdateRole = is_array($capabilities) ? (bool) ($capabilities['can_edit_role'] ?? false) : false;
|
$canUpdateRole = is_array($capabilities) ? (bool) ($capabilities['can_edit_role'] ?? false) : false;
|
||||||
$canDeleteRole = is_array($capabilities) ? (bool) ($capabilities['can_delete_role'] ?? false) : false;
|
$canDeleteRole = is_array($capabilities) ? (bool) ($capabilities['can_delete_role'] ?? false) : false;
|
||||||
|
|
||||||
app(\MintyPHP\Service\Audit\AuditMetadataEnricher::class)->enrich($role);
|
app(\MintyPHP\Service\Audit\AuditMetadataEnricherInterface::class)->enrich($role);
|
||||||
|
|
||||||
$errorBag = formErrors();
|
$errorBag = formErrors();
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ $ssoConfig = $canManageSso ? $tenantSsoService->getTenantMicrosoftAuth($tenantId
|
|||||||
$ssoUiState = $canManageSso ? $tenantSsoService->buildMicrosoftUiState($tenantId, $ssoConfig) : [];
|
$ssoUiState = $canManageSso ? $tenantSsoService->buildMicrosoftUiState($tenantId, $ssoConfig) : [];
|
||||||
$ldapConfig = $canManageSso ? $tenantSsoService->getTenantLdapAuth($tenantId) : [];
|
$ldapConfig = $canManageSso ? $tenantSsoService->getTenantLdapAuth($tenantId) : [];
|
||||||
$ldapUiState = $canManageSso ? $tenantSsoService->buildLdapUiState($tenantId) : [];
|
$ldapUiState = $canManageSso ? $tenantSsoService->buildLdapUiState($tenantId) : [];
|
||||||
app(\MintyPHP\Service\Audit\AuditMetadataEnricher::class)->enrich($tenant, ['created_by', 'modified_by', 'status_changed_by']);
|
app(\MintyPHP\Service\Audit\AuditMetadataEnricherInterface::class)->enrich($tenant, ['created_by', 'modified_by', 'status_changed_by']);
|
||||||
|
|
||||||
$errorBag = formErrors();
|
$errorBag = formErrors();
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ if (!$canViewPage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($canViewUserMeta || $canViewUserAudit) {
|
if ($canViewUserMeta || $canViewUserAudit) {
|
||||||
app(\MintyPHP\Service\Audit\AuditMetadataEnricher::class)->enrich($user, ['created_by', 'modified_by', 'active_changed_by']);
|
app(\MintyPHP\Service\Audit\AuditMetadataEnricherInterface::class)->enrich($user, ['created_by', 'modified_by', 'active_changed_by']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$errorBag = formErrors();
|
$errorBag = formErrors();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use MintyPHP\Http\ApiAuth;
|
use MintyPHP\Http\ApiAuth;
|
||||||
use MintyPHP\Http\ApiBootstrap;
|
use MintyPHP\Http\ApiBootstrap;
|
||||||
use MintyPHP\Http\ApiResponse;
|
use MintyPHP\Http\ApiResponse;
|
||||||
use MintyPHP\Service\Audit\SystemAuditService;
|
use MintyPHP\Service\Audit\AuditRecorderInterface;
|
||||||
use MintyPHP\Service\Settings\SettingsApiPolicyGateway;
|
use MintyPHP\Service\Settings\SettingsApiPolicyGateway;
|
||||||
use MintyPHP\Service\Settings\SettingsAppGateway;
|
use MintyPHP\Service\Settings\SettingsAppGateway;
|
||||||
use MintyPHP\Service\Settings\SettingsDefaultsGateway;
|
use MintyPHP\Service\Settings\SettingsDefaultsGateway;
|
||||||
@@ -103,7 +103,7 @@ if ($method === 'PUT' || $method === 'PATCH') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($errors->hasAny()) {
|
if ($errors->hasAny()) {
|
||||||
app(SystemAuditService::class)->record('admin.settings.update', 'failed', [
|
app(AuditRecorderInterface::class)->record('admin.settings.update', 'failed', [
|
||||||
'actor_user_id' => ApiAuth::userId(),
|
'actor_user_id' => ApiAuth::userId(),
|
||||||
'error_code' => 'validation_error',
|
'error_code' => 'validation_error',
|
||||||
'metadata' => ['errors' => array_keys($errors->toArray())],
|
'metadata' => ['errors' => array_keys($errors->toArray())],
|
||||||
@@ -121,7 +121,7 @@ if ($method === 'PUT' || $method === 'PATCH') {
|
|||||||
$settingsMicrosoftGateway,
|
$settingsMicrosoftGateway,
|
||||||
$settingsSmtpGateway,
|
$settingsSmtpGateway,
|
||||||
);
|
);
|
||||||
app(SystemAuditService::class)->record('admin.settings.update', 'success', [
|
app(AuditRecorderInterface::class)->record('admin.settings.update', 'success', [
|
||||||
'actor_user_id' => ApiAuth::userId(),
|
'actor_user_id' => ApiAuth::userId(),
|
||||||
'before' => $beforeSettings,
|
'before' => $beforeSettings,
|
||||||
'after' => $afterSettings,
|
'after' => $afterSettings,
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ final class AuditModuleIsolationContractTest extends TestCase
|
|||||||
));
|
));
|
||||||
|
|
||||||
$allowed = [
|
$allowed = [
|
||||||
|
'AuditMetadataEnricherInterface.php',
|
||||||
'AuditRecorderInterface.php',
|
'AuditRecorderInterface.php',
|
||||||
'ImportAuditInterface.php',
|
'ImportAuditInterface.php',
|
||||||
|
'NullAuditMetadataEnricher.php',
|
||||||
'NullAuditRecorder.php',
|
'NullAuditRecorder.php',
|
||||||
'NullImportAudit.php',
|
'NullImportAudit.php',
|
||||||
'NullUserLifecycleAudit.php',
|
'NullUserLifecycleAudit.php',
|
||||||
|
|||||||
32
tests/Service/Audit/NullAuditMetadataEnricherTest.php
Normal file
32
tests/Service/Audit/NullAuditMetadataEnricherTest.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Tests\Service\Audit;
|
||||||
|
|
||||||
|
use MintyPHP\Service\Audit\NullAuditMetadataEnricher;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class NullAuditMetadataEnricherTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testEnrichDoesNotModifyEntity(): void
|
||||||
|
{
|
||||||
|
$enricher = new NullAuditMetadataEnricher();
|
||||||
|
$entity = ['created_by' => 10, 'modified_by' => 20, 'name' => 'Test'];
|
||||||
|
|
||||||
|
$enricher->enrich($entity);
|
||||||
|
|
||||||
|
$this->assertSame(['created_by' => 10, 'modified_by' => 20, 'name' => 'Test'], $entity);
|
||||||
|
$this->assertArrayNotHasKey('created_by_label', $entity);
|
||||||
|
$this->assertArrayNotHasKey('modified_by_label', $entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnrichWithCustomFieldsDoesNotModifyEntity(): void
|
||||||
|
{
|
||||||
|
$enricher = new NullAuditMetadataEnricher();
|
||||||
|
$entity = ['status_changed_by' => 5];
|
||||||
|
|
||||||
|
$enricher->enrich($entity, ['status_changed_by']);
|
||||||
|
|
||||||
|
$this->assertSame(['status_changed_by' => 5], $entity);
|
||||||
|
$this->assertArrayNotHasKey('status_changed_by_label', $entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user