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>
<para><ulink url="https://duo.com/">Duo</ulink></para>
</listitem>
<listitem>
<para><ulink
url="https://webauthn.io/">Webauthn/FIDO2</ulink></para>
</listitem>
</itemizedlist>
<para>Configuration options:</para>
@ -752,6 +757,27 @@
</listitem>
</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>
<mediaobject>
<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.")),
"251" => array ("Headline" => _("Remote server"),
"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"),
"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.')

View File

@ -1098,10 +1098,10 @@ class htmlButton extends htmlElement {
}
$id = ' id="btn_' . preg_replace('/[^a-zA-Z0-9_-]/', '', $this->name) . '"';
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 {
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
$icon = '';
if ($this->iconClass != null) {

View File

@ -523,6 +523,62 @@ class PublicKeyCredentialSourceRepositorySQLite implements PublicKeyCredentialSo
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.
*

View File

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

View File

@ -1,5 +1,6 @@
<?php
namespace LAM\CONFIG;
use \LAMCfgMain;
use \htmlTable;
use \htmlTitle;
@ -22,10 +23,11 @@ use \htmlResponsiveInputCheckbox;
use \htmlResponsiveInputField;
use \htmlDiv;
use \htmlHiddenInput;
/*
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
it under the terms of the GNU General Public License as published by
@ -45,11 +47,11 @@ use \htmlHiddenInput;
/**
* Manages the main configuration options.
*
* @package configuration
* @author Roland Gruber
*/
* Manages the main configuration options.
*
* @package configuration
* @author Roland Gruber
*/
/** Access to config functions */
@ -95,8 +97,7 @@ if (isset($_POST['submitFormData'])) {
$cfg->setPassword($_POST['masterpassword']);
$msg = _("New master password set successfully.");
unset($_SESSION["mainconf_password"]);
}
else {
} else {
$errors[] = _("Master passwords are different or empty!");
}
}
@ -126,8 +127,7 @@ if (isset($_POST['submitFormData'])) {
}
}
$allowedHosts = implode(",", $allowedHostsList);
}
else {
} else {
$allowedHosts = "";
}
$cfg->allowedHosts = $allowedHosts;
@ -150,8 +150,7 @@ if (isset($_POST['submitFormData'])) {
}
}
$allowedHostsSelfService = implode(",", $allowedHostsSelfServiceList);
}
else {
} else {
$allowedHostsSelfService = "";
}
$cfg->allowedHostsSelfService = $allowedHostsSelfService;
@ -169,22 +168,18 @@ if (isset($_POST['submitFormData'])) {
// set log destination
if ($_POST['logDestination'] == "none") {
$cfg->logDestination = "NONE";
}
elseif ($_POST['logDestination'] == "syslog") {
} elseif ($_POST['logDestination'] == "syslog") {
$cfg->logDestination = "SYSLOG";
}
elseif ($_POST['logDestination'] == "remote") {
} elseif ($_POST['logDestination'] == "remote") {
$cfg->logDestination = "REMOTE:" . $_POST['logRemote'];
$remoteParts = explode(':', $_POST['logRemote']);
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\".");
}
}
else {
} else {
if (isset($_POST['logFile']) && ($_POST['logFile'] != "") && preg_match("/^[a-z0-9\\/\\\\:\\._-]+$/i", $_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 -.");
}
}
@ -207,16 +202,14 @@ if (isset($_POST['submitFormData'])) {
if (isset($_POST['sslCaCertUpload'])) {
if (!isset($_FILES['sslCaCert']) || ($_FILES['sslCaCert']['size'] == 0)) {
$errors[] = _('No file selected.');
}
else {
} else {
$handle = fopen($_FILES['sslCaCert']['tmp_name'], "r");
$data = fread($handle, 10000000);
fclose($handle);
$sslReturn = $cfg->uploadSSLCaCert($data);
if ($sslReturn !== true) {
$errors[] = $sslReturn;
}
else {
} else {
$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[] = _('You might need to restart your webserver for changes to take effect.');
$cfg->uploadSSLCaCert($pemResult);
}
else {
} else {
$errors[] = _('Unable to import server certificate. Please use the upload function.');
}
}
else {
} else {
$errors[] = _('Invalid server name. Please enter "server" or "server:port".');
}
}
@ -270,264 +261,279 @@ if (isset($_POST['submitFormData'])) {
echo $_SESSION['header'];
printHeaderContents(_("Edit general settings"), '../..');
?>
</head>
<body class="admin">
<table border=0 width="100%" class="lamHeader ui-corner-all">
<tr>
<td align="left" height="30">
<a class="lamLogo" href="http://www.ldap-account-manager.org/" target="new_window">
<?php echo getLAMVersionText(); ?>
</a>
</td>
</tr>
</table>
<br>
<!-- form for adding/renaming/deleting profiles -->
<form enctype="multipart/form-data" action="mainmanage.php" method="post">
</head>
<body class="admin">
<table border=0 width="100%" class="lamHeader ui-corner-all">
<tr>
<td align="left" height="30">
<a class="lamLogo" href="http://www.ldap-account-manager.org/" target="new_window">
<?php echo getLAMVersionText(); ?>
</a>
</td>
</tr>
</table>
<br>
<!-- form for adding/renaming/deleting profiles -->
<form enctype="multipart/form-data" action="mainmanage.php" method="post">
<?php
// include all JavaScript files
printJsIncludes('../..');
<?php
// include all JavaScript files
printJsIncludes('../..');
$tabindex = 1;
$tabindex = 1;
$row = new htmlResponsiveRow();
$row->add(new htmlTitle(_('General settings')), 12);
$row = new htmlResponsiveRow();
$row->add(new htmlTitle(_('General settings')), 12);
// print messages
for ($i = 0; $i < sizeof($errors); $i++) {
$row->add(new htmlStatusMessage("ERROR", $errors[$i]), 12);
}
for ($i = 0; $i < sizeof($messages); $i++) {
$row->add(new htmlStatusMessage("INFO", $messages[$i]), 12);
}
// check if config file is writable
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);
}
// license
if (isLAMProVersion()) {
$row->add(new htmlSubTitle(_('Licence')), 12);
$row->add(new htmlResponsiveInputTextarea('license', implode("\n", $cfg->getLicenseLines()), null, 10, _('Licence'), '287'), 12);
$row->add(new htmlSpacer(null, '1rem'), true);
}
// security settings
$row->add(new htmlSubTitle(_("Security settings")), 12);
$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 htmlResponsiveInputTextarea('allowedHosts', implode("\n", explode(",", $cfg->allowedHosts)), null, '7', _("Allowed hosts"), '241'), 12);
if (isLAMProVersion()) {
$row->add(new htmlResponsiveInputTextarea('allowedHostsSelfService', implode("\n", explode(",", $cfg->allowedHostsSelfService)), null, '7', _("Allowed hosts (self service)"), '241'), 12);
}
$encryptSession = ($cfg->encryptSession === 'true');
$encryptSessionBox = new htmlResponsiveInputCheckbox('encryptSession', $encryptSession, _('Encrypt session'), '245');
$encryptSessionBox->setIsEnabled(function_exists('openssl_random_pseudo_bytes'));
$row->add($encryptSessionBox, 12);
// SSL certificate
$row->addVerticalSpacer('1rem');
$row->addLabel(new htmlOutputText(_('SSL certificates')));
$sslMethod = _('use system certificates');
$sslFileName = $cfg->getSSLCaCertTempFileName();
if ($sslFileName != null) {
$sslMethod = _('use custom CA certificates');
}
$sslDelSaveGroup = new htmlGroup();
$sslDelSaveGroup->addElement(new htmlOutputText($sslMethod));
$sslDelSaveGroup->addElement(new htmlSpacer('5px', null));
// delete+download button
if ($sslFileName != null) {
$sslDownloadBtn = new htmlLink('', '../../tmp/' . $sslFileName, '../../graphics/save.png');
$sslDownloadBtn->setTargetWindow('_blank');
$sslDownloadBtn->setTitle(_('Download CA certificates'));
$sslDelSaveGroup->addElement($sslDownloadBtn);
$sslDeleteBtn = new htmlButton('sslCaCertDelete', 'delete.png', true);
$sslDeleteBtn->setTitle(_('Delete all CA certificates'));
$sslDelSaveGroup->addElement($sslDeleteBtn);
}
$sslDelSaveGroup->addElement(new htmlHelpLink('204'));
$row->addField($sslDelSaveGroup);
$row->addLabel(new htmlInputFileUpload('sslCaCert'));
$sslUploadBtn = new htmlButton('sslCaCertUpload', _('Upload'));
$sslUploadBtn->setIconClass('upButton');
$sslUploadBtn->setTitle(_('Upload CA certificate in DER/PEM format.'));
$row->addField($sslUploadBtn);
if (function_exists('stream_socket_client') && function_exists('stream_context_get_params')) {
$sslImportServerUrl = !empty($_POST['serverurl']) ? $_POST['serverurl'] : 'ldaps://';
$serverUrlUpload = new htmlInputField('serverurl', $sslImportServerUrl);
$row->addLabel($serverUrlUpload);
$sslImportBtn = new htmlButton('sslCaCertImport', _('Import from server'));
$sslImportBtn->setIconClass('downButton');
$sslImportBtn->setTitle(_('Imports the certificate directly from your LDAP server.'));
$row->addField($sslImportBtn);
}
$sslCerts = $cfg->getSSLCaCertificates();
if (sizeof($sslCerts) > 0) {
$certsTitles = array(_('Common name'), _('Valid to'), _('Serial number'), _('Delete'));
$certsData = array();
for ($i = 0; $i < sizeof($sslCerts); $i++) {
$serial = isset($sslCerts[$i]['serialNumber']) ? $sslCerts[$i]['serialNumber'] : '';
$validTo = isset($sslCerts[$i]['validTo_time_t']) ? $sslCerts[$i]['validTo_time_t'] : '';
$cn = isset($sslCerts[$i]['subject']['CN']) ? $sslCerts[$i]['subject']['CN'] : '';
$delBtn = new htmlButton('deleteCert_' . $i, 'delete.png', true);
$certsData[] = array(
new htmlOutputText($cn),
new htmlOutputText($validTo),
new htmlOutputText($serial),
$delBtn
);
// print messages
for ($i = 0; $i < sizeof($errors); $i++) {
$row->add(new htmlStatusMessage("ERROR", $errors[$i]), 12);
}
for ($i = 0; $i < sizeof($messages); $i++) {
$row->add(new htmlStatusMessage("INFO", $messages[$i]), 12);
}
$certsTable = new \htmlResponsiveTable($certsTitles, $certsData);
$row->add($certsTable, 12);
}
// password policy
$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);
$options4 = array(0, 1, 2, 3, 4);
$row->add(new htmlResponsiveSelect('passwordMinLength', $options20, array($cfg->passwordMinLength), _('Minimum password length'), '242'), 12);
$row->addVerticalSpacer('1rem');
$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('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('passwordMinClasses', $options4, array($cfg->passwordMinClasses), _('Minimum character classes'), '242'), 12);
$row->addVerticalSpacer('1rem');
$rulesCountOptions = array(_('all') => '-1', '3' => '3', '4' => '4');
$rulesCountSelect = new htmlResponsiveSelect('passwordRulesCount', $rulesCountOptions, array($cfg->checkedRulesCount), _('Number of rules that must match'), '246');
$rulesCountSelect->setHasDescriptiveElements(true);
$row->add($rulesCountSelect, 12);
$passwordMustNotContainUser = ($cfg->passwordMustNotContainUser === 'true');
$row->add(new htmlResponsiveInputCheckbox('passwordMustNotContainUser',$passwordMustNotContainUser , _('Password must not contain user name'), '247'), 12);
$passwordMustNotContain3Chars = ($cfg->passwordMustNotContain3Chars === 'true');
$row->add(new htmlResponsiveInputCheckbox('passwordMustNotContain3Chars', $passwordMustNotContain3Chars, _('Password must not contain part of user/first/last name'), '248'), 12);
if (function_exists('curl_init')) {
// check if config file is writable
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);
}
// license
if (isLAMProVersion()) {
$row->add(new htmlSubTitle(_('Licence')), 12);
$row->add(new htmlResponsiveInputTextarea('license', implode("\n", $cfg->getLicenseLines()), null, 10, _('Licence'), '287'), 12);
$row->add(new htmlSpacer(null, '1rem'), true);
}
// security settings
$row->add(new htmlSubTitle(_("Security settings")), 12);
$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 htmlResponsiveInputTextarea('allowedHosts', implode("\n", explode(",", $cfg->allowedHosts)), null, '7', _("Allowed hosts"), '241'), 12);
if (isLAMProVersion()) {
$row->add(new htmlResponsiveInputTextarea('allowedHostsSelfService', implode("\n", explode(",", $cfg->allowedHostsSelfService)), null, '7', _("Allowed hosts (self service)"), '241'), 12);
}
$encryptSession = ($cfg->encryptSession === 'true');
$encryptSessionBox = new htmlResponsiveInputCheckbox('encryptSession', $encryptSession, _('Encrypt session'), '245');
$encryptSessionBox->setIsEnabled(function_exists('openssl_random_pseudo_bytes'));
$row->add($encryptSessionBox, 12);
// SSL certificate
$row->addVerticalSpacer('1rem');
$row->add(new htmlResponsiveInputField(_('External password check'), 'externalPwdCheckUrl', $cfg->externalPwdCheckUrl, '249'), 12);
}
// logging
$row->add(new htmlSubTitle(_("Logging")), 12);
$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->setHasDescriptiveElements(true);
$row->add($levelSelect, 12);
$destinationOptions = array(
_("No logging") => "none",
_("System logging") => "syslog",
_("File") => 'file',
_("Remote") => 'remote',
);
$destinationSelected = 'file';
$destinationPath = $cfg->logDestination;
$destinationRemote = '';
if ($cfg->logDestination == 'NONE') {
$destinationSelected = 'none';
$destinationPath = '';
}
elseif ($cfg->logDestination == 'SYSLOG') {
$destinationSelected = 'syslog';
$destinationPath = '';
}
elseif (strpos($cfg->logDestination, 'REMOTE') === 0) {
$destinationSelected = 'remote';
$remoteParts = explode(':', $cfg->logDestination, 2);
$destinationRemote = empty($remoteParts[1]) ? '' : $remoteParts[1];
$destinationPath = '';
}
$logDestinationSelect = new htmlResponsiveSelect('logDestination', $destinationOptions, array($destinationSelected), _("Log destination"), '240');
$logDestinationSelect->setTableRowsToHide(array(
'none' => array('logFile', 'logRemote'),
'syslog' => array('logFile', 'logRemote'),
'remote' => array('logFile'),
'file' => array('logRemote'),
));
$logDestinationSelect->setTableRowsToShow(array(
'file' => array('logFile'),
'remote' => array('logRemote'),
));
$logDestinationSelect->setHasDescriptiveElements(true);
$row->add($logDestinationSelect, 12);
$row->add(new htmlResponsiveInputField(_('File'), 'logFile', $destinationPath), 12);
$row->add(new htmlResponsiveInputField(_('Remote server'), 'logRemote', $destinationRemote, '251'), 12);
$errorLogOptions = array(
_('PHP system setting') => LAMCfgMain::ERROR_REPORTING_SYSTEM,
_('default') => LAMCfgMain::ERROR_REPORTING_DEFAULT,
_('all') => LAMCfgMain::ERROR_REPORTING_ALL
);
$errorLogSelect = new htmlResponsiveSelect('errorReporting', $errorLogOptions, array($cfg->errorReporting), _('PHP error reporting'), '244');
$errorLogSelect->setHasDescriptiveElements(true);
$row->add($errorLogSelect, 12);
// additional options
if (isLAMProVersion()) {
$row->add(new htmlSubTitle(_('Additional options')), 12);
$mailEOLOptions = array(
_('Default (\r\n)') => 'default',
_('Non-standard (\n)') => 'unix'
);
$mailEOLSelect = new htmlResponsiveSelect('mailEOL', $mailEOLOptions, array($cfg->mailEOL), _('Email format'), '243');
$mailEOLSelect->setHasDescriptiveElements(true);
$row->add($mailEOLSelect, 12);
}
$row->addVerticalSpacer('3rem');
// change master password
$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->addElement(new htmlButton('submit', _("Ok")));
$buttonTable->addElement(new htmlSpacer('1rem', null));
$buttonTable->addElement(new htmlButton('cancel', _("Cancel")));
$row->add($buttonTable, 12);
$row->add(new htmlHiddenInput('submitFormData', '1'), 12);
}
$box = new htmlDiv(null, $row);
$box->setCSSClasses(array('ui-corner-all', 'roundedShadowBox'));
parseHtml(null, $box, array(), false, $tabindex, 'user');
/**
* Formats an LDAP time string (e.g. from createTimestamp).
*
* @param String $time LDAP time value
* @return String formated time
*/
function formatSSLTimestamp($time) {
if (!empty($time)) {
$timeZone = 'UTC';
$sysTimeZone = @date_default_timezone_get();
if (!empty($sysTimeZone)) {
$timeZone = $sysTimeZone;
}
$date = new DateTime('@' . $time, new DateTimeZone($timeZone));
return $date->format('d.m.Y');
$row->addLabel(new htmlOutputText(_('SSL certificates')));
$sslMethod = _('use system certificates');
$sslFileName = $cfg->getSSLCaCertTempFileName();
if ($sslFileName != null) {
$sslMethod = _('use custom CA certificates');
}
return '';
}
$sslDelSaveGroup = new htmlGroup();
$sslDelSaveGroup->addElement(new htmlOutputText($sslMethod));
$sslDelSaveGroup->addElement(new htmlSpacer('5px', null));
// delete+download button
if ($sslFileName != null) {
$sslDownloadBtn = new htmlLink('', '../../tmp/' . $sslFileName, '../../graphics/save.png');
$sslDownloadBtn->setTargetWindow('_blank');
$sslDownloadBtn->setTitle(_('Download CA certificates'));
$sslDelSaveGroup->addElement($sslDownloadBtn);
$sslDeleteBtn = new htmlButton('sslCaCertDelete', 'delete.png', true);
$sslDeleteBtn->setTitle(_('Delete all CA certificates'));
$sslDelSaveGroup->addElement($sslDeleteBtn);
}
$sslDelSaveGroup->addElement(new htmlHelpLink('204'));
$row->addField($sslDelSaveGroup);
$row->addLabel(new htmlInputFileUpload('sslCaCert'));
$sslUploadBtn = new htmlButton('sslCaCertUpload', _('Upload'));
$sslUploadBtn->setIconClass('upButton');
$sslUploadBtn->setTitle(_('Upload CA certificate in DER/PEM format.'));
$row->addField($sslUploadBtn);
if (function_exists('stream_socket_client') && function_exists('stream_context_get_params')) {
$sslImportServerUrl = !empty($_POST['serverurl']) ? $_POST['serverurl'] : 'ldaps://';
$serverUrlUpload = new htmlInputField('serverurl', $sslImportServerUrl);
$row->addLabel($serverUrlUpload);
$sslImportBtn = new htmlButton('sslCaCertImport', _('Import from server'));
$sslImportBtn->setIconClass('downButton');
$sslImportBtn->setTitle(_('Imports the certificate directly from your LDAP server.'));
$row->addField($sslImportBtn);
}
$sslCerts = $cfg->getSSLCaCertificates();
if (sizeof($sslCerts) > 0) {
$certsTitles = array(_('Common name'), _('Valid to'), _('Serial number'), _('Delete'));
$certsData = array();
for ($i = 0; $i < sizeof($sslCerts); $i++) {
$serial = isset($sslCerts[$i]['serialNumber']) ? $sslCerts[$i]['serialNumber'] : '';
$validTo = isset($sslCerts[$i]['validTo_time_t']) ? $sslCerts[$i]['validTo_time_t'] : '';
$cn = isset($sslCerts[$i]['subject']['CN']) ? $sslCerts[$i]['subject']['CN'] : '';
$delBtn = new htmlButton('deleteCert_' . $i, 'delete.png', true);
$certsData[] = array(
new htmlOutputText($cn),
new htmlOutputText($validTo),
new htmlOutputText($serial),
$delBtn
);
}
$certsTable = new \htmlResponsiveTable($certsTitles, $certsData);
$row->add($certsTable, 12);
}
// password policy
$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);
$options4 = array(0, 1, 2, 3, 4);
$row->add(new htmlResponsiveSelect('passwordMinLength', $options20, array($cfg->passwordMinLength), _('Minimum password length'), '242'), 12);
$row->addVerticalSpacer('1rem');
$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('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('passwordMinClasses', $options4, array($cfg->passwordMinClasses), _('Minimum character classes'), '242'), 12);
$row->addVerticalSpacer('1rem');
$rulesCountOptions = array(_('all') => '-1', '3' => '3', '4' => '4');
$rulesCountSelect = new htmlResponsiveSelect('passwordRulesCount', $rulesCountOptions, array($cfg->checkedRulesCount), _('Number of rules that must match'), '246');
$rulesCountSelect->setHasDescriptiveElements(true);
$row->add($rulesCountSelect, 12);
$passwordMustNotContainUser = ($cfg->passwordMustNotContainUser === 'true');
$row->add(new htmlResponsiveInputCheckbox('passwordMustNotContainUser', $passwordMustNotContainUser, _('Password must not contain user name'), '247'), 12);
$passwordMustNotContain3Chars = ($cfg->passwordMustNotContain3Chars === 'true');
$row->add(new htmlResponsiveInputCheckbox('passwordMustNotContain3Chars', $passwordMustNotContain3Chars, _('Password must not contain part of user/first/last name'), '248'), 12);
if (function_exists('curl_init')) {
$row->addVerticalSpacer('1rem');
$row->add(new htmlResponsiveInputField(_('External password check'), 'externalPwdCheckUrl', $cfg->externalPwdCheckUrl, '249'), 12);
}
// logging
$row->add(new htmlSubTitle(_("Logging")), 12);
$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->setHasDescriptiveElements(true);
$row->add($levelSelect, 12);
$destinationOptions = array(
_("No logging") => "none",
_("System logging") => "syslog",
_("File") => 'file',
_("Remote") => 'remote',
);
$destinationSelected = 'file';
$destinationPath = $cfg->logDestination;
$destinationRemote = '';
if ($cfg->logDestination == 'NONE') {
$destinationSelected = 'none';
$destinationPath = '';
} elseif ($cfg->logDestination == 'SYSLOG') {
$destinationSelected = 'syslog';
$destinationPath = '';
} elseif (strpos($cfg->logDestination, 'REMOTE') === 0) {
$destinationSelected = 'remote';
$remoteParts = explode(':', $cfg->logDestination, 2);
$destinationRemote = empty($remoteParts[1]) ? '' : $remoteParts[1];
$destinationPath = '';
}
$logDestinationSelect = new htmlResponsiveSelect('logDestination', $destinationOptions, array($destinationSelected), _("Log destination"), '240');
$logDestinationSelect->setTableRowsToHide(array(
'none' => array('logFile', 'logRemote'),
'syslog' => array('logFile', 'logRemote'),
'remote' => array('logFile'),
'file' => array('logRemote'),
));
$logDestinationSelect->setTableRowsToShow(array(
'file' => array('logFile'),
'remote' => array('logRemote'),
));
$logDestinationSelect->setHasDescriptiveElements(true);
$row->add($logDestinationSelect, 12);
$row->add(new htmlResponsiveInputField(_('File'), 'logFile', $destinationPath), 12);
$row->add(new htmlResponsiveInputField(_('Remote server'), 'logRemote', $destinationRemote, '251'), 12);
$errorLogOptions = array(
_('PHP system setting') => LAMCfgMain::ERROR_REPORTING_SYSTEM,
_('default') => LAMCfgMain::ERROR_REPORTING_DEFAULT,
_('all') => LAMCfgMain::ERROR_REPORTING_ALL
);
$errorLogSelect = new htmlResponsiveSelect('errorReporting', $errorLogOptions, array($cfg->errorReporting), _('PHP error reporting'), '244');
$errorLogSelect->setHasDescriptiveElements(true);
$row->add($errorLogSelect, 12);
// additional options
if (isLAMProVersion()) {
$row->add(new htmlSubTitle(_('Additional options')), 12);
$mailEOLOptions = array(
_('Default (\r\n)') => 'default',
_('Non-standard (\n)') => 'unix'
);
$mailEOLSelect = new htmlResponsiveSelect('mailEOL', $mailEOLOptions, array($cfg->mailEOL), _('Email format'), '243');
$mailEOLSelect->setHasDescriptiveElements(true);
$row->add($mailEOLSelect, 12);
}
$row->addVerticalSpacer('3rem');
// webauthn management
if ((version_compare(phpversion(), '7.2.0') >= 0)
&& extension_loaded('PDO')
&& in_array('sqlite', \PDO::getAvailableDrivers())) {
include_once __DIR__ . '/../../lib/webauthn.inc';
$database = new \LAM\LOGIN\WEBAUTHN\PublicKeyCredentialSourceRepositorySQLite();
if ($database->hasRegisteredCredentials()) {
$row->add(new htmlSubTitle(_('Webauthn devices')), 12);
$row->add(new htmlResponsiveInputField(_('User DN'), 'webauthn_userDN', null, '252'), 12);
$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);
}
}
// change master password
$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->addElement(new htmlButton('submit', _("Ok")));
$buttonTable->addElement(new htmlSpacer('1rem', null));
$buttonTable->addElement(new htmlButton('cancel', _("Cancel")));
$row->add($buttonTable, 12);
$row->add(new htmlHiddenInput('submitFormData', '1'), 12);
}
$box = new htmlDiv(null, $row);
$box->setCSSClasses(array('ui-corner-all', 'roundedShadowBox'));
parseHtml(null, $box, array(), false, $tabindex, 'user');
/**
* Formats an LDAP time string (e.g. from createTimestamp).
*
* @param String $time LDAP time value
* @return String formated time
*/
function formatSSLTimestamp($time) {
if (!empty($time)) {
$timeZone = 'UTC';
$sysTimeZone = @date_default_timezone_get();
if (!empty($sysTimeZone)) {
$timeZone = $sysTimeZone;
}
$date = new DateTime('@' . $time, new DateTimeZone($timeZone));
return $date->format('d.m.Y');
}
return '';
}
?>
</form>
<p><br></p>
?>
</body>
</form>
<p><br></p>
</body>
</html>

