RemoteUserHandler   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 223
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
dl 0
loc 223
c 0
b 0
f 0
wmc 21
lcom 1
cbo 10
rs 10

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A loadAPIUserByRemoteUser() 0 11 2
A createRepoUser() 0 34 1
B updateRepoUser() 0 59 7
A getRemoteIdFromProfile() 0 4 1
getGroupsFromProfile() 0 1 ?
setFieldValuesFromProfile() 0 1 ?
A localUserNeedsUpdating() 0 4 1
A profileHash() 0 4 1
A createTempFile() 0 8 1
A cleanUpAfterUserCreation() 0 7 3
A cleanUpAfterUserUpdate() 0 7 3
1
<?php
2
3
namespace Kaliop\IdentityManagementBundle\Security\User;
4
5
use Kaliop\IdentityManagementBundle\Adapter\ClientInterface;
6
use eZ\Publish\API\Repository\Repository;
7
use eZ\Publish\API\Repository\Values\Content\Query;
8
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
9
use eZ\Publish\API\Repository\Values\User\User;
10
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
11
12
/**
13
 * A 'generic' Remote user handler class.
14
 *
15
 * For the common cases, you will need to implement only getGroupsFromProfile() and setFieldValuesFromProfile().
16
 * But you can subclass more methods for more complex scenarios :-)
17
 *
18
 * It handles
19
 * - multiple user groups assignments per user
20
 * - automatic update of the ez user when its ldap profile has changed compared to the stored one
21
 */
