mapik/audit-log

CakePHP的灵活且稳固的审计日志跟踪插件

安装次数: 4,394

依赖项: 2

建议者: 0

安全: 0

星标: 0

关注者: 0

分支: 37

开放问题: 0

类型:cakephp-plugin

5.0.0 2023-07-01 18:11 UTC

This package is auto-updated.

Last update: 2024-09-22 09:03:13 UTC


README

此插件是从 lorenzo/audit-stash 分支出来的。

尽管上面的插件非常适合存储变更历史,但它存在以下问题或没有我需要的所有功能。

  • 删除事件时没有记录原始数据。

    • 如果你在添加一些数据后添加该插件,这将很有用。
  • 关联表记录没有正确保存(在这种情况下,我考虑了两个具有“hasMany”关系的模型)

    • 目前,在CakePHP(4.x)ORM中,当存在“hasMany”关系(例如,考虑两个数据库表:items和item_attributes)时,EntityTrait::extractOriginal(array $fields)不会返回关联表(item_attributes)的原始数据,而是返回修改后的值。
    • 还有另一个CakePHP(4.x)ORM的bug,它将关联的('hasMany')实体标记为脏的,即使关联表数据没有更改。
    • 无法仅记录关联表的更改数据列
  • 当调用saveMany()保存多个实体时,无法记录审计日志

  • 无法通过app.php设置AuditLog行为和/或Table Persister的一些常用配置

  • 无法记录来自外键的人类友好数据字段

  • 创建事件将相同的数据添加到'original'和'changed'列

  • 'id'(主键)字段添加到'original'和'changed'数据中,除非你在每个模型类中将其列入黑名单。(主键作为单独的字段记录)

  • 无法在数据库中存储用户友好的字段值。这有助于轻松识别记录;特别是当数据库只存储更改数据时。因此,我使用了CakePHP Model::setDisplayField()来检索用户友好的字段值。

因此,我决定从原始项目分支出来,并改进它以支持上述缺失的功能。

安装

您可以使用 composer 将此插件安装到您的CakePHP应用程序中,并在应用程序根目录中执行以下命令。

composer require mapik/audit-log
bin/cake plugin load AuditLog

如果您计划使用ElasticSearch作为存储引擎,请参阅 lorenzo/audit-stash

配置

表 / 常规数据库

如果您想使用常规数据库,即可以通过CakePHP ORM API使用的引擎,那么您可以使用此插件附带的自带表持久化器。

为此,您需要相应地配置AuditLog.persister选项。在您的config/app.php文件中添加以下配置

'AuditLog' => [
    'persister' => 'AuditLog\Persister\TablePersister'
]

然后,默认情况下,插件将尝试将日志存储在名为audit_logs的表中,通过具有别名AuditLogs的表类,您可以在需要时创建/覆盖它。

您可以在本插件的 config/migration 文件夹中找到一个迁移文件,您可以将其应用到您的数据库中,这将添加一个名为 audit_logs 的表,其中包含所有默认列。或者,您也可以自己创建迁移来创建表。之后,您可以迁移相应的表类。

如果您使用的是插件的默认迁移,可以使用以下命令创建表和模型类。

bin/cake migrations migrate -p AuditLog -t 20171018185609
bin/cake bake model AuditLogs

表持久化配置

表持久化支持各种配置选项,请参阅其API文档以获取更多信息。通常,配置可以通过其 config()(或 setConfig())方法应用。

$this->addBehavior('AuditLog.AuditLog');
$this->behaviors()->get('AuditLog')->persister()->config([
    'extractMetaFields' => [
        'user.name' => 'username'
    ]
]);

此外,您还可以通过 app.php 设置一些常用配置。目前,插件支持 'extractMetaFields' 和 'blacklist'。

...
'AuditLog' => [
    'persister' => 'AuditLog\Persister\TablePersister',
    'extractMetaFields' => [
            'user.username' => 'username',
            'user.customer_id' => 'customer_id',
        ],
    'blacklist' => ['customer_id'],
],

使用 AuditLog

