[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 to Phabricator.'); 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('Login or Register with LDAP')); 84 $dialog->addSubmitButton(pht('Login or Register')); 85 } else { 86 $dialog->setTitle(pht('Login with LDAP')); 87 $dialog->addSubmitButton(pht('Login')); 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('LDAP Username') 114 ->setName('ldap_username') 115 ->setValue($v_user) 116 ->setError($e_user)) 117 ->appendChild( 118 id(new AphrontFormPasswordControl()) 119 ->setLabel('LDAP Password') 120 ->setName('ldap_password') 121 ->setError($e_pass)); 122 123 if ($errors) { 124 $errors = id(new AphrontErrorView())->setErrors($errors); 125 } 126 127 $dialog->appendChild($errors); 128 $dialog->appendChild($form); 129 130 131 return $dialog; 132 } 133 134 public function processLoginRequest( 135 PhabricatorAuthLoginController $controller) { 136 137 $request = $controller->getRequest(); 138 $viewer = $request->getUser(); 139 $response = null; 140 $account = null; 141 142 $username = $request->getStr('ldap_username'); 143 $password = $request->getStr('ldap_password'); 144 $has_password = strlen($password); 145 $password = new PhutilOpaqueEnvelope($password); 146 147 if (!strlen($username) || !$has_password) { 148 $response = $controller->buildProviderPageResponse( 149 $this, 150 $this->renderLoginForm($request, 'login')); 151 return array($account, $response); 152 } 153 154 if ($request->isFormPost()) { 155 try { 156 if (strlen($username) && $has_password) { 157 $adapter = $this->getAdapter(); 158 $adapter->setLoginUsername($username); 159 $adapter->setLoginPassword($password); 160 161 // TODO: This calls ldap_bind() eventually, which dumps cleartext 162 // passwords to the error log. See note in PhutilLDAPAuthAdapter. 163 // See T3351. 164 165 DarkConsoleErrorLogPluginAPI::enableDiscardMode(); 166 $account_id = $adapter->getAccountID(); 167 DarkConsoleErrorLogPluginAPI::disableDiscardMode(); 168 } else { 169 throw new Exception('Username and password are required!'); 170 } 171 } catch (PhutilAuthCredentialException $ex) { 172 $response = $controller->buildProviderPageResponse( 173 $this, 174 $this->renderLoginForm($request, 'login')); 175 return array($account, $response); 176 } catch (Exception $ex) { 177 // TODO: Make this cleaner. 178 throw $ex; 179 } 180 } 181 182 return array($this->loadOrCreateAccount($account_id), $response); 183 } 184 185 186 const KEY_HOSTNAME = 'ldap:host'; 187 const KEY_PORT = 'ldap:port'; 188 const KEY_DISTINGUISHED_NAME = 'ldap:dn'; 189 const KEY_SEARCH_ATTRIBUTES = 'ldap:search-attribute'; 190 const KEY_USERNAME_ATTRIBUTE = 'ldap:username-attribute'; 191 const KEY_REALNAME_ATTRIBUTES = 'ldap:realname-attributes'; 192 const KEY_VERSION = 'ldap:version'; 193 const KEY_REFERRALS = 'ldap:referrals'; 194 const KEY_START_TLS = 'ldap:start-tls'; 195 const KEY_ANONYMOUS_USERNAME = 'ldap:anoynmous-username'; 196 const KEY_ANONYMOUS_PASSWORD = 'ldap:anonymous-password'; 197 const KEY_ALWAYS_SEARCH = 'ldap:always-search'; 198 const KEY_ACTIVEDIRECTORY_DOMAIN = 'ldap:activedirectory-domain'; 199 200 private function getPropertyKeys() { 201 return array_keys($this->getPropertyLabels()); 202 } 203 204 private function getPropertyLabels() { 205 return array( 206 self::KEY_HOSTNAME => pht('LDAP Hostname'), 207 self::KEY_PORT => pht('LDAP Port'), 208 self::KEY_DISTINGUISHED_NAME => pht('Base Distinguished Name'), 209 self::KEY_SEARCH_ATTRIBUTES => pht('Search Attributes'), 210 self::KEY_ALWAYS_SEARCH => pht('Always Search'), 211 self::KEY_ANONYMOUS_USERNAME => pht('Anonymous Username'), 212 self::KEY_ANONYMOUS_PASSWORD => pht('Anonymous Password'), 213 self::KEY_USERNAME_ATTRIBUTE => pht('Username Attribute'), 214 self::KEY_REALNAME_ATTRIBUTES => pht('Realname Attributes'), 215 self::KEY_VERSION => pht('LDAP Version'), 216 self::KEY_REFERRALS => pht('Enable Referrals'), 217 self::KEY_START_TLS => pht('Use TLS'), 218 self::KEY_ACTIVEDIRECTORY_DOMAIN => pht('ActiveDirectory Domain'), 219 ); 220 } 221 222 public function readFormValuesFromProvider() { 223 $properties = array(); 224 foreach ($this->getPropertyLabels() as $key => $ignored) { 225 $properties[$key] = $this->getProviderConfig()->getProperty($key); 226 } 227 return $properties; 228 } 229 230 public function readFormValuesFromRequest(AphrontRequest $request) { 231 $values = array(); 232 foreach ($this->getPropertyKeys() as $key) { 233 switch ($key) { 234 case self::KEY_REALNAME_ATTRIBUTES: 235 $values[$key] = $request->getStrList($key, array()); 236 break; 237 default: 238 $values[$key] = $request->getStr($key); 239 break; 240 } 241 } 242 243 return $values; 244 } 245 246 public function processEditForm( 247 AphrontRequest $request, 248 array $values) { 249 $errors = array(); 250 $issues = array(); 251 return array($errors, $issues, $values); 252 } 253 254 public static function assertLDAPExtensionInstalled() { 255 if (!function_exists('ldap_bind')) { 256 throw new Exception( 257 pht( 258 'Before you can set up or use LDAP, you need to install the PHP '. 259 'LDAP extension. It is not currently installed, so PHP can not '. 260 'talk to LDAP. Usually you can install it with '. 261 '`yum install php-ldap`, `apt-get install php5-ldap`, or a '. 262 'similar package manager command.')); 263 } 264 } 265 266 public function extendEditForm( 267 AphrontRequest $request, 268 AphrontFormView $form, 269 array $values, 270 array $issues) { 271 272 self::assertLDAPExtensionInstalled(); 273 274 $labels = $this->getPropertyLabels(); 275 276 $captions = array( 277 self::KEY_HOSTNAME => 278 pht('Example: %s%sFor LDAPS, use: %s', 279 phutil_tag('tt', array(), pht('ldap.example.com')), 280 phutil_tag('br'), 281 phutil_tag('tt', array(), pht('ldaps://ldaps.example.com/'))), 282 self::KEY_DISTINGUISHED_NAME => 283 pht('Example: %s', 284 phutil_tag('tt', array(), pht('ou=People, dc=example, dc=com'))), 285 self::KEY_USERNAME_ATTRIBUTE => 286 pht('Example: %s', 287 phutil_tag('tt', array(), pht('sn'))), 288 self::KEY_REALNAME_ATTRIBUTES => 289 pht('Example: %s', 290 phutil_tag('tt', array(), pht('firstname, lastname'))), 291 self::KEY_REFERRALS => 292 pht('Follow referrals. Disable this for Windows AD 2003.'), 293 self::KEY_START_TLS => 294 pht('Start TLS after binding to the LDAP server.'), 295 self::KEY_ALWAYS_SEARCH => 296 pht('Always bind and search, even without a username and password.'), 297 ); 298 299 $types = array( 300 self::KEY_REFERRALS => 'checkbox', 301 self::KEY_START_TLS => 'checkbox', 302 self::KEY_SEARCH_ATTRIBUTES => 'textarea', 303 self::KEY_REALNAME_ATTRIBUTES => 'list', 304 self::KEY_ANONYMOUS_PASSWORD => 'password', 305 self::KEY_ALWAYS_SEARCH => 'checkbox', 306 ); 307 308 $instructions = array( 309 self::KEY_SEARCH_ATTRIBUTES => pht( 310 "When a user types their LDAP username and password into Phabricator, ". 311 "Phabricator can either bind to LDAP with those credentials directly ". 312 "(which is simpler, but not as powerful) or bind to LDAP with ". 313 "anonymous credentials, then search for record matching the supplied ". 314 "credentials (which is more complicated, but more powerful).\n\n". 315 "For many installs, direct binding is sufficient. However, you may ". 316 "want to search first if:\n\n". 317 " - You want users to be able to login with either their username ". 318 " or their email address.\n". 319 " - The login/username is not part of the distinguished name in ". 320 " your LDAP records.\n". 321 " - You want to restrict logins to a subset of users (like only ". 322 " those in certain departments).\n". 323 " - Your LDAP server is configured in some other way that prevents ". 324 " direct binding from working correctly.\n\n". 325 "**To bind directly**, enter the LDAP attribute corresponding to the ". 326 "login name into the **Search Attributes** box below. Often, this is ". 327 "something like `sn` or `uid`. This is the simplest configuration, ". 328 "but will only work if the username is part of the distinguished ". 329 "name, and won't let you apply complex restrictions to logins.\n\n". 330 " lang=text,name=Simple Direct Binding\n". 331 " sn\n\n". 332 "**To search first**, provide an anonymous username and password ". 333 "below (or check the **Always Search** checkbox), then enter one ". 334 "or more search queries into this field, one per line. ". 335 "After binding, these queries will be used to identify the ". 336 "record associated with the login name the user typed.\n\n". 337 "Searches will be tried in order until a matching record is found. ". 338 "Each query can be a simple attribute name (like `sn` or `mail`), ". 339 "which will search for a matching record, or it can be a complex ". 340 "query that uses the string `\$login}` to represent the login ". 341 "name.\n\n". 342 "A common simple configuration is just an attribute name, like ". 343 "`sn`, which will work the same way direct binding works:\n\n". 344 " lang=text,name=Simple Example\n". 345 " sn\n\n". 346 "A slightly more complex configuration might let the user login with ". 347 "either their login name or email address:\n\n". 348 " lang=text,name=Match Several Attributes\n". 349 " mail\n". 350 " sn\n\n". 351 "If your LDAP directory is more complex, or you want to perform ". 352 "sophisticated filtering, you can use more complex queries. Depending ". 353 "on your directory structure, this example might allow users to login ". 354 "with either their email address or username, but only if they're in ". 355 "specific departments:\n\n". 356 " lang=text,name=Complex Example\n". 357 " (&(mail=\$login})(|(departmentNumber=1)(departmentNumber=2)))\n". 358 " (&(sn=\$login})(|(departmentNumber=1)(departmentNumber=2)))\n\n". 359 "All of the attribute names used here are just examples: your LDAP ". 360 "server may use different attribute names."), 361 self::KEY_ALWAYS_SEARCH => pht( 362 'To search for an LDAP record before authenticating, either check '. 363 'the **Always Search** checkbox or enter an anonymous '. 364 'username and password to use to perform the search.'), 365 self::KEY_USERNAME_ATTRIBUTE => pht( 366 'Optionally, specify a username attribute to use to prefill usernames '. 367 'when registering a new account. This is purely cosmetic and does not '. 368 'affect the login process, but you can configure it to make sure '. 369 'users get the same default username as their LDAP username, so '. 370 'usernames remain consistent across systems.'), 371 self::KEY_REALNAME_ATTRIBUTES => pht( 372 'Optionally, specify one or more comma-separated attributes to use to '. 373 'prefill the "Real Name" field when registering a new account. This '. 374 'is purely cosmetic and does not affect the login process, but can '. 375 'make registration a little easier.'), 376 ); 377 378 foreach ($labels as $key => $label) { 379 $caption = idx($captions, $key); 380 $type = idx($types, $key); 381 $value = idx($values, $key); 382 383 $control = null; 384 switch ($type) { 385 case 'checkbox': 386 $control = id(new AphrontFormCheckboxControl()) 387 ->addCheckbox( 388 $key, 389 1, 390 hsprintf('<strong>%s:</strong> %s', $label, $caption), 391 $value); 392 break; 393 case 'list': 394 $control = id(new AphrontFormTextControl()) 395 ->setName($key) 396 ->setLabel($label) 397 ->setCaption($caption) 398 ->setValue($value ? implode(', ', $value) : null); 399 break; 400 case 'password': 401 $control = id(new AphrontFormPasswordControl()) 402 ->setName($key) 403 ->setLabel($label) 404 ->setCaption($caption) 405 ->setDisableAutocomplete(true) 406 ->setValue($value); 407 break; 408 case 'textarea': 409 $control = id(new AphrontFormTextAreaControl()) 410 ->setName($key) 411 ->setLabel($label) 412 ->setCaption($caption) 413 ->setValue($value); 414 break; 415 default: 416 $control = id(new AphrontFormTextControl()) 417 ->setName($key) 418 ->setLabel($label) 419 ->setCaption($caption) 420 ->setValue($value); 421 break; 422 } 423 424 $instruction_text = idx($instructions, $key); 425 if (strlen($instruction_text)) { 426 $form->appendRemarkupInstructions($instruction_text); 427 } 428 429 $form->appendChild($control); 430 } 431 } 432 433 public function renderConfigPropertyTransactionTitle( 434 PhabricatorAuthProviderConfigTransaction $xaction) { 435 436 $author_phid = $xaction->getAuthorPHID(); 437 $old = $xaction->getOldValue(); 438 $new = $xaction->getNewValue(); 439 $key = $xaction->getMetadataValue( 440 PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY); 441 442 $labels = $this->getPropertyLabels(); 443 if (isset($labels[$key])) { 444 $label = $labels[$key]; 445 446 $mask = false; 447 switch ($key) { 448 case self::KEY_ANONYMOUS_PASSWORD: 449 $mask = true; 450 break; 451 } 452 453 if ($mask) { 454 return pht( 455 '%s updated the "%s" value.', 456 $xaction->renderHandleLink($author_phid), 457 $label); 458 } 459 460 if ($old === null || $old === '') { 461 return pht( 462 '%s set the "%s" value to "%s".', 463 $xaction->renderHandleLink($author_phid), 464 $label, 465 $new); 466 } else { 467 return pht( 468 '%s changed the "%s" value from "%s" to "%s".', 469 $xaction->renderHandleLink($author_phid), 470 $label, 471 $old, 472 $new); 473 } 474 } 475 476 return parent::renderConfigPropertyTransactionTitle($xaction); 477 } 478 479 public static function getLDAPProvider() { 480 $providers = self::getAllEnabledProviders(); 481 482 foreach ($providers as $provider) { 483 if ($provider instanceof PhabricatorLDAPAuthProvider) { 484 return $provider; 485 } 486 } 487 488 return null; 489 } 490 491 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |