From ba8925a531c7101f3e035bb24215d2e2e3465aa5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 17 Mar 2014 15:08:30 -0700 Subject: [PATCH] Support multiple LDAP filters in the Phabricator UI Summary: Ref T3208. Not ready for prime time yet. Test Plan: ldap T.T Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley, frgtn, aran Maniphest Tasks: T3208 Differential Revision: https://secure.phabricator.com/D8160 --- .../provider/PhabricatorAuthProviderLDAP.php | 115 ++++++++++++++---- 1 file changed, 94 insertions(+), 21 deletions(-) diff --git a/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php b/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php index 1c9844a34f..bf89074b1b 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php @@ -30,6 +30,10 @@ final class PhabricatorAuthProviderLDAP $realname_attributes = array(); } + $search_attributes = $conf->getProperty(self::KEY_SEARCH_ATTRIBUTES); + $search_attributes = phutil_split_lines($search_attributes, false); + $search_attributes = array_filter($search_attributes); + $adapter = id(new PhutilAuthAdapterLDAP()) ->setHostname( $conf->getProperty(self::KEY_HOSTNAME)) @@ -37,8 +41,7 @@ final class PhabricatorAuthProviderLDAP $conf->getProperty(self::KEY_PORT)) ->setBaseDistinguishedName( $conf->getProperty(self::KEY_DISTINGUISHED_NAME)) - ->setSearchAttribute( - $conf->getProperty(self::KEY_SEARCH_ATTRIBUTE)) + ->setSearchAttributes($search_attributes) ->setUsernameAttribute( $conf->getProperty(self::KEY_USERNAME_ATTRIBUTE)) ->setRealNameAttributes($realname_attributes) @@ -53,8 +56,6 @@ final class PhabricatorAuthProviderLDAP ->setAnonymousPassword( new PhutilOpaqueEnvelope( $conf->getProperty(self::KEY_ANONYMOUS_PASSWORD))) - ->setSearchFirst( - $conf->getProperty(self::KEY_SEARCH_FIRST)) ->setActiveDirectoryDomain( $conf->getProperty(self::KEY_ACTIVEDIRECTORY_DOMAIN)); $this->adapter = $adapter; @@ -167,6 +168,11 @@ final class PhabricatorAuthProviderLDAP } else { throw new Exception("Username and password are required!"); } + } catch (PhutilAuthCredentialException $ex) { + $response = $controller->buildProviderPageResponse( + $this, + $this->renderLoginForm($request, 'login')); + return array($account, $response); } catch (Exception $ex) { // TODO: Make this cleaner. throw $ex; @@ -180,7 +186,7 @@ final class PhabricatorAuthProviderLDAP 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_SEARCH_ATTRIBUTES = 'ldap:search-attribute'; const KEY_USERNAME_ATTRIBUTE = 'ldap:username-attribute'; const KEY_REALNAME_ATTRIBUTES = 'ldap:realname-attributes'; const KEY_VERSION = 'ldap:version'; @@ -188,7 +194,6 @@ final class PhabricatorAuthProviderLDAP 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() { @@ -200,15 +205,14 @@ final class PhabricatorAuthProviderLDAP 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_SEARCH_ATTRIBUTES => pht('Search Attributes'), + self::KEY_ANONYMOUS_USERNAME => pht('Anonymous Username'), + self::KEY_ANONYMOUS_PASSWORD => pht('Anonymous Password'), 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'), ); } @@ -262,24 +266,16 @@ final class PhabricatorAuthProviderLDAP self::KEY_DISTINGUISHED_NAME => pht('Example: %s', phutil_tag('tt', array(), pht('ou=People, dc=example, dc=com'))), - self::KEY_SEARCH_ATTRIBUTE => + self::KEY_USERNAME_ATTRIBUTE => pht('Example: %s', phutil_tag('tt', array(), pht('sn'))), - self::KEY_USERNAME_ATTRIBUTE => - pht('Optional, if different from search attribute.'), self::KEY_REALNAME_ATTRIBUTES => - pht('Optional. Example: %s', + pht('Example: %s', phutil_tag('tt', array(), 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 => @@ -289,11 +285,76 @@ final class PhabricatorAuthProviderLDAP $types = array( self::KEY_REFERRALS => 'checkbox', self::KEY_START_TLS => 'checkbox', - self::KEY_SEARCH_FIRST => 'checkbox', + self::KEY_SEARCH_ATTRIBUTES => 'textarea', self::KEY_REALNAME_ATTRIBUTES => 'list', self::KEY_ANONYMOUS_PASSWORD => 'password', ); + $instructions = array( + self::KEY_SEARCH_ATTRIBUTES => pht( + "When a user types their LDAP username and password into Phabricator, ". + "Phabricator can either bind to LDAP with those credentials directly ". + "(which is simpler, but not as powerful) or bind to LDAP with ". + "anonymous credentials, then search for record matching the supplied ". + "credentials (which is more complicated, but more powerful).\n\n". + "For many installs, direct binding is sufficient. However, you may ". + "want to search first if:\n\n". + " - You want users to be able to login with either their username ". + " or their email address.\n". + " - The login/username is not part of the distinguished name in ". + " your LDAP records.\n". + " - You want to restrict logins to a subset of users (like only ". + " those in certain departments).\n". + " - Your LDAP server is configured in some other way that prevents ". + " direct binding from working correctly.\n\n". + "**To bind directly**, enter the LDAP attribute corresponding to the ". + "login name into this box. Often, this is something like `sn` or ". + "`uid`. This is the simplest configuration, but will only work if the ". + "username is part of the distinguished name, and won't let you apply ". + "complex restrictions to logins.\n\n". + " lang=text,name=Simple Direct Binding\n". + " sn\n\n". + "**To search first**, provide an anonymous username and password ". + "below, then enter one or more search queries into this field, one ". + "per line. After binding, these queries will be used to identify the ". + "record associated with the login name the user typed.\n\n". + "Searches will be tried in order until a matching record is found. ". + "Each query can be a simple attribute name (like `sn` or `mail`), ". + "which will search for a matching record, or it can be a complex ". + "query that uses the string `\${login}` to represent the login ". + "name.\n\n". + "A common simple configuration is just an attribute name, like ". + "`sn`, which will work the same way direct binding works:\n\n". + " lang=text,name=Simple Example\n". + " sn\n\n". + "A slightly more complex configuration might let the user login with ". + "either their login name or email address:\n\n". + " lang=text,name=Match Several Attributes\n". + " mail\n". + " sn\n\n". + "If your LDAP directory is more complex, or you want to perform ". + "sophisticated filtering, you can use one or more queries. Depending ". + "on your directory structure, this example might allow users to login ". + "with either their email address or username, but only if they're in ". + "specific departments:\n\n". + " lang=text,name=Complex Example\n". + " (&(mail=\${login})(|(departmentNumber=1)(departmentNumber=2)))\n". + " (&(sn=\${login})(|(departmentNumber=1)(departmentNumber=2)))\n\n". + "All of the attribute names used here are just examples: your LDAP ". + "server may use different attribute names."), + self::KEY_USERNAME_ATTRIBUTE => pht( + "Optionally, specify a username attribute to use to prefill usernames ". + "when registering a new account. This is purely cosmetic and does not ". + "affect the login process, but you can configure it to make sure ". + "users get the same default username as their LDAP username, so ". + "usernames remain consistent across systems."), + self::KEY_REALNAME_ATTRIBUTES => pht( + "Optionally, specify one or more comma-separated attributes to use to ". + "prefill the \"Real Name\" field when registering a new account. This ". + "is purely cosmetic and does not affect the login process, but can ". + "make registration a little easier."), + ); + foreach ($labels as $key => $label) { $caption = idx($captions, $key); $type = idx($types, $key); @@ -323,6 +384,13 @@ final class PhabricatorAuthProviderLDAP ->setCaption($caption) ->setValue($value); break; + case 'textarea': + $control = id(new AphrontFormTextAreaControl()) + ->setName($key) + ->setLabel($label) + ->setCaption($caption) + ->setValue($value); + break; default: $control = id(new AphrontFormTextControl()) ->setName($key) @@ -332,6 +400,11 @@ final class PhabricatorAuthProviderLDAP break; } + $instruction_text = idx($instructions, $key); + if (strlen($instruction_text)) { + $form->appendRemarkupInstructions($instruction_text); + } + $form->appendChild($control); } }