[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/auth/provider/ -> PhabricatorLDAPAuthProvider.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1