Passed
Push — master ( 5e96c0...23583c )
by Angel Fernando Quiroz
08:21
created

GenericAuthenticator::getCustomBadge()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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