diff --git a/conf/default.conf.php b/conf/default.conf.php index 15a0f3742f..889e80591f 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -548,6 +548,27 @@ return array( // The Google "Client Secret" to use for Google API access. 'google.application-secret' => null, +// -- LDAP Auth ----------------------------------------------------- // + // Enable ldap auth + 'ldap.auth-enabled' => false, + + // The LDAP server hostname + 'ldap.hostname' => '', + + // The LDAP base domain name + 'ldap.base_dn' => '', + + // The attribute to be regarded as 'username'. Has to be unique + 'ldap.search_attribute' => '', + + // The attribute(s) to be regarded as 'real name'. + // If more then one attribute is supplied the values of the attributes in + // the array will be joined + 'ldap.real_name_attributes' => array(), + + // The LDAP version + 'ldap.version' => 3, + // -- Disqus OAuth ---------------------------------------------------------- // // Can users use Disqus credentials to login to Phabricator? diff --git a/resources/sql/patches/ldapinfo.sql b/resources/sql/patches/ldapinfo.sql new file mode 100644 index 0000000000..16ff1fc496 --- /dev/null +++ b/resources/sql/patches/ldapinfo.sql @@ -0,0 +1,9 @@ +CREATE TABLE {$NAMESPACE}_user.user_ldapinfo ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `userID` int(10) unsigned NOT NULL, + `ldapUsername` varchar(255) NOT NULL, + `dateCreated` int(10) unsigned NOT NULL, + `dateModified` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY (`userID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 278fb7e335..b5dedd5606 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -699,6 +699,10 @@ phutil_register_library_map(array( 'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php', 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php', 'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php', + 'PhabricatorLDAPLoginController' => 'applications/auth/controller/PhabricatorLDAPLoginController.php', + 'PhabricatorLDAPProvider' => 'applications/auth/ldap/PhabricatorLDAPProvider.php', + 'PhabricatorLDAPRegistrationController' => 'applications/auth/controller/PhabricatorLDAPRegistrationController.php', + 'PhabricatorLDAPUnlinkController' => 'applications/auth/controller/PhabricatorLDAPUnlinkController.php', 'PhabricatorLintEngine' => 'infrastructure/lint/PhabricatorLintEngine.php', 'PhabricatorLiskDAO' => 'applications/base/storage/PhabricatorLiskDAO.php', 'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php', @@ -973,6 +977,8 @@ phutil_register_library_map(array( 'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php', 'PhabricatorUserEmailPreferenceSettingsPanelController' => 'applications/people/controller/settings/panels/PhabricatorUserEmailPreferenceSettingsPanelController.php', 'PhabricatorUserEmailSettingsPanelController' => 'applications/people/controller/settings/panels/PhabricatorUserEmailSettingsPanelController.php', + 'PhabricatorUserLDAPInfo' => 'applications/people/storage/PhabricatorUserLDAPInfo.php', + 'PhabricatorUserLDAPSettingsPanelController' => 'applications/people/controller/settings/panels/PhabricatorUserLDAPSettingsPanelController.php', 'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php', 'PhabricatorUserOAuthInfo' => 'applications/people/storage/PhabricatorUserOAuthInfo.php', 'PhabricatorUserOAuthSettingsPanelController' => 'applications/people/controller/settings/panels/PhabricatorUserOAuthSettingsPanelController.php', @@ -1669,6 +1675,9 @@ phutil_register_library_map(array( 'PhabricatorInlineCommentController' => 'PhabricatorController', 'PhabricatorInlineSummaryView' => 'AphrontView', 'PhabricatorJavelinLinter' => 'ArcanistLinter', + 'PhabricatorLDAPLoginController' => 'PhabricatorAuthController', + 'PhabricatorLDAPRegistrationController' => 'PhabricatorAuthController', + 'PhabricatorLDAPUnlinkController' => 'PhabricatorAuthController', 'PhabricatorLintEngine' => 'PhutilLintEngine', 'PhabricatorLiskDAO' => 'LiskDAO', 'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine', @@ -1901,6 +1910,8 @@ phutil_register_library_map(array( 'PhabricatorUserEmail' => 'PhabricatorUserDAO', 'PhabricatorUserEmailPreferenceSettingsPanelController' => 'PhabricatorUserSettingsPanelController', 'PhabricatorUserEmailSettingsPanelController' => 'PhabricatorUserSettingsPanelController', + 'PhabricatorUserLDAPInfo' => 'PhabricatorUserDAO', + 'PhabricatorUserLDAPSettingsPanelController' => 'PhabricatorUserSettingsPanelController', 'PhabricatorUserLog' => 'PhabricatorUserDAO', 'PhabricatorUserOAuthInfo' => 'PhabricatorUserDAO', 'PhabricatorUserOAuthSettingsPanelController' => 'PhabricatorUserSettingsPanelController', diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php index 71532f4e92..3e047ff694 100644 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php @@ -146,6 +146,11 @@ class AphrontDefaultApplicationConfiguration ), ), + '/ldap/' => array( + 'login/' => 'PhabricatorLDAPLoginController', + 'unlink/' => 'PhabricatorLDAPUnlinkController', + ), + '/oauthserver/' => array( 'auth/' => 'PhabricatorOAuthServerAuthController', 'test/' => 'PhabricatorOAuthServerTestController', diff --git a/src/applications/auth/controller/PhabricatorLDAPLoginController.php b/src/applications/auth/controller/PhabricatorLDAPLoginController.php new file mode 100644 index 0000000000..0525625373 --- /dev/null +++ b/src/applications/auth/controller/PhabricatorLDAPLoginController.php @@ -0,0 +1,187 @@ +provider = new PhabricatorLDAPProvider(); + } + + public function processRequest() { + if (!$this->provider->isProviderEnabled()) { + return new Aphront400Response(); + } + + $current_user = $this->getRequest()->getUser(); + $request = $this->getRequest(); + + if ($request->isFormPost()) { + try { + $this->provider->auth($request->getStr('username'), + $request->getStr('password')); + + } catch (Exception $e) { + $errors[] = $e->getMessage(); + } + + if (empty($errors)) { + $ldap_info = $this->retrieveLDAPInfo($this->provider); + + if ($current_user->getPHID()) { + if ($ldap_info->getID()) { + $existing_ldap = id(new PhabricatorUserLDAPInfo())->loadOneWhere( + 'userID = %d', + $current_user->getID()); + + if ($ldap_info->getUserID() != $current_user->getID() || + $existing_ldap) { + $dialog = new AphrontDialogView(); + $dialog->setUser($current_user); + $dialog->setTitle('Already Linked to Another Account'); + $dialog->appendChild( + '
The LDAP account you just authorized is already linked to '. + 'another Phabricator account. Before you can link it to a '. + 'different LDAP account, you must unlink the old account.
' + ); + $dialog->addCancelButton('/settings/page/ldap/'); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } else { + return id(new AphrontRedirectResponse()) + ->setURI('/settings/page/ldap/'); + } + } + + if (!$request->isDialogFormPost()) { + $dialog = new AphrontDialogView(); + $dialog->setUser($current_user); + $dialog->setTitle('Link LDAP Account'); + $dialog->appendChild( + 'Link your LDAP account to your Phabricator account?
'); + $dialog->addHiddenInput('username', $request->getStr('username')); + $dialog->addHiddenInput('password', $request->getStr('password')); + $dialog->addSubmitButton('Link Accounts'); + $dialog->addCancelButton('/settings/page/ldap/'); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + + $ldap_info->setUserID($current_user->getID()); + + $this->saveLDAPInfo($ldap_info); + + return id(new AphrontRedirectResponse()) + ->setURI('/settings/page/ldap/'); + } + + if ($ldap_info->getID()) { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + + $known_user = id(new PhabricatorUser())->load( + $ldap_info->getUserID()); + + $session_key = $known_user->establishSession('web'); + + $this->saveLDAPInfo($ldap_info); + + $request->setCookie('phusr', $known_user->getUsername()); + $request->setCookie('phsid', $session_key); + + $uri = new PhutilURI('/login/validate/'); + $uri->setQueryParams( + array( + 'phusr' => $known_user->getUsername(), + )); + + return id(new AphrontRedirectResponse())->setURI((string)$uri); + } + + $controller = newv('PhabricatorLDAPRegistrationController', + array($this->getRequest())); + $controller->setLDAPProvider($this->provider); + $controller->setLDAPInfo($ldap_info); + + return $this->delegateToController($controller); + } + } + + $ldap_username = $request->getCookie('phusr'); + $ldap_form = new AphrontFormView(); + $ldap_form + ->setUser($request->getUser()) + ->setAction('/ldap/login/') + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('LDAP username') + ->setName('username') + ->setValue($ldap_username)) + ->appendChild( + id(new AphrontFormPasswordControl()) + ->setLabel('Password') + ->setName('password')); + + $ldap_form + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Login')); + + $panel = new AphrontPanelView(); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + $panel->appendChild('You will not be able to login using this account '. + 'once you unlink it. Continue?
'); + $dialog->addSubmitButton('Unlink Account'); + $dialog->addCancelButton('/settings/page/ldap/'); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + + $ldap_info->delete(); + + return id(new AphrontRedirectResponse()) + ->setURI('/settings/page/ldap/'); + } + +} diff --git a/src/applications/auth/controller/PhabricatorLoginController.php b/src/applications/auth/controller/PhabricatorLoginController.php index 5e7df9ee59..bedd20965a 100644 --- a/src/applications/auth/controller/PhabricatorLoginController.php +++ b/src/applications/auth/controller/PhabricatorLoginController.php @@ -187,6 +187,30 @@ final class PhabricatorLoginController // $panel->setCreateButton('Register New Account', '/login/register/'); $forms['Phabricator Login'] = $form; + + $ldap_provider = new PhabricatorLDAPProvider(); + if ($ldap_provider->isProviderEnabled()) { + $ldap_form = new AphrontFormView(); + $ldap_form + ->setUser($request->getUser()) + ->setAction('/ldap/login/') + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('LDAP username') + ->setName('username') + ->setValue($username_or_email)) + ->appendChild( + id(new AphrontFormPasswordControl()) + ->setLabel('Password') + ->setName('password')); + + $ldap_form + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Login')); + + $forms['LDAP Login'] = $ldap_form; + } } $providers = PhabricatorOAuthProvider::getAllProviders(); diff --git a/src/applications/auth/ldap/PhabricatorLDAPProvider.php b/src/applications/auth/ldap/PhabricatorLDAPProvider.php new file mode 100644 index 0000000000..d1d0b29c31 --- /dev/null +++ b/src/applications/auth/ldap/PhabricatorLDAPProvider.php @@ -0,0 +1,149 @@ +connection)) { + ldap_unbind($this->connection); + } + } + + public function isProviderEnabled() { + return PhabricatorEnv::getEnvConfig('ldap.auth-enabled'); + } + + public function getHostname() { + return PhabricatorEnv::getEnvConfig('ldap.hostname'); + } + + public function getBaseDN() { + return PhabricatorEnv::getEnvConfig('ldap.base_dn'); + } + + public function getSearchAttribute() { + return PhabricatorEnv::getEnvConfig('ldap.search_attribute'); + } + + public function getLDAPVersion() { + return PhabricatorEnv::getEnvConfig('ldap.version'); + } + + public function retrieveUserEmail() { + return $this->userData['mail'][0]; + } + + public function retrieveUserRealName() { + $name_attributes = PhabricatorEnv::getEnvConfig( + 'ldap.real_name_attributes'); + + $real_name = ''; + if (is_array($name_attributes)) { + foreach ($name_attributes AS $attribute) { + if (isset($this->userData[$attribute][0])) { + $real_name .= $this->userData[$attribute][0] . ' '; + } + } + + trim($real_name); + } else if (isset($this->userData[$name_attributes][0])) { + $real_name = $this->userData[$name_attributes][0]; + } + + if ($real_name == '') { + return null; + } + + return $real_name; + } + + public function retrieveUsername() { + return $this->userData[$this->getSearchAttribute()][0]; + } + + public function getConnection() { + if (!isset($this->connection)) { + $this->connection = ldap_connect($this->getHostname()); + + if (!$this->connection) { + throw new Exception('Could not connect to LDAP host at ' . + $this->getHostname()); + } + + ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, + $this->getLDAPVersion()); + } + + return $this->connection; + } + + public function getUserData() { + return $this->userData; + } + + public function auth($username, $password) { + if (strlen(trim($username)) == 0 || strlen(trim($password)) == 0) { + throw new Exception('Username and/or password can not be empty'); + } + + $result = ldap_bind($this->getConnection(), + $this->getSearchAttribute() . '=' . $username . ',' . + $this->getBaseDN(), + $password); + + if (!$result) { + throw new Exception('Bad username/password.'); + } + + $this->userData = $this->getUser($username); + return $this->userData; + } + + private function getUser($username) { + $result = ldap_search($this->getConnection(), $this->getBaseDN(), + $this->getSearchAttribute() . '=' . $username); + + if (!$result) { + throw new Exception('Search failed. Please check your LDAP and HTTP '. + 'logs for more information.'); + } + + $entries = ldap_get_entries($this->getConnection(), $result); + + if ($entries === false) { + throw new Exception('Could not get entries'); + } + + if ($entries['count'] > 1) { + throw new Exception('Found more then one user with this ' . + $this->getSearchAttribute()); + } + + if ($entries['count'] == 0) { + throw new Exception('Could not find user'); + } + + return $entries[0]; + } +} diff --git a/src/applications/people/controller/PhabricatorUserSettingsController.php b/src/applications/people/controller/PhabricatorUserSettingsController.php index a5d3dc4b1f..199cb5cad9 100644 --- a/src/applications/people/controller/PhabricatorUserSettingsController.php +++ b/src/applications/people/controller/PhabricatorUserSettingsController.php @@ -65,6 +65,9 @@ final class PhabricatorUserSettingsController case 'search': $delegate = new PhabricatorUserSearchSettingsPanelController($request); break; + case 'ldap': + $delegate = new PhabricatorUserLDAPSettingsPanelController($request); + break; default: $delegate = new PhabricatorUserOAuthSettingsPanelController($request); $delegate->setOAuthProvider($oauth_providers[$this->page]); @@ -125,6 +128,11 @@ final class PhabricatorUserSettingsController $items[$key] = $name.' Account'; } + $ldap_provider = new PhabricatorLDAPProvider(); + if ($ldap_provider->isProviderEnabled()) { + $items['ldap'] = 'LDAP Account'; + } + if ($items) { $sidenav->addSpacer(); $sidenav->addLabel('Linked Accounts'); diff --git a/src/applications/people/controller/settings/panels/PhabricatorUserLDAPSettingsPanelController.php b/src/applications/people/controller/settings/panels/PhabricatorUserLDAPSettingsPanelController.php new file mode 100644 index 0000000000..c422c1db73 --- /dev/null +++ b/src/applications/people/controller/settings/panels/PhabricatorUserLDAPSettingsPanelController.php @@ -0,0 +1,87 @@ +getRequest(); + $user = $request->getUser(); + + $ldap_info = id(new PhabricatorUserLDAPInfo())->loadOneWhere( + 'userID = %d', + $user->getID()); + + $forms = array(); + + if (!$ldap_info) { + $unlink = 'Link LDAP Account'; + $unlink_form = new AphrontFormView(); + $unlink_form + ->setUser($user) + ->setAction('/ldap/login/') + ->appendChild( + 'There is currently no '. + 'LDAP account linked to your Phabricator account. You can link an ' . + 'account, which will allow you to use it to log into Phabricator
') + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('LDAP username') + ->setName('username')) + ->appendChild( + id(new AphrontFormPasswordControl()) + ->setLabel('Password') + ->setName('password')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue("Link LDAP Account \xC2\xBB")); + + $forms['Link Account'] = $unlink_form; + } else { + $unlink = 'Unlink LDAP Account'; + $unlink_form = new AphrontFormView(); + $unlink_form + ->setUser($user) + ->appendChild( + 'You may unlink this account '. + 'from your LDAP account. This will prevent you from logging in with '. + 'your LDAP credentials.
') + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton('/ldap/unlink/', $unlink)); + + $forms['Unlink Account'] = $unlink_form; + } + + $panel = new AphrontPanelView(); + $panel->setHeader('LDAP Account Settings'); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + foreach ($forms as $name => $form) { + if ($name) { + $panel->appendChild('