roundcube / roundcubemail

The Roundcube Webmail suite
https://roundcube.net
GNU General Public License v3.0
5.57k stars 1.6k forks source link

ldap_public: no option to use uid instead of cn as LDAP login? #9420

Open jejbq opened 1 month ago

jejbq commented 1 month ago

Hello,

My goal is to automatically populate identities (mail, cn or displayName, o) using LDAP ($config['ldap_public']) I tried new_user_identity without success, so I decided to switch to identity_from_directory.

Our LDAP entries look like this one: (the IMAP login equal to uid not cn)

dn: employeeNumber=42,ou=people,dc=example,dc=com
objectClass: inetOrgPerson
employeeNumber: 42
displayName: First LAST
mail: user@example.com
givenName: First
cn: First LAST
sn: LAST
uid: user

After enabling $config['ldap_debug'] = true, I discovered that new_user_identity cannot discover the user at login because it uses the cn instead of the uid and the search filter is not used and it will automatically add (|(cn=user)) instead of (|(uid=user)). The result is S: 0 record(s) found without the patch below and S: 1 record(s) found with.

program/lib/Roundcube/rcube_ldap.php :

C: Search base dn: [ou=people,dc=example,dc=com] scope [sub] with filter [(&(&(objectClass=inetOrgPerson)(mail=*@*))(|(cn=user)))]
Using function ldap_search on scope sub ($ns_function is ldap_search)
C: (Without VLV) Setting a filter of (&(&(objectClass=inetOrgPerson)(mail=*@*))(|(cn=user)))
Executing search with return attributes: array (
  0 => 'cn',
  1 => 'mail',
  ...
)
S: 0 record(s) found

The following patch allows you to work around the problem:

--- program/lib/Roundcube/rcube_ldap_generic.php    2024-01-20 11:15:04.000000000 +0100
+++ program/lib/Roundcube/rcube_ldap_generic.php    2024-04-18 18:02:06.416238000 +0200
@@ -326,7 +326,7 @@
     public static function fulltext_search_filter($value, $attributes, $mode = 1)
     {
         if (empty($attributes)) {
-            $attributes = ['cn'];
+            $attributes = ['uid'];
         }

         $groups = [];

The log after application of the patch below:

C: Search base dn: [ou=people,dc=example,dc=com] scope [sub] with filter [(&(&(objectClass=inetOrgPerson)(mail=*@*))(|(uid=user)))]
Using function ldap_search on scope sub ($ns_function is ldap_search)
C: (Without VLV) Setting a filter of (&(&(objectClass=inetOrgPerson)(mail=*@*))(|(uid=user)))
Executing search with return attributes: array (
  0 => 'cn',
  1 => 'mail',
  ...
)
S: 1 record(s) found

The $attributes are therefore not correctly defined in program/lib/Roundcube/rcube_ldap.php or in $config['ldap_public'][...]

            // map address book fields into ldap attributes
            foreach ((array) $fields as $field) {
                if (!empty($this->coltypes[$field]) && !empty($this->coltypes[$field]['attributes'])) {
                    $attributes = array_merge($attributes, (array) $this->coltypes[$field]['attributes']);
                }
            }

            // compose a full-text-like search filter
            $filter = rcube_ldap_generic::fulltext_search_filter($value, $attributes, $mode & ~rcube_addressbook::SEARCH_GROUPS);
        }

config/config.inc.php

$config['ldap_public']['People'] = [
  'name' => 'People',
  'hosts' => array('ldaps://ldap.example.com:636'),
  'ldap_version' => 3,
  'user_specific' => false,
  'base_dn'       => 'ou=people,dc=example,dc=com',
  'search_base_dn' => 'ou=people,dc=example,dc=com',
  'search_filter'  => '(&(objectClass=inetOrgPerson)(uid=%u))',
  'hidden'        => false,
  'searchonly'    => false,
  'writable'       => false,
  'LDAP_Object_Classes' => ['top', 'inetOrgPerson'],
  'LDAP_rdn'       => 'uid',
  'required_fields' => ['uid', 'mail', 'cn', 'sn', 'givenName'],
  'search_fields'   => ['name', 'surname', 'firstname', 'email'],
  //'search_fields'   => ['uid', 'mail', 'sn', 'cn'],
  'fieldmap' => [
    // Roundcube  => LDAP:limit
    'name'        => 'cn',
    'surname'     => 'sn',
    'firstname'   => 'givenName',
    'jobtitle'    => 'title',
    'email'       => 'mail:*',
    'phone:work'  => 'telephoneNumber',
    'street'      => 'street',
    'zipcode'     => 'postalCode',
    'region'      => 'st',
    'locality'    => 'l',
    'country'      => 'c',
    'organization' => 'o',
    'notes'        => 'roomNumber',
    'photo'        => 'jpegPhoto',
  ],
  'sort' => 'cn',
  'scope' => 'list',
  'filter'         => '(&(objectClass=inetOrgPerson)(mail=*@*))',
  'global_search'  => true,
  'fuzzy_search' => true,
  'vlv' => false,
  'vlv_search' => false,
  'referrals'      => false,
  'dereference'    => 0,
]; 

