@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorLDAPAuthProvider extends PhabricatorAuthProvider {
4
5 private $adapter;
6
7 public function getProviderName() {
8 return pht('LDAP');
9 }
10
11 protected function getLoginIcon() {
12 return 'LDAP';
13 }
14
15 public function getDescriptionForCreate() {
16 return pht(
17 'Configure a connection to an LDAP server so that users can use their '.
18 'LDAP credentials to log in.');
19 }
20
21 public function getDefaultProviderConfig() {
22 return parent::getDefaultProviderConfig()
23 ->setProperty(self::KEY_PORT, 389)
24 ->setProperty(self::KEY_VERSION, 3);
25 }
26
27 public function getAdapter() {
28 if (!$this->adapter) {
29 $conf = $this->getProviderConfig();
30
31 $realname_attributes = $conf->getProperty(self::KEY_REALNAME_ATTRIBUTES);
32 if (!is_array($realname_attributes)) {
33 $realname_attributes = array();
34 }
35
36 $search_attributes = $conf->getProperty(self::KEY_SEARCH_ATTRIBUTES);
37 $search_attributes = phutil_split_lines($search_attributes, false);
38 $search_attributes = array_filter($search_attributes);
39
40 $adapter = id(new PhutilLDAPAuthAdapter())
41 ->setHostname(
42 $conf->getProperty(self::KEY_HOSTNAME))
43 ->setPort(
44 $conf->getProperty(self::KEY_PORT))
45 ->setBaseDistinguishedName(
46 $conf->getProperty(self::KEY_DISTINGUISHED_NAME))
47 ->setSearchAttributes($search_attributes)
48 ->setUsernameAttribute(
49 $conf->getProperty(self::KEY_USERNAME_ATTRIBUTE))
50 ->setRealNameAttributes($realname_attributes)
51 ->setLDAPVersion(
52 $conf->getProperty(self::KEY_VERSION))
53 ->setLDAPReferrals(
54 $conf->getProperty(self::KEY_REFERRALS))
55 ->setLDAPStartTLS(
56 $conf->getProperty(self::KEY_START_TLS))
57 ->setAlwaysSearch($conf->getProperty(self::KEY_ALWAYS_SEARCH))
58 ->setAnonymousUsername(
59 $conf->getProperty(self::KEY_ANONYMOUS_USERNAME))
60 ->setAnonymousPassword(
61 new PhutilOpaqueEnvelope(
62 $conf->getProperty(self::KEY_ANONYMOUS_PASSWORD)))
63 ->setActiveDirectoryDomain(
64 $conf->getProperty(self::KEY_ACTIVEDIRECTORY_DOMAIN));
65 $this->adapter = $adapter;
66 }
67 return $this->adapter;
68 }
69
70 protected function renderLoginForm(AphrontRequest $request, $mode) {
71 $viewer = $request->getUser();
72
73 $dialog = id(new AphrontDialogView())
74 ->setSubmitURI($this->getLoginURI())
75 ->setUser($viewer);
76
77 if ($mode == 'link') {
78 $dialog->setTitle(pht('Link LDAP Account'));
79 $dialog->addSubmitButton(pht('Link Accounts'));
80 $dialog->addCancelButton($this->getSettingsURI());
81 } else if ($mode == 'refresh') {
82 $dialog->setTitle(pht('Refresh LDAP Account'));
83 $dialog->addSubmitButton(pht('Refresh Account'));
84 $dialog->addCancelButton($this->getSettingsURI());
85 } else {
86 if ($this->shouldAllowRegistration()) {
87 $dialog->setTitle(pht('Log In or Register with LDAP'));
88 $dialog->addSubmitButton(pht('Log In or Register'));
89 } else {
90 $dialog->setTitle(pht('Log In with LDAP'));
91 $dialog->addSubmitButton(pht('Log In'));
92 }
93 if ($mode == 'login') {
94 $dialog->addCancelButton($this->getStartURI());
95 }
96 }
97
98 $v_user = $request->getStr('ldap_username');
99
100 $e_user = null;
101 $e_pass = null;
102
103 $errors = array();
104 if ($request->isHTTPPost()) {
105 // NOTE: This is intentionally vague so as not to disclose whether a
106 // given username exists.
107 $e_user = pht('Invalid');
108 $e_pass = pht('Invalid');
109 $errors[] = pht('Username or password are incorrect.');
110 }
111
112 $form = id(new PHUIFormLayoutView())
113 ->setUser($viewer)
114 ->setFullWidth(true)
115 ->appendChild(
116 id(new AphrontFormTextControl())
117 ->setLabel(pht('LDAP Username'))
118 ->setName('ldap_username')
119 ->setAutofocus(true)
120 ->setValue($v_user)
121 ->setError($e_user))
122 ->appendChild(
123 id(new AphrontFormPasswordControl())
124 ->setLabel(pht('LDAP Password'))
125 ->setName('ldap_password')
126 ->setError($e_pass));
127
128 if ($errors) {
129 $errors = id(new PHUIInfoView())->setErrors($errors);
130 }
131
132 $dialog->appendChild($errors);
133 $dialog->appendChild($form);
134
135
136 return $dialog;
137 }
138
139 public function processLoginRequest(
140 PhabricatorAuthLoginController $controller) {
141
142 $request = $controller->getRequest();
143 $viewer = $request->getUser();
144 $response = null;
145 $account = null;
146
147 $username = $request->getStr('ldap_username');
148 $password = $request->getStr('ldap_password');
149 $has_password = phutil_nonempty_string($password);
150 $password = new PhutilOpaqueEnvelope($password);
151
152 if (!phutil_nonempty_string($username) || !$has_password) {
153 $response = $controller->buildProviderPageResponse(
154 $this,
155 $this->renderLoginForm($request, 'login'));
156 return array($account, $response);
157 }
158
159 if ($request->isFormPost()) {
160 try {
161 $adapter = $this->getAdapter();
162 $adapter->setLoginUsername($username);
163 $adapter->setLoginPassword($password);
164
165 // TODO: This calls ldap_bind() eventually, which dumps cleartext
166 // passwords to the error log. See note in PhutilLDAPAuthAdapter.
167 // See T3351.
168
169 DarkConsoleErrorLogPluginAPI::enableDiscardMode();
170 $identifiers = $adapter->getAccountIdentifiers();
171 DarkConsoleErrorLogPluginAPI::disableDiscardMode();
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 php-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 (phutil_nonempty_string($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}