diff --git a/phpgwapi/doc/ldap/README b/phpgwapi/doc/ldap/README index 8bb99980f3..9f18d4cbb6 100644 --- a/phpgwapi/doc/ldap/README +++ b/phpgwapi/doc/ldap/README @@ -5,4 +5,25 @@ eGroupWare needs no more special LDAP schemas since version 1.3.007: - valid eGroupWare users have a posixAccount and shadowAccount object class. - valid Groups have a posixGroup object class and store there members in the memberuid attribute. -Ralf \ No newline at end of file +If you want to use group-addressbooks in LDAP, the ACL requires that groups get expanded by the LDAP server. +To do so, we need to use groupOfNames together with posixGroup (groupOfNames stores the dn, posixGroup only the uid). +If your LDAP uses the original nis.schema, posixGroup is a structural object and can NOT be used together! +Newer SuSE distributions use a rfc2307bis schema, which can be used on other distributions too +(instead of the nis.schema, NOT together). The schema is in the same directory as this README. + +To change to the rfc2307bis.schema (not needed with newer SuSE distros!): +---------------------------------- +- create an ldif from your ldap: slapcat > my.ldif +- add objectclass groupOfNames to every group (only the groups!) +- edit your slapd.conf: + + remove the include of the nis.schema + + include the rfc2307bis.schema in this dir +- stoping ldap +- empty the ldap database (eg. by removing the content of /var/lib/ldap) +- add the edited ldif file +- start ldap again + +eGroupWare detects if it can use groupOfNames together with posixGroup and fills the member attribute, +if you edit the group or changes the members. + +Ralf diff --git a/phpgwapi/doc/ldap/rfc2307bis.schema b/phpgwapi/doc/ldap/rfc2307bis.schema new file mode 100644 index 0000000000..aad7d08c5a --- /dev/null +++ b/phpgwapi/doc/ldap/rfc2307bis.schema @@ -0,0 +1,296 @@ +# +# This schema is a RFC draft and replaces the nis.schema +# +# You can NOT install it together with the nis.schema! +# +# The purpose of it is, to use posixGroup together with groupOfNames +# +# uidNumber and gidNumber might be buildin, in that case you +# need to comment out both (putting a # infront the whole entry) +# +# $Id$ +# +attributetype ( 1.3.6.1.1.1.1.0 NAME 'uidNumber' + DESC 'An integer uniquely identifying a user in an administrative domain' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.1 NAME 'gidNumber' + DESC 'An integer uniquely identifying a group in an + administrative domain' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.2 NAME 'gecos' + DESC 'The GECOS field; the common name' + EQUALITY caseIgnoreIA5Match + SUBSTR caseIgnoreIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.3 NAME 'homeDirectory' + DESC 'The absolute path to the home directory' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.4 NAME 'loginShell' + DESC 'The path to the login shell' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.6 NAME 'shadowMin' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.7 NAME 'shadowMax' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.8 NAME 'shadowWarning' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.9 NAME 'shadowInactive' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.10 NAME 'shadowExpire' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.11 NAME 'shadowFlag' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.12 NAME 'memberUid' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +attributetype ( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup' + EQUALITY caseExactIA5Match + SUBSTR caseExactIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +attributetype ( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple' + DESC 'Netgroup triple' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +attributetype ( 1.3.6.1.1.1.1.15 NAME 'ipServicePort' + DESC 'Service port number' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol' + DESC 'Service protocol name' + SUP name ) + +attributetype ( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber' + DESC 'IP protocol number' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber' + DESC 'ONC RPC number' + EQUALITY integerMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 + SINGLE-VALUE ) +attributetype ( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber' + DESC 'IPv4 addresses as a dotted decimal omitting leading + zeros or IPv6 addresses as defined in RFC2373' + SUP name ) + +attributetype ( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber' + DESC 'IP network as a dotted decimal, eg. 192.168, + omitting leading zeros' + SUP name + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber' + DESC 'IP netmask as a dotted decimal, eg. 255.255.255.0, + omitting leading zeros' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.22 NAME 'macAddress' + DESC 'MAC address in maximal, colon separated hex + notation, eg. 00:00:92:90:ee:e2' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +attributetype ( 1.3.6.1.1.1.1.23 NAME 'bootParameter' + DESC 'rpc.bootparamd parameter' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +attributetype ( 1.3.6.1.1.1.1.24 NAME 'bootFile' + DESC 'Boot image name' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +attributetype ( 1.3.6.1.1.1.1.26 NAME 'nisMapName' + DESC 'Name of a A generic NIS map' + SUP name ) + +attributetype ( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry' + DESC 'A generic NIS entry' + EQUALITY caseExactIA5Match + SUBSTR caseExactIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 + SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.28 NAME 'nisPublicKey' + DESC 'NIS public key' + EQUALITY octetStringMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.29 NAME 'nisSecretKey' + DESC 'NIS secret key' + EQUALITY octetStringMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.30 NAME 'nisDomain' + DESC 'NIS domain' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26) + +attributetype ( 1.3.6.1.1.1.1.31 NAME 'automountMapName' + DESC 'automount Map Name' + EQUALITY caseExactIA5Match + SUBSTR caseExactIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.32 NAME 'automountKey' + DESC 'Automount Key value' + EQUALITY caseExactIA5Match + SUBSTR caseExactIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) + +attributetype ( 1.3.6.1.1.1.1.33 NAME 'automountInformation' + DESC 'Automount information' + EQUALITY caseExactIA5Match + SUBSTR caseExactIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) + +objectclass ( 1.3.6.1.1.1.2.0 NAME 'posixAccount' SUP top AUXILIARY + DESC 'Abstraction of an account with POSIX attributes' + MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) + MAY ( userPassword $ loginShell $ gecos $ + description ) ) + +objectclass ( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' SUP top AUXILIARY + DESC 'Additional attributes for shadow passwords' + MUST uid + MAY ( userPassword $ description $ + shadowLastChange $ shadowMin $ shadowMax $ + shadowWarning $ shadowInactive $ + shadowExpire $ shadowFlag ) ) + +objectclass ( 1.3.6.1.1.1.2.2 NAME 'posixGroup' SUP top AUXILIARY + DESC 'Abstraction of a group of accounts' + MUST gidNumber + MAY ( userPassword $ memberUid $ + description ) ) + +objectclass ( 1.3.6.1.1.1.2.3 NAME 'ipService' SUP top STRUCTURAL + DESC 'Abstraction an Internet Protocol service. + Maps an IP port and protocol (such as tcp or udp) + to one or more names; the distinguished value of + the cn attribute denotes the services canonical + name' + MUST ( cn $ ipServicePort $ ipServiceProtocol ) + MAY description ) + +objectclass ( 1.3.6.1.1.1.2.4 NAME 'ipProtocol' SUP top STRUCTURAL + DESC 'Abstraction of an IP protocol. Maps a protocol number + to one or more names. The distinguished value of the cn + attribute denotes the protocols canonical name' + MUST ( cn $ ipProtocolNumber ) + MAY description ) + +objectclass ( 1.3.6.1.1.1.2.5 NAME 'oncRpc' SUP top STRUCTURAL + DESC 'Abstraction of an Open Network Computing (ONC) + [RFC1057] Remote Procedure Call (RPC) binding. + This class maps an ONC RPC number to a name. + The distinguished value of the cn attribute denotes + the RPC services canonical name' + MUST ( cn $ oncRpcNumber ) + MAY description ) + +objectclass ( 1.3.6.1.1.1.2.6 NAME 'ipHost' SUP top AUXILIARY + DESC 'Abstraction of a host, an IP device. The distinguished + value of the cn attribute denotes the hosts canonical + name. Device SHOULD be used as a structural class' + MUST ( cn $ ipHostNumber ) + MAY ( userPassword $ l $ description $ manager ) ) + +objectclass ( 1.3.6.1.1.1.2.7 NAME 'ipNetwork' SUP top STRUCTURAL + DESC 'Abstraction of a network. The distinguished value of + the cn attribute denotes the networks canonical name' + MUST ipNetworkNumber + MAY ( cn $ ipNetmaskNumber $ l $ description $ manager ) ) + +objectclass ( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' SUP top STRUCTURAL + DESC 'Abstraction of a netgroup. May refer to other netgroups' + MUST cn + MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) ) + +objectclass ( 1.3.6.1.1.1.2.9 NAME 'nisMap' SUP top STRUCTURAL + DESC 'A generic abstraction of a NIS map' + MUST nisMapName + MAY description ) + +objectclass ( 1.3.6.1.1.1.2.10 NAME 'nisObject' SUP top STRUCTURAL + DESC 'An entry in a NIS map' + MUST ( cn $ nisMapEntry $ nisMapName ) + MAY description ) + +objectclass ( 1.3.6.1.1.1.2.11 NAME 'ieee802Device' SUP top AUXILIARY + DESC 'A device with a MAC address; device SHOULD be + used as a structural class' + MAY macAddress ) + +objectclass ( 1.3.6.1.1.1.2.12 NAME 'bootableDevice' SUP top AUXILIARY + DESC 'A device with boot parameters; device SHOULD be + used as a structural class' + MAY ( bootFile $ bootParameter ) ) + +objectclass ( 1.3.6.1.1.1.2.14 NAME 'nisKeyObject' SUP top AUXILIARY + DESC 'An object with a public and secret key' + MUST ( cn $ nisPublicKey $ nisSecretKey ) + MAY ( uidNumber $ description ) ) + +objectclass ( 1.3.6.1.1.1.2.15 NAME 'nisDomainObject' SUP top AUXILIARY + DESC 'Associates a NIS domain with a naming context' + MUST nisDomain ) + +objectclass ( 1.3.6.1.1.1.2.16 NAME 'automountMap' SUP top STRUCTURAL + MUST ( automountMapName ) + MAY description ) + +objectclass ( 1.3.6.1.1.1.2.17 NAME 'automount' SUP top STRUCTURAL + DESC 'Automount information' + MUST ( automountKey $ automountInformation ) + MAY description ) +## namedObject is needed for groups without members +objectclass ( 1.3.6.1.4.1.5322.13.1.1 NAME 'namedObject' SUP top + STRUCTURAL MAY cn ) + diff --git a/phpgwapi/inc/class.accounts_ldap.inc.php b/phpgwapi/inc/class.accounts_ldap.inc.php index 57a124c783..c3d585086b 100644 --- a/phpgwapi/inc/class.accounts_ldap.inc.php +++ b/phpgwapi/inc/class.accounts_ldap.inc.php @@ -63,6 +63,8 @@ class accounts_backend * @var int */ var $total; + + var $ldapServerInfo; /** * required classe for user and groups @@ -74,10 +76,7 @@ class accounts_backend 'top','person','organizationalperson','inetorgperson','posixaccount','shadowaccount' ), 'group' => array( - 'top','posixgroup', - // some newer ldap require namedObject here, as none of the above is a structural object there - // this gets now autodetected - //'namedObject' + 'top','posixgroup','groupofnames' ) ); /** @@ -95,7 +94,7 @@ class accounts_backend function accounts_backend() { // enable the caching in the session, done by the accounts class extending this class. - $this->use_session_cache = true; + $this->use_session_cache = true; $this->ds = $GLOBALS['egw']->common->ldapConnect(); if(!@is_object($GLOBALS['egw']->translation)) @@ -139,7 +138,12 @@ class accounts_backend $is_group = $data['account_id'] < 0 || $data['account_type'] === 'g'; $data_utf8 = $this->translation->convert($data,$this->translation->charset(),'utf-8'); + $members = $data['account_members']; + if (!is_object($this->ldapServerInfo)) + { + $this->ldapServerInfo = $GLOBALS['egw']->ldap->getLDAPServerInfo($GLOBALS['egw_info']['server']['ldap_host']); + } // common code for users and groups // checks if accout_lid (dn) has been changed or required objectclass'es are missing if ($data_utf8['account_id'] && $data_utf8['account_lid']) @@ -160,16 +164,26 @@ class accounts_backend { $old['objectclass'][$n] = strtolower($class); } - if ($is_group && ($old['cn'] != $data_utf8['account_lid'] || substr($old['dn'],0,3) != 'cn=') || + $key = false; + if ($is_group && ($key = array_search('namedobject',$old['objectclass'])) !== false || + $is_group && ($old['cn'] != $data_utf8['account_lid'] || substr($old['dn'],0,3) != 'cn=') || !$is_group && ($old['uid'] != $data_utf8['account_lid'] || substr($old['dn'],0,4) != 'uid=')) { // query the memberships to set them again later - if (!$is_group) $memberships = $this->memberships($data['account_id']); - + if (!$is_group) + { + $memberships = $this->memberships($data['account_id']); + } + else + { + $members = $old ? $old['memberuid'] : $this->members($data['account_id']); + } // if dn has changed --> delete the old entry, as we cant rename the dn // $this->delete would call accounts::delete, which will delete als ACL of the user too! accounts_backend::delete($data['account_id']); unset($old['dn']); + // removing the namedObject object-class, if it's included + if ($key !== false) unset($old['objectclass'][$key]); $to_write = $old; unset($old); } @@ -205,6 +219,13 @@ class accounts_backend { $to_write = $this->_merge_group($to_write,$data_utf8); $data['account_type'] = 'g'; + + $groupOfNames = in_array('groupofnames',$old ? $old['objectclass'] : $to_write['objectclass']); + if (!$old && $groupOfNames || $members) + { + $to_write = array_merge($to_write,accounts_backend::set_members($members, + $data['account_id'],$groupOfNames,$dn)); + } } else { @@ -217,9 +238,11 @@ class accounts_backend !$old && !@ldap_add($this->ds,$dn,$to_write)) { $err = true; - if (!$old && $is_group) + if (!$old && $is_group && ($key = array_search('groupofnames',$to_write['objectclass'])) !== false) { - $to_write['objectclass'][] = 'namedobject'; + // try again with removed groupOfNames stuff, as I cant detect if posixGroup is a structural object + unset($to_write['objectclass'][$key]); + unset($to_write['member']); $err = !ldap_add($this->ds,$dn,$to_write); } if ($err) @@ -229,7 +252,7 @@ class accounts_backend return false; } } - if ($memberships) // setting the previous memberships of the renamed account + if ($memberships) { $this->set_memberships($memberships,$data['account_id']); } @@ -305,7 +328,7 @@ class accounts_backend function _read_group($account_id) { $sri = ldap_search($this->ds, $this->group_context, 'gidnumber=' . abs($account_id), - array('dn','gidnumber','cn')); + array('dn','gidnumber','cn','objectclass')); $data = ldap_get_entries($this->ds, $sri); if (!$data['count']) @@ -314,14 +337,20 @@ class accounts_backend } $data = $this->translation->convert($data[0],'utf-8'); - return array( + $group = array( 'account_dn' => $data['dn'], 'account_id' => -$data['gidnumber'][0], 'account_lid' => $data['cn'][0], 'account_type' => 'g', 'account_firstname' => $data['cn'][0], 'account_lastname' => lang('Group'), + 'groupOfNames' => in_array('groupOfNames',$data['objectclass']), ); + if (!is_object($this->ldapServerInfo)) + { + $this->ldapServerInfo = $GLOBALS['egw']->ldap->getLDAPServerInfo($GLOBALS['egw_info']['server']['ldap_host']); + } + return $group; } /** @@ -748,23 +777,42 @@ class accounts_backend * * @param array $members array with uidnumber or uid's * @param int $gid gidnumber of group to set + * @param boolean $groupOfNames=null should we set the member attribute of groupOfNames (default detect it) + * @param string $use_cn=null if set $cn is used instead $gid and the attributes are returned, not written to ldap + * @return boolean/array false on failure, array or true otherwise */ - function set_members($members,$gid) + function set_members($members,$gid,$groupOfNames=null,$use_cn=null) { //echo "
accounts_ldap::set_members(".print_r($members,true).",$gid)
\n"; - if (!($cn = $this->id2name($gid))) return false; + if (!($cn = $use_cn) && !($cn = $this->id2name($gid))) return false; + + // do that group is a groupOfNames? + if (is_null($groupOfNames)) $groupOfNames = $this->id2name($gid,'groupOfNames'); - foreach($members as $key => $member) + $to_write = array(); + foreach((array)$members as $key => $member) { - if (is_numeric($member) && ($member = $this->id2name($member))) + if (is_numeric($member)) $member = $this->id2name($member); + + if ($member) { - $members[$key] = $member; + $to_write['memberuid'][] = $member; + if ($groupOfNames) $to_write['member'][] = 'uid='.$member.','.$this->user_context; } } - if (!ldap_modify($this->ds,'cn='.ldap::quote($cn).','.$this->group_context,array('memberUid' => array_values(array_unique($members))))) + if ($groupOfNames && !$to_write['member']) { - echo "ldap_modify(,'cn=$cn,$this->group_context',array('memberUid' => ".print_r(array_values(array_unique($members)),true)."))\n"; + // hack as groupOfNames requires the member attribute + $to_write['member'][] = 'uid=dummy'.','.$this->user_context; } + if ($use_cn) return $to_write; + + if (!ldap_modify($this->ds,'cn='.ldap::quote($cn).','.$this->group_context,$to_write)) + { + echo "ldap_modify(,'cn=$cn,$this->group_context',".print_r($to_write,true)."))\n"; + return false; + } + return true; } /**