Passed
Push — dependabot/npm_and_yarn/microm... ( e84ba6...f2f212 )
by
unknown
10:03
created

GenericAuthenticator::supports()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Security\Authenticator\OAuth2;
8
9
use Chamilo\CoreBundle\Entity\AccessUrl;
10
use Chamilo\CoreBundle\Entity\AccessUrlRelUser;
11
use Chamilo\CoreBundle\Entity\User;
12
use Chamilo\CoreBundle\Repository\ExtraFieldRepository;
13
use Chamilo\CoreBundle\Repository\ExtraFieldValuesRepository;
14
use Chamilo\CoreBundle\Repository\Node\AccessUrlRepository;
15
use Chamilo\CoreBundle\Repository\Node\UserRepository;
16
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
17
use Chamilo\CoreBundle\ServiceHelper\AuthenticationConfigHelper;
18
use Doctrine\ORM\EntityManagerInterface;
19
use ExtraField;
20
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
21
use League\OAuth2\Client\Provider\GenericResourceOwner;
22
use League\OAuth2\Client\Token\AccessToken;
23
use League\OAuth2\Client\Tool\ArrayAccessorTrait;
24
use Symfony\Component\HttpFoundation\Request;
25
use Symfony\Component\Routing\RouterInterface;
26
use Symfony\Component\Security\Core\Exception\AuthenticationException;
27
use UnexpectedValueException;
28
29
class GenericAuthenticator extends AbstractAuthenticator
30
{
31
    use ArrayAccessorTrait;
32
33
    public const EXTRA_FIELD_OAUTH2_ID = 'oauth2_id';
34
35
    protected string $providerName = 'generic';
36
37
    public function __construct(
38
        ClientRegistry $clientRegistry,
39
        RouterInterface $router,
40
        UserRepository $userRepository,
41
        AuthenticationConfigHelper $authenticationConfigHelper,
42
        AccessUrlHelper $urlHelper,
43
        EntityManagerInterface $entityManager,
44
        protected readonly ExtraFieldRepository $extraFieldRepository,
45
        protected readonly ExtraFieldValuesRepository $extraFieldValuesRepository,
46
        protected readonly AccessUrlRepository $accessUrlRepository,
47
    ) {
48
        parent::__construct(
49
            $clientRegistry,
50
            $router,
51
            $userRepository,
52
            $authenticationConfigHelper,
53
            $urlHelper,
54
            $entityManager,
55
        );
56
    }
57
58
    public function supports(Request $request): ?bool
59
    {
60
        return 'chamilo.oauth2_generic_check' === $request->attributes->get('_route');
61
    }
62
63
    protected function userLoader(AccessToken $accessToken): User
64
    {
65
        $providerParams = $this->authenticationConfigHelper->getParams('generic');
66
67
        /** @var GenericResourceOwner $resourceOwner */
68
        $resourceOwner = $this->client->fetchUserFromToken($accessToken);
69
        $resourceOwnerData = $resourceOwner->toArray();
70
        $resourceOwnerId = $resourceOwner->getId();
71
72
        if (empty($resourceOwnerId)) {
73
            throw new UnexpectedValueException('Value for the resource owner identifier not found at the configured key');
74
        }
75
76
        $fieldType = (int) ExtraField::getExtraFieldTypeFromString('user');
77
        $extraField = $this->extraFieldRepository->findByVariable($fieldType, self::EXTRA_FIELD_OAUTH2_ID);
78
79
        $existingUserExtraFieldValue = $this->extraFieldValuesRepository->findByVariableAndValue(
80
            $extraField,
81
            $resourceOwnerId
82
        );
83
84
        if (null === $existingUserExtraFieldValue) {
85
            $username = $this->getValueByKey(
86
                $resourceOwnerData,
87
                $providerParams['resource_owner_username_field'],
88
                "oauth2user_$resourceOwnerId"
89
            );
90
91
            /** @var User $user */
92
            $user = $this->userRepository->findOneBy(['username' => $username]);
93
94
            if (!$user || 'platform' !== $user->getAuthSource()) {
0 ignored issues
show
introduced by
$user is of type Chamilo\CoreBundle\Entity\User, thus it always evaluated to true.
Loading history...
95
                if (!$providerParams['allow_create_new_users']) {
96
                    throw new AuthenticationException('This user doesn\'t have an account yet and auto-provisioning is not enabled. Please contact this portal administration team to request access.');
97
                }
98
99
                // set default values, real values are set in self::updateUserInfo method
100
                $user = (new User())
101
                    ->setFirstname('OAuth2 User default firstname')
102
                    ->setLastname('OAuth2 User default firstname')
103
                    ->setEmail('oauth2user_'.$resourceOwnerId.'@'.(gethostname() ?: 'localhost'))
104
                    ->setUsername($username)
105
                    ->setPlainPassword($username)
106
                    ->setStatus(STUDENT)
107
                    ->setCreatorId($this->userRepository->getRootUser()->getId())
108
                ;
109
            }
110
111
            $this->saveUserInfo($user, $resourceOwnerData, $providerParams);
112
113
            $this->extraFieldValuesRepository->updateItemData(
114
                $extraField,
115
                $user,
116
                $resourceOwnerId
117
            );
118
119
            $this->updateUrls($user, $resourceOwnerData, $providerParams);
120
        } else {
121
            /** @var User $user */
122
            $user = $this->userRepository->find(
123
                $existingUserExtraFieldValue->getItemId()
124
            );
125
126
            if ($providerParams['allow_update_user_info']) {
127
                $this->saveUserInfo($user, $resourceOwnerData, $providerParams);
128
129
                $this->updateUrls($user, $resourceOwnerData, $providerParams);
130
            }
131
        }
132
133
        return $user;
134
    }
135
136
    /**
137
     * Set user information from the resource owner's data or the user itself.
138
     */
139
    public function saveUserInfo(User $user, array $resourceOwnerData, array $providerParams): void
140
    {
141
        $status = $this->getUserStatus($resourceOwnerData, $user->getStatus(), $providerParams);
142
143
        $user
144
            ->setFirstname(
145
                $this->getValueByKey(
146
                    $resourceOwnerData,
147
                    $providerParams['resource_owner_firstname_field'],
148
                    $user->getFirstname()
149
                )
150
            )
151
            ->setLastname(
152
                $this->getValueByKey(
153
                    $resourceOwnerData,
154
                    $providerParams['resource_owner_lastname_field'],
155
                    $user->getLastname()
156
                )
157
            )
158
            ->setUsername(
159
                $this->getValueByKey(
160
                    $resourceOwnerData,
161
                    $providerParams['resource_owner_username_field'],
162
                    $user->getUsername()
163
                )
164
            )
165
            ->setEmail(
166
                $this->getValueByKey(
167
                    $resourceOwnerData,
168
                    $providerParams['resource_owner_email_field'],
169
                    $user->getEmail()
170
                )
171
            )
172
            ->setAuthSource('oauth2')
173
            ->setStatus($status)
174
            ->setRoleFromStatus($status)
175
        ;
176
177
        $this->userRepository->updateUser($user);
178
179
        $url = $this->urlHelper->getCurrent();
180
        $url->addUser($user);
181
182
        $this->entityManager->flush();
183
    }
184
185
    private function getUserStatus(array $resourceOwnerData, int $defaultStatus, array $providerParams): int
186
    {
187
        $status = $this->getValueByKey(
188
            $resourceOwnerData,
189
            $providerParams['resource_owner_status_field'],
190
            $defaultStatus
191
        );
192
193
        $responseStatus = [];
194
195
        if ($teacherStatus = $providerParams['resource_owner_teacher_status_field']) {
196
            $responseStatus[COURSEMANAGER] = $teacherStatus;
197
        }
198
199
        if ($sessAdminStatus = $providerParams['resource_owner_sessadmin_status_field']) {
200
            $responseStatus[SESSIONADMIN] = $sessAdminStatus;
201
        }
202
203
        if ($drhStatus = $providerParams['resource_owner_hr_status_field']) {
204
            $responseStatus[DRH] = $drhStatus;
205
        }
206
207
        if ($studentStatus = $providerParams['resource_owner_status_status_field']) {
208
            $responseStatus[STUDENT] = $studentStatus;
209
        }
210
211
        if ($anonStatus = $providerParams['resource_owner_anon_status_field']) {
212
            $responseStatus[ANONYMOUS] = $anonStatus;
213
        }
214
215
        $map = array_flip($responseStatus);
216
217
        return $map[$status] ?? $status;
218
    }
219
220
    private function updateUrls(User $user, array $resourceOwnerData, array $providerParams): void
221
    {
222
        if (!($urlsField = $providerParams['resource_owner_urls_field'])) {
223
            return;
224
        }
225
226
        $availableUrls = [];
227
228
        $urls = $this->accessUrlRepository->findAll();
229
230
        /** @var AccessUrl $existingUrl */
231
        foreach ($urls as $existingUrl) {
232
            $availableUrls[(string) $existingUrl->getId()] = $existingUrl->getId();
233
            $availableUrls[$existingUrl->getUrl()] = $existingUrl->getId();
234
        }
235
236
        $allowedUrlIds = [];
237
238
        foreach ($this->getValueByKey($resourceOwnerData, $urlsField) as $value) {
239
            if (\array_key_exists($value, $availableUrls)) {
240
                $allowedUrlIds[] = $availableUrls[$value];
241
            } else {
242
                $newValue = ('/' === $value[-1]) ? substr($value, 0, -1) : $value.'/';
243
244
                if (\array_key_exists($newValue, $availableUrls)) {
245
                    $allowedUrlIds[] = $availableUrls[$newValue];
246
                }
247
            }
248
        }
249
250
        $grantedUrlIds = [];
251
252
        foreach ($this->accessUrlRepository->findByUser($user) as $grantedUrl) {
253
            $grantedUrlIds[] = $grantedUrl->getId();
254
        }
255
256
        $urlRelUserRepo = $this->entityManager->getRepository(AccessUrlRelUser::class);
257
258
        foreach (array_diff($grantedUrlIds, $allowedUrlIds) as $extraUrlId) {
259
            $urlRelUser = $urlRelUserRepo->findOneBy(['user' => $user, 'url' => $extraUrlId]);
260
261
            if ($urlRelUser) {
262
                $this->entityManager->remove($urlRelUser);
263
            }
264
        }
265
266
        $this->entityManager->flush();
267
268
        foreach (array_diff($allowedUrlIds, $grantedUrlIds) as $missingUrlId) {
269
            /** @var AccessUrl $missingUrl */
270
            $missingUrl = $this->accessUrlRepository->find($missingUrlId);
271
            $missingUrl->addUser($user);
272
        }
273
274
        $this->entityManager->flush();
275
    }
276
}
277