diff --git a/lam/HISTORY b/lam/HISTORY index 47ebe3aa..5ad4d74b 100644 --- a/lam/HISTORY +++ b/lam/HISTORY @@ -1,5 +1,6 @@ December 2018 6.6 - New import/export in tools menu + - YubiKey support 25.09.2018 6.5 diff --git a/lam/docs/manual-sources/chapter-modules.xml b/lam/docs/manual-sources/chapter-modules.xml index 00263ab7..837d79fb 100644 --- a/lam/docs/manual-sources/chapter-modules.xml +++ b/lam/docs/manual-sources/chapter-modules.xml @@ -1992,6 +1992,74 @@ ldapsearch -x -h $server -p $port -b $baseDN -s sub "(&(objectclass=posixAcc AuthorizedKeysCommandUser root +
+ YubiKey + + You can manage your YubiKey ids with LAM. It supports the yubiKeyUser schema + or any other attribute mapping. + + Configuration + + First, you need to activate the YubiKey module for users in your + LAM server profile. + + + + + + Second, you need to specify which object class and attribute name + should be used. + + Object class: If you have an object class just for the YubiKey ids + then enter it here. LAM will then provide options to add and remove it. + In case you reuse some existing attribute from e.g. inetOrgPerson please + leave object class name blank. + + Attribute name: please enter the attribute name that is used for + the key ids. + + + + + + You will then be able to manage the key ids for your users. + + + + + + Self Service (LAM Pro) + + This will allow your users to update their own keys. + + You need to configure the object class and attribute name first. + This is done on tab "Module settings" in self service profile. + + Attention: Please note that both + fields are mandatory here. Even if you reused an attribute from some + existing object class you need to set it here. LAM needs this to detect + if the user can add keys. + + + + + + Then add the YubiKey ids field to your self service profile on tab + "Page layout". + + + + + + When a user with the specified object class logs in then the key + input fields are shown. + + + + +
+
Authorized services diff --git a/lam/docs/manual-sources/images/mod_yubikey1.png b/lam/docs/manual-sources/images/mod_yubikey1.png new file mode 100644 index 00000000..fddd5d7d Binary files /dev/null and b/lam/docs/manual-sources/images/mod_yubikey1.png differ diff --git a/lam/docs/manual-sources/images/mod_yubikey2.png b/lam/docs/manual-sources/images/mod_yubikey2.png new file mode 100644 index 00000000..0cf49c3e Binary files /dev/null and b/lam/docs/manual-sources/images/mod_yubikey2.png differ diff --git a/lam/docs/manual-sources/images/mod_yubikey3.png b/lam/docs/manual-sources/images/mod_yubikey3.png new file mode 100644 index 00000000..acd5c378 Binary files /dev/null and b/lam/docs/manual-sources/images/mod_yubikey3.png differ diff --git a/lam/docs/manual-sources/images/mod_yubikey4.png b/lam/docs/manual-sources/images/mod_yubikey4.png new file mode 100644 index 00000000..c1439fc7 Binary files /dev/null and b/lam/docs/manual-sources/images/mod_yubikey4.png differ diff --git a/lam/docs/manual-sources/images/mod_yubikey5.png b/lam/docs/manual-sources/images/mod_yubikey5.png new file mode 100644 index 00000000..eac43233 Binary files /dev/null and b/lam/docs/manual-sources/images/mod_yubikey5.png differ diff --git a/lam/docs/manual-sources/images/mod_yubikey6.png b/lam/docs/manual-sources/images/mod_yubikey6.png new file mode 100644 index 00000000..02b94efc Binary files /dev/null and b/lam/docs/manual-sources/images/mod_yubikey6.png differ diff --git a/lam/graphics/yubikey.png b/lam/graphics/yubikey.png new file mode 100755 index 00000000..bcd4d48c Binary files /dev/null and b/lam/graphics/yubikey.png differ diff --git a/lam/lib/baseModule.inc b/lam/lib/baseModule.inc index 9b948af0..2f2bd6af 100644 --- a/lam/lib/baseModule.inc +++ b/lam/lib/baseModule.inc @@ -68,7 +68,10 @@ abstract class baseModule { /** configuration settings of all modules */ protected $moduleSettings; - /** self service profile with settings of all modules */ + /** + * self service profile with settings of all modules + * @var selfServiceProfile profile + */ protected $selfServiceSettings; /** name of parent accountContainer ($_SESSION[$base]) */ diff --git a/lam/lib/modules/yubiKeyUser.inc b/lam/lib/modules/yubiKeyUser.inc new file mode 100644 index 00000000..06439c4c --- /dev/null +++ b/lam/lib/modules/yubiKeyUser.inc @@ -0,0 +1,592 @@ +autoAddObjectClasses = false; + } + + /** + * 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'] = 'yubikey.png'; + // alias name + $return["alias"] = _("YubiKey"); + // module dependencies + $return['dependencies'] = array('depends' => array(), 'conflicts' => array()); + // managed object classes + $objectClass = $this->getObjectClassName(); + if (!empty($objectClass)) { + $return['objectClasses'] = array($objectClass); + } + // managed attributes + $attributeName = $this->getAttributeName(); + $return['attributes'] = array($attributeName); + // help Entries + $return['help'] = array( + $attributeName => array( + "Headline" => _("YubiKey ids"), 'attr' => $attributeName, + "Text" => _("Please enter your YubiKey ids.") + ), + 'keyList' => array( + "Headline" => _("YubiKey ids"), 'attr' => $attributeName, + "Text" => _("Please a comma separated list of your YubiKey ids.") + ), + 'objectClass' => array( + "Headline" => _("Object class"), + "Text" => _("Please enter the object class that should be used for YubiKey (e.g. 'yubiKeyUser').") + ), + 'attributeName' => array( + "Headline" => _("Attribute name"), + "Text" => _("Please enter the attribute name that should be used for YubiKey (e.g. 'yubiKeyId').") + ), + ); + // upload fields + $return['upload_columns'] = array( + array( + 'name' => 'yubiKeyUser_yubiKeyId', + 'description' => _('YubiKey ids'), + 'help' => 'keyList', + 'example' => 'abcd1234, vwyxz12345' + ) + ); + // available PDF fields + $return['PDF_fields'] = array( + 'yubiKeyId' => _('YubiKey ids') + ); + // self service field settings + $return['selfServiceFieldSettings'] = array( + 'yubiKeyId' => _('YubiKey ids'), + ); + $return['selfServiceReadOnlyFields'] = array('yubiKeyId'); + return $return; + } + + /** + * This function fills the message array. + **/ + protected function load_Messages() { + $this->messages['yubiKeyId'][0] = array('ERROR', _('Please enter a valid key id.')); + $this->messages['yubiKeyId'][1] = array('ERROR', _('Account %s:') . ' yubiKeyUser_yubiKeyId', _('Please enter a valid key id.')); + } + + /** + * Returns the HTML meta data for the main account page. + * + * @return htmlElement HTML meta data + */ + public function display_html_attributes() { + $return = new htmlTable(); + $objectClass = $this->getObjectClassName(); + $attributeName = $this->getAttributeName(); + if (empty($attributeName)) { + $message = new htmlStatusMessage('ERROR', _('Invalid configuration detected. Please edit your server profile (module settings) and fill all required fields.')); + $return->addElement($message, true); + return $return; + } + if (empty($objectClass) || in_array($objectClass, $this->attributes['objectClass'])) { + $this->addMultiValueInputTextField($return, $attributeName, _('YubiKey ids'), false, '256', false, null, '50'); + if (!empty($objectClass)) { + $return->addElement(new htmlSpacer(null, '30px'), true); + + $remButton = new htmlButton('remObjectClass', _('Remove YubiKey extension')); + $remButton->colspan = 3; + $return->addElement($remButton); + } + } + else { + $return->addElement(new htmlButton('addObjectClass', _('Add YubiKey extension'))); + } + return $return; + } + + /** + * Processes user input of the primary module 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() { + $messages = array(); + $objectClass = $this->getObjectClassName(); + $attributeName = $this->getAttributeName(); + // add extension + if (isset($_POST['addObjectClass'])) { + $this->attributes['objectClass'][] = $objectClass; + return array(); + } + // remove extension + elseif (isset($_POST['remObjectClass'])) { + $this->attributes['objectClass'] = array_delete(array($objectClass), $this->attributes['objectClass']); + if (!empty($this->attributes[$attributeName])) { + unset($this->attributes[$attributeName]); + } + return array(); + } + // skip processing if extension is not active + if (!empty($objectClass) && !in_array($objectClass, $this->attributes['objectClass'])) { + return array(); + } + $this->processMultiValueInputTextField($attributeName, $messages, 'ascii'); + $this->attributes[$attributeName] = array_values(array_unique($this->attributes[$attributeName])); + return $messages; + } + + /** + * Returns a list of modifications which have to be made to the LDAP account. + * + * @return array list of modifications + *
This function returns an array with 3 entries: + *
array( DN1 ('add' => array($attr), 'remove' => array($attr), 'modify' => array($attr)), DN2 .... ) + *
DN is the DN to change. It may be possible to change several DNs (e.g. create a new user and add him to some groups via attribute memberUid) + *
"add" are attributes which have to be added to LDAP entry + *
"remove" are attributes which have to be removed from LDAP entry + *
"modify" are attributes which have to been modified in LDAP entry + *
"info" are values with informational value (e.g. to be used later by pre/postModify actions) + */ + public function save_attributes() { + $objectClass = $this->getObjectClassName(); + if (!empty($objectClass) && !in_array($objectClass, $this->attributes['objectClass']) && !in_array($objectClass, $this->orig['objectClass'])) { + // skip saving if the extension was not added/modified + return array(); + } + return parent::save_attributes(); + } + + /** + * {@inheritDoc} + * @see baseModule::build_uploadAccounts() + */ + public function build_uploadAccounts($rawAccounts, $ids, &$partialAccounts, $selectedModules, &$type) { + $objectClass = $this->getObjectClassName(); + $attributeName = $this->getAttributeName(); + $messages = array(); + for ($i = 0; $i < sizeof($rawAccounts); $i++) { + // add object class + if (!empty($objectClass) && !in_array($objectClass, $partialAccounts[$i]['objectClass'])) { + $partialAccounts[$i]['objectClass'][] = $objectClass; + } + // add keys + $message = $this->messages['yubiKeyId'][1]; + $this->mapSimpleUploadField($rawAccounts, $ids, $partialAccounts, $i, 'yubiKeyUser_yubiKeyId', $attributeName, 'ascii', $message, $messages, '/,[ ]*/'); + } + return $messages; + } + + /** + * {@inheritDoc} + * @see baseModule::get_pdfEntries() + */ + public function get_pdfEntries($pdfKeys, $typeId) { + $return = array(); + $attributeName = $this->getAttributeName(); + if (!empty($this->attributes[$attributeName])) { + $pdfTable = new PDFTable(_('YubiKey ids')); + for ($i = 0; $i < sizeof($this->attributes[$attributeName]); $i++) { + $pdfRow = new PDFTableRow(); + $pdfRow->cells[] = new PDFTableCell($this->attributes[$attributeName][$i]); + $pdfTable->rows[] = $pdfRow; + } + $this->addPDFTable($return, 'yubiKeyId', $pdfTable); + } + return $return; + } + + /** + * Returns the meta HTML code for each input field. + * format: array( => array(), ...) + * It is not possible to display help links. + * + * @param array $fields list of active fields + * @param array $attributes attributes of LDAP account + * @param boolean $passwordChangeOnly indicates that the user is only allowed to change his password and no LDAP content is readable + * @param array $readOnlyFields list of read-only fields + * @return array list of meta HTML elements (field name => htmlResponsiveRow) + */ + public function getSelfServiceOptions($fields, $attributes, $passwordChangeOnly, $readOnlyFields) { + $return = array(); + if ($passwordChangeOnly) { + return $return; // no fields as long no LDAP content can be read + } + $attributes = array_change_key_case($attributes, CASE_LOWER); + $objectClass = strtolower($this->getObjectClassName()); + if (!in_array_ignore_case($objectClass, $attributes['objectclass'])) { + return $return; + } + if (in_array('yubiKeyId', $fields)) { + $attributeName = strtolower($this->getAttributeName()); + $keyIds = array(); + if (isset($attributes[$attributeName][0])) { + $keyIds = $attributes[$attributeName]; + } + $_SESSION[self::SESS_KEY_LIST] = $keyIds; + $keyTable = new htmlTable(); + // JavaScript functions + $keyTable->addElement($this->getSelfServiceKeysJSBlock(), true); + // input fields + $keyTable->addElement(new htmlDiv('yubiKeyIdDiv', $this->getSelfServiceKeys()), true); + $keyLabel = new htmlOutputText($this->getSelfServiceLabel('yubiKeyId', _('YubiKey ids'))); + $row = new htmlResponsiveRow(); + $row->addLabel($keyLabel); + $row->addField($keyTable); + $return['yubiKeyId'] = $row; + } + return $return; + } + + /** + * Returns the meta HTML code to display the key area. + * This also includes the file upload. + * + * @return htmlTable key content + */ + private function getSelfServiceKeys() { + $keys = $_SESSION[self::SESS_KEY_LIST]; + $content = new htmlResponsiveRow(); + if (sizeof($keys) > 0) { + for ($i = 0; $i < sizeof($keys); $i++) { + $group = new htmlGroup(); + $keyInput = new htmlInputField('yubiKeyId_' . $i, $keys[$i]); + $keyInput->setFieldMaxLength(16384); + $group->addElement($keyInput); + $delLink = new htmlLink('', '#', '../../graphics/del.png'); + $delLink->setTitle(_('Delete')); + $delLink->setOnClick('yubiKeyIdDeleteKey(' . $i . ', ' . sizeof($keys) . ');return false;'); + $group->addElement($delLink); + if ($i == (sizeof($keys) - 1)) { + $addLink = new htmlLink('', '#', '../../graphics/add.png'); + $addLink->setTitle(_('Add')); + $addLink->setOnClick('yubiKeyIdAddKey(' . sizeof($keys) . ');return false;'); + $group->addElement($addLink); + } + $content->add($group, 12, 12, 12, 'nowrap'); + } + } + else { + $addLink = new htmlLink('', '#', '../../graphics/add.png'); + $addLink->setTitle(_('Add')); + $addLink->setOnClick('yubiKeyIdAddKey(' . sizeof($keys) . ');return false;'); + $content->add($addLink, 12); + } + return $content; + } + + /** + * Returns the Java Script functions to manage the keys. + * + * @return htmlJavaScript JS block + */ + private static function getSelfServiceKeysJSBlock() { + $content = ' + function yubiKeyIdDeleteKey(id, count) { + var actionJSON = { + "action": "deleteKey", + "id": id + }; + for (c = 0; c < count; c++) { + actionJSON["yubiKeyId_" + c] = jQuery(\'#yubiKeyId_\' + c).val(); + } + var data = {jsonInput: actionJSON}; + data["' . getSecurityTokenName() . '"] = "' . getSecurityTokenValue() . '"; + jQuery.post(\'../misc/ajax.php?selfservice=1&module=yubiKeyUser&scope=user\', + data, function(data) {yubiKeyIdDeleteKeyHandleReply(data);}, \'json\'); + } + + function yubiKeyIdDeleteKeyHandleReply(data) { + if (data.errorsOccured == "false") { + jQuery(\'#yubiKeyIdDiv\').html(data.html); + } + else { + alert(data.errormessage); + } + } + + function yubiKeyIdAddKey(count) { + var actionJSON = { + "action": "addKey" + }; + for (c = 0; c < count; c++) { + actionJSON["yubiKeyId_" + c] = jQuery(\'#yubiKeyId_\' + c).val(); + } + var data = {jsonInput: actionJSON}; + data["' . getSecurityTokenName() . '"] = "' . getSecurityTokenValue() . '"; + jQuery.post(\'../misc/ajax.php?selfservice=1&module=yubiKeyUser&scope=user' + . '&' . getSecurityTokenName() . '=' . getSecurityTokenValue() + . '\', data, function(data) {yubiKeyIdAddKeyHandleReply(data);}, \'json\'); + } + + function yubiKeyIdAddKeyHandleReply(data) { + if (data.errorsOccured == "false") { + jQuery(\'#yubiKeyIdDiv\').html(data.html); + } + else { + alert(data.errormessage); + } + } + + '; + return new htmlJavaScript($content); + } + + /** + * Checks if all input values are correct and returns the LDAP attributes which should be changed. + *
Return values: + *
messages: array of parameters to create status messages + *
add: array of attributes to add + *
del: array of attributes to remove + *
mod: array of attributes to modify + *
info: array of values with informational value (e.g. to be used later by pre/postModify actions) + * + * Calling this method does not require the existence of an enclosing {@link accountContainer}. + * + * @param string $fields input fields + * @param array $attributes LDAP attributes + * @param boolean $passwordChangeOnly indicates that the user is only allowed to change his password and no LDAP content is readable + * @param array $readOnlyFields list of read-only fields + * @return array messages and attributes (array('messages' => array(), 'add' => array('mail' => array('test@test.com')), 'del' => array(), 'mod' => array(), 'info' => array())) + */ + public function checkSelfServiceOptions($fields, $attributes, $passwordChangeOnly, $readOnlyFields) { + $return = array('messages' => array(), 'add' => array(), 'del' => array(), 'mod' => array(), 'info' => array()); + if ($passwordChangeOnly) { + return $return; // skip processing if only a password change is done + } + $attributes = array_change_key_case($attributes, CASE_LOWER); + $objectClass = strtolower($this->getObjectClassName()); + if (!in_array_ignore_case($objectClass, $attributes['objectclass'])) { + return $return; + } + if (in_array('yubiKeyId', $fields)) { + $attributeName = strtolower($this->getAttributeName()); + $newKeys = array(); + $counter = 0; + while (isset($_POST['yubiKeyId_' . $counter])) { + $newKeys[] = $_POST['yubiKeyId_' . $counter]; + $counter++; + } + $count = sizeof($newKeys); + for ($i = 0; $i < $count; $i++) { + if (trim($newKeys[$i]) == '') { + unset($newKeys[$i]); + } + elseif (!get_preg($newKeys[$i], 'ascii')) { + $return['messages'][] = array('ERROR', _('Please enter a valid key id.'), htmlspecialchars($newKeys[$i])); + } + } + $newKeys = array_values(array_unique($newKeys)); + $oldKeys = array(); + if (isset($attributes[$attributeName][0])) { + $oldKeys = $attributes[$attributeName]; + } + $update = false; + if (sizeof($newKeys) != sizeof($oldKeys)) { + $update = true; + } + else { + for ($i = 0; $i < sizeof($newKeys); $i++) { + if (!in_array($newKeys[$i], $oldKeys)) { + $update = true; + break; + } + } + } + if ($update) { + if (sizeof($oldKeys) == 0) { + $return['add'][$attributeName] = $newKeys; + } + elseif (sizeof($newKeys) == 0) { + $return['del'][$attributeName] = $newKeys; + } + else { + $return['mod'][$attributeName] = $newKeys; + } + } + } + return $return; + } + + /** + * Manages AJAX requests. + * This function may be called with or without an account container. + */ + public function handleAjaxRequest() { + $jsonInput = $_POST['jsonInput']; + $jsonReturn = self::invalidAjaxRequest(); + if (isset($jsonInput['action'])) { + if ($jsonInput['action'] == 'deleteKey') { + $jsonReturn = $this->ajaxDeleteSelfServiceKey($jsonInput); + } + elseif ($jsonInput['action'] == 'addKey') { + $_SESSION[self::SESS_KEY_LIST][] = ''; + ob_start(); + $contentElement = $this->getSelfServiceKeys(); + ob_end_clean(); + ob_start(); + $tabindex = 999; + parseHtml(null, $contentElement, array(), true, $tabindex, $this->get_scope()); + $content = ob_get_contents(); + ob_end_clean(); + $jsonReturn = array( + 'errorsOccured' => 'false', + 'html' => $content, + ); + } + } + echo json_encode($jsonReturn); + } + + /** + * Manages the deletion of a key. + * + * @param array $data JSON data + */ + private function ajaxDeleteSelfServiceKey($data) { + if (!isset($data['id'])) { + return self::invalidAjaxRequest(); + } + $index = $data['id']; + if (array_key_exists($index, $_SESSION[self::SESS_KEY_LIST])) { + unset($_SESSION[self::SESS_KEY_LIST][$index]); + $_SESSION[self::SESS_KEY_LIST] = array_values($_SESSION[self::SESS_KEY_LIST]); + } + ob_start(); + $contentElement = $this->getSelfServiceKeys(); + ob_end_clean(); + ob_start(); + $tabindex = 999; + parseHtml(null, $contentElement, array(), true, $tabindex, $this->get_scope()); + $content = ob_get_contents(); + ob_end_clean(); + return array( + 'errorsOccured' => 'false', + 'html' => $content, + ); + } + + /** + * Invalid AJAX request received. + * + * @param String $message error message + */ + public static function invalidAjaxRequest($message = null) { + if ($message == null) { + $message = _('Invalid request'); + } + return array('errorsOccured' => 'true', 'errormessage' => $message); + } + + /** + * Returns the object class used for YubiKey. + * + * @return string object class + */ + private function getObjectClassName() { + if (!empty($this->moduleSettings['yubiKeyUser_objectClass'][0])) { + return $this->moduleSettings['yubiKeyUser_objectClass'][0]; + } + elseif (!empty($this->selfServiceSettings->moduleSettings['yubiKeyUser_objectClass'][0])) { + return $this->selfServiceSettings->moduleSettings['yubiKeyUser_objectClass'][0]; + } + return null; + } + + /** + * Returns the attribute name used for YubiKey. + * + * @return string attribute name + */ + private function getAttributeName() { + if (!empty($this->moduleSettings['yubiKeyUser_attributeName'][0])) { + return $this->moduleSettings['yubiKeyUser_attributeName'][0]; + } + elseif (!empty($this->selfServiceSettings->moduleSettings['yubiKeyUser_attributeName'][0])) { + return $this->selfServiceSettings->moduleSettings['yubiKeyUser_attributeName'][0]; + } + return null; + } + + /** + * {@inheritDoc} + * @see baseModule::get_configOptions() + */ + public function get_configOptions($scopes, $allScopes) { + $configContainer = new htmlResponsiveRow(); + $configContainer->add(new htmlResponsiveInputField(_('Object class'), 'yubiKeyUser_objectClass', null, 'objectClass'), 12); + $configContainer->add(new htmlResponsiveInputField(_('Attribute name'), 'yubiKeyUser_attributeName', 'yubiKeyId', 'attributeName', true), 12); + return $configContainer; + } + + /** + * {@inheritDoc} + * @see baseModule::getSelfServiceSettings() + */ + public function getSelfServiceSettings($profile) { + $container = new htmlResponsiveRow(); + $container->add(new htmlResponsiveInputField(_('Object class'), 'yubiKeyUser_objectClass', null, array('objectClass', 'yubiKeyUser')), 12); + $container->add(new htmlResponsiveInputField(_('Attribute name'), 'yubiKeyUser_attributeName', 'yubiKeyId', array('attributeName', 'yubiKeyUser')), 12); + return $container; + } + +} + + +?>