Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php
13422 views
1
<?php
2
3
final class PhabricatorLDAPAuthProvider extends PhabricatorAuthProvider {
4
5
private $adapter;
6
7
public function getProviderName() {
8
return pht('LDAP');
9
}
10
11
public function getDescriptionForCreate() {
12
return pht(
13
'Configure a connection to an LDAP server so that users can use their '.
14
'LDAP credentials to log in.');
15
}
16
17
public function getDefaultProviderConfig() {
18
return parent::getDefaultProviderConfig()
19
->setProperty(self::KEY_PORT, 389)
20
->setProperty(self::KEY_VERSION, 3);
21
}
22
23
public function getAdapter() {
24
if (!$this->adapter) {
25
$conf = $this->getProviderConfig();
26
27
$realname_attributes = $conf->getProperty(self::KEY_REALNAME_ATTRIBUTES);
28
if (!is_array($realname_attributes)) {
29
$realname_attributes = array();
30
}
31
32
$search_attributes = $conf->getProperty(self::KEY_SEARCH_ATTRIBUTES);
33
$search_attributes = phutil_split_lines($search_attributes, false);
34
$search_attributes = array_filter($search_attributes);
35
36
$adapter = id(new PhutilLDAPAuthAdapter())
37
->setHostname(
38
$conf->getProperty(self::KEY_HOSTNAME))
39
->setPort(
40
$conf->getProperty(self::KEY_PORT))
41
->setBaseDistinguishedName(
42
$conf->getProperty(self::KEY_DISTINGUISHED_NAME))
43
->setSearchAttributes($search_attributes)
44
->setUsernameAttribute(
45
$conf->getProperty(self::KEY_USERNAME_ATTRIBUTE))
46
->setRealNameAttributes($realname_attributes)
47
->setLDAPVersion(
48
$conf->getProperty(self::KEY_VERSION))
49
->setLDAPReferrals(
50
$conf->getProperty(self::KEY_REFERRALS))
51
->setLDAPStartTLS(
52
$conf->getProperty(self::KEY_START_TLS))
53
->setAlwaysSearch($conf->getProperty(self::KEY_ALWAYS_SEARCH))
54
->setAnonymousUsername(
55
$conf->getProperty(self::KEY_ANONYMOUS_USERNAME))
56
->setAnonymousPassword(
57
new PhutilOpaqueEnvelope(
58
$conf->getProperty(self::KEY_ANONYMOUS_PASSWORD)))
59
->setActiveDirectoryDomain(
60
$conf->getProperty(self::KEY_ACTIVEDIRECTORY_DOMAIN));
61
$this->adapter = $adapter;
62
}
63
return $this->adapter;
64
}
65
66
protected function renderLoginForm(AphrontRequest $request, $mode) {
67
$viewer = $request->getUser();
68
69
$dialog = id(new AphrontDialogView())
70
->setSubmitURI($this->getLoginURI())
71
->setUser($viewer);
72
73
if ($mode == 'link') {
74
$dialog->setTitle(pht('Link LDAP Account'));
75
$dialog->addSubmitButton(pht('Link Accounts'));
76
$dialog->addCancelButton($this->getSettingsURI());
77
} else if ($mode == 'refresh') {
78
$dialog->setTitle(pht('Refresh LDAP Account'));
79
$dialog->addSubmitButton(pht('Refresh Account'));
80
$dialog->addCancelButton($this->getSettingsURI());
81
} else {
82
if ($this->shouldAllowRegistration()) {
83
$dialog->setTitle(pht('Log In or Register with LDAP'));
84
$dialog->addSubmitButton(pht('Log In or Register'));
85
} else {
86
$dialog->setTitle(pht('Log In with LDAP'));
87
$dialog->addSubmitButton(pht('Log In'));
88
}
89
if ($mode == 'login') {
90
$dialog->addCancelButton($this->getStartURI());
91
}
92
}
93
94
$v_user = $request->getStr('ldap_username');
95
96
$e_user = null;
97
$e_pass = null;
98
99
$errors = array();
100
if ($request->isHTTPPost()) {
101
// NOTE: This is intentionally vague so as not to disclose whether a
102
// given username exists.
103
$e_user = pht('Invalid');
104
$e_pass = pht('Invalid');
105
$errors[] = pht('Username or password are incorrect.');
106
}
107
108
$form = id(new PHUIFormLayoutView())
109
->setUser($viewer)
110
->setFullWidth(true)
111
->appendChild(
112
id(new AphrontFormTextControl())
113
->setLabel(pht('LDAP Username'))
114
->setName('ldap_username')
115
->setAutofocus(true)
116
->setValue($v_user)
117
->setError($e_user))
118
->appendChild(
119
id(new AphrontFormPasswordControl())
120
->setLabel(pht('LDAP Password'))
121
->setName('ldap_password')
122
->setError($e_pass));
123
124
if ($errors) {
125
$errors = id(new PHUIInfoView())->setErrors($errors);
126
}
127
128
$dialog->appendChild($errors);
129
$dialog->appendChild($form);
130
131
132
return $dialog;
133
}
134
135
public function processLoginRequest(
136
PhabricatorAuthLoginController $controller) {
137
138
$request = $controller->getRequest();
139
$viewer = $request->getUser();
140
$response = null;
141
$account = null;
142
143
$username = $request->getStr('ldap_username');
144
$password = $request->getStr('ldap_password');
145
$has_password = strlen($password);
146
$password = new PhutilOpaqueEnvelope($password);
147
148
if (!strlen($username) || !$has_password) {
149
$response = $controller->buildProviderPageResponse(
150
$this,
151
$this->renderLoginForm($request, 'login'));
152
return array($account, $response);
153
}
154
155
if ($request->isFormPost()) {
156
try {
157
if (strlen($username) && $has_password) {
158
$adapter = $this->getAdapter();
159
$adapter->setLoginUsername($username);
160
$adapter->setLoginPassword($password);
161
162
// TODO: This calls ldap_bind() eventually, which dumps cleartext
163
// passwords to the error log. See note in PhutilLDAPAuthAdapter.
164
// See T3351.
165
166
DarkConsoleErrorLogPluginAPI::enableDiscardMode();
167
$identifiers = $adapter->getAccountIdentifiers();
168
DarkConsoleErrorLogPluginAPI::disableDiscardMode();
169
} else {
170
throw new Exception(pht('Username and password are required!'));
171
}
172
} catch (PhutilAuthCredentialException $ex) {
173
$response = $controller->buildProviderPageResponse(
174
$this,
175
$this->renderLoginForm($request, 'login'));
176
return array($account, $response);
177
} catch (Exception $ex) {
178
// TODO: Make this cleaner.
179
throw $ex;
180
}
181
}
182
183
$account = $this->newExternalAccountForIdentifiers($identifiers);
184
185
return array($account, $response);
186
}
187
188
189
const KEY_HOSTNAME = 'ldap:host';
190
const KEY_PORT = 'ldap:port';
191
const KEY_DISTINGUISHED_NAME = 'ldap:dn';
192
const KEY_SEARCH_ATTRIBUTES = 'ldap:search-attribute';
193
const KEY_USERNAME_ATTRIBUTE = 'ldap:username-attribute';
194
const KEY_REALNAME_ATTRIBUTES = 'ldap:realname-attributes';
195
const KEY_VERSION = 'ldap:version';
196
const KEY_REFERRALS = 'ldap:referrals';
197
const KEY_START_TLS = 'ldap:start-tls';
198
// TODO: This is misspelled! See T13005.
199
const KEY_ANONYMOUS_USERNAME = 'ldap:anoynmous-username';
200
const KEY_ANONYMOUS_PASSWORD = 'ldap:anonymous-password';
201
const KEY_ALWAYS_SEARCH = 'ldap:always-search';
202
const KEY_ACTIVEDIRECTORY_DOMAIN = 'ldap:activedirectory-domain';
203
204
private function getPropertyKeys() {
205
return array_keys($this->getPropertyLabels());
206
}
207
208
private function getPropertyLabels() {
209
return array(
210
self::KEY_HOSTNAME => pht('LDAP Hostname'),
211
self::KEY_PORT => pht('LDAP Port'),
212
self::KEY_DISTINGUISHED_NAME => pht('Base Distinguished Name'),
213
self::KEY_SEARCH_ATTRIBUTES => pht('Search Attributes'),
214
self::KEY_ALWAYS_SEARCH => pht('Always Search'),
215
self::KEY_ANONYMOUS_USERNAME => pht('Anonymous Username'),
216
self::KEY_ANONYMOUS_PASSWORD => pht('Anonymous Password'),
217
self::KEY_USERNAME_ATTRIBUTE => pht('Username Attribute'),
218
self::KEY_REALNAME_ATTRIBUTES => pht('Realname Attributes'),
219
self::KEY_VERSION => pht('LDAP Version'),
220
self::KEY_REFERRALS => pht('Enable Referrals'),
221
self::KEY_START_TLS => pht('Use TLS'),
222
self::KEY_ACTIVEDIRECTORY_DOMAIN => pht('ActiveDirectory Domain'),
223
);
224
}
225
226
public function readFormValuesFromProvider() {
227
$properties = array();
228
foreach ($this->getPropertyLabels() as $key => $ignored) {
229
$properties[$key] = $this->getProviderConfig()->getProperty($key);
230
}
231
return $properties;
232
}
233
234
public function readFormValuesFromRequest(AphrontRequest $request) {
235
$values = array();
236
foreach ($this->getPropertyKeys() as $key) {
237
switch ($key) {
238
case self::KEY_REALNAME_ATTRIBUTES:
239
$values[$key] = $request->getStrList($key, array());
240
break;
241
default:
242
$values[$key] = $request->getStr($key);
243
break;
244
}
245
}
246
247
return $values;
248
}
249
250
public function processEditForm(
251
AphrontRequest $request,
252
array $values) {
253
$errors = array();
254
$issues = array();
255
return array($errors, $issues, $values);
256
}
257
258
public static function assertLDAPExtensionInstalled() {
259
if (!function_exists('ldap_bind')) {
260
throw new Exception(
261
pht(
262
'Before you can set up or use LDAP, you need to install the PHP '.
263
'LDAP extension. It is not currently installed, so PHP can not '.
264
'talk to LDAP. Usually you can install it with '.
265
'`%s`, `%s`, or a similar package manager command.',
266
'yum install php-ldap',
267
'apt-get install php5-ldap'));
268
}
269
}
270
271
public function extendEditForm(
272
AphrontRequest $request,
273
AphrontFormView $form,
274
array $values,
275
array $issues) {
276
277
self::assertLDAPExtensionInstalled();
278
279
$labels = $this->getPropertyLabels();
280
281
$captions = array(
282
self::KEY_HOSTNAME =>
283
pht('Example: %s%sFor LDAPS, use: %s',
284
phutil_tag('tt', array(), pht('ldap.example.com')),
285
phutil_tag('br'),
286
phutil_tag('tt', array(), pht('ldaps://ldaps.example.com/'))),
287
self::KEY_DISTINGUISHED_NAME =>
288
pht('Example: %s',
289
phutil_tag('tt', array(), pht('ou=People, dc=example, dc=com'))),
290
self::KEY_USERNAME_ATTRIBUTE =>
291
pht('Example: %s',
292
phutil_tag('tt', array(), pht('sn'))),
293
self::KEY_REALNAME_ATTRIBUTES =>
294
pht('Example: %s',
295
phutil_tag('tt', array(), pht('firstname, lastname'))),
296
self::KEY_REFERRALS =>
297
pht('Follow referrals. Disable this for Windows AD 2003.'),
298
self::KEY_START_TLS =>
299
pht('Start TLS after binding to the LDAP server.'),
300
self::KEY_ALWAYS_SEARCH =>
301
pht('Always bind and search, even without a username and password.'),
302
);
303
304
$types = array(
305
self::KEY_REFERRALS => 'checkbox',
306
self::KEY_START_TLS => 'checkbox',
307
self::KEY_SEARCH_ATTRIBUTES => 'textarea',
308
self::KEY_REALNAME_ATTRIBUTES => 'list',
309
self::KEY_ANONYMOUS_PASSWORD => 'password',
310
self::KEY_ALWAYS_SEARCH => 'checkbox',
311
);
312
313
$instructions = array(
314
self::KEY_SEARCH_ATTRIBUTES => pht(
315
"When a user provides their LDAP username and password, this ".
316
"software can either bind to LDAP with those credentials directly ".
317
"(which is simpler, but not as powerful) or bind to LDAP with ".
318
"anonymous credentials, then search for record matching the supplied ".
319
"credentials (which is more complicated, but more powerful).\n\n".
320
"For many installs, direct binding is sufficient. However, you may ".
321
"want to search first if:\n\n".
322
" - You want users to be able to log in with either their username ".
323
" or their email address.\n".
324
" - The login/username is not part of the distinguished name in ".
325
" your LDAP records.\n".
326
" - You want to restrict logins to a subset of users (like only ".
327
" those in certain departments).\n".
328
" - Your LDAP server is configured in some other way that prevents ".
329
" direct binding from working correctly.\n\n".
330
"**To bind directly**, enter the LDAP attribute corresponding to the ".
331
"login name into the **Search Attributes** box below. Often, this is ".
332
"something like `sn` or `uid`. This is the simplest configuration, ".
333
"but will only work if the username is part of the distinguished ".
334
"name, and won't let you apply complex restrictions to logins.\n\n".
335
" lang=text,name=Simple Direct Binding\n".
336
" sn\n\n".
337
"**To search first**, provide an anonymous username and password ".
338
"below (or check the **Always Search** checkbox), then enter one ".
339
"or more search queries into this field, one per line. ".
340
"After binding, these queries will be used to identify the ".
341
"record associated with the login name the user typed.\n\n".
342
"Searches will be tried in order until a matching record is found. ".
343
"Each query can be a simple attribute name (like `sn` or `mail`), ".
344
"which will search for a matching record, or it can be a complex ".
345
"query that uses the string `\${login}` to represent the login ".
346
"name.\n\n".
347
"A common simple configuration is just an attribute name, like ".
348
"`sn`, which will work the same way direct binding works:\n\n".
349
" lang=text,name=Simple Example\n".
350
" sn\n\n".
351
"A slightly more complex configuration might let the user log in with ".
352
"either their login name or email address:\n\n".
353
" lang=text,name=Match Several Attributes\n".
354
" mail\n".
355
" sn\n\n".
356
"If your LDAP directory is more complex, or you want to perform ".
357
"sophisticated filtering, you can use more complex queries. Depending ".
358
"on your directory structure, this example might allow users to log ".
359
"in with either their email address or username, but only if they're ".
360
"in specific departments:\n\n".
361
" lang=text,name=Complex Example\n".
362
" (&(mail=\${login})(|(departmentNumber=1)(departmentNumber=2)))\n".
363
" (&(sn=\${login})(|(departmentNumber=1)(departmentNumber=2)))\n\n".
364
"All of the attribute names used here are just examples: your LDAP ".
365
"server may use different attribute names."),
366
self::KEY_ALWAYS_SEARCH => pht(
367
'To search for an LDAP record before authenticating, either check '.
368
'the **Always Search** checkbox or enter an anonymous '.
369
'username and password to use to perform the search.'),
370
self::KEY_USERNAME_ATTRIBUTE => pht(
371
'Optionally, specify a username attribute to use to prefill usernames '.
372
'when registering a new account. This is purely cosmetic and does not '.
373
'affect the login process, but you can configure it to make sure '.
374
'users get the same default username as their LDAP username, so '.
375
'usernames remain consistent across systems.'),
376
self::KEY_REALNAME_ATTRIBUTES => pht(
377
'Optionally, specify one or more comma-separated attributes to use to '.
378
'prefill the "Real Name" field when registering a new account. This '.
379
'is purely cosmetic and does not affect the login process, but can '.
380
'make registration a little easier.'),
381
);
382
383
foreach ($labels as $key => $label) {
384
$caption = idx($captions, $key);
385
$type = idx($types, $key);
386
$value = idx($values, $key);
387
388
$control = null;
389
switch ($type) {
390
case 'checkbox':
391
$control = id(new AphrontFormCheckboxControl())
392
->addCheckbox(
393
$key,
394
1,
395
hsprintf('<strong>%s:</strong> %s', $label, $caption),
396
$value);
397
break;
398
case 'list':
399
$control = id(new AphrontFormTextControl())
400
->setName($key)
401
->setLabel($label)
402
->setCaption($caption)
403
->setValue($value ? implode(', ', $value) : null);
404
break;
405
case 'password':
406
$control = id(new AphrontFormPasswordControl())
407
->setName($key)
408
->setLabel($label)
409
->setCaption($caption)
410
->setDisableAutocomplete(true)
411
->setValue($value);
412
break;
413
case 'textarea':
414
$control = id(new AphrontFormTextAreaControl())
415
->setName($key)
416
->setLabel($label)
417
->setCaption($caption)
418
->setValue($value);
419
break;
420
default:
421
$control = id(new AphrontFormTextControl())
422
->setName($key)
423
->setLabel($label)
424
->setCaption($caption)
425
->setValue($value);
426
break;
427
}
428
429
$instruction_text = idx($instructions, $key);
430
if (strlen($instruction_text)) {
431
$form->appendRemarkupInstructions($instruction_text);
432
}
433
434
$form->appendChild($control);
435
}
436
}
437
438
public function renderConfigPropertyTransactionTitle(
439
PhabricatorAuthProviderConfigTransaction $xaction) {
440
441
$author_phid = $xaction->getAuthorPHID();
442
$old = $xaction->getOldValue();
443
$new = $xaction->getNewValue();
444
$key = $xaction->getMetadataValue(
445
PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
446
447
$labels = $this->getPropertyLabels();
448
if (isset($labels[$key])) {
449
$label = $labels[$key];
450
451
$mask = false;
452
switch ($key) {
453
case self::KEY_ANONYMOUS_PASSWORD:
454
$mask = true;
455
break;
456
}
457
458
if ($mask) {
459
return pht(
460
'%s updated the "%s" value.',
461
$xaction->renderHandleLink($author_phid),
462
$label);
463
}
464
465
if ($old === null || $old === '') {
466
return pht(
467
'%s set the "%s" value to "%s".',
468
$xaction->renderHandleLink($author_phid),
469
$label,
470
$new);
471
} else {
472
return pht(
473
'%s changed the "%s" value from "%s" to "%s".',
474
$xaction->renderHandleLink($author_phid),
475
$label,
476
$old,
477
$new);
478
}
479
}
480
481
return parent::renderConfigPropertyTransactionTitle($xaction);
482
}
483
484
public static function getLDAPProvider() {
485
$providers = self::getAllEnabledProviders();
486
487
foreach ($providers as $provider) {
488
if ($provider instanceof PhabricatorLDAPAuthProvider) {
489
return $provider;
490
}
491
}
492
493
return null;
494
}
495
496
}
497
498