Completed
Push — master ( 7401b1...7c15e2 )
by Damien
09:56
created

User::construct()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 0
cts 14
cp 0
rs 8.8337
c 0
b 0
f 0
cc 6
nc 9
nop 2
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\elements\User as UserElement;
10
use flipbox\saml\core\exceptions\InvalidMessage;
11
use flipbox\saml\core\helpers\MessageHelper;
12
use flipbox\saml\core\helpers\ProviderHelper;
13
use flipbox\saml\sp\helpers\UserHelper;
14
use flipbox\saml\sp\records\ProviderIdentityRecord;
15
use flipbox\saml\sp\Saml;
16
use yii\base\UserException;
17
use SAML2\Response as SamlResponse;
18
19
/**
20
 * Class User
21
 * @package flipbox\saml\sp\services
22
 */
23
class User
24
{
25
    use AssertionTrait;
26
27
    /**
28
     * @param SamlResponse $response
29
     * @return UserElement
30
     * @throws InvalidMessage
31
     * @throws UserException
32
     */
33
    public function getByResponse(SamlResponse $response)
34
    {
35
36
        $assertion = $this->getFirstAssertion($response);
37
38
        if (! $assertion->getNameId()) {
39
            throw new InvalidMessage('Name ID is missing.');
40
        }
41
42
        Saml::debug('NameId: ' . $assertion->getNameId()->getValue());
43
        /**
44
         * Get username from the NameID
45
         *
46
         * @todo Give an option to map another attribute value to $username (like email)
47
         */
48
        $username = $assertion->getNameId()->getValue();
49
50
        return $this->find($username);
51
    }
52
53
    /**
54
     * @param ProviderIdentityRecord $identity
55
     * @return bool
56
     * @throws UserException
57
     * @throws \Throwable
58
     */
59
    public function login(\flipbox\saml\sp\records\ProviderIdentityRecord $identity)
60
    {
61
        if ($identity->getUser()->getStatus() !== UserElement::STATUS_ACTIVE) {
62
            if (! \Craft::$app->getUsers()->activateUser($identity->getUser())) {
63
                throw new UserException("Can't activate user.");
64
            }
65
        }
66
67
        if (\Craft::$app->getUser()->login(
68
            $identity->getUser(),
69
            /**
70
             * @todo read session duration from the response
71
             */
72
            \Craft::$app->getConfig()->getGeneral()->userSessionDuration
73
        )
74
        ) {
75
            $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...
76
        } else {
77
            throw new UserException("User login failed.");
78
        }
79
80
        return true;
81
    }
82
83
    /**
84
     * @param UserElement $user
85
     * @param SamlResponse $response
86
     * @throws UserException
87
     * @throws \Throwable
88
     * @throws \craft\errors\ElementNotFoundException
89
     * @throws \yii\base\Exception
90
     */
91
    public function sync(UserElement $user, SamlResponse $response)
92
    {
93
94
        // enable and transform the user
95
        $this->construct($user, $response);
96
97
98
        // Save
99
        $this->save($user);
100
101
102
        // Sync groups depending on the plugin setting.
103
        Saml::getInstance()->getUserGroups()->sync($user, $response);
104
105
106
        // Sync defaults
107
        Saml::getInstance()->getUserGroups()->assignDefaultGroups($user);
108
    }
109
110
    /**
111
     * @param UserElement $user
112
     * @return bool
113
     * @throws UserException
114
     * @throws \Throwable
115
     * @throws \craft\errors\ElementNotFoundException
116
     * @throws \yii\base\Exception
117
     */
118
    protected function save(UserElement $user)
119
    {
120
        if (! \Craft::$app->getElements()->saveElement($user)) {
121
            Saml::error(
122
                'User save failed: ' . \json_encode($user->getErrors())
123
            );
124
            throw new UserException("User save failed: " . \json_encode($user->getErrors()));
125
        }
126
127
        return true;
128
    }
129
130
    /**
131
     * Response Based Methods
132
     */
133
134
    /**
135
     * @param UserElement $user
136
     * @param SamlResponse $response
137
     * @throws UserException
138
     * @throws \Throwable
139
     */
140
    protected function construct(UserElement $user, SamlResponse $response)
141
    {
142
        /**
143
         * Is User Active?
144
         */
145
        if (! UserHelper::isUserActive($user)) {
146
            if (! Saml::getInstance()->getSettings()->enableUsers) {
147
                throw new UserException('User access denied.');
148
            }
149
            UserHelper::enableUser($user);
150
        }
151
152
        foreach ($this->getAssertions($response) as $assertion) {
153
            $hasAttributes = count($assertion->getAttributes()) > 1;
154
            Saml::debug('assertion attributes: ' . \json_encode($assertion->getAttributes()));
155
            if ($hasAttributes) {
156
                $this->transform($response, $user);
157
            } else {
158
                /**
159
                 * There doesn't seem to be any attribute statements.
160
                 * Try and use username for the email and move on.
161
                 */
162
                Saml::warning(
163
                    'No attribute statements found! Trying to assign username as the email.'
164
                );
165
                $user->email = $user->email ?: $user->username;
166
            }
167
        }
168
    }
169
170
    /**
171
     * @param SamlResponse $response
172
     * @param UserElement $user
173
     * @return UserElement
174
     */
175
    protected function transform(
176
        SamlResponse $response,
177
        UserElement $user
178
    ) {
179
180
        foreach ($this->getAssertions($response) as $assertion) {
181
            /**
182
             * Check the provider first
183
             */
184
            $attributeMap = ProviderHelper::providerMappingToKeyValue(
185
                $idpProvider = Saml::getInstance()->getProvider()->findByEntityId(
186
                    MessageHelper::getIssuer($response->getIssuer())
187
                )->one()
188
            ) ?:
189
                Saml::getInstance()->getSettings()->responseAttributeMap;
190
191
            Saml::debug('Attribute Map: ' . json_encode($attributeMap));
192
193
            /**
194
             * Loop thru attributes and set to the user
195
             */
196
            foreach ($assertion->getAttributes() as $attributeName => $attributeValue) {
197
                Saml::debug('Attributes: ' . $attributeName . ' ' . json_encode($attributeValue));
198
                if (isset($attributeMap[$attributeName])) {
199
                    $craftProperty = $attributeMap[$attributeName];
200
                    $this->assignProperty(
201
                        $user,
202
                        $attributeName,
203
                        $attributeValue,
204
                        $craftProperty
205
                    );
206
                } else {
207
                    Saml::debug('No match for: ' . $attributeName);
208
                }
209
            }
210
        }
211
212
        return $user;
213
    }
214
215
    protected function assignProperty(
216
        UserElement $user,
217
        $attributeName,
218
        $attributeValue,
219
        $craftProperty
220
    ) {
221
222
        $originalValues = $attributeValue;
223
        if (is_array($attributeValue)) {
224
            $attributeValue = isset($attributeValue[0]) ? $attributeValue[0] : null;
225
        }
226
227
        if (is_string($craftProperty) && in_array($craftProperty, $user->attributes())) {
228
            Saml::debug(
229
                sprintf(
230
                    'Attribute %s is scalar and should set value "%s" to user->%s',
231
                    $attributeName,
232
                    $attributeValue,
233
                    $craftProperty
234
                )
235
            );
236
            $user->{$craftProperty} = $attributeValue;
237
        } elseif (is_callable($craftProperty)) {
238
            Saml::debug(
239
                sprintf(
240
                    'Attribute %s is handled with a callable.',
241
                    $attributeName
242
                )
243
            );
244
245
            call_user_func($craftProperty, $user, [
246
                $attributeName => $originalValues,
247
            ]);
248
        }
249
    }
250
251
    /**************************************************
252
     * Craft User Methods
253
     **************************************************/
254
255
    /**
256
     * @param $username
257
     * @return UserElement
258
     * @throws UserException
259
     */
260
    protected function find($username)
261
    {
262
        return $this->forceGet($username);
263
    }
264
265
    /**
266
     * @param $username
267
     * @return UserElement
268
     * @throws UserException
269
     */
270
    protected function forceGet($username)
271
    {
272
273
        /**
274
         * Is there a user that exists already?
275
         */
276
        if ($user = $this->getByUsernameOrEmail($username)) {
277
            /**
278
             * System check for whether we are allowed merge with this this user
279
             */
280
            if (! Saml::getInstance()->getSettings()->mergeLocalUsers) {
281
                //don't continue
282
                throw new UserException(
283
                    sprintf(
284
                        "User (%s) already exists.",
285
                        $username
286
                    )
287
                );
288
            }
289
        } else {
290
            /**
291
             * New UserElement
292
             */
293
            $user = new UserElement(
294
                [
295
                    'username' => $username,
296
                ]
297
            );
298
        }
299
300
        return $user;
301
    }
302
303
    /**
304
     * @param $emailOrUsername
305
     * @return UserElement|null
306
     */
307
    protected function getByUsernameOrEmail($usernameOrEmail, bool $archived = false)
308
    {
309
310
        return UserElement::find()
311
            ->where(
312
                [
313
                    'or',
314
                    ['username' => $usernameOrEmail],
315
                    ['email' => $usernameOrEmail],
316
                ]
317
            )
318
            ->addSelect(['users.password', 'users.passwordResetRequired'])
319
            ->status(null)
320
            ->archived($archived)
321
            ->one();
322
    }
323
}
324