This commit is contained in:
Roland Gruber 2020-01-04 18:28:25 +01:00
parent ef9b3dd64e
commit 38addc429c
9 changed files with 630 additions and 277 deletions

View File

@ -655,6 +655,11 @@
<listitem> <listitem>
<para><ulink url="https://duo.com/">Duo</ulink></para> <para><ulink url="https://duo.com/">Duo</ulink></para>
</listitem> </listitem>
<listitem>
<para><ulink
url="https://webauthn.io/">Webauthn/FIDO2</ulink></para>
</listitem>
</itemizedlist> </itemizedlist>
<para>Configuration options:</para> <para>Configuration options:</para>
@ -752,6 +757,27 @@
</listitem> </listitem>
</itemizedlist> </itemizedlist>
<para><emphasis role="bold">Webauthn/FIDO2</emphasis></para>
<para>Users will be asked to register a device during login if no
device is setup.</para>
<itemizedlist>
<listitem>
<para>Domain: Please enter the WebAuthn domain. This is the public
domain of the web server (e.g. "example.com"). Do not include
protocol or port. Browsers will reject authentication if the
domain does not match the web server domain.</para>
</listitem>
<listitem>
<para>Optional: By default LAM will enforce to use a 2FA device
and reject users that do not setup one. You can set this check to
optional. But if a user has setup a device then this will always
be required.</para>
</listitem>
</itemizedlist>
<screenshot> <screenshot>
<mediaobject> <mediaobject>
<imageobject> <imageobject>

View File

