Merge remote-tracking branch 'origin/develop' into 413-update-dependencies

This commit is contained in:
Roland Gruber 2025-03-14 07:47:02 +01:00
commit 45baad01cf
12 changed files with 3896 additions and 3330 deletions

View file

@ -14,7 +14,8 @@
"ext-xmlreader": "*", "ext-xmlreader": "*",
"ext-zip": "*", "ext-zip": "*",
"ext-gd": "*", "ext-gd": "*",
"ext-imagick": "*" "ext-imagick": "*",
"ext-gettext": "*"
}, },
"scripts": { "scripts": {
"test": "vendor/bin/phpunit" "test": "vendor/bin/phpunit"

View file

@ -1,8 +1,8 @@
ldap-account-manager (9.1.RC1-1) unstable; urgency=medium ldap-account-manager (9.1-1) unstable; urgency=medium
* new upstream release * new upstream release
-- Roland Gruber <post@rolandgruber.de> Tue, 25 Feb 2024 07:36:27 +0200 -- Roland Gruber <post@rolandgruber.de> Thu, 13 Mar 2025 07:36:27 +0200
ldap-account-manager (9.0-1) unstable; urgency=medium ldap-account-manager (9.0-1) unstable; urgency=medium

View file

@ -29,7 +29,7 @@
FROM debian:bookworm-slim FROM debian:bookworm-slim
LABEL maintainer="Roland Gruber <post@rolandgruber.de>" LABEL maintainer="Roland Gruber <post@rolandgruber.de>"
ARG LAM_RELEASE=9.1.RC1 ARG LAM_RELEASE=9.1
EXPOSE 80 EXPOSE 80
ENV \ ENV \

View file

@ -3,7 +3,7 @@ services:
ldap-account-manager: ldap-account-manager:
build: build:
context: . context: .
image: ldapaccountmanager/lam:9.1.RC1 image: ldapaccountmanager/lam:9.1
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8080:80" - "8080:80"

View file

@ -1,16 +1,20 @@
March 2025 9.1 June 2025 9.2
- Active Directory: allow to restore deleted entries in tree view (415)
13.03.2025 9.1
- Usability improvements (347, 348, 360, 403) - Usability improvements (347, 348, 360, 403)
- Active Directory: deleted entries in "CN=Deleted Objects" can be shown (option in server profile, advanced settings) - Active Directory: deleted entries in "CN=Deleted Objects" can be shown (option in server profile, advanced settings)
- Security: LAM no longer ships with any default passwords, main configuration password is requested on login if not yet set (390) - Security: LAM no longer ships with any default passwords, main configuration password is requested on login if not yet set (390)
- Docker: support to read e.g. configuration password from file to support Docker swarm - Docker: support to read e.g. configuration password from file to support Docker swarm
- LAM Pro:
-> Added support to manage DNS entries of bind-dyndb-ldap (361)
-> Unix users: support to create a group with same name for rfc2307bis (404)
- Fixed bugs: - Fixed bugs:
-> Ambiguous tooltip on profile editor for Shadow users (394) -> Ambiguous tooltip on profile editor for Shadow users (394)
-> Self service photo file enhancements (396) -> Self service photo file enhancements (396)
-> Tree view: delete does not work in French (406) -> Tree view: delete does not work in French (406)
-> Cron job mails: show all values for multi-value attribute wildcards (411) -> Cron job mails: show all values for multi-value attribute wildcards (411)
- LAM Pro:
-> Added support to manage DNS entries of bind-dyndb-ldap (361)
-> Unix users: support to create a group with same name for rfc2307bis (404)
17.12.2024 9.0 17.12.2024 9.0

View file

@ -1 +1 @@
9.1.RC1 9.1

View file

@ -543,10 +543,7 @@
<para>Show deleted entries: This is for Active Directory and Samba 4 <para>Show deleted entries: This is for Active Directory and Samba 4
only. It will unhide LDAP entries in "CN=Deleted Objects,DC=...". You only. It will unhide LDAP entries in "CN=Deleted Objects,DC=...". You
can use this to browse these entries in tree view. To restore an entry can use this to browse and restore these entries in tree view.</para>
run "Restore-ADObject -Identity GUID" in PowerShell where GUID is the
value of the "objectGUID" attribute (you might need to base64 decode
it).</para>
<para>Referential integrity overlay: Activate this checkbox if you <para>Referential integrity overlay: Activate this checkbox if you
have any server side extension for referential integrity in place. In have any server side extension for referential integrity in place. In

View file

@ -70,6 +70,7 @@ include_once __DIR__ . '/tools/treeview.inc';
* @package LAM\TOOLS\TREEVIEW * @package LAM\TOOLS\TREEVIEW
*/ */
class TreeView { class TreeView {
const AD_DELETED_DELIMITER = '\0ADEL:';
/** /**
* @var array schema attributes * @var array schema attributes
@ -124,6 +125,10 @@ class TreeView {
$this->ensureWriteAccess(); $this->ensureWriteAccess();
$this->validateDn($dn); $this->validateDn($dn);
return $this->deleteNode($dn); return $this->deleteNode($dn);
case 'restoreNode':
$this->ensureWriteAccess();
$this->validateDn($dn);
return $this->restoreNode($dn);
case 'search': case 'search':
$this->validateDn($dn); $this->validateDn($dn);
return $this->search($dn); return $this->search($dn);
@ -366,10 +371,10 @@ class TreeView {
$row->addVerticalSpacer('1rem'); $row->addVerticalSpacer('1rem');
} }
$row->add(new htmlTitle(unescapeLdapSpecialCharacters($dn))); $row->add(new htmlTitle(unescapeLdapSpecialCharacters($dn)));
$this->addActionBar($row, $dn); $attributes = ldapGetDN($dn, ['*']);
$this->addActionBar($row, $dn, $attributes);
$row->add(new htmlDiv('ldap_actionarea_messages', new htmlOutputText(''))); $row->add(new htmlDiv('ldap_actionarea_messages', new htmlOutputText('')));
$row->add(new htmlSubTitle(_('Attributes'))); $row->add(new htmlSubTitle(_('Attributes')));
$attributes = ldapGetDN($dn, ['*']);
unset($attributes['dn']); unset($attributes['dn']);
ksort($attributes); ksort($attributes);
logNewMessage(LOG_DEBUG, 'LDAP attributes for ' . $dn . ': ' . print_r($attributes, true)); logNewMessage(LOG_DEBUG, 'LDAP attributes for ' . $dn . ': ' . print_r($attributes, true));
@ -471,11 +476,13 @@ class TreeView {
* *
* @param htmlResponsiveRow $row container * @param htmlResponsiveRow $row container
* @param string $dn entry DN * @param string $dn entry DN
* @param array $attributes LDAP attributes
*/ */
private function addActionBar(htmlResponsiveRow $row, string $dn): void { private function addActionBar(htmlResponsiveRow $row, string $dn, array $attributes): void {
$buttonGroup = new htmlGroup(); $buttonGroup = new htmlGroup();
$buttonSize = '16px'; $buttonSize = '16px';
$buttonClasses = ['clickable', 'lam-margin-small', 'icon']; $buttonClasses = ['clickable', 'lam-margin-small', 'icon'];
$isRestorable = isset($attributes['isdeleted'][0]) && ($attributes['isdeleted'][0] === 'TRUE') && str_contains($dn, '\0ADEL');
if (checkIfWriteAccessIsAllowed()) { if (checkIfWriteAccessIsAllowed()) {
$createButton = new htmlImage('../../graphics/add.svg', $buttonSize, $buttonSize, _('Create a child entry'), _('Create a child entry')); $createButton = new htmlImage('../../graphics/add.svg', $buttonSize, $buttonSize, _('Create a child entry'), _('Create a child entry'));
@ -484,6 +491,7 @@ class TreeView {
$createButton->setCSSClasses($buttonClasses); $createButton->setCSSClasses($buttonClasses);
$buttonGroup->addElement($createButton); $buttonGroup->addElement($createButton);
if (!$isRestorable) {
$deleteButton = new htmlImage('../../graphics/delete.svg', $buttonSize, $buttonSize, _('Delete'), _('Delete')); $deleteButton = new htmlImage('../../graphics/delete.svg', $buttonSize, $buttonSize, _('Delete'), _('Delete'));
$deleteButton->setOnClick('window.lam.treeview.deleteNode(\'' . getSecurityTokenName() . '\', ' $deleteButton->setOnClick('window.lam.treeview.deleteNode(\'' . getSecurityTokenName() . '\', '
. '\'' . getSecurityTokenValue() . '\', \'' . base64_encode($dn) . '\', \'' . base64_encode(htmlspecialchars(extractRDN($dn))) . '\', ' . '\'' . getSecurityTokenValue() . '\', \'' . base64_encode($dn) . '\', \'' . base64_encode(htmlspecialchars(extractRDN($dn))) . '\', '
@ -492,6 +500,7 @@ class TreeView {
$deleteButton->setCSSClasses($buttonClasses); $deleteButton->setCSSClasses($buttonClasses);
$buttonGroup->addElement($deleteButton); $buttonGroup->addElement($deleteButton);
} }
}
$refreshButton = new htmlImage('../../graphics/refresh.svg', $buttonSize, $buttonSize, _('Refresh'), _('Refresh')); $refreshButton = new htmlImage('../../graphics/refresh.svg', $buttonSize, $buttonSize, _('Refresh'), _('Refresh'));
$refreshButton->setOnClick('mar10.Wunderbaum.getTree(\'#ldap_tree\').findKey(\'' . base64_encode($dn) . '\').loadLazy(true); ' $refreshButton->setOnClick('mar10.Wunderbaum.getTree(\'#ldap_tree\').findKey(\'' . base64_encode($dn) . '\').loadLazy(true); '
@ -534,7 +543,29 @@ class TreeView {
$exportButton->setCSSClasses($buttonClasses); $exportButton->setCSSClasses($buttonClasses);
$buttonGroup->addElement($exportButton); $buttonGroup->addElement($exportButton);
} }
$row->add($buttonGroup); $row->add($buttonGroup);
if (checkIfWriteAccessIsAllowed() && $isRestorable) {
$row->addVerticalSpacer('1rem');
$row->add(new htmlSubTitle(_('Restore')));
$targetDn = extractDNSuffix(extractDNSuffix($dn));
if (!empty($attributes['lastknownparent'][0]) && !str_contains($attributes['lastknownparent'][0], self::AD_DELETED_DELIMITER)) {
$targetDn = $attributes['lastknownparent'][0];
}
$restoreTargetDn = new htmlResponsiveInputField(_('Target DN'), 'treeview-restore-dn', $targetDn);
$restoreTargetDn->showDnSelection();
$row->add($restoreTargetDn);
$row->addVerticalSpacer('0.5rem');
$restoreButton = new htmlButton('restorebutton', _('Restore'));
$rdn = explode(self::AD_DELETED_DELIMITER, $dn)[0];
$restoreButton->setOnClick('window.lam.treeview.restoreNode(\'' . getSecurityTokenName() . '\', '
. '\'' . getSecurityTokenValue() . '\', \'' . base64_encode($dn) . '\', \'' . base64_encode(htmlspecialchars($rdn)) . '\', '
. '\'' . addslashes(_('Restore')) . '\', \'' . addslashes(_('Cancel')) . '\', \'' . addslashes(_('Restore this entry')) . '\', \'' . addslashes(_('Ok')) . '\', '
. '\'' . addslashes(_('Error')) . '\');');
$restoreButton->setCSSClasses(['lam-secondary']);
$row->add($restoreButton, 12, 12, 12, 'text-center');
}
} }
/** /**
@ -1359,6 +1390,38 @@ class TreeView {
return json_encode([]); return json_encode([]);
} }
/**
* Restores a node in LDAP.
*
* @param string $dn DN
* @return string JSON
*/
private function restoreNode(string $dn): string {
$rdn = explode(self::AD_DELETED_DELIMITER, $dn)[0];
$targetDn = $_POST['targetDn'];
$changes = [
[
'attrib' => 'isDeleted',
'modtype' => LDAP_MODIFY_BATCH_REMOVE_ALL
],
[
'attrib' => 'distinguishedName',
'modtype' => LDAP_MODIFY_BATCH_REPLACE,
'values' => [$rdn . ',' . $targetDn]
],
];
$success = ldap_modify_batch(getLDAPServerHandle(), $dn, $changes, getCommonLdapControls());
if ($success) {
return json_encode([]);
}
$error = getDefaultLDAPErrorString(getLDAPServerHandle());
logNewMessage(LOG_ERR, 'Tree view restore node failed: ' . $error);
return json_encode([
'errorTitle' => htmlspecialchars(sprintf(_('Was unable to restore %s.'), $rdn)),
'errorText' => $error
]);
}
/** /**
* Stops processing if DN is invalid. * Stops processing if DN is invalid.
* *

File diff suppressed because it is too large Load diff

View file

@ -2497,6 +2497,64 @@ window.lam.treeview.deleteNode = function (tokenName, tokenValue, dn, text, okTe
}); });
} }
/**
* Restores a node in tree view.
*
* @param tokenName security token name
* @param tokenValue security token value
* @param dn base64 encoded DN
* @param text text for dialog body
* @param okText text for OK button
* @param cancelText text for cancel button
* @param title dialog title
* @param errorOkText text for OK button in error dialog
* @param errorTitle dialog title in case of error
*/
window.lam.treeview.restoreNode = function (tokenName, tokenValue, dn, text, okText, cancelText, title, errorOkText, errorTitle) {
const textSpan = document.querySelector('.treeview-restore-entry');
textSpan.innerText = window.atob(text);
const dialogContent = document.getElementById('treeview_restore_dlg').cloneNode(true);
dialogContent.classList.remove('hidden');
const targetDn = document.getElementById('treeview-restore-dn').value;
Swal.fire({
title: title,
confirmButtonText: okText,
cancelButtonText: cancelText,
showCancelButton: true,
html: dialogContent.outerHTML,
width: '48em'
}).then(result => {
if (result.isConfirmed) {
let data = new FormData();
data.append(tokenName, tokenValue);
data.append('dn', dn)
data.append('targetDn', targetDn)
fetch("../misc/ajax.php?function=treeview&command=restoreNode", {
method: 'POST',
body: data
})
.then(async response => {
const jsonData = await response.json();
window.lam.treeview.checkSession(jsonData);
const tree = mar10.Wunderbaum.getTree("#ldap_tree");
const parentNode = tree.findKey(dn).parent;
await parentNode.setActive();
parentNode.loadLazy(true);
window.lam.treeview.getNodeContent(tokenName, tokenValue, parentNode.key);
if (jsonData['errorTitle']) {
const errTextTitle = jsonData['errorTitle'];
const textSpanErrorTitle = document.querySelector('.treeview-error-title');
textSpanErrorTitle.innerText = errTextTitle;
const errText = jsonData['errorText'];
const textSpanErrorText = document.querySelector('.treeview-error-text');
textSpanErrorText.innerText = errText;
window.lam.dialog.showSimpleDialog(errorTitle, null, errorOkText, null, 'treeview_error_dlg');
}
});
}
});
}
/** /**
* Returns the node content in tree view action area. * Returns the node content in tree view action area.
* *

View file

@ -2,6 +2,7 @@
namespace LAM\TOOLS\TREEVIEW; namespace LAM\TOOLS\TREEVIEW;
use htmlDiv; use htmlDiv;
use htmlForm; use htmlForm;
use htmlInputField;
use htmlJavaScript; use htmlJavaScript;
use htmlOutputText; use htmlOutputText;
use htmlResponsiveInputField; use htmlResponsiveInputField;
@ -142,10 +143,20 @@ function showTree(): void {
$deleteDialogDiv = new htmlDiv('treeview_delete_dlg', $deleteDialogContent, ['hidden']); $deleteDialogDiv = new htmlDiv('treeview_delete_dlg', $deleteDialogContent, ['hidden']);
$row->add($deleteDialogDiv); $row->add($deleteDialogDiv);
$restoreDialogContent = new htmlResponsiveRow();
$restoreDialogContent->add(new htmlOutputText(_('Do you really want to restore this entry?')));
$restoreDialogContent->addVerticalSpacer('0.5rem');
$restoreDialogEntryText = new htmlOutputText('');
$restoreDialogEntryText->setCSSClasses(['treeview-restore-entry']);
$restoreDialogContent->add($restoreDialogEntryText);
$restoreDialogDiv = new htmlDiv('treeview_restore_dlg', $restoreDialogContent, ['hidden']);
$row->add($restoreDialogDiv);
$errorDialogContent = new htmlResponsiveRow(); $errorDialogContent = new htmlResponsiveRow();
$errorDialogEntryTitle = new htmlOutputText(''); $errorDialogEntryTitle = new htmlOutputText('');
$errorDialogEntryTitle->setCSSClasses(['treeview-error-title']); $errorDialogEntryTitle->setCSSClasses(['treeview-error-title']);
$errorDialogContent->add($errorDialogEntryTitle); $errorDialogContent->add($errorDialogEntryTitle);
$errorDialogContent->addVerticalSpacer('0.5rem');
$errorDialogEntryText = new htmlOutputText(''); $errorDialogEntryText = new htmlOutputText('');
$errorDialogEntryText->setCSSClasses(['treeview-error-text']); $errorDialogEntryText->setCSSClasses(['treeview-error-text']);
$errorDialogContent->add($errorDialogEntryText); $errorDialogContent->add($errorDialogEntryText);