Completed
Push — master ( 9e6620...192877 )
by Bart
07:34 queued 04:01
created

ShibbolethSwitchUserListener::handle()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 38
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 38
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 28
nc 8
nop 1
1
<?php
2
3
namespace Kuleuven\AuthenticationBundle\Security;
4
5
use Kuleuven\AuthenticationBundle\Model\KuleuvenUserInterface;
6
use Kuleuven\AuthenticationBundle\Traits\LoggerTrait;
7
use Psr\Log\LoggerAwareInterface;
8
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
9
use Symfony\Component\Security\Core\User\UserProviderInterface;
10
use Symfony\Component\Security\Core\User\UserCheckerInterface;
11
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
12
use Psr\Log\LoggerInterface;
13
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
14
use Symfony\Component\Security\Core\Exception\AuthenticationException;
15
use Symfony\Component\HttpFoundation\RedirectResponse;
16
use Symfony\Component\HttpFoundation\Request;
17
use Symfony\Component\Security\Core\Role\SwitchUserRole;
18
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
19
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
20
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
21
use Symfony\Component\Security\Http\SecurityEvents;
22
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
23
24
class ShibbolethSwitchUserListener implements ListenerInterface, LoggerAwareInterface
25
{
26
    use LoggerTrait;
27
28
    protected $tokenStorage;
29
    protected $provider;
30
    protected $userChecker;
31
    protected $providerKey;
32
    protected $accessDecisionManager;
33
    protected $usernameParameter;
34
    protected $role;
35
    protected $dispatcher;
36
37
    public function __construct(
38
        TokenStorageInterface $tokenStorage,
39
        UserProviderInterface $provider,
40
        UserCheckerInterface $userChecker,
41
        $providerKey,
42
        AccessDecisionManagerInterface $accessDecisionManager,
43
        LoggerInterface $logger = null,
44
        $usernameParameter = '_switch_user',
45
        $role = 'ROLE_ALLOWED_TO_SWITCH',
46
        EventDispatcherInterface $dispatcher = null
47
    )
48
    {
49
        if (empty($providerKey)) {
50
            throw new \InvalidArgumentException('$providerKey must not be empty.');
51
        }
52
53
        $this->tokenStorage = $tokenStorage;
54
        $this->provider = $provider;
55
        $this->userChecker = $userChecker;
56
        $this->providerKey = $providerKey;
57
        $this->accessDecisionManager = $accessDecisionManager;
58
        // use LoggerTrait
59
        if (!empty($logger)) {
60
            $this->setLogger($logger);
61
        }
62
        $this->usernameParameter = $usernameParameter;
63
        $this->role = $role;
64
        $this->dispatcher = $dispatcher;
65
    }
66
67
    /**
68
     * Handles the switch to another user.
69
     *
70
     * @param GetResponseEvent $event A GetResponseEvent instance
71
     * @throws AccessDeniedException
72
     * @throws AuthenticationException
73
     */
74
    public function handle(GetResponseEvent $event)
75
    {
76
        $request = $event->getRequest();
77
        $username = $event->getRequest()->get($this->usernameParameter);
78
        if (empty($username)) {
79
            // This is not a switch request
80
            return;
81
        }
82
83
        $token = $this->tokenStorage->getToken();
84
        if (!empty($token) && $token instanceof KuleuvenUserToken) {
85
            if ('_exit' === $username) {
86
                $originalToken = $this->getOriginalToken($token);
87
                if (false === $originalToken) {
88
                    $this->log('Exit attempt ignored, no original token found');
89
                } else {
90
                    $authenticationToken = $this->attemptSwitchUser($request, $originalToken->getUsername());
91
                    $this->log(sprintf('Switch to new authentication token: %s', $authenticationToken));
92
                    $this->tokenStorage->setToken($authenticationToken);
93
                }
94
            } else {
95
                try {
96
                    $authenticationToken = $this->attemptSwitchUser($request, $request->get($this->usernameParameter));
97
                    $this->log(sprintf('Switch to new authentication token: %s', $authenticationToken));
98
                    $this->tokenStorage->setToken($authenticationToken);
99
                } catch (AuthenticationException $e) {
100
                    $this->log(sprintf('Switch User failed: "%s"', $e->getMessage()));
101
                    throw $e;
102
                }
103
            }
104
        }
105
106
        $this->log(sprintf('Redirect to original url: %s', $request->getUri()));
107
        $request->query->remove($this->usernameParameter);
108
        $request->server->set('QUERY_STRING', http_build_query($request->query->all()));
109
        $response = new RedirectResponse($request->getUri(), 302);
110
        $event->setResponse($response);
111
    }
112
113
    /**
114
     * Attempts to switch to another user.
115
     *
116
     * @param Request $request A Request instance
117
     * @param string  $username
118
     * @return null|TokenInterface The new TokenInterface if successfully switched, null otherwise
119
     * @throws AccessDeniedException
120
     */
121
    private function attemptSwitchUser(Request $request, $username)
122
    {
123
        /** @var KuleuvenUserToken $token */
124
        $token = $this->tokenStorage->getToken();
125
        $token->setUser($this->provider->refreshUser($token->getUser()));
0 ignored issues
show
Documentation introduced by
$token->getUser() is of type object|string, but the function expects a object<Symfony\Component...ore\User\UserInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
126
127
        if ($token->getUsername() === $username) {
128
            $this->log(sprintf('Token already for username "%s", keep token: %s', $username, $token));
129
            return $token;
130
        }
131
132
        $originalToken = $this->getOriginalToken($token);
133
        if (false !== $originalToken) {
134
            $this->log(sprintf('Original token found: %s', $originalToken));
135
            // User is impersonating someone, they are trying to switch directly to another user, make sure original user has access.
136 View Code Duplication
            if (false === $this->accessDecisionManager->decide($originalToken, [$this->role])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
137
                $this->log(sprintf('Original token has no right to impersonate "%s", access denied: %s', $username, $originalToken));
138
                throw new AccessDeniedException(sprintf('Original token has no right to impersonate "%s", access denied: %s', $username, $originalToken));
139
            }
140
            if ($originalToken->getUsername() === $username) {
141
                $this->log(sprintf('Original token is already for "%s", switching to original token: %s', $username, $originalToken));
142
                $this->dispatchSwitchUserEvent($request, $token);
143
                return $originalToken;
144
            }
145 View Code Duplication
        } elseif (false === $this->accessDecisionManager->decide($token, [$this->role])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
146
            $this->log(sprintf('Token has no right to impersonate "%s", access denied: %s', $username, $originalToken));
147
            throw new AccessDeniedException(sprintf('Token has no right to impersonate "%s", access denied: %s', $username, $originalToken));
148
        }
149
150
        $this->log(sprintf('Attempting to impersonate "%s"', $username));
151
152
        /** @var KuleuvenUserInterface $user */
153
        $user = $this->provider->loadUserByUsername($username);
154
        $this->userChecker->checkPostAuth($user);
155
156
        $attributes = $user->getAttributes();
157
        $roles = $user->getRoles();
158
159
        // If there is an original token, only let them switch back to that user.
160
        if ($originalToken) {
161
            $roles[] = new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $originalToken);
162
        } else {
163
            $roles[] = new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $token);
164
        }
165
166
        $token = new KuleuvenUserToken($user, $attributes, $this->providerKey, $roles);
167
        $token->setAuthenticated(true);
168
        $this->dispatchSwitchUserEvent($request, $token);
169
        return $token;
170
    }
171
172
    /**
173
     * Gets the original Token from a switched one.
174
     *
175
     * @param KuleuvenUserToken $token A switched TokenInterface instance
176
     * @return TokenInterface|false The original TokenInterface instance, false if the current TokenInterface is not switched
177
     */
178
    private function getOriginalToken(KuleuvenUserToken $token)
179
    {
180
        foreach ($token->getRoles() as $role) {
181
            if ($role instanceof SwitchUserRole) {
182
                return $role->getSource();
183
            }
184
        }
185
186
        return false;
187
    }
188
189
    private function dispatchSwitchUserEvent(Request $request, KuleuvenUserToken $token)
190
    {
191
        if (null !== $this->dispatcher) {
192
            $switchEvent = new ShibbolethSwitchUserEvent($request, $token->getUser(), $token);
0 ignored issues
show
Documentation introduced by
$token->getUser() is of type object|string, but the function expects a object<Symfony\Component...ore\User\UserInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
193
            $this->dispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
194
        }
195
    }
196
}
197