在您的任何表类中启用审计日志,只需在 initialize() 函数中添加一个行为即可。

class ArticlesTable extends Table
{
    public function initialize(array $config = [])
    {
        $this->setDisplayField('article_name');
        ...
        $this->addBehavior('AuditLog.AuditLog');
    }
}

配置行为

默认情况下,AuditLog 行为会忽略您表中的某些字段,它默认忽略 idcreatedmodified 字段。

class ArticlesTable extends Table
{
    public function initialize(array $config = [])
    {
        $this->setDisplayField('article_name');
        ...
        $this->addBehavior('AuditLog.AuditLog', [
            'blacklist' => ['created', 'modified', 'another_field_name']
        ]);
    }
}

如果您愿意,可以使用 whitelist 代替。这意味着只有该数组中列出的字段会被行为跟踪。

class ArticlesTable extends Table
{
    public function initialize(array $config = [])
    {
        $this->setDisplayField('article_name');
        ...
        $this->addBehavior('AuditLog.AuditLog', [
            'whitelist' => ['title', 'description', 'author_id']
        ]);
    }
}

如果您需要从相关表(即具有外键的表)中检索人类友好的数据字段,您可以设置 foreignKeys 如下。

public function initialize(array $config = [])
{
    $this->setDisplayField('article_name');
    ...
    $this->addBehavior('AuditLog.AuditLog', [
        'blacklist' => ['customer_id', 'product_id'],
        'foreignKeys' => [
            'Categories' => 'name', // foreign key Model => human-friendly field name
            'ProductStatuses' => 'status',
        ],
        'unsetAssociatedEntityFieldsNotDirtyByFieldName' => [
            'associated_table_name' => 'field_name_in_associated_table'
        ]
    ]);
}

如项目描述中所述,CakePHP (4.x) ORM即使在关联数据没有更改的情况下也会返回所有关联数据。因此,如果您需要从关联实体中删除未更改的数据,您需要设置 unsetAssociatedEntityFieldsNotDirtyByFieldName,如上述示例所示。

存储登录用户

将触发更改的特定表的用户的标识符存储起来通常很有用。为此目的,AuditLog 提供了 RequestMetadata 监听器类,该类可以存储当前URL、IP和登录用户。您需要在应用程序中的 AppController::beforeFilter() 方法中添加此监听器。

use AuditLog\Meta\RequestMetadata;
...

class AppController extends Controller
{
    public function beforeFilter(Event $event)
    {
        ...
        $eventManager = $this->loadModel()->eventManager();
        $identity = $this->request->getAttribute('identity');
        if ($identity != null) {
            $eventManager->on(
                 new RequestMetadata($this->request, [
                    'username' => $identity['username'],
                    'customer_id' => $identity['customer_id'],
                ])
            );
        }
    }
}

上述代码假设您将从控制器触发表操作,使用默认的Table类。如果您计划在同一控制器中为保存或删除使用其他Table类,建议您全局绑定监听器。

use AuditLog\Meta\RequestMetadata;
use Cake\Event\EventManager;
...

class AppController extends Controller
{
    public function beforeFilter(Event $event)
    {
        ...
        $identity = $this->request->getAttribute('identity');
        if ($identity != null) {
            EventManager::instance()->on(
                new RequestMetadata($this->request, [
                    'username' => $identity['username'],
                    'customer_id' => $identity['customer_id'],
                ])
            );
        }
    }
}

在日志中存储额外信息

AuditLog 还能够为每个已记录的事件存储任意数据。您可以使用 ApplicationMetadata 监听器或创建自己的。如果您选择使用 ApplicationMetadata,则您的日志将包含存储的 app_name 键以及您可能提供的任何额外信息。您可以在应用程序中的任何位置配置此监听器,例如 bootstrap.php 文件或直接在您的 AppController 中。

use AuditLog\Meta\ApplicationMetadata;
use Cake\Event\EventManager;

EventManager::instance()->on(new ApplicationMetadata('my_blog_app', [
    'server' => $theServerID,
    'extra' => $somExtraInformation,
    'moon_phase' => $currentMoonPhase
]));

