Passed
Pull Request — master (#5)
by Robbie
02:50 queued 32s
created

LDAPMemberExtension::onBeforeChangePassword()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\LDAP\Extensions;
4
5
use Exception;
6
use SilverStripe\LDAP\Services\LDAPService;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\Forms\FieldList;
9
use SilverStripe\Forms\LiteralField;
10
use SilverStripe\Forms\ReadonlyField;
11
use SilverStripe\ORM\DataExtension;
12
use SilverStripe\ORM\ValidationResult;
13
use SilverStripe\ORM\ValidationException;
14
use SilverStripe\Security\Member;
15
16
/**
17
 * Class LDAPMemberExtension.
18
 *
19
 * Adds mappings from AD attributes to SilverStripe {@link Member} fields.
20
 *
21
 * @package activedirectory
22
 */
23
class LDAPMemberExtension extends DataExtension
24
{
25
    /**
26
     * @var array
27
     */
28
    private static $db = [
0 ignored issues
show
Unused Code introduced by
The property $db is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
29
        // Unique user identifier
30
        'GUID' => 'Varchar(50)',
31
        'Username' => 'Varchar(64)',
32
        'IsExpired' => 'Boolean',
33
        'LastSynced' => 'DBDatetime',
34
    ];
35
36
    /**
37
     * These fields are used by {@link LDAPMemberSync} to map specific AD attributes
38
     * to {@link Member} fields.
39
     *
40
     * @var array
41
     * @config
42
     */
43
    private static $ldap_field_mappings = [
0 ignored issues
show
Unused Code introduced by
The property $ldap_field_mappings is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
44
        'givenname' => 'FirstName',
45
        'samaccountname' => 'Username',
46
        'sn' => 'Surname',
47
        'mail' => 'Email',
48
    ];
49
50
    /**
51
     * The location (relative to /assets) where to save thumbnailphoto data.
52
     *
53
     * @var string
54
     * @config
55
     */
56
    private static $ldap_thumbnail_path = 'Uploads';
0 ignored issues
show
Unused Code introduced by
The property $ldap_thumbnail_path is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
57
58
    /**
59
     * When enabled, LDAP managed Member records (GUID flag)
60
     * have their data written back to LDAP on write, and synchronise
61
     * membership to groups mapped to LDAP.
62
     *
63
     * Keep in mind this will currently NOT trigger if there are no
64
     * field changes due to onAfterWrite in framework not being called
65
     * when there are no changes.
66
     *
67
     * This requires setting write permissions on the user configured in the LDAP
68
     * credentials, which is why this is disabled by default.
69
     *
70
     * @var bool
71
     * @config
72
     */
73
    private static $update_ldap_from_local = false;
0 ignored issues
show
Unused Code introduced by
The property $update_ldap_from_local is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
74
75
    /**
76
     * If enabled, Member records with a Username field have the user created in LDAP
77
     * on write.
78
     *
79
     * This requires setting write permissions on the user configured in the LDAP
80
     * credentials, which is why this is disabled by default.
81
     *
82
     * @var bool
83
     * @config
84
     */
85
    private static $create_users_in_ldap = false;
0 ignored issues
show
Unused Code introduced by
The property $create_users_in_ldap is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
86
87
    /**
88
     * If enabled, deleting Member records mapped to LDAP deletes the LDAP user.
89
     *
90
     * This requires setting write permissions on the user configured in the LDAP
91
     * credentials, which is why this is disabled by default.
92
     *
93
     * @var bool
94
     * @config
95
     */
96
    private static $delete_users_in_ldap = false;
0 ignored issues
show
Unused Code introduced by
The property $delete_users_in_ldap is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
97
98
    /**
99
     * @param FieldList $fields
100
     */
101
    public function updateCMSFields(FieldList $fields)
102
    {
103
        // Redo LDAP metadata fields as read-only and move to LDAP tab.
104
        $ldapMetadata = [];
105
        $fields->replaceField('GUID', $ldapMetadata[] = ReadonlyField::create('GUID'));
0 ignored issues
show
Bug introduced by
'GUID' of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

105
        $fields->replaceField('GUID', $ldapMetadata[] = ReadonlyField::create(/** @scrutinizer ignore-type */ 'GUID'));
Loading history...
106
        $fields->replaceField(
107
            'IsExpired',
108
            $ldapMetadata[] = ReadonlyField::create(
109
                'IsExpired',
110
                _t(__CLASS__ . '.ISEXPIRED', 'Has user\'s LDAP/AD login expired?')
111
            )
112
        );
113
        $fields->replaceField(
114
            'LastSynced',
115
            $ldapMetadata[] = ReadonlyField::create(
116
                'LastSynced',
117
                _t(__CLASS__ . '.LASTSYNCED', 'Last synced')
118
            )
119
        );
120
        $fields->addFieldsToTab('Root.LDAP', $ldapMetadata);
121
122
        $message = '';
123
        if ($this->owner->GUID && $this->owner->config()->update_ldap_from_local) {
124
            $message = _t(
125
                __CLASS__ . '.CHANGEFIELDSUPDATELDAP',
126
                'Changing fields here will update them in LDAP.'
127
            );
128
        } elseif ($this->owner->GUID && !$this->owner->config()->update_ldap_from_local) {
129
            // Transform the automatically mapped fields into read-only. This doesn't
130
            // apply if updating LDAP from local is enabled, as changing data locally can be written back.
131
            foreach ($this->owner->config()->ldap_field_mappings as $name) {
132
                $field = $fields->dataFieldByName($name);
133
                if (!empty($field)) {
134
                    // Set to readonly, but not disabled so that the data is still sent to the
135
                    // server and doesn't break Member_Validator
136
                    $field->setReadonly(true);
137
                    $field->setTitle($field->Title()._t(__CLASS__ . '.IMPORTEDFIELD', ' (imported)'));
138
                }
139
            }
140
            $message = _t(
141
                __CLASS__ . '.INFOIMPORTED',
142
                'This user is automatically imported from LDAP. '.
143
                    'Manual changes to imported fields will be removed upon sync.'
144
            );
145
        }
146
        if ($message) {
147
            $fields->addFieldToTab(
148
                'Root.Main',
149
                LiteralField::create(
150
                    'Info',
151
                    sprintf('<p class="message warning">%s</p>', $message)
152
                ),
153
                'FirstName'
154
            );
155
        }
156
    }
157
158
    /**
159
     * @param  ValidationResult
160
     * @throws ValidationException
161
     */
162
    public function validate(ValidationResult $validationResult)
163
    {
164
        // We allow empty Username for registration purposes, as we need to
165
        // create Member records with empty Username temporarily. Forms should explicitly
166
        // check for Username not being empty if they require it not to be.
167
        if (empty($this->owner->Username) || !$this->owner->config()->create_users_in_ldap) {
168
            return;
169
        }
170
171
        if (!preg_match('/^[a-z0-9\.]+$/', $this->owner->Username)) {
172
            $validationResult->addError(
173
                'Username must only contain lowercase alphanumeric characters and dots.',
174
                'bad'
175
            );
176
            throw new ValidationException($validationResult);
177
        }
178
    }
179
180
    /**
181
     * Create the user in LDAP, provided this configuration is enabled
182
     * and a username was passed to a new Member record.
183
     */
184 View Code Duplication
    public function onBeforeWrite()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
185
    {
186
        if ($this->owner->LDAPMemberExtension_NoSync) {
187
            return;
188
        }
189
190
        $service = Injector::inst()->get(LDAPService::class);
191
        if (!$service->enabled()
192
            || !$this->owner->config()->create_users_in_ldap
193
            || !$this->owner->Username
194
            || $this->owner->GUID
195
        ) {
196
            return;
197
        }
198
199
        $service->createLDAPUser($this->owner);
200
    }
201
202
    public function onAfterWrite()
203
    {
204
        if ($this->owner->LDAPMemberExtension_NoSync) {
205
            return;
206
        }
207
208
        $service = Injector::inst()->get(LDAPService::class);
209
        if (!$service->enabled() ||
210
            !$this->owner->config()->update_ldap_from_local ||
211
            !$this->owner->GUID
212
        ) {
213
            return;
214
        }
215
        $this->sync();
216
    }
217
218 View Code Duplication
    public function onAfterDelete()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
219
    {
220
        if ($this->owner->LDAPMemberExtension_NoSync) {
221
            return;
222
        }
223
224
        $service = Injector::inst()->get(LDAPService::class);
225
        if (!$service->enabled() ||
226
            !$this->owner->config()->delete_users_in_ldap ||
227
            !$this->owner->GUID
228
        ) {
229
            return;
230
        }
231
232
        $service->deleteLDAPMember($this->owner);
233
    }
234
235
    /**
236
     * Write DataObject without triggering this extension's hooks.
237
     *
238
     * @throws Exception
239
     */
240
    public function writeWithoutSync()
241
    {
242
        $this->owner->LDAPMemberExtension_NoSync = true;
243
        try {
244
            $this->owner->write();
245
        } finally {
246
            $this->owner->LDAPMemberExtension_NoSync = false;
247
        }
248
    }
249
250
    /**
251
     * Update the local data with LDAP, and ensure local membership is also set in
252
     * LDAP too. This writes into LDAP, provided that feature is enabled.
253
     */
254
    public function sync()
255
    {
256
        $service = Injector::inst()->get(LDAPService::class);
257
        if (!$service->enabled() ||
258
            !$this->owner->GUID
259
        ) {
260
            return;
261
        }
262
        $service->updateLDAPFromMember($this->owner);
263
        $service->updateLDAPGroupsForMember($this->owner);
264
    }
265
266
    /**
267
     * Triggered by {@link Member::logIn()} when successfully logged in,
268
     * this will update the Member record from AD data.
269
     */
270
    public function memberLoggedIn()
271
    {
272
        if ($this->owner->GUID) {
273
            Injector::inst()
274
                ->get(LDAPService::class)
275
                ->updateMemberFromLDAP($this->owner);
276
        }
277
    }
278
279
    /**
280
     * Synchronise password changes to AD when they happen in SilverStripe
281
     *
282
     * @param string           $newPassword
283
     * @param ValidationResult $validation
284
     */
285
    public function onBeforeChangePassword($newPassword, $validation)
286
    {
287
        // Don't do anything if there's already a validation failure
288
        if (!$validation->isValid()) {
289
            return;
290
        }
291
292
        Injector::inst()->get(LDAPService::class)
293
            ->setPassword($this->owner, $newPassword);
294
    }
295
}
296