22
abstract class RemoteUserHandler implements RemoteUserHandlerInterface
23
{
24
    protected $client;
25
    protected $repository;
26
    protected $settings;
27
    protected $tempFiles = array();
28
29
    protected $remoteIdPrefix = 'ldap_md5:';
30
31
    /**
32
     * @param ClientInterface $client Note: this is currently unused and left in for BC
33
     * @param Repository $repository
34
     * @param array $settings
35
     */
36
    public function __construct(ClientInterface $client, Repository $repository, array $settings)
37
    {
38
        $this->client = $client;
39
        $this->repository = $repository;
40
        $this->settings = $settings;
41
    }
42
43
    /**
44
     * Returns the API user corresponding to a given remoteUser (if it exists), or false.
45
     * @see \eZ\Publish\Core\MVC\Symfony\Security\User\Provider::loadUserByUsername()
46
     *
47
     * @param RemoteUser $remoteUser
48
     * @return \eZ\Publish\API\Repository\Values\User\User|false
49
     */
50
    public function loadAPIUserByRemoteUser(RemoteUser $remoteUser)
51
    {
52
        try
53
        {
54
            return $this->repository->getUserService()->loadUserByLogin($remoteUser->getUsername());
55
        }
56
        catch (NotFoundException $e)
57
        {
58
            return false;
59
        }
60
    }
61
62
    /**
63
     * @param RemoteUser $user
64
     * @return \eZ\Publish\API\Repository\Values\User\User
65
     */
66
    public function createRepoUser(RemoteUser $user)
67
    {
68
        return $this->repository->sudo(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\API\Repository\Repository as the method sudo() does only exist in the following implementations of said interface: eZ\Publish\Core\Repository\Repository, eZ\Publish\Core\SignalSlot\Repository.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
69
            function() use ($user)
70
            {
71
                /// @todo support creating users using a different user account
72
                //$this->repository->setCurrentUser($userService->loadUser($this->settings['user_creator']));
0 ignored issues
show
Unused Code Comprehensibility introduced by
79% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
73
74
                $userService = $this->repository->getUserService();
75
                $profile = $user->getProfile();
76
77
                // the user passwords we do not store locally
78
                $userCreateStruct = $userService->newUserCreateStruct(
79
                    // is 128 bytes enough for everyone? (pun intended)
80
                    $user->getUsername(), $user->getEmail(), bin2hex(random_bytes(128)),
81
                    $this->settings['default_content_language'],
82
                    $this->repository->getContentTypeService()->loadContentTypeByIdentifier($this->settings['user_contenttype'])
83
                );
84
85
                $this->setFieldValuesFromProfile($profile, $userCreateStruct);
86
87
                // store an md5 of the profile, to allow efficient checking of the need for updates
88
                $userCreateStruct->remoteId = $this->getRemoteIdFromProfile($profile);
89
90
                /// @todo test/document what happens when we get an empty array...
91
                $userGroups = $this->getGroupsFromProfile($profile);
92
                $repoUser = $userService->createUser($userCreateStruct, $userGroups);
93
94
                $this->cleanUpAfterUserCreation();
95
96
                return $repoUser;
97
            }
98
        );
99
    }
100
101
    /**
102
     * @param RemoteUser $user
103
     * @param $eZUser (is this an eZ\Publish\API\Repository\Values\User\User ?)
104
     */
105
    public function updateRepoUser(RemoteUser $user, $eZUser)
106
    {
107
        if ($this->localUserNeedsUpdating($user, $eZUser)) {
108
            return $this->repository->sudo(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface eZ\Publish\API\Repository\Repository as the method sudo() does only exist in the following implementations of said interface: eZ\Publish\Core\Repository\Repository, eZ\Publish\Core\SignalSlot\Repository.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
109
                function() use ($user, $eZUser)
110
                {
111
                    $userService = $this->repository->getUserService();
112
                    $contentService = $this->repository->getContentService();
113
                    $profile = $user->getProfile();
114
115
                    $userUpdateStruct = $userService->newUserUpdateStruct();
116
                    $contentUpdateStruct = $contentService->newContentUpdateStruct();
117
                    $this->setFieldValuesFromProfile($profile, $contentUpdateStruct);
0 ignored issues
show
Documentation introduced by
$contentUpdateStruct is of type object<eZ\Publish\API\Re...nt\ContentUpdateStruct>, but the function expects a object<eZ\Publish\API\Re...nt\ContentCreateStruct>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
118
                    $userUpdateStruct->contentUpdateStruct = $contentUpdateStruct;
119
120
                    // update the stored md5 of the profile, to allow efficient checking of the need for updates in the future
121
                    $contentMetadataUpdateStruct = $contentService->newContentMetadataUpdateStruct();
122
                    $contentMetadataUpdateStruct->remoteId = $this->getRemoteIdFromProfile($profile);
123
124
                    // we use a transaction since there are multiple db operations
125
                    try {
126
                        $this->repository->beginTransaction();
127
128
                        $repoUser = $userService->updateUser($eZUser, $userUpdateStruct);
129
130
                        $content = $contentService->updateContentMetadata($repoUser->contentInfo, $contentMetadataUpdateStruct);
0 ignored issues
show
Unused Code introduced by
$content is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
131
132
                        // fix user groups assignments: first add new ones, then remove unused current ones (we can not hit 0 groups during the updating :-) )
133
                        /// @todo test/document what happens when we get an empty array...
134
                        $newUserGroups = $this->getGroupsFromProfile($profile);
135
                        $currentUserGroups = $userService->loadUserGroupsOfUser($eZUser);
136
                        $groupsToRemove = array();
137
                        foreach($currentUserGroups as $currentUserGroup) {
138
                            if (!array_key_exists($currentUserGroup->id, $newUserGroups)) {
139
                                $groupsToRemove[] = $currentUserGroup;
140
                            } else {
141
                                unset($newUserGroups[$currentUserGroup->id]);
142
                            }
143
                        }
144
                        foreach ($newUserGroups as $newUserGroup) {
145
                            $userService->assignUserToUserGroup($repoUser, $newUserGroup);
146
                        }
147
                        foreach ($groupsToRemove as $groupToRemove) {
148
                            $userService->unAssignUserFromUserGroup($repoUser, $groupToRemove);
149
                        }
150
151
                        $this->repository->commit();
152
                    } catch (\Exception $e) {
153
                        $this->repository->rollback();
154
                        $this->cleanUpAfterUserUpdate();
155
                        throw $e;
156
                    }
157
158
                    $this->cleanUpAfterUserUpdate();
159
                    return $repoUser;
160
                }
161
            );
162
        }
163
    }
164
165
    protected function getRemoteIdFromProfile($profile)
166
    {
167
        return $this->remoteIdPrefix . $this->profileHash($profile);
168
    }
169
170
    /**
171
     * Load (and possibly create on the fly) all the user groups needed for this user, based on his profile.
172
     *
173
     * @param $profile
174
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[] indexed by group id
175
     */
176
    abstract protected function getGroupsFromProfile($profile);
177
178
    /**
179
     * @param $profile
180
     * @param \eZ\Publish\API\Repository\Values\Content\ContentCreateStruct $userCreateStruct
181
     *
182
     * @todo allow to define simple field mappings in settings
183
     */
184
    abstract protected function setFieldValuesFromProfile($profile, $userCreateStruct);
185
186
    /**
187
     * Checks if the local user profile needs updating compared to the remote user profile
188
     *
189
     * @param RemoteUser $remoteUser
190
     * @param $eZUser (is this an eZ\Publish\API\Repository\Values\User\User ?)
191
     * @return bool
192
     */
193
    protected function localUserNeedsUpdating(RemoteUser $remoteUser, $eZUser)
194
    {
195
        return $this->getRemoteIdFromProfile($remoteUser->getProfile()) !== $eZUser->contentInfo->remoteId;
196
    }
197
198
    /**
199
     * Generates a unique hash for the user profile
200
     * @param $profile
201
     * @return string
202
     */
203
    protected function profileHash($profile)
204
    {
205
        return md5(var_export($profile, true));
206
    }
207
208
    /**
209
     * A helper for importing data into image/file fields
210
     * @param string $data
211
     * @param string $prefix
212
     * @return string
213
     */
214
    protected function createTempFile($data, $prefix='')
215
    {
216
        $imageFileName = trim(tempnam(sys_get_temp_dir(), $prefix), '.');
217
        file_put_contents($imageFileName, $data);
218
        $this->tempFiles[] = $imageFileName;
219
220
        return $imageFileName;
221
    }
222
223
    /**
224
     * Needed to clean up after createTempFile()
225
     */
226
    protected function cleanUpAfterUserCreation()
227
    {
228
        foreach ($this->tempFiles as $fileName) {
229
            if (is_file( $fileName))
230
                unlink($fileName);
231
        }
232
    }
233
234
    /**
235
     * Needed to clean up after createTempFile()
236
     */
237
    protected function cleanUpAfterUserUpdate()
238
    {
239
        foreach ($this->tempFiles as $fileName) {
240
            if (is_file( $fileName))
241
                unlink($fileName);
242
        }
243
    }
244
}
245