实现自己的元数据监听器非常简单,只需将其绑定到 AuditLog.beforeLog 事件。例如

EventManager::instance()->on('AuditLog.beforeLog', function ($event, array $logs) {
    foreach ($logs as $log) {
        $log->setMetaInfo($log->getMetaInfo() + ['extra' => 'This is extra data to be stored']);
    }
});

实现您自己的持久化策略

使用不同的持久化引擎来存储审计日志有合理的理由。幸运的是,此插件允许您实现自己的存储引擎。这只需要实现 PersisterInterface 接口。

use AuditLog\PersisterInterface;

class MyPersister implements PersisterInterface
{
    public function logEvents(array $auditLogs)
    {
        foreach ($auditLogs as $log) {
            $eventType = $log->getEventType();
            $data = [
                'timestamp' => $log->getTimestamp(),
                'transaction' => $log->getTransactionId(),
                'type' => $log->getEventType(),
                'primary_key' => $log->getId(),
                'display_value' => $event->getDisplayValue(),
                'source' => $log->getSourceName(),
                'parent_source' => $log->getParentSourceName(),
                'original' => json_encode($log->getOriginal()),
                'changed' => $eventType === 'delete' ? null : json_encode($log->getChanged()),
                'meta' => json_encode($log->getMetaInfo())
            ];
            $storage = new MyStorage();
            $storage->save($data);
        }
    }
}

最后,您需要配置 AuditLog 以使用您的新持久化器。在 config/app.php 文件中添加以下行

'AuditLog' => [
    'persister' => 'App\Namespace\For\Your\Persister'
]

或者如果您使用的是独立版本

\Cake\Core\Configure::write('AuditLog.persister', 'App\Namespace\For\Your\DatabasePersister');

配置包含您持久化器的完全命名空间类名。

与事务查询一起工作

有时,您可能需要将多个数据库更改封装在一个事务中,以便在过程的一部分失败时可以回滚。为了在事务期间创建审计日志,需要一些额外的配置。首先创建文件 src/Model/Audit/AuditLog.php,内容如下:

<?php
namespace App\Model\Audit;

use Cake\Utility\Text;
use SplObjectStorage;

class AuditLog
{
    protected $_auditQueue;
    protected $_auditTransaction;

    public function __construct()
    {
        $this->_auditQueue = new SplObjectStorage;
        $this->_auditTransaction = Text::uuid();
    }

    public function toSaveOptions()
    {
        return [
            '_auditQueue' => $this->_auditQueue,
            '_auditTransaction' => $this->_auditTransaction
        ];
    }
}

在您想要使用 Connection::transactional() 的任何地方,您需要首先在文件顶部包含以下内容:

use App\Model\Audit\AuditLog;
use Cake\Event\Event;

然后,您的交易应该类似于以下 BookmarksController 的示例:

$auditLog = new AuditLog();
$success = $this->Bookmarks->connection()->transactional(function () use ($trail) {
    $bookmark = $this->Bookmarks->newEntity();
    $bookmark1->save($data1, $trail->toSaveOptions());
    $bookmark2 = $this->Bookmarks->newEntity();
    $bookmark2->save($data2, $trail->toSaveOptions());
    ...
    $bookmarkN = $this->Bookmarks->newEntity();
    $bookmarkN->save($dataN, $trail->toSaveOptions());

    return true;
});

if ($success) {
    $event = new Event('Model.afterCommit', $this->Bookmarks);
    $table->behaviors()->get('AuditLog')->afterCommit($event, $result, $auditLog->toSaveOptions());
}

这将保存您的对象的全部审计信息,以及任何相关数据的审计。请注意,$result 必须是一个对象的实例。不要更改文本 "Model.afterCommit"。

保存多个实体

按照上述部分创建文件 src/Model/Audit/AuditLog.php

...
$auditLog = new AuditLog();

if ($this->Bookmarks->saveMany($entities, $auditLog->toSaveOptions())) {
    ...                   
}