Completed
Push — master ( 217691...aee7cd )
by Damien
04:21
created

User::setSimpleProperty()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 0
cts 12
cp 0
rs 9.6333
c 0
b 0
f 0
cc 3
nc 2
nop 3
crap 12
1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 */
6
7
namespace flipbox\saml\sp\services\login;
8
9
use craft\base\Field;
10
use craft\elements\User as UserElement;
11
use craft\models\FieldLayout;
12
use flipbox\saml\core\exceptions\InvalidMessage;
13
use flipbox\saml\core\helpers\MessageHelper;
14
use flipbox\saml\core\helpers\ProviderHelper;
15
use flipbox\saml\sp\helpers\UserHelper;
16
use flipbox\saml\sp\records\ProviderIdentityRecord;
17
use flipbox\saml\sp\Saml;
18
use SAML2\Response as SamlResponse;
19
use yii\base\UserException;
20
21
/**
22
 * Class User
23
 * @package flipbox\saml\sp\services
24
 */
25
class User
26
{
27
    use AssertionTrait;
28
    /**
29
     * @var FieldLayout|null
30
     */
31
    private $fieldLayout;
32
    /**
33
     * @var Field[]
34
     */
35
    private $fields = [];
36
37
    /**
38
     * @param SamlResponse $response
39
     * @return UserElement
40
     * @throws InvalidMessage
41
     * @throws UserException
42
     */
43
    public function getByResponse(SamlResponse $response)
44
    {
45
46
        $assertion = $this->getFirstAssertion($response);
47
48
        if (! $assertion->getNameId()) {
49
            throw new InvalidMessage('Name ID is missing.');
50
        }
51
52
        Saml::debug('NameId: ' . $assertion->getNameId()->getValue());
53
        /**
54
         * Get username from the NameID
55
         *
56
         * @todo Give an option to map another attribute value to $username (like email)
57
         */
58
        $username = $assertion->getNameId()->getValue();
59
60
        return $this->find($username);
61
    }
62
63
    /**
64
     * @param ProviderIdentityRecord $identity
65
     * @return bool
66
     * @throws UserException
67
     * @throws \Throwable
68
     */
69
    public function login(\flipbox\saml\sp\records\ProviderIdentityRecord $identity)
70
    {
71
        if ($identity->getUser()->getStatus() !== UserElement::STATUS_ACTIVE) {
72
            if (! \Craft::$app->getUsers()->activateUser($identity->getUser())) {
73
                throw new UserException("Can't activate user.");
74
            }
75
        }
76
77
        if (\Craft::$app->getUser()->login(
78
            $identity->getUser(),
79
            /**
80
             * @todo read session duration from the response
81
             */
82
            \Craft::$app->getConfig()->getGeneral()->userSessionDuration
83
        )
84
        ) {
85
            $identity->lastLoginDate = new \DateTime();
0 ignored issues
show
Documentation introduced by
The property lastLoginDate does not exist on object<flipbox\saml\sp\r...ProviderIdentityRecord>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
86
        } else {
87
            throw new UserException("User login failed.");
88
        }
89
90
        return true;
91
    }
92
93
    /**
94
     * @param UserElement $user
95
     * @param SamlResponse $response
96
     * @throws UserException
97
     * @throws \Throwable
98
     * @throws \craft\errors\ElementNotFoundException
99
     * @throws \yii\base\Exception
100
     */
101
    public function sync(UserElement $user, SamlResponse $response)
102
    {
103
104
        // enable and transform the user
105
        $this->construct($user, $response);
106
107
108
        // Save
109
        $this->save($user);
110
111
112
        // Sync groups depending on the plugin setting.
113
        Saml::getInstance()->getUserGroups()->sync($user, $response);
114
115
116
        // Sync defaults
117
        Saml::getInstance()->getUserGroups()->assignDefaultGroups($user);
118
    }
119
120
    /**
121
     * @param UserElement $user
122
     * @return bool
123
     * @throws UserException
124
     * @throws \Throwable
125
     * @throws \craft\errors\ElementNotFoundException
126
     * @throws \yii\base\Exception
127
     */
128
    protected function save(UserElement $user)
129
    {
130
        if (! \Craft::$app->getElements()->saveElement($user)) {
131
            Saml::error(
132
                'User save failed: ' . \json_encode($user->getErrors())
133
            );
134
            throw new UserException("User save failed: " . \json_encode($user->getErrors()));
135
        }
136
137
        return true;
138
    }
139
140
    /**
141
     * Response Based Methods
142
     */
143
144
    /**
145
     * @param UserElement $user
146
     * @param SamlResponse $response
147
     * @throws UserException
148
     * @throws \Throwable
149
     */
150
    protected function construct(UserElement $user, SamlResponse $response)
151
    {
152
        /**
153
         * Is User Active?
154
         */
155
        if (! UserHelper::isUserActive($user)) {
156
            if (! Saml::getInstance()->getSettings()->enableUsers) {
157
                throw new UserException('User access denied.');
158
            }
159
            UserHelper::enableUser($user);
160
        }
161
162
        foreach ($this->getAssertions($response) as $assertion) {
163
            $hasAttributes = count($assertion->getAttributes()) > 0;
164
            Saml::debug('assertion attributes: ' . \json_encode($assertion->getAttributes()));
165
            if ($hasAttributes) {
166
                $this->transform($response, $user);
167
            } else {
168
                /**
169
                 * There doesn't seem to be any attribute statements.
170
                 * Try and use username for the email and move on.
171
                 */
172
                Saml::warning(
173
                    'No attribute statements found! Trying to assign username as the email.'
174
                );
175
                $user->email = $user->email ?: $user->username;
176
            }
177
        }
178
    }
179
180
    /**
181
     * @param SamlResponse $response
182
     * @param UserElement $user
183
     * @return UserElement
184
     */
185
    protected function transform(
186
        SamlResponse $response,
187
        UserElement $user
188
    ) {
189
190
        foreach ($this->getAssertions($response) as $assertion) {
191
            /**
192
             * Check the provider first
193
             */
194
            $attributeMap = ProviderHelper::providerMappingToKeyValue(
195
                $idpProvider = Saml::getInstance()->getProvider()->findByEntityId(
196
                    MessageHelper::getIssuer($response->getIssuer())
197
                )->one()
198
            ) ?:
199
                Saml::getInstance()->getSettings()->responseAttributeMap;
200
201
            Saml::debug('Attribute Map: ' . json_encode($attributeMap));
202
203
            /**
204
             * Loop thru attributes and set to the user
205
             */
206
            foreach ($assertion->getAttributes() as $attributeName => $attributeValue) {
207
                Saml::debug('Attributes: ' . $attributeName . ' ' . json_encode($attributeValue));
208
                if (isset($attributeMap[$attributeName])) {
209
                    $craftProperty = $attributeMap[$attributeName];
210
                    $this->assignProperty(
211
                        $user,
212
                        $attributeName,
213
                        $attributeValue,
214
                        $craftProperty
215
                    );
216
                } else {
217
                    Saml::debug('No match for: ' . $attributeName);
218
                }
219
            }
220
        }
221
222
        return $user;
223
    }
224
225
    /**
226
     * @param UserElement $user
227
     * @param $attributeName
228
     * @param $attributeValue
229
     * @param $craftProperty
230
     */
231
    protected function assignProperty(
232
        UserElement $user,
233
        $attributeName,
234
        $attributeValue,
235
        $craftProperty
236
    ) {
237
238
        $originalValues = $attributeValue;
239
        if (is_array($attributeValue)) {
240
            $attributeValue = isset($attributeValue[0]) ? $attributeValue[0] : null;
241
        }
242
243
        if (is_string($craftProperty) && in_array($craftProperty, $user->attributes())) {
244
            Saml::debug(
245
                sprintf(
246
                    'Attribute %s is scalar and should set value "%s" to user->%s',
247
                    $attributeName,
248
                    $attributeValue,
249
                    $craftProperty
250
                )
251
            );
252
253
            $this->setSimpleProperty($user, $craftProperty, $attributeValue);
254
        } elseif (is_callable($craftProperty)) {
255
            Saml::debug(
256
                sprintf(
257
                    'Attribute %s is handled with a callable.',
258
                    $attributeName
259
                )
260
            );
261
262
            call_user_func($craftProperty, $user, [
263
                $attributeName => $originalValues,
264
            ]);
265
        }
266
    }
267
268
    /**
269
     * @param UserElement $user
270
     * @return Field|null
271
     */
272
    protected function getFieldLayoutField(UserElement $user, $fieldHandle)
273
    {
274
        if (! $this->fieldLayout) {
275
            $this->fieldLayout = $user->getFieldLayout();
276
        }
277
        if (is_null($this->fieldLayout)) {
278
            return null;
279
        }
280
281
        if (! isset($this->fields[$fieldHandle])) {
282
            $this->fields[$fieldHandle] = $this->fieldLayout->getFieldByHandle($fieldHandle);
283
        }
284
285
286
        return $this->fields[$fieldHandle];
287
    }
288
289
    /**
290
     * @param UserElement $user
291
     * @param string $name
292
     * @param mixed $value
293
     */
294
    private function setSimpleProperty(UserElement $user, $name, $value)
295
    {
296
        $field = $this->getFieldLayoutField($user, $name);
297
298
        Saml::info(
299
            sprintf(
300
                '%s as %s. Is Field? %s',
301
                $name,
302
                $value,
303
                $field instanceof Field ? $field->id : 'Nope'
304
            )
305
        );
306
307
        if (! is_null($field)) {
308
            $user->setFieldValue($name, $value);
309
        } else {
310
            $user->{$name} = $value;
311
        }
312
    }
313
314
    /**************************************************
315
     * Craft User Methods
316
     **************************************************/
317
318
    /**
319
     * @param $username
320
     * @return UserElement
321
     * @throws UserException
322
     */
323
    protected function find($username)
324
    {
325
        return $this->forceGet($username);
326
    }
327
328
    /**
329
     * @param $username
330
     * @return UserElement
331
     * @throws UserException
332
     */
333
    protected function forceGet($username)
334
    {
335
336
        /**
337
         * Is there a user that exists already?
338
         */
339
        if ($user = $this->getByUsernameOrEmail($username)) {
340
            /**
341
             * System check for whether we are allowed merge with this this user
342
             */
343
            if (! Saml::getInstance()->getSettings()->mergeLocalUsers) {
344
                //don't continue
345
                throw new UserException(
346
                    sprintf(
347
                        "User (%s) already exists.",
348
                        $username
349
                    )
350
                );
351
            }
352
        } else {
353
            /**
354
             * New UserElement
355
             */
356
            $user = new UserElement(
357
                [
358
                    'username' => $username,
359
                ]
360
            );
361
        }
362
363
        return $user;
364
    }
365
366
    /**
367
     * @param $emailOrUsername
368
     * @return UserElement|null
369
     */
370
    protected function getByUsernameOrEmail($usernameOrEmail, bool $archived = false)
371
    {
372
373
        return UserElement::find()
374
            ->where(
375
                [
376
                    'or',
377
                    ['username' => $usernameOrEmail],
378
                    ['email' => $usernameOrEmail],
379
                ]
380
            )
381
            ->addSelect(['users.password', 'users.passwordResetRequired'])
382
            ->status(null)
383
            ->archived($archived)
384
            ->one();
385
    }
386
}
387