diff --git a/lam/HISTORY b/lam/HISTORY index 472e45c0..6d07de65 100644 --- a/lam/HISTORY +++ b/lam/HISTORY @@ -1,6 +1,7 @@ June 2015 - Microsoft IE 8 no longer supported - Security: added CSRF protection + - NIS net groups: user module to manage NIS net groups on user page - Zarafa users: allow to change display format of "Send As" - User list: support to filter by account status - Lamdaemon: update group of home directory if user's primary group changes diff --git a/lam/lib/modules/nisNetGroupUser.inc b/lam/lib/modules/nisNetGroupUser.inc new file mode 100644 index 00000000..c7f792d3 --- /dev/null +++ b/lam/lib/modules/nisNetGroupUser.inc @@ -0,0 +1,561 @@ + '', dn => '', host => '', domain => ''))) */ + private $groups = array(); + /** list of NIS netgroups the user was memberOf (array(array(name => '', dn => '', host => '', domain => ''))) */ + private $groupsOrig = array(); + /** group cache (array(array(cn => '', dn => '', nisnetgrouptriple => array()))) */ + private $groupCache = null; + + /** + * Returns true if this module can manage accounts of the current type, otherwise false. + * + * @return boolean true if module fits + */ + public function can_manage() { + return in_array($this->get_scope(), array('user')); + } + + /** + * Returns meta data that is interpreted by parent class + * + * @return array array with meta data + * + * @see baseModule::get_metaData() + */ + public function get_metaData() { + $return = array(); + // icon + $return['icon'] = 'groupBig.png'; + // module dependencies + $return['dependencies'] = array('depends' => array(array('posixAccount', 'inetOrgPerson')), 'conflicts' => array()); + // alias name + $return["alias"] = _("NIS net groups"); + // available PDF fields + $return['PDF_fields']['memberships'] = _('NIS net groups'); + // help Entries + $return['help'] = array( + 'addgroup' => array( + 'Headline' => _('Groups of names'), + 'Text' => _("Hold the CTRL-key to (de)select multiple groups."). ' '. _("Can be left empty.") + ), + 'addgroup_upload' => array( + "Headline" => _("Groups of names"), + "Text" => _("Here you can enter a list of additional group memberships. The group names are separated by commas.") + ), + ); + // upload columns + $return['upload_columns'][] = array( + 'name' => 'nisNetGroupUser_memberships', + 'description' => _('Memberships'), + 'help' => 'memberships_upload', + 'example' => 'group1##host##domain,group2##host##domain' + ); + return $return; + } + + /** + * This function fills the $messages variable with output messages from this module. + */ + function load_Messages() { + $this->messages['host'][0] = array('ERROR', _('Host name'), _('Host name contains invalid characters. Valid characters are: a-z, A-Z, 0-9 and .-_ !')); + $this->messages['domain'][0] = array('ERROR', _('Domain name'), _('Domain name is invalid!')); + } + + /** + * Initializes the module after it became part of an accountContainer + * + * @param string $base the name of the accountContainer object ($_SESSION[$base]) + */ + function init($base) { + // call parent init + parent::init($base); + $this->groups = array(); + $this->groupsOrig = array(); + } + + /** + * This function loads all needed LDAP attributes. + * + * @param array $attr list of attributes + */ + function load_attributes($attr) { + parent::load_attributes($attr); + if (empty($attr['uid'][0])) { + return; + } + $uid = $attr['uid'][0]; + $types = array('netgroup'); + $typeSettings = $_SESSION['config']->get_typeSettings(); + $groupList = array(); + $filter = '(&(objectClass=nisNetgroup)(nisnetgrouptriple=*))'; + if (!empty($typeSettings['filter_' . $types[0]])) { + $typeFilter = $typeSettings['filter_' . $types[0]]; + if (strpos($typeFilter, '(') !== 0) { + $typeFilter = '(' . $typeFilter . ')'; + } + $filter = '(&' . $filter . $typeFilter . ')'; + } + $groupList = searchLDAPByFilter($filter, array('dn', 'cn', 'nisnetgrouptriple'), $types); + $this->groupsOrig = array(); + $tripleRegex = '/^\\(([^,]*),([^,]*),([^,]*)\\)$/'; + foreach ($groupList as $group) { + foreach ($group['nisnetgrouptriple'] as $triple) { + $matches = array(); + if (preg_match($tripleRegex, $triple, $matches) == 0) { + continue; + } + $host = $matches[1]; + $user = $matches[2]; + $domain = $matches[3]; + if ($user == $uid) { + $this->groupsOrig[] = array( + 'name' => $group['cn'][0], + 'dn' => $group['dn'], + 'host' => $host, + 'domain' => $domain + ); + } + } + } + usort($this->groupsOrig, array($this, 'sortTriple')); + $this->groups = $this->groupsOrig; + } + + /** + * Displays the group selection. + * + * @return htmlElement meta HTML code + */ + public function display_html_attributes() { + $return = new htmlTable(); + $return->addElement(new htmlOutputText(_('Group'))); + $return->addElement(new htmlOutputText(_('Host name'))); + $return->addElement(new htmlOutputText(_('Domain name')), true); + for ($i = 0; $i < sizeof($this->groups); $i++) { + $group = $this->groups[$i]; + $return->addElement(new htmlOutputText($group['name'])); + $return->addElement(new htmlInputField('host_' . $i, $group['host'])); + $return->addElement(new htmlInputField('domain_' . $i, $group['domain'])); + $delButton = new htmlButton('del_' . $i, 'del.png', true); + $delButton->setTitle(_('Delete')); + $return->addElement($delButton, true); + } + $return->addVerticalSpace('20px'); + + // new entry + $groupList = array(); + $groupData = $this->findGroups(); + if (sizeof($groupData) > 0) { + foreach ($groupData as $group) { + $groupList[$group['cn'][0]] = $group['cn'][0] . '#+#' . $group['dn']; + } + $groupSelect = new htmlSelect('group_add', $groupList); + $groupSelect->setHasDescriptiveElements(true); + $return->addElement($groupSelect); + $return->addElement(new htmlInputField('host_add')); + $return->addElement(new htmlInputField('domain_add')); + $addButton = new htmlButton('addGroup', 'add.png', true); + $addButton->setTitle(_('Add')); + $return->addElement($addButton, true); + } + return $return; + } + + /** + * Processes user input of the group selection page. + * It checks if all input values are correct and updates the associated LDAP attributes. + * + * @return array list of info/error messages + */ + public function process_attributes() { + $errors = array(); + // add new entry + if (isset($_POST['addGroup'])) { + $parts = explode('#+#', $_POST['group_add']); + $this->groups[] = array( + 'name' => $parts[0], + 'dn' => $parts[1], + 'host' => $_POST['host_add'], + 'domain' => $_POST['domain_add'] + ); + if (!empty($_POST['host_add']) && !get_preg($_POST['host_add'], 'DNSname')) { + $message = $this->messages['host'][0]; + $message[2] = $message[2] . '