View File

@ -1522,6 +1522,96 @@ window.lam.webauthn.arrayToBase64String = function(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() {
window.lam.gui.equalHeight();
window.lam.form.autoTrim();
@ -1533,6 +1623,7 @@ jQuery(document).ready(function() {
window.lam.html.activateLightboxes();
window.lam.html.preventEnter();
window.lam.dynamicSelect.activate();
window.lam.webauthn.setupDeviceManagement();
});
/**

View File

@ -1,5 +1,7 @@
<?php
namespace LAM\AJAX;
use htmlResponsiveTable;
use htmlStatusMessage;
use \LAM\TOOLS\IMPORT_EXPORT\Importer;
use \LAM\TOOLS\IMPORT_EXPORT\Exporter;
use \LAM\TYPES\TypeManager;
@ -7,7 +9,8 @@ use \htmlResponsiveRow;
use \htmlLink;
use \htmlOutputText;
use \htmlButton;
use LAM\LOGIN\WEBAUTHN\WebauthnManager;
use \LAM\LOGIN\WEBAUTHN\WebauthnManager;
use \LAMCfgMain;
/*
@ -108,6 +111,11 @@ class Ajax {
$this->manageWebauthn($isSelfService);
die();
}
if ($function === 'webauthnDevices') {
$this->enforceUserIsLoggedInToMainConfiguration();
$this->manageWebauthnDevices();
die();
}
enforceUserIsLoggedIn();
if ($function == 'passwordChange') {
$this->managePasswordChange($jsonInput);
@ -219,6 +227,102 @@ class Ajax {
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.
*
@ -341,6 +445,23 @@ class Ajax {
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')));
}
}