Completed
Push — develop ( 53ea00...48c889 )
by Damien
03:25 queued 20s
created

User   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 290
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 12
dl 0
loc 290
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getByResponse() 0 18 2
A login() 0 23 4
A sync() 0 17 1
A save() 0 11 2
A construct() 0 28 5
A transform() 0 34 4
A assignProperty() 0 27 4
A find() 0 4 1
A forceGet() 0 32 3
A getByUsernameOrEmail() 0 16 1
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\sp\helpers\ProviderHelper;
12
use flipbox\saml\sp\helpers\UserHelper;
13
use flipbox\saml\sp\records\ProviderIdentityRecord;
14
use flipbox\saml\sp\Saml;
15
use LightSaml\Error\LightSamlException;
16
use LightSaml\Model\Assertion\Attribute;
17
use LightSaml\Model\Protocol\Response as SamlResponse;
18
use yii\base\UserException;
19
20
/**
21
 * Class User
22
 * @package flipbox\saml\sp\services
23
 */
24
class User
25
{
26
    use AssertionTrait;
27
28
    /**
29
     * @param SamlResponse $response
30
     * @return UserElement
31
     * @throws InvalidMessage
32
     * @throws UserException
33
     */
34
    public function getByResponse(\LightSaml\Model\Protocol\Response $response)
35
    {
36
37
        $assertion = $this->getFirstAssertion($response);
38
39
        if (! $assertion->getSubject()->getNameID()) {
40
            throw new LightSamlException('Name ID is missing.');
41
        }
42
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->getSubject()->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, \LightSaml\Model\Protocol\Response $response)
92
    {
93
        /**
94
         * enable and transform the user
95
         */
96
        $this->construct($user, $response);
97
98
        /**
99
         * Save
100
         */
101
        $this->save($user);
102
103
        /**
104
         * Sync groups depending on the plugin setting.
105
         */
106
        Saml::getInstance()->getUserGroups()->syncByAssertion($user, $this->getFirstAssertion($response));
107
    }
108
109
    /**
110
     * @param UserElement $user
111
     * @return bool
112
     * @throws UserException
113
     * @throws \Throwable
114
     * @throws \craft\errors\ElementNotFoundException
115
     * @throws \yii\base\Exception
116
     */
117
    protected function save(UserElement $user)
118
    {
119
        if (! \Craft::$app->getElements()->saveElement($user)) {
120
            Saml::error(
121
                'User save failed: ' . json_encode($user->getErrors())
122
            );
123
            throw new UserException("User save failed: " . json_encode($user->getErrors()));
124
        }
125
126
        return true;
127
    }
128
129
    /**
130
     * Response Based Methods
131
     */
132
133
    /**
134
     * @param UserElement $user
135
     * @param SamlResponse $response
136
     * @throws UserException
137
     * @throws \Throwable
138
     */
139
    protected function construct(UserElement $user, \LightSaml\Model\Protocol\Response $response)
140
    {
141
        /**
142
         * Is User Active?
143
         */
144
        if (! UserHelper::isUserActive($user)) {
145
            if (! Saml::getInstance()->getSettings()->enableUsers) {
146
                throw new UserException('User access denied.');
147
            }
148
            UserHelper::enableUser($user);
149
        }
150
151
        $assertion = $this->getFirstAssertion($response);
152
153
        if ($assertion->getFirstAttributeStatement()) {
154
            $this->transform($response, $user);
155
        } else {
156
            /**
157
             * There doesn't seem to be any attribute statements.
158
             * Try and use username for the email and move on.
159
             */
160
            \Craft::warning(
161
                'No attribute statements found! Trying to assign username as the email.',
162
                Saml::getInstance()->getHandle()
163
            );
164
            $user->email = $user->email ?: $user->username;
165
        }
166
    }
167
168
    /**
169
     * @param SamlResponse $response
170
     * @param UserElement $user
171
     * @return UserElement
172
     */
173
    protected function transform(
174
        SamlResponse $response,
175
        UserElement $user
176
    )
177
    {
178
179
        $assertion = $response->getFirstAssertion();
180
181
        /**
182
         * Check the provider first
183
         */
184
        $attributeMap = ProviderHelper::providerMappingToKeyValue(
185
            $idpProvider = Saml::getInstance()->getProvider()->findByEntityId(
186
                $response->getIssuer()->getValue()
187
            )->one()
188
        ) ?:
189
            Saml::getInstance()->getSettings()->responseAttributeMap;
190
191
        /**
192
         * Loop thru attributes and set to the user
193
         */
194
        foreach ($assertion->getFirstAttributeStatement()->getAllAttributes() as $attribute) {
195
            if (isset($attributeMap[$attribute->getName()])) {
196
                $craftProperty = $attributeMap[$attribute->getName()];
197
                $this->assignProperty(
198
                    $user,
199
                    $attribute,
200
                    $craftProperty
201
                );
202
            }
203
        }
204
205
        return $user;
206
    }
207
208
    /**
209
     * @param User $user
210
     * @param Attribute $attribute
211
     * @param mixed $craftProperty
212
     */
213
    protected function assignProperty(
214
        UserElement $user,
215
        Attribute $attribute,
216
        $craftProperty
217
    )
218
    {
219
220
        if (is_string($craftProperty) && property_exists($user, $craftProperty)) {
221
            Saml::debug(
222
                sprintf(
223
                    'Attribute %s is scalar and should set value "%s" to user->%s',
224
                    $attribute->getName(),
225
                    $attribute->getFirstAttributeValue(),
226
                    $craftProperty
227
                )
228
            );
229
            $user->{$craftProperty} = $attribute->getFirstAttributeValue();
230
        } elseif (is_callable($craftProperty)) {
231
            Saml::debug(
232
                sprintf(
233
                    'Attribute %s is handled with a callable.',
234
                    $attribute->getName()
235
                )
236
            );
237
            call_user_func($craftProperty, $user, $attribute);
238
        }
239
    }
240
241
    /**************************************************
242
     * Craft User Methods
243
     **************************************************/
244
245
    /**
246
     * @param $username
247
     * @return UserElement
248
     * @throws UserException
249
     */
250
    protected function find($username)
251
    {
252
        return $this->forceGet($username);
253
    }
254
255
    /**
256
     * @param $username
257
     * @return UserElement
258
     * @throws UserException
259
     */
260
    protected function forceGet($username)
261
    {
262
263
        /**
264
         * Is there a user that exists already?
265
         */
266
        if ($user = $this->getByUsernameOrEmail($username)) {
267
            /**
268
             * System check for whether we are allowed merge with this this user
269
             */
270
            if (! Saml::getInstance()->getSettings()->mergeLocalUsers) {
271
                //don't continue
272
                throw new UserException(
273
                    sprintf(
274
                        "User (%s) already exists.",
275
                        $username
276
                    )
277
                );
278
            }
279
        } else {
280
            /**
281
             * New UserElement
282
             */
283
            $user = new UserElement(
284
                [
285
                    'username' => $username
286
                ]
287
            );
288
        }
289
290
        return $user;
291
    }
292
293
    /**
294
     * @param $emailOrUsername
295
     * @return UserElement|null
296
     */
297
    protected function getByUsernameOrEmail($usernameOrEmail, bool $archived = false)
298
    {
299
300
        return UserElement::find()
301
            ->where(
302
                [
303
                    'or',
304
                    ['username' => $usernameOrEmail],
305
                    ['email' => $usernameOrEmail]
306
                ]
307
            )
308
            ->addSelect(['users.password', 'users.passwordResetRequired'])
309
            ->status(null)
310
            ->archived($archived)
311
            ->one();
312
    }
313
}
314