From c4e6691b281f1d54e4da4f4b645a8fbe2e4aafd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 22 May 2026 16:14:47 +0300 Subject: [PATCH] feat(audit): ImpersonationAuditService (saas_admin_audit_log + pd on verify) --- .../Services/Pd/ImpersonationAuditService.php | 71 +++++++++++++++++++ .../Pd/ImpersonationAuditServiceTest.php | 54 ++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 app/app/Services/Pd/ImpersonationAuditService.php create mode 100644 app/tests/Unit/Services/Pd/ImpersonationAuditServiceTest.php diff --git a/app/app/Services/Pd/ImpersonationAuditService.php b/app/app/Services/Pd/ImpersonationAuditService.php new file mode 100644 index 00000000..f56e77c2 --- /dev/null +++ b/app/app/Services/Pd/ImpersonationAuditService.php @@ -0,0 +1,71 @@ + $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, + ]); + } +} diff --git a/app/tests/Unit/Services/Pd/ImpersonationAuditServiceTest.php b/app/tests/Unit/Services/Pd/ImpersonationAuditServiceTest.php new file mode 100644 index 00000000..3bb8a259 --- /dev/null +++ b/app/tests/Unit/Services/Pd/ImpersonationAuditServiceTest.php @@ -0,0 +1,54 @@ +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); +});