Completed
Push — master ( eef796...09e4be )
by Damien
02:31
created

User::assignProperty()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 36
ccs 0
cts 20
cp 0
rs 8.7217
c 0
b 0
f 0
cc 6
nc 9
nop 4
crap 42
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\ProviderHelper;
14
use flipbox\saml\sp\helpers\UserHelper;
15
use flipbox\saml\sp\models\Settings;
16
use flipbox\saml\sp\records\ProviderIdentityRecord;
17
use flipbox\saml\sp\records\ProviderRecord;
18
use flipbox\saml\sp\Saml;
19
use SAML2\Response as SamlResponse;
20
use SAML2\XML\saml\Attribute;
21
use yii\base\UserException;
22
23
/**
24
 * Class User
25
 * @package flipbox\saml\sp\services
26
 */
27
class User
28
{
29
    use AssertionTrait;
30
    /**
31
     * @var FieldLayout|null
32
     */
33
    private $fieldLayout;
34
    /**
35
     * @var Field[]
36
     */
37
    private $fields = [];
38
39
    /**
40
     * @param SamlResponse $response
41
     * @return UserElement
42
     * @throws InvalidMessage
43
     * @throws UserException
44
     */
45
    public function getByResponse(SamlResponse $response, ProviderRecord $serviceProvider, Settings $settings)
46
    {
47
48
        $username = null;
49
50
        if (!is_null($settings->nameIdAttributeOverride)) {
51
            // use override
52
            foreach ($this->getAssertions(
53
                $response,
54
                $serviceProvider
55
            ) as $assertion) {
56
                $attributes = $assertion->getAttributes();
57
                if (isset($attributes[$settings->nameIdAttributeOverride])) {
58
                    $attributeValue = $attributes[$settings->nameIdAttributeOverride];
59
                    $username = $this->getAttributeValue($attributeValue);
60
                }
61
            }
62
        } else {
63
            // use nameid
64
            $assertion = $this->getFirstAssertion($response, $serviceProvider);
65
66
            if (! $assertion->getNameId()) {
67
                throw new InvalidMessage('Name ID is missing.');
68
            }
69
            $username = $assertion->getNameId()->getValue();
70
71
            Saml::debug('NameId: ' . $assertion->getNameId()->getValue());
72
        }
73
74
        return $this->find($username);
75
    }
76
77
    /**
78
     * @param ProviderIdentityRecord $identity
79
     * @return bool
80
     * @throws UserException
81
     * @throws \Throwable
82
     */
83
    public function login(\flipbox\saml\sp\records\ProviderIdentityRecord $identity)
84
    {
85
        if ($identity->getUser()->getStatus() !== UserElement::STATUS_ACTIVE) {
86
            if (! \Craft::$app->getUsers()->activateUser($identity->getUser())) {
87
                throw new UserException("Can't activate user.");
88
            }
89
        }
90
91
        if (\Craft::$app->getUser()->login(
92
            $identity->getUser(),
93
            /**
94
             * @todo read session duration from the response
95
             */
96
            \Craft::$app->getConfig()->getGeneral()->userSessionDuration
97
        )
98
        ) {
99
            $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...
100
        } else {
101
            throw new UserException("User login failed.");
102
        }
103
104
        return true;
105
    }
106
107
    /**
108
     * @param UserElement $user
109
     * @param SamlResponse $response
110
     * @param ProviderRecord $idp
111
     * @param Settings $settings
112
     * @throws UserException
113
     * @throws \Throwable
114
     * @throws \craft\errors\ElementNotFoundException
115
     * @throws \yii\base\Exception
116
     */
117
    public function sync(
118
        UserElement $user,
119
        SamlResponse $response,
120
        ProviderRecord $idp,
121
        ProviderRecord $sp,
122
        Settings $settings
123
    ) {
124
125
        // enable and transform the user
126
        $this->construct(
127
            $user,
128
            $response,
129
            $idp,
130
            $sp,
131
            $settings
132
        );
133
134
        // Save
135
        $this->save($user);
136
    }
137
138
    /**
139
     * @param UserElement $user
140
     * @return bool
141
     * @throws UserException
142
     * @throws \Throwable
143
     * @throws \craft\errors\ElementNotFoundException
144
     * @throws \yii\base\Exception
145
     */
146
    protected function save(UserElement $user)
147
    {
148
        if (! \Craft::$app->getElements()->saveElement($user)) {
149
            Saml::error(
150
                'User save failed: ' . \json_encode($user->getErrors())
151
            );
152
            throw new UserException("User save failed: " . \json_encode($user->getErrors()));
153
        }
154
155
        return true;
156
    }
157
158
    /**
159
     * Response Based Methods
160
     */
161
162
    /**
163
     * @param UserElement $user
164
     * @param SamlResponse $response
165
     * @param ProviderRecord $idp
166
     * @param Settings $settings
167
     * @throws UserException
168
     * @throws \Throwable
169
     */
170
    protected function construct(
171
        UserElement $user,
172
        SamlResponse $response,
173
        ProviderRecord $idp,
174
        ProviderRecord $sp,
175
        Settings $settings
176
    ) {
177
        /**
178
         * Is User Active?
179
         */
180
        if (! UserHelper::isUserActive($user)) {
181
            if (! $settings->enableUsers) {
182
                throw new UserException('User access denied.');
183
            }
184
            UserHelper::enableUser($user);
185
        }
186
187
        foreach ($this->getAssertions($response, $sp) as $assertion) {
188
            $hasAttributes = count($assertion->getAttributes()) > 0;
189
            Saml::debug('assertion attributes: ' . \json_encode($assertion->getAttributes()));
190
            if ($hasAttributes) {
191
                $this->transform(
192
                    $user,
193
                    $response,
194
                    $idp,
195
                    $sp,
196
                    $settings
197
                );
198
            } else {
199
                /**
200
                 * There doesn't seem to be any attribute statements.
201
                 * Try and use username for the email and move on.
202
                 */
203
                Saml::warning(
204
                    'No attribute statements found! Trying to assign username as the email.'
205
                );
206
                $user->email = $user->email ?: $user->username;
207
            }
208
        }
209
    }
210
211
    /**
212
     * @param UserElement $user
213
     * @param SamlResponse $response
214
     * @return UserElement
215
     */
216
    protected function transform(
217
        UserElement $user,
218
        SamlResponse $response,
219
        ProviderRecord $idp,
220
        ProviderRecord $sp,
221
        Settings $settings
222
    ) {
223
224
        foreach ($this->getAssertions($response, $sp) as $assertion) {
225
            /**
226
             * Check the provider first
227
             */
228
            $attributeMap = ProviderHelper::providerMappingToKeyValue(
229
                $idp
230
            ) ?:
231
                $settings->responseAttributeMap;
0 ignored issues
show
Deprecated Code introduced by
The property flipbox\saml\sp\models\S...::$responseAttributeMap has been deprecated.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
232
233
            Saml::debug('Attribute Map: ' . \json_encode($attributeMap));
234
235
            /**
236
             * Loop thru attributes and set to the user
237
             */
238
            foreach ($assertion->getAttributes() as $attributeName => $attributeValue) {
239
                Saml::debug('Attributes: ' . $attributeName . ' ' . \json_encode($attributeValue));
240
                if (isset($attributeMap[$attributeName])) {
241
                    $craftProperty = $attributeMap[$attributeName];
242
                    $this->assignProperty(
243
                        $user,
244
                        $attributeName,
245
                        $attributeValue,
246
                        $craftProperty
247
                    );
248
                } else {
249
                    Saml::debug('No match for: ' . $attributeName);
250
                }
251
            }
252
        }
253
254
        return $user;
255
    }
256
257
    /**
258
     * @param UserElement $user
259
     * @param $attributeName
260
     * @param $attributeValue
261
     * @param $craftProperty
262
     */
263
    protected function assignProperty(
264
        UserElement $user,
265
        $attributeName,
266
        $attributeValue,
267
        $craftProperty
268
    ) {
269
270
        $originalValues = $attributeValue;
271
        if (is_array($attributeValue)) {
272
            $attributeValue = isset($attributeValue[0]) ? $attributeValue[0] : null;
273
        }
274
275
        if (is_string($craftProperty) && in_array($craftProperty, $user->attributes())) {
276
            Saml::debug(
277
                sprintf(
278
                    'Attribute %s is scalar and should set value "%s" to user->%s',
279
                    $attributeName,
280
                    $attributeValue,
281
                    $craftProperty
282
                )
283
            );
284
285
            $this->setSimpleProperty($user, $craftProperty, $attributeValue);
286
        } elseif (is_callable($craftProperty)) {
287
            Saml::debug(
288
                sprintf(
289
                    'Attribute %s is handled with a callable.',
290
                    $attributeName
291
                )
292
            );
293
294
            call_user_func($craftProperty, $user, [
295
                $attributeName => $originalValues,
296
            ]);
297
        }
298
    }
299
300
    /**
301
     * @param UserElement $user
302
     * @return Field|null
303
     */
304
    protected function getFieldLayoutField(UserElement $user, $fieldHandle)
305
    {
306
        if (! $this->fieldLayout) {
307
            $this->fieldLayout = $user->getFieldLayout();
308
        }
309
        if (is_null($this->fieldLayout)) {
310
            return null;
311
        }
312
313
        if (! isset($this->fields[$fieldHandle])) {
314
            $this->fields[$fieldHandle] = $this->fieldLayout->getFieldByHandle($fieldHandle);
315
        }
316
317
318
        return $this->fields[$fieldHandle];
319
    }
320
321
    /**
322
     * @param UserElement $user
323
     * @param string $name
324
     * @param mixed $value
325
     */
326
    private function setSimpleProperty(UserElement $user, $name, $value)
327
    {
328
        $field = $this->getFieldLayoutField($user, $name);
329
330
        Saml::info(
331
            sprintf(
332
                '%s as %s. Is Field? %s',
333
                $name,
334
                $value,
335
                $field instanceof Field ? $field->id : 'Nope'
336
            )
337
        );
338
339
        if (! is_null($field)) {
340
            $user->setFieldValue($name, $value);
341
        } else {
342
            $user->{$name} = $value;
343
        }
344
    }
345
346
    /**************************************************
347
     * Craft User Methods
348
     **************************************************/
349
350
    /**
351
     * @param $username
352
     * @return UserElement
353
     * @throws UserException
354
     */
355
    protected function find($username)
356
    {
357
        return $this->forceGet($username);
358
    }
359
360
    /**
361
     * @param $username
362
     * @return UserElement
363
     * @throws UserException
364
     */
365
    protected function forceGet($username)
366
    {
367
368
        /**
369
         * Is there a user that exists already?
370
         */
371
        if (!($user = $this->getByUsernameOrEmail($username))) {
372
            // Should we create a new user? what's the setting say?
373
            if (! Saml::getInstance()->getSettings()->createUser) {
374
                throw new UserException("System doesn't have permission to create a new user.");
375
            }
376
377
            // new user!
378
            $user = new UserElement(
379
                [
380
                    'username' => $username,
381
                ]
382
            );
383
        }
384
385
        return $user;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $user; (array|boolean|craft\base\ElementInterface) is incompatible with the return type documented by flipbox\saml\sp\services\login\User::forceGet of type craft\elements\User.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
386
    }
387
388
    /**
389
     * @param $usernameOrEmail
390
     * @param bool $archived
391
     * @return array|bool|\craft\base\ElementInterface|UserElement|null
392
     */
393
    protected function getByUsernameOrEmail($usernameOrEmail, $archived = false)
394
    {
395
396
        return UserElement::find()
397
            ->where(
398
                [
399
                    'or',
400
                    ['username' => $usernameOrEmail],
401
                    ['email' => $usernameOrEmail],
402
                ]
403
            )
404
            ->status(null)
405
            ->archived($archived)
406
            ->one();
407
    }
408
409
    private function getAttributeValue($attributeValue)
410
    {
411
412
        if (is_array($attributeValue)) {
413
            $attributeValue = isset($attributeValue[0]) ? $attributeValue[0] : null;
414
        }
415
416
        return $attributeValue;
417
    }
418
}
419