@ -179,6 +179,8 @@ $helpArray = array (
"Text" => _("Here you can input simple filter expressions (e.g. 'value' or 'v*'). The filter is case-insensitive.")), "Text" => _("Here you can input simple filter expressions (e.g. 'value' or 'v*'). The filter is case-insensitive.")),
"251" => array ("Headline" => _("Remote server"), "251" => array ("Headline" => _("Remote server"),
"Text" => _("Please enter the syslog remote server in format \"server:port\".")), "Text" => _("Please enter the syslog remote server in format \"server:port\".")),
"252" => array ("Headline" => _("User DN"),
"Text" => _("Please enter a part of the user's DN to search for registered devices.")),
"260" => array ("Headline" => _("Additional LDAP filter"), "260" => array ("Headline" => _("Additional LDAP filter"),
"Text" => _('Use this to enter an additional LDAP filter (e.g. "(cn!=admin)") to reduce the number of visible elements for this account type.') "Text" => _('Use this to enter an additional LDAP filter (e.g. "(cn!=admin)") to reduce the number of visible elements for this account type.')
. ' ' . _('You can use the wildcard @@LOGIN_DN@@ which will be substituted with the DN of the user who is currently logged in to LAM.') . ' ' . _('You can use the wildcard @@LOGIN_DN@@ which will be substituted with the DN of the user who is currently logged in to LAM.')

View File

@ -1098,10 +1098,10 @@ class htmlButton extends htmlElement {
} }
$id = ' id="btn_' . preg_replace('/[^a-zA-Z0-9_-]/', '', $this->name) . '"'; $id = ' id="btn_' . preg_replace('/[^a-zA-Z0-9_-]/', '', $this->name) . '"';
if ($this->isImageButton) { if ($this->isImageButton) {
echo '<input type="submit" ' . $id . ' value=" "' . $name . $onClick . $fieldTabIndex . $style . $class . $title . $disabled . '>'; echo '<input type="submit" ' . $id . ' value=" "' . $name . $onClick . $fieldTabIndex . $style . $class . $title . $disabled . $this->getDataAttributesAsString() . '>';
} }
else { else {
echo '<button' . $id . $name . $fieldTabIndex . $type . $onClick . $style . $class . $title . $disabled . '>' . $this->value . '</button>'; echo '<button' . $id . $name . $fieldTabIndex . $type . $onClick . $style . $class . $title . $disabled . $this->getDataAttributesAsString() . '>' . $this->value . '</button>';
// text buttons get JQuery style // text buttons get JQuery style
$icon = ''; $icon = '';
if ($this->iconClass != null) { if ($this->iconClass != null) {

View File

@ -523,6 +523,62 @@ class PublicKeyCredentialSourceRepositorySQLite implements PublicKeyCredentialSo
return 'sqlite:' . $fileName; return 'sqlite:' . $fileName;
} }
/**
* Returns if there are any credentials in the database.
*
* @return bool at least one credential in the database
*/
public function hasRegisteredCredentials() {
$pdo = $this->getPDO();
$statement = $pdo->prepare('select count(*) from ' . self::TABLE_NAME);
$statement->execute();
$results = $statement->fetchAll();
return ($results[0][0] > 0);
}
/**
* Performs a full-text search on the user names and returns all devices found.
*
* @param string $searchTerm search term for user field
* @return array list of devices array('dn' => ..., 'credentialId' => ..., 'lastUseTime' => ..., 'registrationTime' => ...)
*/
public function searchDevices(string $searchTerm) {
$pdo = $this->getPDO();
$statement = $pdo->prepare('select * from ' . self::TABLE_NAME . ' where userId like :searchTerm');
$statement->execute(array(
':searchTerm' => '%' . $searchTerm . '%'
));
$results = $statement->fetchAll();
$devices = array();
foreach ($results as $result) {
$devices[] = array(
'dn' => $result['userId'],
'credentialId' => $result['credentialId'],
'lastUseTime' => $result['lastUseTime'],
'registrationTime' => $result['registrationTime']
);
}
return $devices;
}
/**
* Deletes a single device from the database.
*
* @param string $dn user DN
* @param string $credentialId credential id
* @return bool deletion was ok
*/
public function deleteDevice(string $dn, string $credentialId) {
logNewMessage(LOG_NOTICE, 'Delete webauthn device ' . $credentialId . ' of ' . $dn);
$pdo = $this->getPDO();
$statement = $pdo->prepare('delete from ' . self::TABLE_NAME . ' where userId = :userId and credentialId = :credentialId');
$result = $statement->execute(array(
':userId' => $dn,
':credentialId' => $credentialId
));
return $statement->rowCount() > 0;
}
/** /**
* Returns the PDO. * Returns the PDO.
* *

View File

@ -547,6 +547,9 @@ input.markOk {
text-decoration: line-through; text-decoration: line-through;
} }
div.lam-webauthn-results {
max-height: 10rem;
}
/** /**
* table style for delete.php * table style for delete.php

View File

@ -1,5 +1,6 @@
<?php <?php
namespace LAM\CONFIG; namespace LAM\CONFIG;
use \LAMCfgMain; use \LAMCfgMain;
use \htmlTable; use \htmlTable;
use \htmlTitle; use \htmlTitle;
@ -22,10 +23,11 @@ use \htmlResponsiveInputCheckbox;
use \htmlResponsiveInputField; use \htmlResponsiveInputField;
use \htmlDiv; use \htmlDiv;
use \htmlHiddenInput; use \htmlHiddenInput;
/* /*
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
Copyright (C) 2003 - 2019 Roland Gruber Copyright (C) 2003 - 2020 Roland Gruber
This program is free software; you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -45,11 +47,11 @@ use \htmlHiddenInput;
/** /**
* Manages the main configuration options. * Manages the main configuration options.
* *
* @package configuration * @package configuration
* @author Roland Gruber * @author Roland Gruber
*/ */
/** Access to config functions */ /** Access to config functions */
@ -95,8 +97,7 @@ if (isset($_POST['submitFormData'])) {
$cfg->setPassword($_POST['masterpassword']); $cfg->setPassword($_POST['masterpassword']);
$msg = _("New master password set successfully."); $msg = _("New master password set successfully.");
unset($_SESSION["mainconf_password"]); unset($_SESSION["mainconf_password"]);
} } else {
else {
$errors[] = _("Master passwords are different or empty!"); $errors[] = _("Master passwords are different or empty!");
} }
} }
@ -126,8 +127,7 @@ if (isset($_POST['submitFormData'])) {
} }
} }
$allowedHosts = implode(",", $allowedHostsList); $allowedHosts = implode(",", $allowedHostsList);
} } else {
else {
$allowedHosts = ""; $allowedHosts = "";
} }
$cfg->allowedHosts = $allowedHosts; $cfg->allowedHosts = $allowedHosts;
@ -150,8 +150,7 @@ if (isset($_POST['submitFormData'])) {
} }
} }
$allowedHostsSelfService = implode(",", $allowedHostsSelfServiceList); $allowedHostsSelfService = implode(",", $allowedHostsSelfServiceList);
} } else {
else {
$allowedHostsSelfService = ""; $allowedHostsSelfService = "";
} }
$cfg->allowedHostsSelfService = $allowedHostsSelfService; $cfg->allowedHostsSelfService = $allowedHostsSelfService;
@ -169,22 +168,18 @@ if (isset($_POST['submitFormData'])) {
// set log destination // set log destination
if ($_POST['logDestination'] == "none") { if ($_POST['logDestination'] == "none") {
$cfg->logDestination = "NONE"; $cfg->logDestination = "NONE";
} } elseif ($_POST['logDestination'] == "syslog") {
elseif ($_POST['logDestination'] == "syslog") {
$cfg->logDestination = "SYSLOG"; $cfg->logDestination = "SYSLOG";
} } elseif ($_POST['logDestination'] == "remote") {
elseif ($_POST['logDestination'] == "remote") {
$cfg->logDestination = "REMOTE:" . $_POST['logRemote']; $cfg->logDestination = "REMOTE:" . $_POST['logRemote'];
$remoteParts = explode(':', $_POST['logRemote']); $remoteParts = explode(':', $_POST['logRemote']);
if ((sizeof($remoteParts) !== 2) || !get_preg($remoteParts[0], 'DNSname') || !get_preg($remoteParts[1], 'digit')) { if ((sizeof($remoteParts) !== 2) || !get_preg($remoteParts[0], 'DNSname') || !get_preg($remoteParts[1], 'digit')) {
$errors[] = _("Please enter a valid remote server in format \"server:port\"."); $errors[] = _("Please enter a valid remote server in format \"server:port\".");
} }
} } else {
else {
if (isset($_POST['logFile']) && ($_POST['logFile'] != "") && preg_match("/^[a-z0-9\\/\\\\:\\._-]+$/i", $_POST['logFile'])) { if (isset($_POST['logFile']) && ($_POST['logFile'] != "") && preg_match("/^[a-z0-9\\/\\\\:\\._-]+$/i", $_POST['logFile'])) {
$cfg->logDestination = $_POST['logFile']; $cfg->logDestination = $_POST['logFile'];
} } else {
else {
$errors[] = _("The log file is empty or contains invalid characters! Valid characters are: a-z, A-Z, 0-9, /, \\, ., :, _ and -."); $errors[] = _("The log file is empty or contains invalid characters! Valid characters are: a-z, A-Z, 0-9, /, \\, ., :, _ and -.");
} }
} }
@ -207,16 +202,14 @@ if (isset($_POST['submitFormData'])) {
if (isset($_POST['sslCaCertUpload'])) { if (isset($_POST['sslCaCertUpload'])) {
if (!isset($_FILES['sslCaCert']) || ($_FILES['sslCaCert']['size'] == 0)) { if (!isset($_FILES['sslCaCert']) || ($_FILES['sslCaCert']['size'] == 0)) {
$errors[] = _('No file selected.'); $errors[] = _('No file selected.');
} } else {
else {
$handle = fopen($_FILES['sslCaCert']['tmp_name'], "r"); $handle = fopen($_FILES['sslCaCert']['tmp_name'], "r");
$data = fread($handle, 10000000); $data = fread($handle, 10000000);
fclose($handle); fclose($handle);
$sslReturn = $cfg->uploadSSLCaCert($data); $sslReturn = $cfg->uploadSSLCaCert($data);
if ($sslReturn !== true) { if ($sslReturn !== true) {
$errors[] = $sslReturn; $errors[] = $sslReturn;
} } else {
else {
$messages[] = _('You might need to restart your webserver for changes to take effect.'); $messages[] = _('You might need to restart your webserver for changes to take effect.');
} }
} }
@ -237,12 +230,10 @@ if (isset($_POST['submitFormData'])) {
$messages[] = _('Imported certificate from server.'); $messages[] = _('Imported certificate from server.');
$messages[] = _('You might need to restart your webserver for changes to take effect.'); $messages[] = _('You might need to restart your webserver for changes to take effect.');
$cfg->uploadSSLCaCert($pemResult); $cfg->uploadSSLCaCert($pemResult);
} } else {
else {
$errors[] = _('Unable to import server certificate. Please use the upload function.'); $errors[] = _('Unable to import server certificate. Please use the upload function.');
} }
} } else {
else {
$errors[] = _('Invalid server name. Please enter "server" or "server:port".'); $errors[] = _('Invalid server name. Please enter "server" or "server:port".');
} }
} }
@ -270,9 +261,9 @@ if (isset($_POST['submitFormData'])) {
echo $_SESSION['header']; echo $_SESSION['header'];
printHeaderContents(_("Edit general settings"), '../..'); printHeaderContents(_("Edit general settings"), '../..');
?> ?>
</head> </head>
<body class="admin"> <body class="admin">
<table border=0 width="100%" class="lamHeader ui-corner-all"> <table border=0 width="100%" class="lamHeader ui-corner-all">
<tr> <tr>
<td align="left" height="30"> <td align="left" height="30">
<a class="lamLogo" href="http://www.ldap-account-manager.org/" target="new_window"> <a class="lamLogo" href="http://www.ldap-account-manager.org/" target="new_window">
@ -280,66 +271,66 @@ printHeaderContents(_("Edit general settings"), '../..');
</a> </a>
</td> </td>
</tr> </tr>
</table> </table>
<br> <br>
<!-- form for adding/renaming/deleting profiles --> <!-- form for adding/renaming/deleting profiles -->
<form enctype="multipart/form-data" action="mainmanage.php" method="post"> <form enctype="multipart/form-data" action="mainmanage.php" method="post">
<?php <?php
// include all JavaScript files // include all JavaScript files
printJsIncludes('../..'); printJsIncludes('../..');
$tabindex = 1; $tabindex = 1;
$row = new htmlResponsiveRow(); $row = new htmlResponsiveRow();
$row->add(new htmlTitle(_('General settings')), 12); $row->add(new htmlTitle(_('General settings')), 12);
// print messages // print messages
for ($i = 0; $i < sizeof($errors); $i++) { for ($i = 0; $i < sizeof($errors); $i++) {
$row->add(new htmlStatusMessage("ERROR", $errors[$i]), 12); $row->add(new htmlStatusMessage("ERROR", $errors[$i]), 12);
} }
for ($i = 0; $i < sizeof($messages); $i++) { for ($i = 0; $i < sizeof($messages); $i++) {
$row->add(new htmlStatusMessage("INFO", $messages[$i]), 12); $row->add(new htmlStatusMessage("INFO", $messages[$i]), 12);
} }
// check if config file is writable // check if config file is writable
if (!$cfg->isWritable()) { if (!$cfg->isWritable()) {
$row->add(new htmlStatusMessage('WARN', 'The config file is not writable.', 'Your changes cannot be saved until you make the file writable for the webserver user.'), 12); $row->add(new htmlStatusMessage('WARN', 'The config file is not writable.', 'Your changes cannot be saved until you make the file writable for the webserver user.'), 12);
} }
// license // license
if (isLAMProVersion()) { if (isLAMProVersion()) {
$row->add(new htmlSubTitle(_('Licence')), 12); $row->add(new htmlSubTitle(_('Licence')), 12);
$row->add(new htmlResponsiveInputTextarea('license', implode("\n", $cfg->getLicenseLines()), null, 10, _('Licence'), '287'), 12); $row->add(new htmlResponsiveInputTextarea('license', implode("\n", $cfg->getLicenseLines()), null, 10, _('Licence'), '287'), 12);
$row->add(new htmlSpacer(null, '1rem'), true); $row->add(new htmlSpacer(null, '1rem'), true);
} }
// security settings // security settings
$row->add(new htmlSubTitle(_("Security settings")), 12); $row->add(new htmlSubTitle(_("Security settings")), 12);
$options = array(5, 10, 20, 30, 60, 90, 120, 240); $options = array(5, 10, 20, 30, 60, 90, 120, 240);
$row->add(new htmlResponsiveSelect('sessionTimeout', $options, array($cfg->sessionTimeout), _("Session timeout"), '238'), 12); $row->add(new htmlResponsiveSelect('sessionTimeout', $options, array($cfg->sessionTimeout), _("Session timeout"), '238'), 12);
$row->add(new htmlResponsiveInputTextarea('allowedHosts', implode("\n", explode(",", $cfg->allowedHosts)), null, '7', _("Allowed hosts"), '241'), 12); $row->add(new htmlResponsiveInputTextarea('allowedHosts', implode("\n", explode(",", $cfg->allowedHosts)), null, '7', _("Allowed hosts"), '241'), 12);
if (isLAMProVersion()) { if (isLAMProVersion()) {
$row->add(new htmlResponsiveInputTextarea('allowedHostsSelfService', implode("\n", explode(",", $cfg->allowedHostsSelfService)), null, '7', _("Allowed hosts (self service)"), '241'), 12); $row->add(new htmlResponsiveInputTextarea('allowedHostsSelfService', implode("\n", explode(",", $cfg->allowedHostsSelfService)), null, '7', _("Allowed hosts (self service)"), '241'), 12);
} }
$encryptSession = ($cfg->encryptSession === 'true'); $encryptSession = ($cfg->encryptSession === 'true');
$encryptSessionBox = new htmlResponsiveInputCheckbox('encryptSession', $encryptSession, _('Encrypt session'), '245'); $encryptSessionBox = new htmlResponsiveInputCheckbox('encryptSession', $encryptSession, _('Encrypt session'), '245');
$encryptSessionBox->setIsEnabled(function_exists('openssl_random_pseudo_bytes')); $encryptSessionBox->setIsEnabled(function_exists('openssl_random_pseudo_bytes'));
$row->add($encryptSessionBox, 12); $row->add($encryptSessionBox, 12);
// SSL certificate // SSL certificate
$row->addVerticalSpacer('1rem'); $row->addVerticalSpacer('1rem');
$row->addLabel(new htmlOutputText(_('SSL certificates'))); $row->addLabel(new htmlOutputText(_('SSL certificates')));
$sslMethod = _('use system certificates'); $sslMethod = _('use system certificates');
$sslFileName = $cfg->getSSLCaCertTempFileName(); $sslFileName = $cfg->getSSLCaCertTempFileName();
if ($sslFileName != null) { if ($sslFileName != null) {
$sslMethod = _('use custom CA certificates'); $sslMethod = _('use custom CA certificates');
} }
$sslDelSaveGroup = new htmlGroup(); $sslDelSaveGroup = new htmlGroup();
$sslDelSaveGroup->addElement(new htmlOutputText($sslMethod)); $sslDelSaveGroup->addElement(new htmlOutputText($sslMethod));
$sslDelSaveGroup->addElement(new htmlSpacer('5px', null)); $sslDelSaveGroup->addElement(new htmlSpacer('5px', null));
// delete+download button // delete+download button
if ($sslFileName != null) { if ($sslFileName != null) {
$sslDownloadBtn = new htmlLink('', '../../tmp/' . $sslFileName, '../../graphics/save.png'); $sslDownloadBtn = new htmlLink('', '../../tmp/' . $sslFileName, '../../graphics/save.png');
$sslDownloadBtn->setTargetWindow('_blank'); $sslDownloadBtn->setTargetWindow('_blank');
$sslDownloadBtn->setTitle(_('Download CA certificates')); $sslDownloadBtn->setTitle(_('Download CA certificates'));
@ -347,15 +338,15 @@ if ($sslFileName != null) {
$sslDeleteBtn = new htmlButton('sslCaCertDelete', 'delete.png', true); $sslDeleteBtn = new htmlButton('sslCaCertDelete', 'delete.png', true);
$sslDeleteBtn->setTitle(_('Delete all CA certificates')); $sslDeleteBtn->setTitle(_('Delete all CA certificates'));
$sslDelSaveGroup->addElement($sslDeleteBtn); $sslDelSaveGroup->addElement($sslDeleteBtn);
} }
$sslDelSaveGroup->addElement(new htmlHelpLink('204')); $sslDelSaveGroup->addElement(new htmlHelpLink('204'));
$row->addField($sslDelSaveGroup); $row->addField($sslDelSaveGroup);
$row->addLabel(new htmlInputFileUpload('sslCaCert')); $row->addLabel(new htmlInputFileUpload('sslCaCert'));
$sslUploadBtn = new htmlButton('sslCaCertUpload', _('Upload')); $sslUploadBtn = new htmlButton('sslCaCertUpload', _('Upload'));
$sslUploadBtn->setIconClass('upButton'); $sslUploadBtn->setIconClass('upButton');
$sslUploadBtn->setTitle(_('Upload CA certificate in DER/PEM format.')); $sslUploadBtn->setTitle(_('Upload CA certificate in DER/PEM format.'));
$row->addField($sslUploadBtn); $row->addField($sslUploadBtn);
if (function_exists('stream_socket_client') && function_exists('stream_context_get_params')) { if (function_exists('stream_socket_client') && function_exists('stream_context_get_params')) {
$sslImportServerUrl = !empty($_POST['serverurl']) ? $_POST['serverurl'] : 'ldaps://'; $sslImportServerUrl = !empty($_POST['serverurl']) ? $_POST['serverurl'] : 'ldaps://';
$serverUrlUpload = new htmlInputField('serverurl', $sslImportServerUrl); $serverUrlUpload = new htmlInputField('serverurl', $sslImportServerUrl);
$row->addLabel($serverUrlUpload); $row->addLabel($serverUrlUpload);
@ -363,10 +354,10 @@ if (function_exists('stream_socket_client') && function_exists('stream_context_g
$sslImportBtn->setIconClass('downButton'); $sslImportBtn->setIconClass('downButton');
$sslImportBtn->setTitle(_('Imports the certificate directly from your LDAP server.')); $sslImportBtn->setTitle(_('Imports the certificate directly from your LDAP server.'));
$row->addField($sslImportBtn); $row->addField($sslImportBtn);
} }
$sslCerts = $cfg->getSSLCaCertificates(); $sslCerts = $cfg->getSSLCaCertificates();
if (sizeof($sslCerts) > 0) { if (sizeof($sslCerts) > 0) {
$certsTitles = array(_('Common name'), _('Valid to'), _('Serial number'), _('Delete')); $certsTitles = array(_('Common name'), _('Valid to'), _('Serial number'), _('Delete'));
$certsData = array(); $certsData = array();
for ($i = 0; $i < sizeof($sslCerts); $i++) { for ($i = 0; $i < sizeof($sslCerts); $i++) {
@ -383,88 +374,86 @@ if (sizeof($sslCerts) > 0) {
} }
$certsTable = new \htmlResponsiveTable($certsTitles, $certsData); $certsTable = new \htmlResponsiveTable($certsTitles, $certsData);
$row->add($certsTable, 12); $row->add($certsTable, 12);
} }
// password policy // password policy
$row->add(new htmlSubTitle(_("Password policy")), 12); $row->add(new htmlSubTitle(_("Password policy")), 12);
$options20 = array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20); $options20 = array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
$options4 = array(0, 1, 2, 3, 4); $options4 = array(0, 1, 2, 3, 4);
$row->add(new htmlResponsiveSelect('passwordMinLength', $options20, array($cfg->passwordMinLength), _('Minimum password length'), '242'), 12); $row->add(new htmlResponsiveSelect('passwordMinLength', $options20, array($cfg->passwordMinLength), _('Minimum password length'), '242'), 12);
$row->addVerticalSpacer('1rem'); $row->addVerticalSpacer('1rem');
$row->add(new htmlResponsiveSelect('passwordMinLower', $options20, array($cfg->passwordMinLower), _('Minimum lowercase characters'), '242'), 12); $row->add(new htmlResponsiveSelect('passwordMinLower', $options20, array($cfg->passwordMinLower), _('Minimum lowercase characters'), '242'), 12);
$row->add(new htmlResponsiveSelect('passwordMinUpper', $options20, array($cfg->passwordMinUpper), _('Minimum uppercase characters'), '242'), 12); $row->add(new htmlResponsiveSelect('passwordMinUpper', $options20, array($cfg->passwordMinUpper), _('Minimum uppercase characters'), '242'), 12);
$row->add(new htmlResponsiveSelect('passwordMinNumeric', $options20, array($cfg->passwordMinNumeric), _('Minimum numeric characters'), '242'), 12); $row->add(new htmlResponsiveSelect('passwordMinNumeric', $options20, array($cfg->passwordMinNumeric), _('Minimum numeric characters'), '242'), 12);
$row->add(new htmlResponsiveSelect('passwordMinSymbol', $options20, array($cfg->passwordMinSymbol), _('Minimum symbolic characters'), '242'), 12); $row->add(new htmlResponsiveSelect('passwordMinSymbol', $options20, array($cfg->passwordMinSymbol), _('Minimum symbolic characters'), '242'), 12);
$row->add(new htmlResponsiveSelect('passwordMinClasses', $options4, array($cfg->passwordMinClasses), _('Minimum character classes'), '242'), 12); $row->add(new htmlResponsiveSelect('passwordMinClasses', $options4, array($cfg->passwordMinClasses), _('Minimum character classes'), '242'), 12);
$row->addVerticalSpacer('1rem'); $row->addVerticalSpacer('1rem');
$rulesCountOptions = array(_('all') => '-1', '3' => '3', '4' => '4'); $rulesCountOptions = array(_('all') => '-1', '3' => '3', '4' => '4');
$rulesCountSelect = new htmlResponsiveSelect('passwordRulesCount', $rulesCountOptions, array($cfg->checkedRulesCount), _('Number of rules that must match'), '246'); $rulesCountSelect = new htmlResponsiveSelect('passwordRulesCount', $rulesCountOptions, array($cfg->checkedRulesCount), _('Number of rules that must match'), '246');
$rulesCountSelect->setHasDescriptiveElements(true); $rulesCountSelect->setHasDescriptiveElements(true);
$row->add($rulesCountSelect, 12); $row->add($rulesCountSelect, 12);
$passwordMustNotContainUser = ($cfg->passwordMustNotContainUser === 'true'); $passwordMustNotContainUser = ($cfg->passwordMustNotContainUser === 'true');
$row->add(new htmlResponsiveInputCheckbox('passwordMustNotContainUser',$passwordMustNotContainUser , _('Password must not contain user name'), '247'), 12); $row->add(new htmlResponsiveInputCheckbox('passwordMustNotContainUser', $passwordMustNotContainUser, _('Password must not contain user name'), '247'), 12);
$passwordMustNotContain3Chars = ($cfg->passwordMustNotContain3Chars === 'true'); $passwordMustNotContain3Chars = ($cfg->passwordMustNotContain3Chars === 'true');
$row->add(new htmlResponsiveInputCheckbox('passwordMustNotContain3Chars', $passwordMustNotContain3Chars, _('Password must not contain part of user/first/last name'), '248'), 12); $row->add(new htmlResponsiveInputCheckbox('passwordMustNotContain3Chars', $passwordMustNotContain3Chars, _('Password must not contain part of user/first/last name'), '248'), 12);
if (function_exists('curl_init')) { if (function_exists('curl_init')) {
$row->addVerticalSpacer('1rem'); $row->addVerticalSpacer('1rem');
$row->add(new htmlResponsiveInputField(_('External password check'), 'externalPwdCheckUrl', $cfg->externalPwdCheckUrl, '249'), 12); $row->add(new htmlResponsiveInputField(_('External password check'), 'externalPwdCheckUrl', $cfg->externalPwdCheckUrl, '249'), 12);
} }
// logging // logging
$row->add(new htmlSubTitle(_("Logging")), 12); $row->add(new htmlSubTitle(_("Logging")), 12);
$levelOptions = array(_("Debug") => LOG_DEBUG, _("Notice") => LOG_NOTICE, _("Warning") => LOG_WARNING, _("Error") => LOG_ERR); $levelOptions = array(_("Debug") => LOG_DEBUG, _("Notice") => LOG_NOTICE, _("Warning") => LOG_WARNING, _("Error") => LOG_ERR);
$levelSelect = new htmlResponsiveSelect('logLevel', $levelOptions, array($cfg->logLevel), _("Log level"), '239'); $levelSelect = new htmlResponsiveSelect('logLevel', $levelOptions, array($cfg->logLevel), _("Log level"), '239');
$levelSelect->setHasDescriptiveElements(true); $levelSelect->setHasDescriptiveElements(true);
$row->add($levelSelect, 12); $row->add($levelSelect, 12);
$destinationOptions = array( $destinationOptions = array(
_("No logging") => "none", _("No logging") => "none",
_("System logging") => "syslog", _("System logging") => "syslog",
_("File") => 'file', _("File") => 'file',
_("Remote") => 'remote', _("Remote") => 'remote',
); );
$destinationSelected = 'file'; $destinationSelected = 'file';
$destinationPath = $cfg->logDestination; $destinationPath = $cfg->logDestination;
$destinationRemote = ''; $destinationRemote = '';
if ($cfg->logDestination == 'NONE') { if ($cfg->logDestination == 'NONE') {
$destinationSelected = 'none'; $destinationSelected = 'none';
$destinationPath = ''; $destinationPath = '';
} } elseif ($cfg->logDestination == 'SYSLOG') {
elseif ($cfg->logDestination == 'SYSLOG') {
$destinationSelected = 'syslog'; $destinationSelected = 'syslog';
$destinationPath = ''; $destinationPath = '';
} } elseif (strpos($cfg->logDestination, 'REMOTE') === 0) {
elseif (strpos($cfg->logDestination, 'REMOTE') === 0) {
$destinationSelected = 'remote'; $destinationSelected = 'remote';
$remoteParts = explode(':', $cfg->logDestination, 2); $remoteParts = explode(':', $cfg->logDestination, 2);
$destinationRemote = empty($remoteParts[1]) ? '' : $remoteParts[1]; $destinationRemote = empty($remoteParts[1]) ? '' : $remoteParts[1];
$destinationPath = ''; $destinationPath = '';
} }
$logDestinationSelect = new htmlResponsiveSelect('logDestination', $destinationOptions, array($destinationSelected), _("Log destination"), '240'); $logDestinationSelect = new htmlResponsiveSelect('logDestination', $destinationOptions, array($destinationSelected), _("Log destination"), '240');
$logDestinationSelect->setTableRowsToHide(array( $logDestinationSelect->setTableRowsToHide(array(
'none' => array('logFile', 'logRemote'), 'none' => array('logFile', 'logRemote'),
'syslog' => array('logFile', 'logRemote'), 'syslog' => array('logFile', 'logRemote'),
'remote' => array('logFile'), 'remote' => array('logFile'),
'file' => array('logRemote'), 'file' => array('logRemote'),
)); ));
$logDestinationSelect->setTableRowsToShow(array( $logDestinationSelect->setTableRowsToShow(array(
'file' => array('logFile'), 'file' => array('logFile'),
'remote' => array('logRemote'), 'remote' => array('logRemote'),
)); ));
$logDestinationSelect->setHasDescriptiveElements(true); $logDestinationSelect->setHasDescriptiveElements(true);
$row->add($logDestinationSelect, 12); $row->add($logDestinationSelect, 12);
$row->add(new htmlResponsiveInputField(_('File'), 'logFile', $destinationPath), 12); $row->add(new htmlResponsiveInputField(_('File'), 'logFile', $destinationPath), 12);
$row->add(new htmlResponsiveInputField(_('Remote server'), 'logRemote', $destinationRemote, '251'), 12); $row->add(new htmlResponsiveInputField(_('Remote server'), 'logRemote', $destinationRemote, '251'), 12);
$errorLogOptions = array( $errorLogOptions = array(
_('PHP system setting') => LAMCfgMain::ERROR_REPORTING_SYSTEM, _('PHP system setting') => LAMCfgMain::ERROR_REPORTING_SYSTEM,
_('default') => LAMCfgMain::ERROR_REPORTING_DEFAULT, _('default') => LAMCfgMain::ERROR_REPORTING_DEFAULT,
_('all') => LAMCfgMain::ERROR_REPORTING_ALL _('all') => LAMCfgMain::ERROR_REPORTING_ALL
); );
$errorLogSelect = new htmlResponsiveSelect('errorReporting', $errorLogOptions, array($cfg->errorReporting), _('PHP error reporting'), '244'); $errorLogSelect = new htmlResponsiveSelect('errorReporting', $errorLogOptions, array($cfg->errorReporting), _('PHP error reporting'), '244');
$errorLogSelect->setHasDescriptiveElements(true); $errorLogSelect->setHasDescriptiveElements(true);
$row->add($errorLogSelect, 12); $row->add($errorLogSelect, 12);
// additional options // additional options
if (isLAMProVersion()) { if (isLAMProVersion()) {
$row->add(new htmlSubTitle(_('Additional options')), 12); $row->add(new htmlSubTitle(_('Additional options')), 12);
$mailEOLOptions = array( $mailEOLOptions = array(
_('Default (\r\n)') => 'default', _('Default (\r\n)') => 'default',
@ -473,42 +462,60 @@ if (isLAMProVersion()) {
$mailEOLSelect = new htmlResponsiveSelect('mailEOL', $mailEOLOptions, array($cfg->mailEOL), _('Email format'), '243'); $mailEOLSelect = new htmlResponsiveSelect('mailEOL', $mailEOLOptions, array($cfg->mailEOL), _('Email format'), '243');
$mailEOLSelect->setHasDescriptiveElements(true); $mailEOLSelect->setHasDescriptiveElements(true);
$row->add($mailEOLSelect, 12); $row->add($mailEOLSelect, 12);
} }
$row->addVerticalSpacer('3rem'); $row->addVerticalSpacer('3rem');
// change master password // webauthn management
$row->add(new htmlSubTitle(_("Change master password")), 12); if ((version_compare(phpversion(), '7.2.0') >= 0)
$pwd1 = new htmlResponsiveInputField(_("New master password"), 'masterpassword', '', '235'); && extension_loaded('PDO')
$pwd1->setIsPassword(true, false, true); && in_array('sqlite', \PDO::getAvailableDrivers())) {
$row->add($pwd1, 12); include_once __DIR__ . '/../../lib/webauthn.inc';
$pwd2 = new htmlResponsiveInputField(_("Reenter password"), 'masterpassword2', ''); $database = new \LAM\LOGIN\WEBAUTHN\PublicKeyCredentialSourceRepositorySQLite();
$pwd2->setIsPassword(true, false, true); if ($database->hasRegisteredCredentials()) {
$pwd2->setSameValueFieldID('masterpassword'); $row->add(new htmlSubTitle(_('Webauthn devices')), 12);
$row->add($pwd2, 12); $row->add(new htmlResponsiveInputField(_('User DN'), 'webauthn_userDN', null, '252'), 12);
$row->addVerticalSpacer('3rem'); $row->addVerticalSpacer('0.5rem');
$row->add(new htmlButton('webauthn_search', _('Search')), 12, 12, 12, 'text-center');
$resultDiv = new htmlDiv('webauthn_results', new htmlOutputText(''), array('lam-webauthn-results'));
addSecurityTokenToSession(false);
$resultDiv->addDataAttribute('sec_token_value', getSecurityTokenValue());
$row->add($resultDiv, 12);
}
}
// buttons // change master password
if ($cfg->isWritable()) { $row->add(new htmlSubTitle(_("Change master password")), 12);
$pwd1 = new htmlResponsiveInputField(_("New master password"), 'masterpassword', '', '235');
$pwd1->setIsPassword(true, false, true);
$row->add($pwd1, 12);
$pwd2 = new htmlResponsiveInputField(_("Reenter password"), 'masterpassword2', '');
$pwd2->setIsPassword(true, false, true);
$pwd2->setSameValueFieldID('masterpassword');
$row->add($pwd2, 12);
$row->addVerticalSpacer('3rem');
// buttons
if ($cfg->isWritable()) {
$buttonTable = new htmlTable(); $buttonTable = new htmlTable();
$buttonTable->addElement(new htmlButton('submit', _("Ok"))); $buttonTable->addElement(new htmlButton('submit', _("Ok")));
$buttonTable->addElement(new htmlSpacer('1rem', null)); $buttonTable->addElement(new htmlSpacer('1rem', null));
$buttonTable->addElement(new htmlButton('cancel', _("Cancel"))); $buttonTable->addElement(new htmlButton('cancel', _("Cancel")));
$row->add($buttonTable, 12); $row->add($buttonTable, 12);
$row->add(new htmlHiddenInput('submitFormData', '1'), 12); $row->add(new htmlHiddenInput('submitFormData', '1'), 12);
} }
$box = new htmlDiv(null, $row); $box = new htmlDiv(null, $row);
$box->setCSSClasses(array('ui-corner-all', 'roundedShadowBox')); $box->setCSSClasses(array('ui-corner-all', 'roundedShadowBox'));
parseHtml(null, $box, array(), false, $tabindex, 'user'); parseHtml(null, $box, array(), false, $tabindex, 'user');
/** /**
* Formats an LDAP time string (e.g. from createTimestamp). * Formats an LDAP time string (e.g. from createTimestamp).
* *
* @param String $time LDAP time value * @param String $time LDAP time value
* @return String formated time * @return String formated time
*/ */
function formatSSLTimestamp($time) { function formatSSLTimestamp($time) {
if (!empty($time)) { if (!empty($time)) {
$timeZone = 'UTC'; $timeZone = 'UTC';
$sysTimeZone = @date_default_timezone_get(); $sysTimeZone = @date_default_timezone_get();
@ -519,15 +526,14 @@ function formatSSLTimestamp($time) {
return $date->format('d.m.Y'); return $date->format('d.m.Y');
} }
return ''; return '';
} }
?>
?> </form>
<p><br></p>
</form> </body>
<p><br></p>
</body>
</html> </html>

View File

@ -1522,6 +1522,96 @@ window.lam.webauthn.arrayToBase64String = function(input) {
return btoa(String.fromCharCode(...input)); return btoa(String.fromCharCode(...input));
} }
/**
* Sets up the device management on the main configuration page.
*/
window.lam.webauthn.setupDeviceManagement = function() {
const searchButton = jQuery('#btn_webauthn_search');
if (searchButton) {
searchButton.click(window.lam.webauthn.searchDevices);
}
}
/**
* Searches for devices via Ajax call.
*
* @param event button click event
* @returns {boolean} false
*/
window.lam.webauthn.searchDevices = function(event) {
if (event !== null) {
event.preventDefault();
}
const resultDiv = jQuery('#webauthn_results');
const tokenValue = resultDiv.data('sec_token_value');
const searchData = jQuery('#webauthn_userDN').val();
const data = {
action: 'search',
jsonInput: '',
sec_token: tokenValue,
searchTerm: searchData
};
jQuery.ajax({
url: '../misc/ajax.php?function=webauthnDevices',
method: 'POST',
data: data
})
.done(function(jsonData) {
resultDiv.html(jsonData.content);
window.lam.webauthn.addDeviceActionListeners();
})
.fail(function() {
console.log('Webauthn search failed');
});
return false;
}
/**
* Adds listeners to the device action buttons.
*/
window.lam.webauthn.addDeviceActionListeners = function() {
const inputs = jQuery('.webauthn-delete');
inputs.each(function() {
jQuery(this).click(function(event) {
window.lam.webauthn.removeDevice(event);
});
});
}
/**
* Removes a webauthn device.
*
* @param element button
*/
window.lam.webauthn.removeDevice = function(event) {
event.preventDefault();
const element = jQuery(event.target);
const dn = element.data('dn');
const credential = element.data('credential');
const resultDiv = jQuery('#webauthn_results');
const tokenValue = resultDiv.data('sec_token_value');
const searchData = jQuery('#webauthn_userDN').val();
const data = {
action: 'delete',
jsonInput: '',
sec_token: tokenValue,
dn: dn,
credentialId: credential
};
jQuery.ajax({
url: '../misc/ajax.php?function=webauthnDevices',
method: 'POST',
data: data
})
.done(function(jsonData) {
resultDiv.html(jsonData.content);
})
.fail(function() {
console.log('Webauthn device deletion failed');
});
return false;
}
jQuery(document).ready(function() { jQuery(document).ready(function() {
window.lam.gui.equalHeight(); window.lam.gui.equalHeight();
window.lam.form.autoTrim(); window.lam.form.autoTrim();
@ -1533,6 +1623,7 @@ jQuery(document).ready(function() {
window.lam.html.activateLightboxes(); window.lam.html.activateLightboxes();
window.lam.html.preventEnter(); window.lam.html.preventEnter();
window.lam.dynamicSelect.activate(); window.lam.dynamicSelect.activate();
window.lam.webauthn.setupDeviceManagement();
}); });
/** /**

View File

@ -1,5 +1,7 @@
<?php <?php
namespace LAM\AJAX; namespace LAM\AJAX;
use htmlResponsiveTable;
use htmlStatusMessage;
use \LAM\TOOLS\IMPORT_EXPORT\Importer; use \LAM\TOOLS\IMPORT_EXPORT\Importer;
use \LAM\TOOLS\IMPORT_EXPORT\Exporter; use \LAM\TOOLS\IMPORT_EXPORT\Exporter;
use \LAM\TYPES\TypeManager; use \LAM\TYPES\TypeManager;
@ -7,7 +9,8 @@ use \htmlResponsiveRow;
use \htmlLink; use \htmlLink;
use \htmlOutputText; use \htmlOutputText;
use \htmlButton; use \htmlButton;
use LAM\LOGIN\WEBAUTHN\WebauthnManager; use \LAM\LOGIN\WEBAUTHN\WebauthnManager;
use \LAMCfgMain;
/* /*
@ -108,6 +111,11 @@ class Ajax {
$this->manageWebauthn($isSelfService); $this->manageWebauthn($isSelfService);
die(); die();
} }
if ($function === 'webauthnDevices') {
$this->enforceUserIsLoggedInToMainConfiguration();
$this->manageWebauthnDevices();
die();
}
enforceUserIsLoggedIn(); enforceUserIsLoggedIn();
if ($function == 'passwordChange') { if ($function == 'passwordChange') {
$this->managePasswordChange($jsonInput); $this->managePasswordChange($jsonInput);
@ -219,6 +227,102 @@ class Ajax {
die(); die();
} }
/**
* Webauthn device management.
*/
private function manageWebauthnDevices() {
$action = $_POST['action'];
if ($action === 'search') {
$searchTerm = $_POST['searchTerm'];
if (!empty($searchTerm)) {
$this->manageWebauthnDevicesSearch($searchTerm);
}
}
elseif ($action === 'delete') {
$dn = $_POST['dn'];
$credentialId = $_POST['credentialId'];
if (!empty($dn) && !empty($credentialId)) {
$this->manageWebauthnDevicesDelete($dn, $credentialId);
}
}
}
/**
* Searches for webauthn devices and prints the results as html.
*
* @param string $searchTerm search term
*/
private function manageWebauthnDevicesSearch($searchTerm) {
include_once __DIR__ . '/../../lib/webauthn.inc';
$database = new \LAM\LOGIN\WEBAUTHN\PublicKeyCredentialSourceRepositorySQLite();
$results = $database->searchDevices($searchTerm);
$row = new htmlResponsiveRow();
$row->addVerticalSpacer('0.5rem');
if (empty($results)) {
$row->add(new htmlStatusMessage('INFO', _('No devices found.')), 12);
}
else {
$titles = array(
_('User'),
_('Registration'),
_('Last use'),
_('Delete')
);
$data = array();
$id = 0;
foreach ($results as $result) {
$delButton = new htmlButton('deleteDevice' . $id, 'delete.png', true);
$delButton->addDataAttribute('credential', $result['credentialId']);
$delButton->addDataAttribute('dn', $result['dn']);
$delButton->setCSSClasses(array('webauthn-delete'));
$data[] = array(
new htmlOutputText($result['dn']),
new htmlOutputText(date('Y-m-d H:i:s', $result['registrationTime'])),
new htmlOutputText(date('Y-m-d H:i:s', $result['lastUseTime'])),
$delButton
);
$id++;
}
$table = new htmlResponsiveTable($titles, $data);
$row->add($table, 12);
}
$row->addVerticalSpacer('2rem');
$tabindex = 10000;
ob_start();
$row->generateHTML('none', array(), array(), false, $tabindex, null);
$content = ob_get_contents();
ob_end_clean();
echo json_encode(array('content' => $content));
}
/**
* Deletes a webauthn device.
*
* @param string $dn user DN
* @param string $credentialId base64 encoded credential id
*/
private function manageWebauthnDevicesDelete($dn, $credentialId) {
include_once __DIR__ . '/../../lib/webauthn.inc';
$database = new \LAM\LOGIN\WEBAUTHN\PublicKeyCredentialSourceRepositorySQLite();
$success = $database->deleteDevice($dn, $credentialId);
if ($success) {
$message = new htmlStatusMessage('INFO', _('The device was deleted.'));
}
else {
$message = new htmlStatusMessage('ERROR', _('The device was not found.'));
}
$row = new htmlResponsiveRow();
$row->addVerticalSpacer('0.5rem');
$row->add($message, 12);
$row->addVerticalSpacer('2rem');
ob_start();
$tabindex = 50000;
$row->generateHTML('none', array(), array(), true, $tabindex, null);
$content = ob_get_contents();
ob_end_clean();
echo json_encode(array('content' => $content));
}
/** /**
* Handles DN selection fields. * Handles DN selection fields.
* *
@ -341,6 +445,23 @@ class Ajax {
return $dnList; return $dnList;
} }
/**
* Checks if the user entered the configuration master password.
* Dies if password is not set.
*/
private function enforceUserIsLoggedInToMainConfiguration() {
if (!isset($_SESSION['cfgMain'])) {
$cfg = new LAMCfgMain();
}
else {
$cfg = $_SESSION['cfgMain'];
}
if (isset($_SESSION["mainconf_password"]) && ($cfg->checkPassword($_SESSION["mainconf_password"]))) {
return;
}
die();
}
} }

View File

@ -120,5 +120,53 @@ class PublicKeyCredentialSourceRepositorySQLiteTest extends TestCase {
)); ));
} }
public function test_hasRegisteredCredentials() {
$this->assertFalse($this->database->hasRegisteredCredentials());
$source1 = new PublicKeyCredentialSource(
"id1",
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
array(),
"atype",
new CertificateTrustPath(array('x5c' => 'test')),
\Ramsey\Uuid\Uuid::uuid1(),
"p1",
"uh1",
1);
$this->database->saveCredentialSource($source1);
$this->assertTrue($this->database->hasRegisteredCredentials());
}
public function test_searchDevices() {
$source1 = new PublicKeyCredentialSource(
"id1",
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
array(),
"atype",
new CertificateTrustPath(array('x5c' => 'test')),
\Ramsey\Uuid\Uuid::uuid1(),
"p1",
"uh1",
1);
$this->database->saveCredentialSource($source1);
$this->assertNotEmpty($this->database->searchDevices('h1'));
$this->assertEmpty($this->database->searchDevices('h2'));
}
public function test_deleteDevice() {
$source1 = new PublicKeyCredentialSource(
"id1",
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
array(),
"atype",
new CertificateTrustPath(array('x5c' => 'test')),
\Ramsey\Uuid\Uuid::uuid1(),
"p1",
"uh1",
1);
$this->database->saveCredentialSource($source1);
$this->assertTrue($this->database->deleteDevice('uh1', base64_encode('id1')));
$this->assertFalse($this->database->deleteDevice('uh1', base64_encode('id2')));
}
} }