feat(audit): ImpersonationAuditService (saas_admin_audit_log + pd on verify)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Pd;
|
||||
|
||||
use App\Models\ImpersonationToken;
|
||||
use App\Models\SaasAdminAuditLog;
|
||||
|
||||
/**
|
||||
* Оркестратор аудита impersonation: пишет защищённый saas_admin_audit_log
|
||||
* на init/verify/end и ПДн-след (pd_processing_log) на verify — вход админа
|
||||
* в кабинет тенанта = массовый доступ к ПДн (152-ФЗ).
|
||||
*/
|
||||
final class ImpersonationAuditService
|
||||
{
|
||||
public function __construct(private readonly PdAuditLogger $pd) {}
|
||||
|
||||
public function recordInit(ImpersonationToken $t, int $adminId, ?string $ip): void
|
||||
{
|
||||
SaasAdminAuditLog::create([
|
||||
'admin_user_id' => $adminId,
|
||||
'action' => 'impersonation.init',
|
||||
'target_type' => 'tenant',
|
||||
'target_id' => $t->tenant_id,
|
||||
'target_tenant_id' => $t->tenant_id,
|
||||
'payload_before' => null,
|
||||
'payload_after' => ['token_id' => $t->id, 'expires_at' => $t->expires_at?->toIso8601String()],
|
||||
'reason' => $t->reason,
|
||||
'ip_address' => $ip ?? '127.0.0.1',
|
||||
'user_agent' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function recordVerify(ImpersonationToken $t, int $adminId, ?string $ip): void
|
||||
{
|
||||
SaasAdminAuditLog::create([
|
||||
'admin_user_id' => $adminId,
|
||||
'action' => 'impersonation.verify',
|
||||
'target_type' => 'tenant',
|
||||
'target_id' => $t->tenant_id,
|
||||
'target_tenant_id' => $t->tenant_id,
|
||||
'payload_before' => ['used_at' => null],
|
||||
'payload_after' => ['used_at' => now()->toIso8601String()],
|
||||
'reason' => $t->reason,
|
||||
'ip_address' => $ip ?? '127.0.0.1',
|
||||
'user_agent' => null,
|
||||
]);
|
||||
// ПДн-след: вход админа в кабинет = массовый доступ к ПДн тенанта.
|
||||
$this->pd->record(
|
||||
action: 'viewed', subjectType: 'tenant', subjectId: $t->tenant_id,
|
||||
purpose: 'impersonation_session_'.$t->id,
|
||||
tenantId: $t->tenant_id,
|
||||
actorTenantUserId: null, actorAdminUserId: $adminId, ip: $ip,
|
||||
);
|
||||
}
|
||||
|
||||
public function recordEnd(ImpersonationToken $t, int $adminId, ?string $ip): void
|
||||
{
|
||||
SaasAdminAuditLog::create([
|
||||
'admin_user_id' => $adminId,
|
||||
'action' => 'impersonation.end',
|
||||
'target_type' => 'tenant',
|
||||
'target_id' => $t->tenant_id,
|
||||
'target_tenant_id' => $t->tenant_id,
|
||||
'payload_before' => ['session_ended_at' => null],
|
||||
'payload_after' => ['session_ended_at' => now()->toIso8601String()],
|
||||
'reason' => $t->reason,
|
||||
'ip_address' => $ip ?? '127.0.0.1',
|
||||
'user_agent' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use App\Models\ImpersonationToken;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Pd\ImpersonationAuditService;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, DatabaseTransactions::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->tenant = Tenant::factory()->create();
|
||||
$this->adminId = DB::table('saas_admin_users')->insertGetId([
|
||||
'email' => 'admin-imp-'.uniqid().'@liderra.ru',
|
||||
'full_name' => 'SaaS Admin',
|
||||
'password_hash' => '$2y$04$dummy-hash-for-test',
|
||||
'role' => 'support',
|
||||
'is_active' => true,
|
||||
'sso_provider' => 'local',
|
||||
'is_break_glass' => false,
|
||||
]);
|
||||
$this->token = ImpersonationToken::create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'requested_by' => $this->adminId,
|
||||
'code_hash' => 'h',
|
||||
'reason' => 'support case '.str_repeat('x', 30),
|
||||
'sent_to_email' => 'a@b.ru',
|
||||
'expires_at' => now()->addMinutes(15),
|
||||
]);
|
||||
});
|
||||
|
||||
it('recordInit writes saas_admin_audit_log action=impersonation.init', function () {
|
||||
app(ImpersonationAuditService::class)->recordInit($this->token, adminId: $this->adminId, ip: '1.2.3.4');
|
||||
$row = DB::table('saas_admin_audit_log')->where('action', 'impersonation.init')->latest('id')->first();
|
||||
expect($row)->not->toBeNull()
|
||||
->and((int) $row->target_id)->toBe($this->tenant->id)
|
||||
->and($row->reason)->toBe($this->token->reason);
|
||||
});
|
||||
|
||||
it('recordVerify writes BOTH saas_audit and pd_processing_log', function () {
|
||||
app(ImpersonationAuditService::class)->recordVerify($this->token, adminId: $this->adminId, ip: '1.2.3.4');
|
||||
expect(DB::table('saas_admin_audit_log')->where('action', 'impersonation.verify')->count())->toBe(1)
|
||||
->and(DB::table('pd_processing_log')
|
||||
->where('action', 'viewed')
|
||||
->where('purpose', 'impersonation_session_'.$this->token->id)
|
||||
->where('actor_admin_user_id', $this->adminId)
|
||||
->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('recordEnd writes saas_admin_audit_log action=impersonation.end', function () {
|
||||
app(ImpersonationAuditService::class)->recordEnd($this->token, adminId: $this->adminId, ip: '1.2.3.4');
|
||||
expect(DB::table('saas_admin_audit_log')->where('action', 'impersonation.end')->count())->toBe(1);
|
||||
});
|
||||
Reference in New Issue
Block a user