' . $_POST['host_add']; + $errors[] = $message; + } + if (!empty($_POST['domain_add']) && !get_preg($_POST['domain_add'], 'DNSname')) { + $message = $this->messages['domain'][0]; + $message[2] = $message[2] . '

' . $_POST['domain_add']; + $errors[] = $message; + } + } + // check existing + $counter = 0; + while (isset($_POST['host_' . $counter])) { + if (isset($_POST['del_' . $counter])) { + unset($this->groups[$counter]); + } + else { + $this->groups[$counter]['host'] = $_POST['host_' . $counter]; + if (!empty($_POST['host_' . $counter]) && !get_preg($_POST['host_' . $counter], 'DNSname')) { + $message = $this->messages['host'][0]; + $message[2] = $message[2] . '

' . $_POST['host_' . $counter]; + $errors[] = $message; + } + $this->groups[$counter]['domain'] = $_POST['domain_' . $counter]; + if (!empty($_POST['domain_' . $counter]) && !get_preg($_POST['domain_' . $counter], 'DNSname')) { + $message = $this->messages['domain'][0]; + $message[2] = $message[2] . '

' . $_POST['domain_' . $counter]; + $errors[] = $message; + } + } + $counter++; + } + $this->groups = array_values($this->groups); + usort($this->groups, array($this, 'sortTriple')); + return $errors; + } + + /** + * Runs the postmodify actions. + * + * @see baseModule::postModifyActions() + * + * @param boolean $newAccount + * @param array $attributes LDAP attributes of this entry + * @return array array which contains status messages. Each entry is an array containing the status message parameters. + */ + public function postModifyActions($newAccount, $attributes) { + $moduleAttributes = array(); + if ($this->getAccountContainer()->getAccountModule('posixAccount') != null) { + $moduleAttributes = $this->getAccountContainer()->getAccountModule('posixAccount')->getAttributes(); + } + else { + $moduleAttributes = $this->getAccountContainer()->getAccountModule('inetOrgPerson')->getAttributes(); + } + if (empty($moduleAttributes['uid'][0])) { + return array(); + } + $ldapUser = $_SESSION['ldap']->decrypt_login(); + $ldapUser = $ldapUser[0]; + $uid = $moduleAttributes['uid'][0]; + $messages = array(); + // calculate differences + $toRem = $this->groupsOrig; + $toAdd = $this->groups; + $counter = sizeof($toRem); + for ($i = 0; $i < $counter; $i++) { + $group_orig = $toRem[$i]; + foreach ($toAdd as $k => $group) { + if (($group_orig['dn'] == $group['dn']) + && ($group_orig['domain'] == $group['domain']) + && ($group_orig['host'] == $group['host'])) { + unset($toRem[$i]); + unset($toAdd[$k]); + break; + } + } + } + // group by DN + $changes = array(); + foreach ($toAdd as $add) { + $changes[$add['dn']]['add'][] = '(' . $add['host'] . ',' . $uid . ',' . $add['domain'] . ')'; + } + foreach ($toRem as $del) { + $changes[$del['dn']]['del'][] = '(' . $del['host'] . ',' . $uid . ',' . $del['domain'] . ')'; + } + // add groups + foreach ($changes as $dn => $changeSet) { + $current = ldapGetDN($dn, array('nisnetgrouptriple')); + if (empty($current)) { + $messages[] = array('ERROR', sprintf(_('Was unable to modify attributes of DN: %s.'), $dn)); + continue; + } + $triples = empty($current['nisnetgrouptriple']) ? array() : $current['nisnetgrouptriple']; + if (!empty($changeSet['del'])) { + $triples = array_delete($changeSet['del'], $triples); + } + if (!empty($changeSet['add'])) { + $triples = array_merge($changeSet['add'], $triples); + } + $triples = array_values(array_unique($triples)); + $attributes = array( + 'nisnetgrouptriple' => $triples + ); + $success = @ldap_mod_replace($_SESSION['ldap']->server(), $dn, $attributes); + if (!$success) { + logNewMessage(LOG_ERR, '[' . $ldapUser .'] Unable to modify attributes of DN: ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').'); + $messages[] = array('ERROR', sprintf(_('Was unable to modify attributes of DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())); + } + } + return $messages; + } + + /** + * Additional LDAP operations on delete. + * + * @return List of LDAP operations, same as for save_attributes() + */ + function delete_attributes() { + $return = array(); + // remove from group of names + return $return; + } + + /** + * Returns a list of elements for the account profiles. + * + * @return profile elements + */ + function get_profileOptions() { + $return = new htmlTable(); + // group of names + $gons = $this->findGroupOfNames(); + $gonList = array(); + foreach ($gons as $dn => $attr) { + $gonList[$attr['cn'][0]] = $dn; + } + $gonSelect = new htmlTableExtendedSelect('groupOfNamesUser_gon', $gonList, array(), _('Groups of names'), 'addgroup', 10); + $gonSelect->setHasDescriptiveElements(true); + $gonSelect->setMultiSelect(true); + $gonSelect->setTransformSingleSelect(false); + $return->addElement($gonSelect, true); + return $return; + } + + /** + * Loads the values of an account profile into internal variables. + * + * @param array $profile hash array with profile values (identifier => value) + */ + function load_profile($profile) { + // profile mappings in meta data + parent::load_profile($profile); + // special profile options + // group of names + if (isset($profile['groupOfNamesUser_gon'][0])) { + $this->gonList = $profile['groupOfNamesUser_gon']; + } + } + + /** + * Returns a list of possible PDF entries for this account. + * + * @param array $pdfKeys list of PDF keys that are included in document + * @return list of PDF entries (array( => )) + */ + function get_pdfEntries($pdfKeys) { + $allGons = $this->findGroupOfNames(); + $gons = array(); + for ($i = 0; $i < sizeof($this->gonList); $i++) { + if (isset($allGons[$this->gonList[$i]])) { + $gons[] = $allGons[$this->gonList[$i]]['cn'][0]; + } + } + natcasesort($gons); + $return = array(); + $this->addPDFKeyValue($return, 'gon', _('Groups of names'), $gons); + return $return; + } + + /** + * In this function the LDAP account is built up. + * + * @param array $rawAccounts list of hash arrays (name => value) from user input + * @param array $ids list of IDs for column position (e.g. "posixAccount_uid" => 5) + * @param array $partialAccounts list of hash arrays (name => value) which are later added to LDAP + * @param array $selectedModules list of selected account modules + * @return array list of error messages if any + */ + function build_uploadAccounts($rawAccounts, $ids, &$partialAccounts, $selectedModules) { + $errors = array(); + // get list of existing group of names + $gons = $this->findGroupOfNames(); + $gonList = array(); + foreach ($gons as $dn => $attr) { + $gonList[] = $attr['cn'][0]; + } + // check input + for ($i = 0; $i < sizeof($rawAccounts); $i++) { + // group of names + if ($rawAccounts[$i][$ids['groupOfNamesUser_gon']] != "") { + $groups = explode(",", $rawAccounts[$i][$ids['groupOfNamesUser_gon']]); + for ($g = 0; $g < sizeof($groups); $g++) { + if (!in_array($groups[$g], $gonList)) { + $errors[] = array('ERROR', _('Unable to find group in LDAP.'), $groups[$g]); + } + } + } + } + return $errors; + } + + /** + * This function executes one post upload action. + * + * @param array $data array containing one account in each element + * @param array $ids array( => ) + * @param array $failed list of accounts which were not created successfully + * @param array $temp variable to store temporary data between two post actions + * @param array $accounts list of LDAP entries + * @return array current status + *
array ( + *
'status' => 'finished' | 'inProgress' + *
'progress' => 0..100 + *
'errors' => array () + *
) + */ + function doUploadPostActions(&$data, $ids, $failed, &$temp, &$accounts) { + if (!checkIfWriteAccessIsAllowed($this->get_scope())) { + die(); + } + // on first call generate list of LDAP operations + if (!isset($temp['counter'])) { + $temp['dn_gon'] = array(); + $temp['counter'] = 0; + // get list of existing group of names + $gonList = $this->findGroupOfNames(); + $gonMap = array(); + foreach ($gonList as $dn => $attr) { + $gonMap[$attr['cn'][0]] = $dn; + } + for ($i = 0; $i < sizeof($data); $i++) { + if (in_array($i, $failed)) continue; // ignore failed accounts + if (isset($ids['groupOfNamesUser_gon']) && ($data[$i][$ids['groupOfNamesUser_gon']] != "")) { + $gons = explode(",", $data[$i][$ids['groupOfNamesUser_gon']]); + $memberAttr = 'member'; + for ($g = 0; $g < sizeof($gons); $g++) { + if (in_array('groupOfUniqueNames', $gonList[$gonMap[$gons[$g]]]['objectclass'])) { + $memberAttr = 'uniqueMember'; + } + $temp['dn_gon'][$gonMap[$gons[$g]]][$memberAttr][] = $accounts[$i]['dn']; + } + } + } + $temp['dn_gon_keys'] = array_keys($temp['dn_gon']); + return array( + 'status' => 'inProgress', + 'progress' => 0, + 'errors' => array() + ); + } + // add users to group of names + elseif ($temp['counter'] < sizeof($temp['dn_gon'])) { + $gonDn = $temp['dn_gon_keys'][$temp['counter']]; + $gonAttr = $temp['dn_gon'][$gonDn]; + $success = @ldap_mod_add($_SESSION['ldap']->server(), $gonDn, $gonAttr); + $errors = array(); + if (!$success) { + $errors[] = array( + "ERROR", + _("LAM was unable to modify group memberships for group: %s"), + getDefaultLDAPErrorString($_SESSION['ldap']->server()), + array($temp['groups'][$temp['counter']]) + ); + } + $temp['counter']++; + return array ( + 'status' => 'inProgress', + 'progress' => ($temp['counter'] * 100) / sizeof($temp['dn_gon']), + 'errors' => $errors + ); + } + // all modifications are done + else { + return array ( + 'status' => 'finished', + 'progress' => 100, + 'errors' => array() + ); + } + } + + /** + * Finds all existing LDAP NIS net groups. + * + * @return array groups array(array(cn => array(), dn => '', nisnetgrouptriple => array())) + */ + private function findGroups() { + if ($this->groupCache != null) { + return $this->groupCache; + } + $return = array(); + $types = array('netgroup'); + $typeSettings = $_SESSION['config']->get_typeSettings(); + if (sizeof($types) > 0) { + foreach ($types as $type) { + $filter = '(objectClass=nisNetgroup)'; + if (!empty($typeSettings['filter_' . $type])) { + $typeFilter = $typeSettings['filter_' . $type]; + if (strpos($typeFilter, '(') !== 0) { + $typeFilter = '(' . $typeFilter . ')'; + } + $filter = '(&' . $filter . $typeFilter . ')'; + } + $results = searchLDAPByFilter($filter, array('cn', 'dn', 'nisnetgrouptriple'), array($type)); + for ($i = 0; $i < sizeof($results); $i++) { + if (isset($results[$i]['cn'][0]) && isset($results[$i]['dn'])) { + $return[] = $results[$i]; + } + } + } + } + $this->groupCache = $return; + return $return; + } + + /** + * Sorts NIS netgroup triples by group, host and domain. + * + * @param array $first first array + * @param array $second second array + */ + private function sortTriple($first, $second) { + if ($first['name'] != $second['name']) { + return strnatcasecmp($first['name'], $second['name']); + } + elseif ($first['host'] != $second['host']) { + return strnatcasecmp($first['host'], $second['host']); + } + return strnatcasecmp($first['domain'], $second['domain']); + } + +} + +?>