Files
portal/app/app/Services/Audit/AuditChainConfig.php
T

105 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Audit;
use InvalidArgumentException;
/**
* Shared config hash-chain for 6 audit tables.
*
* Single source of truth for writer (db/schema.sql trigger audit_chain_hash()),
* verify (App\Console\Commands\VerifyAuditChains) and rebuild
* (App\Console\Commands\AuditRebuildChain).
*
* ADR-018: per-tenant via RLS scope for tenant tables,
* global for BYPASSRLS tables.
*
* columns: list in ordinal_position order from db/schema.sql.
* '__log_hash__' -- marker for log_hash position -> NULL::bytea in ROW().
*
* partition: SQL fragment for OVER (PARTITION BY ... ORDER BY id),
* reproducing the RLS-scope of the trigger.
* '' = global chain within partition (for BYPASSRLS tables).
*/
final class AuditChainConfig
{
/**
* @var array<string, array{columns: list<string>, partition: string}>
*/
public const TABLES = [
'auth_log' => [
'columns' => [
'id', 'actor_type', 'tenant_id', 'user_id', 'saas_admin_user_id',
'email', 'event', 'ip_address', 'user_agent', 'failure_reason',
'__log_hash__', 'created_at',
],
'partition' => '',
],
'activity_log' => [
'columns' => [
'id', 'tenant_id', 'user_id', 'deal_id', 'event',
'old_value', 'new_value', 'context', 'ip_address', 'user_agent',
'__log_hash__', 'created_at',
],
'partition' => 'PARTITION BY tenant_id',
],
'tenant_operations_log' => [
'columns' => [
'id', 'tenant_id', 'user_id', 'entity_type', 'entity_id',
'event', 'payload_before', 'payload_after', 'ip_address', 'user_agent',
'__log_hash__', 'created_at',
],
'partition' => 'PARTITION BY tenant_id',
],
'balance_transactions' => [
'columns' => [
'id', 'tenant_id', 'type', 'amount_rub', 'amount_leads',
'balance_rub_after', 'balance_leads_after', 'description',
'related_type', 'related_id', 'user_id', 'admin_user_id',
'__log_hash__', 'created_at',
],
'partition' => 'PARTITION BY tenant_id',
],
'pd_processing_log' => [
'columns' => [
'id', 'tenant_id', 'subject_type', 'subject_id', 'action',
'purpose', 'actor_tenant_user_id', 'actor_admin_user_id', 'ip_address',
'__log_hash__', 'created_at',
],
'partition' => 'PARTITION BY tenant_id',
],
'saas_admin_audit_log' => [
'columns' => [
'id', 'admin_user_id', 'action', 'target_type', 'target_id',
'target_tenant_id', 'payload_before', 'payload_after', 'reason',
'ip_address', 'user_agent', 'requires_approval', 'approved_by', 'approved_at',
'__log_hash__', 'created_at',
],
'partition' => '',
],
];
/**
* Build ROW(col1, col2, ..., NULL::bytea, ..., coln) with NULL::bytea at log_hash position.
*
* @throws InvalidArgumentException if table is not registered in TABLES
*/
public static function rowExpression(string $table): string
{
if (! isset(self::TABLES[$table])) {
throw new InvalidArgumentException(
"Table '{$table}' is not registered in AuditChainConfig::TABLES"
);
}
$parts = [];
foreach (self::TABLES[$table]['columns'] as $col) {
$parts[] = ($col === '__log_hash__') ? 'NULL::bytea' : "t.{$col}";
}
return 'ROW('.implode(', ', $parts).')';
}
}