Thank you in advance for your help.

alecpl commented 3 weeks ago

I think there's a confusion (including myself) about what search_fields are. The documentation in defaults.inc.php is contradicting other places. It says the option contains ldap attribute names, but the option name suggests otherwise. The code assumes they are field names (from the fieldmap) too. The description of new_user_identity_match option is talking about a field, not attribute.

I think we should fix documentation for search_fields (and for required_fields), pointing out they are field names that have to exist in the fieldmap.

Then, in your case adding 'uid' => 'uid' to the fieldmap should solve the issue.

jejbq commented 2 weeks ago

Thank you for your help!

So to make it work, I had to add 'uid' => 'uid', in fieldmap AND set 'search_fields' => ['uid', 'cn', 'sn', 'givenName', 'mail'],

The documentation for search_fields is misleading "If empty, attributes for name, surname, firstname and email fields will be used" because 'search_fields' => ['uid', 'name', 'surname', 'firstname', 'email'], doesn't work and 'uid' will not be part of the query and will be replaced by hard-coded 'cn' if used.

search_filter' is still not used, so I don't understand how to force its use. What is the use case?

My other problem is that we have a second $config['ldap_public'] for Alumni, so I don't know if there's a way to keep it for the address book and define something in the configuration so that it's ignored during authentication. (something like canAuthenticate = false ;)

The config

$config['ldap_public']['People'] = [
  'name'          => 'People',
  'hosts'         => array('ldaps://ldap.example.com:636'),
  'ldap_version'  => 3,       // using LDAPv3
  'user_specific' => false,   // If true the base_dn, bind_dn and bind_pass default to the user's IMAP login.
  'base_dn'       => 'ou=people,dc=example,dc=com',
  'search_base_dn' => 'ou=people,dc=example,dc=com',
  'search_filter'  => '(&(objectClass=inetOrgPerson)(uid=%u))',   // e.g. '(&(objectClass=posixAccount)(uid=%u))'
  'search_bind_dn' => '',
  'search_bind_pw' => '',
  'search_bind_attrib' => [],  // e.g. ['%udc' => 'ou']
  'search_dn_default' => '',
  'hidden'        => false,
  'searchonly'    => false,
  'writable'       => false,
  'LDAP_Object_Classes' => ['top', 'inetOrgPerson'],
  'LDAP_rdn'       => 'uid',
  'required_fields' => ['uid', 'mail', 'cn', 'sn', 'givenName'],
  'search_fields'   => ['uid', 'cn', 'sn', 'givenName', 'mail'],
  'fieldmap' => [
    'uid'         => 'uid',
    'name'        => 'cn',
    'surname'     => 'sn',
    'firstname'   => 'givenName',
    'jobtitle'    => 'title',
    'email'       => 'mail:*',
    'phone:work'  => 'telephoneNumber',
    'street'      => 'street',
    'zipcode'     => 'postalCode',
    'region'      => 'st',
    'locality'    => 'l',
    'country'      => 'c',
    'organization' => 'o',
    'notes'        => 'roomNumber',
    'photo'        => 'jpegPhoto',
  ],
  'sort'           => 'cn',         // The field to sort the listing by.
  'scope'          => 'list',        // search mode: sub|base|list
  'filter'         => '(&(objectClass=inetOrgPerson))',
  'global_search'  => true,
  'fuzzy_search'   => true,         // server allows wildcard search
  'vlv'            => false,        // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
  'vlv_search'     => false,        // Use Virtual List View functions for autocompletion searches (if server supports it)
  'numsub_filter'  => '(objectClass=organizationalUnit)',   // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
  'config_root_dn' => 'cn=config',  // Root DN to search config entries (e.g. vlv indexes)
  'sizelimit'      => '0',          // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
  'timelimit'      => '0',          // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
  'referrals'      => false,        // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
  'dereference'    => 0,            // Sets the LDAP_OPT_DEREF option. One of: LDAP_DEREF_NEVER, LDAP_DEREF_SEARCHING, LDAP_DEREF_FINDING, LDAP_DEREF_ALWAYS
];

The log

C: Connect [ldaps://ldap.example.com:636]
S: OK
C: Search base dn: [ou=people,dc=example,dc=com] scope [list] with filter [(&(&(objectClass=inetOrgPerson))(|(uid=user)))]
Using function ldap_list on scope list ($ns_function is ldap_read)
C: (Without VLV) Setting a filter of (&(&(objectClass=inetOrgPerson))(|(uid=user)))
Executing search with return attributes: array (
  0 => 'uid',
  1 => 'cn',
  2 => 'sn',
  3 => 'givenname',
  4 => 'title',
  5 => 'mail',
  6 => 'telephonenumber',
  7 => 'street',
  8 => 'postalcode',
  9 => 'st',
  10 => 'l',
  11 => 'c',
  12 => 'o',
  13 => 'roomnumber',
  14 => 'jpegphoto',
  15 => 'objectClass',
  16 => 'cn',
)
S: 1 record(s) found
alecpl commented 2 weeks ago

The new_user_identity plugin replaces global search_fields setting with a single field configured in the plugin's config. So, you don't need to change search_fields.