Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.04% covered (success)
98.04%
50 / 51
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImpersonateUserAction
98.04% covered (success)
98.04%
50 / 51
66.67% covered (warning)
66.67%
2 / 3
10
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
7
 logImpersonation
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
1<?php
2
3declare(strict_types=1);
4
5namespace App\Action\Admin;
6
7use App\Domain\Auth\Service\AuthService;
8use App\Domain\Exception\ForbiddenException;
9use App\Domain\Exception\NotFoundException;
10use App\Renderer\JsonRenderer;
11use InvalidArgumentException;
12use PDO;
13use PDOException;
14use Psr\Http\Message\ResponseInterface;
15use Psr\Http\Message\ServerRequestInterface;
16
17/**
18 * Allows admin to impersonate a user by generating tokens for them.
19 *
20 * POST /api/admin/impersonate/{userId}
21 *
22 * Security: This action should only be accessible to admins.
23 * The original admin's info is stored in the token for audit purposes.
24 */
25final class ImpersonateUserAction
26{
27    private PDO $pdo;
28
29    private AuthService $authService;
30
31    private JsonRenderer $renderer;
32
33    public function __construct(
34        PDO $pdo,
35        AuthService $authService,
36        JsonRenderer $renderer,
37    ) {
38        $this->pdo = $pdo;
39        $this->authService = $authService;
40        $this->renderer = $renderer;
41    }
42
43    /**
44     * @param array<string, string> $args
45     * @param ServerRequestInterface $request
46     * @param ResponseInterface $response
47     */
48    public function __invoke(
49        ServerRequestInterface $request,
50        ResponseInterface $response,
51        array $args,
52    ): ResponseInterface {
53        $targetUserId = (int)$args['userId'];
54
55        if ($targetUserId <= 0) {
56            throw new InvalidArgumentException('Invalid user ID');
57        }
58
59        // Get the admin user from the request (set by JWT middleware)
60        $adminUserId = (int)$request->getAttribute('userId');
61        $adminRole = $request->getAttribute('userRole');
62
63        // Verify admin role
64        if ($adminRole !== 'admin' && $adminRole !== 'super_admin') {
65            throw new ForbiddenException('Only admins can impersonate users');
66        }
67
68        // Get target user info
69        $sql = 'SELECT user_id, username, email, role FROM users WHERE user_id = :user_id';
70        $stmt = $this->pdo->prepare($sql);
71        $stmt->execute(['user_id' => $targetUserId]);
72        $targetUser = $stmt->fetch(PDO::FETCH_ASSOC);
73
74        if (!$targetUser) {
75            throw new NotFoundException('User not found');
76        }
77
78        // Prevent impersonating other admins (security measure)
79        if ($targetUser['role'] === 'admin' || $targetUser['role'] === 'super_admin') {
80            throw new ForbiddenException('Cannot impersonate admin users');
81        }
82
83        // Generate tokens for the target user
84        // Note: In a production system, you might want to add an "impersonated_by" claim
85        // to the JWT for audit purposes
86        $tokens = $this->authService->generateTokens([
87            'user_id' => (int)$targetUser['user_id'],
88            'username' => $targetUser['username'],
89            'email' => $targetUser['email'],
90            'role' => $targetUser['role'],
91        ]);
92
93        // Log the impersonation for audit
94        $this->logImpersonation($adminUserId, $targetUserId);
95
96        return $this->renderer->json($response, [
97            'success' => true,
98            'message' => 'Impersonation successful',
99            'data' => [
100                'user' => [
101                    'userId' => (int)$targetUser['user_id'],
102                    'username' => $targetUser['username'],
103                    'email' => $targetUser['email'],
104                    'role' => $targetUser['role'],
105                ],
106                'accessToken' => $tokens['accessToken'],
107                'refreshToken' => $tokens['refreshToken'],
108                'expiresIn' => $tokens['expiresIn'],
109                'tokenType' => 'Bearer',
110                'impersonatedBy' => $adminUserId,
111            ],
112        ]);
113    }
114
115    /**
116     * Log impersonation event for audit trail.
117     *
118     * @param int $adminUserId
119     * @param int $targetUserId
120     */
121    private function logImpersonation(int $adminUserId, int $targetUserId): void
122    {
123        // Insert into audit_log table if it exists
124        $sql = "
125            INSERT INTO audit_log (table_name, record_id, action, old_values, new_values, changed_by)
126            VALUES ('users', :target_user_id, 'impersonate', NULL, :details, :admin_user_id)
127        ";
128
129        try {
130            $stmt = $this->pdo->prepare($sql);
131            $stmt->execute([
132                'target_user_id' => $targetUserId,
133                'details' => json_encode(['impersonated_by' => $adminUserId, 'timestamp' => date('c')]),
134                'admin_user_id' => $adminUserId,
135            ]);
136        } catch (PDOException $e) {
137            // Silently fail if audit_log doesn't have the right structure
138            // In production, you'd want proper error handling here
139        }
140    }
141}