Passed
Push — master ( b2198e...e8e6de )
by Robbie
02:50 queued 10s
created

LDAPMemberExtension::validate()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 3
nop 1
dl 0
loc 15
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\LDAP\Extensions;
4
5
use Exception;
6
use SilverStripe\Core\Injector\Injector;
7
use SilverStripe\Forms\FieldList;
8
use SilverStripe\Forms\LiteralField;
9
use SilverStripe\Forms\ReadonlyField;
10
use SilverStripe\Forms\TextField;
11
use SilverStripe\LDAP\Services\LDAPService;
12
use SilverStripe\ORM\DataExtension;
13
use SilverStripe\ORM\ValidationException;
14
use SilverStripe\ORM\ValidationResult;
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
introduced by
The private property $db is not used, and could 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 = [
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
introduced by
The private property $ldap_thumbnail_path is not used, and could 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;
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;
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;
97
98
    /**
99
     * If enabled, this allows the afterMemberLoggedIn() call to fail to update the user without causing a login failure
100
     * and server error. This can be useful when not all of your web servers have access to the LDAP server (for example
101
     * when your front-line web servers are not the servers that perform the LDAP sync into the database.
102
     *
103
     * Note: If this is enabled, you *must* ensure that a regular sync of both groups and users is carried out by
104
     * running the LDAPGroupSyncTask and LDAPMemberSyncTask. If not, there is no guarantee that this user can still have
105
     * all the permissions that they previously had.
106
     *
107
     * Security risk: If this is enabled, then users who are removed from groups may not have their group membership or
108
     * other information updated until the aforementioned LDAPGroupSyncTask and LDAPMemberSyncTask build tasks are run,
109
     * which can lead to users having incorrect permissions until the next sync happens.
110
     *
111
     * @var bool
112
     * @config
113
     */
114
    private static $allow_update_failure_during_login = false;
0 ignored issues
show
introduced by
The private property $allow_update_failure_during_login is not used, and could be removed.
Loading history...
115
116
    /**
117
     * @param FieldList $fields
118
     */
119
    public function updateCMSFields(FieldList $fields)
120
    {
121
        // Redo LDAP metadata fields as read-only and move to LDAP tab.
122
        $ldapMetadata = [];
123
        $fields->replaceField('GUID', $ldapMetadata[] = ReadonlyField::create('GUID'));
124
        $fields->replaceField(
125
            'IsExpired',
126
            $ldapMetadata[] = ReadonlyField::create(
127
                'IsExpired',
128
                _t(__CLASS__ . '.ISEXPIRED', 'Has user\'s LDAP/AD login expired?')
129
            )
130
        );
131
        $fields->replaceField(
132
            'LastSynced',
133
            $ldapMetadata[] = ReadonlyField::create(
134
                'LastSynced',
135
                _t(__CLASS__ . '.LASTSYNCED', 'Last synced')
136
            )
137
        );
138
        $fields->addFieldsToTab('Root.LDAP', $ldapMetadata);
139
140
        $message = '';
141
        if ($this->owner->GUID && $this->owner->config()->update_ldap_from_local) {
142
            $message = _t(
143
                __CLASS__ . '.CHANGEFIELDSUPDATELDAP',
144
                'Changing fields here will update them in LDAP.'
145
            );
146
        } elseif ($this->owner->GUID && !$this->owner->config()->update_ldap_from_local) {
147
            // Transform the automatically mapped fields into read-only. This doesn't
148
            // apply if updating LDAP from local is enabled, as changing data locally can be written back.
149
            foreach ($this->owner->config()->ldap_field_mappings as $name) {
150
                $field = $fields->dataFieldByName($name);
151
                if (!empty($field)) {
152
                    // Set to readonly, but not disabled so that the data is still sent to the
153
                    // server and doesn't break Member_Validator
154
                    $field->setReadonly(true);
155
                    $field->setTitle($field->Title() . _t(__CLASS__ . '.IMPORTEDFIELD', ' (imported)'));
156
                }
157
            }
158
            $message = _t(
159
                __CLASS__ . '.INFOIMPORTED',
160
                'This user is automatically imported from LDAP. ' .
161
                    'Manual changes to imported fields will be removed upon sync.'
162
            );
163
        }
164
        if ($message) {
165
            $fields->addFieldToTab(
166
                'Root.Main',
167
                LiteralField::create(
168
                    'Info',
169
                    sprintf('<p class="message warning">%s</p>', $message)
170
                ),
171
                'FirstName'
172
            );
173
        }
174
175
        // Ensure blog fields are added after first and last name
176
        $fields->addFieldToTab(
177
            'Root.Main',
178
            TextField::create('Username'),
179
            'Email'
180
        );
181
    }
182
183
    /**
184
     * @param  ValidationResult
185
     * @throws ValidationException
186
     */
187
    public function validate(ValidationResult $validationResult)
188
    {
189
        // We allow empty Username for registration purposes, as we need to
190
        // create Member records with empty Username temporarily. Forms should explicitly
191
        // check for Username not being empty if they require it not to be.
192
        if (empty($this->owner->Username) || !$this->owner->config()->create_users_in_ldap) {
193
            return;
194
        }
195
196
        if (!preg_match('/^[a-z0-9\.]+$/', $this->owner->Username)) {
197
            $validationResult->addError(
198
                'Username must only contain lowercase alphanumeric characters and dots.',
199
                'bad'
200
            );
201
            throw new ValidationException($validationResult);
202
        }
203
    }
204
205
    /**
206
     * Create the user in LDAP, provided this configuration is enabled
207
     * and a username was passed to a new Member record.
208
     */
209
    public function onBeforeWrite()
210
    {
211
        if ($this->owner->LDAPMemberExtension_NoSync) {
212
            return;
213
        }
214
215
        $service = Injector::inst()->get(LDAPService::class);
216
        if (!$service->enabled()
217
            || !$this->owner->config()->create_users_in_ldap
218
            || !$this->owner->Username
219
            || $this->owner->GUID
220
        ) {
221
            return;
222
        }
223
224
        $service->createLDAPUser($this->owner);
225
    }
226
227
    public function onAfterWrite()
228
    {
229
        if ($this->owner->LDAPMemberExtension_NoSync) {
230
            return;
231
        }
232
233
        $service = Injector::inst()->get(LDAPService::class);
234
        if (!$service->enabled() ||
235
            !$this->owner->config()->update_ldap_from_local ||
236
            !$this->owner->GUID
237
        ) {
238
            return;
239
        }
240
        $this->sync();
241
    }
242
243
    public function onAfterDelete()
244
    {
245
        if ($this->owner->LDAPMemberExtension_NoSync) {
246
            return;
247
        }
248
249
        $service = Injector::inst()->get(LDAPService::class);
250
        if (!$service->enabled() ||
251
            !$this->owner->config()->delete_users_in_ldap ||
252
            !$this->owner->GUID
253
        ) {
254
            return;
255
        }
256
257
        $service->deleteLDAPMember($this->owner);
258
    }
259
260
    /**
261
     * Write DataObject without triggering this extension's hooks.
262
     *
263
     * @throws Exception
264
     */
265
    public function writeWithoutSync()
266
    {
267
        $this->owner->LDAPMemberExtension_NoSync = true;
268
        try {
269
            $this->owner->write();
270
        } finally {
271
            $this->owner->LDAPMemberExtension_NoSync = false;
272
        }
273
    }
274
275
    /**
276
     * Update the local data with LDAP, and ensure local membership is also set in
277
     * LDAP too. This writes into LDAP, provided that feature is enabled.
278
     */
279
    public function sync()
280
    {
281
        $service = Injector::inst()->get(LDAPService::class);
282
        if (!$service->enabled() ||
283
            !$this->owner->GUID
284
        ) {
285
            return;
286
        }
287
        $service->updateLDAPFromMember($this->owner);
288
        $service->updateLDAPGroupsForMember($this->owner);
289
    }
290
291
    /**
292
     * @deprecated 1.1.0 Not used by SilverStripe internally and will be removed in 2.0
293
     */
294
    public function memberLoggedIn()
295
    {
296
        return $this->afterMemberLoggedIn();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->afterMemberLoggedIn() targeting SilverStripe\LDAP\Extens...::afterMemberLoggedIn() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
297
    }
298
299
    /**
300
     * Triggered by {@link IdentityStore::logIn()}. When successfully logged in,
301
     * this will update the Member record from LDAP data.
302
     *
303
     * @throws Exception When failures are not acceptable via configuration
304
     */
305
    public function afterMemberLoggedIn()
306
    {
307
        if ($this->owner->GUID) {
308
            try {
309
                Injector::inst()->get(LDAPService::class)->updateMemberFromLDAP($this->owner);
310
            } catch (Exception $e) {
311
                // If the failure is acceptable, then ignore it and return. Otherwise, re-throw the exception
312
                if ($this->owner->config()->get('allow_update_failure_during_login')) {
313
                    return;
314
                }
315
                throw $e;
316
            }
317
        }
318
    }
319
320
    /**
321
     * Synchronise password changes to AD when they happen in SilverStripe
322
     *
323
     * @param string           $newPassword
324
     * @param ValidationResult $validation
325
     */
326
    public function onBeforeChangePassword($newPassword, $validation)
327
    {
328
        // Don't do anything if there's already a validation failure
329
        if (!$validation->isValid()) {
330
            return;
331
        }
332
333
        Injector::inst()->get(LDAPService::class)
334
            ->setPassword($this->owner, $newPassword);
335
    }
336
}
337