mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-11 17:32:41 +01:00
26944e2876
Summary: Ref T1536. Love me some LDAP. Test Plan: Viewed and edited form. Looked through transactions. Reviewers: chad, btrahan Reviewed By: chad CC: aran Maniphest Tasks: T1536 Differential Revision: https://secure.phabricator.com/D6227
415 lines
13 KiB
PHP
415 lines
13 KiB
PHP
<?php
|
|
|
|
final class PhabricatorAuthProviderLDAP
|
|
extends PhabricatorAuthProvider {
|
|
|
|
private $adapter;
|
|
|
|
public function getProviderName() {
|
|
return pht('LDAP');
|
|
}
|
|
|
|
public function getDescriptionForCreate() {
|
|
return pht(
|
|
'Configure a connection to an LDAP server so that users can use their '.
|
|
'LDAP credentials to log in to Phabricator.');
|
|
}
|
|
|
|
|
|
public function isEnabled() {
|
|
return parent::isEnabled() &&
|
|
PhabricatorEnv::getEnvConfig('ldap.auth-enabled');
|
|
}
|
|
|
|
public function getAdapter() {
|
|
if (!$this->adapter) {
|
|
$adapter = id(new PhutilAuthAdapterLDAP())
|
|
->setHostname(PhabricatorEnv::getEnvConfig('ldap.hostname'))
|
|
->setPort(PhabricatorEnv::getEnvConfig('ldap.port'))
|
|
->setBaseDistinguishedName(PhabricatorEnv::getEnvConfig('ldap.base_dn'))
|
|
->setSearchAttribute(
|
|
PhabricatorEnv::getEnvConfig('ldap.search_attribute'))
|
|
->setUsernameAttribute(
|
|
PhabricatorEnv::getEnvConfig('ldap.username-attribute'))
|
|
->setRealNameAttributes(
|
|
PhabricatorEnv::getEnvConfig('ldap.real_name_attributes'))
|
|
->setLDAPVersion(PhabricatorEnv::getEnvConfig('ldap.version'))
|
|
->setLDAPReferrals(PhabricatorEnv::getEnvConfig('ldap.referrals'))
|
|
->setLDAPStartTLS(PhabricatorEnv::getEnvConfig('ldap.start-tls'))
|
|
->setAnonymousUsername(
|
|
PhabricatorEnv::getEnvConfig('ldap.anonymous-user-name'))
|
|
->setAnonymousPassword(
|
|
new PhutilOpaqueEnvelope(
|
|
PhabricatorEnv::getEnvConfig('ldap.anonymous-user-password')))
|
|
->setSearchFirst(PhabricatorEnv::getEnvConfig('ldap.search-first'))
|
|
->setActiveDirectoryDomain(
|
|
PhabricatorEnv::getEnvConfig('ldap.activedirectory_domain'));
|
|
$this->adapter = $adapter;
|
|
}
|
|
return $this->adapter;
|
|
}
|
|
|
|
public function shouldAllowLogin() {
|
|
return true;
|
|
}
|
|
|
|
public function shouldAllowRegistration() {
|
|
return true;
|
|
}
|
|
|
|
public function shouldAllowAccountLink() {
|
|
return true;
|
|
}
|
|
|
|
public function shouldAllowAccountUnlink() {
|
|
return true;
|
|
}
|
|
|
|
protected function renderLoginForm(AphrontRequest $request, $mode) {
|
|
$viewer = $request->getUser();
|
|
|
|
$dialog = id(new AphrontDialogView())
|
|
->setSubmitURI($this->getLoginURI())
|
|
->setUser($viewer);
|
|
|
|
if ($mode == 'link') {
|
|
$dialog->setTitle(pht('Link LDAP Account'));
|
|
$dialog->addSubmitButton(pht('Link Accounts'));
|
|
$dialog->addCancelButton($this->getSettingsURI());
|
|
} else {
|
|
if ($this->shouldAllowRegistration()) {
|
|
$dialog->setTitle(pht('Login or Register with LDAP'));
|
|
$dialog->addSubmitButton(pht('Login or Register'));
|
|
} else {
|
|
$dialog->setTitle(pht('Login with LDAP'));
|
|
$dialog->addSubmitButton(pht('Login'));
|
|
}
|
|
if ($mode == 'login') {
|
|
$dialog->addCancelButton($this->getStartURI());
|
|
}
|
|
}
|
|
|
|
$v_user = $request->getStr('ldap_username');
|
|
|
|
$e_user = null;
|
|
$e_pass = null;
|
|
|
|
$errors = array();
|
|
if ($request->isHTTPPost()) {
|
|
// NOTE: This is intentionally vague so as not to disclose whether a
|
|
// given username exists.
|
|
$e_user = pht('Invalid');
|
|
$e_pass = pht('Invalid');
|
|
$errors[] = pht('Username or password are incorrect.');
|
|
}
|
|
|
|
$form = id(new AphrontFormLayoutView())
|
|
->setUser($viewer)
|
|
->setFullWidth(true)
|
|
->appendChild(
|
|
id(new AphrontFormTextControl())
|
|
->setLabel('LDAP Username')
|
|
->setName('ldap_username')
|
|
->setValue($v_user)
|
|
->setError($e_user))
|
|
->appendChild(
|
|
id(new AphrontFormPasswordControl())
|
|
->setLabel('LDAP Password')
|
|
->setName('ldap_password')
|
|
->setError($e_pass));
|
|
|
|
if ($errors) {
|
|
$errors = id(new AphrontErrorView())->setErrors($errors);
|
|
}
|
|
|
|
$dialog->appendChild($errors);
|
|
$dialog->appendChild($form);
|
|
|
|
|
|
return $dialog;
|
|
}
|
|
|
|
public function processLoginRequest(
|
|
PhabricatorAuthLoginController $controller) {
|
|
|
|
$request = $controller->getRequest();
|
|
$viewer = $request->getUser();
|
|
$response = null;
|
|
$account = null;
|
|
|
|
$username = $request->getStr('ldap_username');
|
|
$password = $request->getStr('ldap_password');
|
|
$has_password = strlen($password);
|
|
$password = new PhutilOpaqueEnvelope($password);
|
|
|
|
if (!strlen($username) || !$has_password) {
|
|
$response = $controller->buildProviderPageResponse(
|
|
$this,
|
|
$this->renderLoginForm($request, 'login'));
|
|
return array($account, $response);
|
|
}
|
|
|
|
try {
|
|
if (strlen($username) && $has_password) {
|
|
$adapter = $this->getAdapter();
|
|
$adapter->setLoginUsername($username);
|
|
$adapter->setLoginPassword($password);
|
|
|
|
// TODO: This calls ldap_bind() eventually, which dumps cleartext
|
|
// passwords to the error log. See note in PhutilAuthAdapterLDAP.
|
|
// See T3351.
|
|
|
|
DarkConsoleErrorLogPluginAPI::enableDiscardMode();
|
|
$account_id = $adapter->getAccountID();
|
|
DarkConsoleErrorLogPluginAPI::disableDiscardMode();
|
|
} else {
|
|
throw new Exception("Username and password are required!");
|
|
}
|
|
} catch (Exception $ex) {
|
|
// TODO: Make this cleaner.
|
|
throw $ex;
|
|
}
|
|
|
|
return array($this->loadOrCreateAccount($account_id), $response);
|
|
}
|
|
|
|
|
|
const KEY_HOSTNAME = 'ldap:host';
|
|
const KEY_PORT = 'ldap:port';
|
|
const KEY_DISTINGUISHED_NAME = 'ldap:dn';
|
|
const KEY_SEARCH_ATTRIBUTE = 'ldap:search-attribute';
|
|
const KEY_USERNAME_ATTRIBUTE = 'ldap:username-attribute';
|
|
const KEY_REALNAME_ATTRIBUTES = 'ldap:realname-attributes';
|
|
const KEY_VERSION = 'ldap:version';
|
|
const KEY_REFERRALS = 'ldap:referrals';
|
|
const KEY_START_TLS = 'ldap:start-tls';
|
|
const KEY_ANONYMOUS_USERNAME = 'ldap:anoynmous-username';
|
|
const KEY_ANONYMOUS_PASSWORD = 'ldap:anonymous-password';
|
|
const KEY_SEARCH_FIRST = 'ldap:search-first';
|
|
const KEY_ACTIVEDIRECTORY_DOMAIN = 'ldap:activedirectory-domain';
|
|
|
|
private function getPropertyKeys() {
|
|
return array(
|
|
self::KEY_HOSTNAME,
|
|
self::KEY_PORT,
|
|
self::KEY_DISTINGUISHED_NAME,
|
|
self::KEY_SEARCH_ATTRIBUTE,
|
|
self::KEY_USERNAME_ATTRIBUTE,
|
|
self::KEY_VERSION,
|
|
self::KEY_REFERRALS,
|
|
self::KEY_START_TLS,
|
|
self::KEY_ANONYMOUS_USERNAME,
|
|
self::KEY_ANONYMOUS_PASSWORD,
|
|
self::KEY_SEARCH_FIRST,
|
|
self::KEY_ACTIVEDIRECTORY_DOMAIN,
|
|
);
|
|
}
|
|
|
|
private function getPropertyLabels() {
|
|
return array(
|
|
self::KEY_HOSTNAME => pht('LDAP Hostname'),
|
|
self::KEY_PORT => pht('LDAP Port'),
|
|
self::KEY_DISTINGUISHED_NAME => pht('Base Distinguished Name'),
|
|
self::KEY_SEARCH_ATTRIBUTE => pht('Search Attribute'),
|
|
self::KEY_USERNAME_ATTRIBUTE => pht('Username Attribute'),
|
|
self::KEY_REALNAME_ATTRIBUTES => pht('Realname Attributes'),
|
|
self::KEY_VERSION => pht('LDAP Version'),
|
|
self::KEY_REFERRALS => pht('Enable Referrals'),
|
|
self::KEY_START_TLS => pht('Use TLS'),
|
|
self::KEY_SEARCH_FIRST => pht('Search First'),
|
|
self::KEY_ANONYMOUS_USERNAME => pht('Anonymous Username'),
|
|
self::KEY_ANONYMOUS_PASSWORD => pht('Anonymous Password'),
|
|
self::KEY_ACTIVEDIRECTORY_DOMAIN => pht('ActiveDirectory Domain'),
|
|
);
|
|
}
|
|
|
|
public function readFormValuesFromProvider() {
|
|
return array(
|
|
self::KEY_HOSTNAME =>
|
|
PhabricatorEnv::getEnvConfig('ldap.hostname'),
|
|
self::KEY_PORT =>
|
|
PhabricatorEnv::getEnvConfig('ldap.port'),
|
|
self::KEY_DISTINGUISHED_NAME =>
|
|
PhabricatorEnv::getEnvConfig('ldap.base_dn'),
|
|
self::KEY_SEARCH_ATTRIBUTE =>
|
|
PhabricatorEnv::getEnvConfig('ldap.search_attribute'),
|
|
self::KEY_USERNAME_ATTRIBUTE =>
|
|
PhabricatorEnv::getEnvConfig('ldap.username-attribute'),
|
|
self::KEY_REALNAME_ATTRIBUTES =>
|
|
PhabricatorEnv::getEnvConfig('ldap.real_name_attributes'),
|
|
self::KEY_VERSION =>
|
|
PhabricatorEnv::getEnvConfig('ldap.version'),
|
|
self::KEY_REFERRALS =>
|
|
PhabricatorEnv::getEnvConfig('ldap.referrals'),
|
|
self::KEY_START_TLS =>
|
|
PhabricatorEnv::getEnvConfig('ldap.start-tls'),
|
|
self::KEY_ANONYMOUS_USERNAME =>
|
|
PhabricatorEnv::getEnvConfig('ldap.anonymous-user-name'),
|
|
self::KEY_ANONYMOUS_PASSWORD =>
|
|
PhabricatorEnv::getEnvConfig('ldap.anonymous-user-password'),
|
|
self::KEY_SEARCH_FIRST =>
|
|
PhabricatorEnv::getEnvConfig('ldap.search-first'),
|
|
self::KEY_ACTIVEDIRECTORY_DOMAIN =>
|
|
PhabricatorEnv::getEnvConfig('ldap.activedirectory_domain'),
|
|
);
|
|
}
|
|
|
|
public function readFormValuesFromRequest(AphrontRequest $request) {
|
|
$values = array();
|
|
foreach ($this->getPropertyKeys() as $key) {
|
|
switch ($key) {
|
|
case self::KEY_REALNAME_ATTRIBUTES:
|
|
$values[$key] = $request->getStrList($key);
|
|
break;
|
|
default:
|
|
$values[$key] = $request->getStr($key);
|
|
break;
|
|
}
|
|
}
|
|
return $values;
|
|
}
|
|
|
|
public function processEditForm(
|
|
AphrontRequest $request,
|
|
array $values) {
|
|
$errors = array();
|
|
$issues = array();
|
|
return array($errors, $issues, $values);
|
|
}
|
|
|
|
public function extendEditForm(
|
|
AphrontRequest $request,
|
|
AphrontFormView $form,
|
|
array $values,
|
|
array $issues) {
|
|
|
|
$labels = $this->getPropertyLabels();
|
|
|
|
$captions = array(
|
|
self::KEY_HOSTNAME =>
|
|
pht('Example: %s',
|
|
hsprintf('<tt>%s</tt>', pht('ldap.example.com'))),
|
|
self::KEY_DISTINGUISHED_NAME =>
|
|
pht('Example: %s',
|
|
hsprintf('<tt>%s</tt>', pht('ou=People, dc=example, dc=com'))),
|
|
self::KEY_SEARCH_ATTRIBUTE =>
|
|
pht('Example: %s',
|
|
hsprintf('<tt>%s</tt>', pht('sn'))),
|
|
self::KEY_USERNAME_ATTRIBUTE =>
|
|
pht('Optional, if different from search attribute.'),
|
|
self::KEY_REALNAME_ATTRIBUTES =>
|
|
pht('Optional. Example: %s',
|
|
hsprintf('<tt>%s</tt>', pht('firstname, lastname'))),
|
|
self::KEY_REFERRALS =>
|
|
pht('Follow referrals. Disable this for Windows AD 2003.'),
|
|
self::KEY_START_TLS =>
|
|
pht('Start TLS after binding to the LDAP server.'),
|
|
self::KEY_SEARCH_FIRST =>
|
|
pht(
|
|
'When the user enters their username, search for a matching '.
|
|
'record using the "Search Attribute", then try to bind using '.
|
|
'the DN for the record. This is useful if usernames are not '.
|
|
'part of the record DN.'),
|
|
self::KEY_ANONYMOUS_USERNAME =>
|
|
pht('Username to bind with before searching.'),
|
|
self::KEY_ANONYMOUS_PASSWORD =>
|
|
pht('Password to bind with before searching.'),
|
|
);
|
|
|
|
$types = array(
|
|
self::KEY_REFERRALS => 'checkbox',
|
|
self::KEY_START_TLS => 'checkbox',
|
|
self::KEY_SEARCH_FIRST => 'checkbox',
|
|
self::KEY_REALNAME_ATTRIBUTES => 'list',
|
|
self::KEY_ANONYMOUS_PASSWORD => 'password',
|
|
);
|
|
|
|
foreach ($labels as $key => $label) {
|
|
$caption = idx($captions, $key);
|
|
$type = idx($types, $key);
|
|
$value = idx($values, $key);
|
|
|
|
$control = null;
|
|
switch ($type) {
|
|
case 'checkbox':
|
|
$control = id(new AphrontFormCheckboxControl())
|
|
->addCheckbox(
|
|
$key,
|
|
1,
|
|
hsprintf('<strong>%s:</strong> %s', $label, $caption),
|
|
$value);
|
|
break;
|
|
case 'list':
|
|
$control = id(new AphrontFormTextControl())
|
|
->setName($key)
|
|
->setLabel($label)
|
|
->setCaption($caption)
|
|
->setValue(implode(', ', $value));
|
|
break;
|
|
case 'password':
|
|
$control = id(new AphrontFormPasswordControl())
|
|
->setName($key)
|
|
->setLabel($label)
|
|
->setCaption($caption)
|
|
->setValue($value);
|
|
break;
|
|
default:
|
|
$control = id(new AphrontFormTextControl())
|
|
->setName($key)
|
|
->setLabel($label)
|
|
->setCaption($caption)
|
|
->setValue($value);
|
|
break;
|
|
}
|
|
|
|
$form->appendChild($control);
|
|
}
|
|
}
|
|
|
|
public function renderConfigPropertyTransactionTitle(
|
|
PhabricatorAuthProviderConfigTransaction $xaction) {
|
|
|
|
$author_phid = $xaction->getAuthorPHID();
|
|
$old = $xaction->getOldValue();
|
|
$new = $xaction->getNewValue();
|
|
$key = $xaction->getMetadataValue(
|
|
PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
|
|
|
|
$labels = $this->getPropertyLabels();
|
|
if (isset($labels[$key])) {
|
|
$label = $labels[$key];
|
|
|
|
$mask = false;
|
|
switch ($key) {
|
|
case self::KEY_ANONYMOUS_PASSWORD:
|
|
$mask = true;
|
|
break;
|
|
}
|
|
|
|
if ($mask) {
|
|
return pht(
|
|
'%s updated the "%s" value.',
|
|
$xaction->renderHandleLink($author_phid),
|
|
$label);
|
|
}
|
|
|
|
if (!strlen($old)) {
|
|
return pht(
|
|
'%s set the "%s" value to "%s".',
|
|
$xaction->renderHandleLink($author_phid),
|
|
$label,
|
|
$new);
|
|
} else {
|
|
return pht(
|
|
'%s changed the "%s" value from "%s" to "%s".',
|
|
$xaction->renderHandleLink($author_phid),
|
|
$label,
|
|
$old,
|
|
$new);
|
|
}
|
|
}
|
|
|
|
return parent::renderConfigPropertyTransactionTitle($xaction);
|
|
}
|
|
|
|
}
|