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

This commit is contained in:
Roland Gruber 2025-03-30 14:26:19 +02:00
commit f5172b6b5a
22 changed files with 725 additions and 159 deletions

View file

@ -1,4 +1,4 @@
This software is copyright (c) 2003 - 2024 by Roland Gruber
This software is copyright (c) 2003 - 2025 by Roland Gruber
If you purchased a copy of LDAP Account Manager Pro then the following
files are licensed under the conditions which you accepted at purchase
@ -17,6 +17,8 @@ time.
* lib/modules/automount.inc
* lib/modules/bindDLZ.inc
* lib/modules/bindDLZXfr.inc
* lib/modules/bindDyndbRecord.inc
* lib/modules/bindDyndbZone.inc
* lib/modules/customBaseType.inc
* lib/modules/customFields.inc
* lib/modules/customScripts.inc
@ -56,6 +58,7 @@ time.
* lib/modules/rfc2307bisAutomount.inc
* lib/modules/rfc2307bisPosixGroup.inc
* lib/modules/selfRegistration.inc
* lib/modules/simpleSecurityObject.inc
* lib/modules/sudoRole.inc
* lib/modules/uidObject.inc
* lib/modules/webauthn.inc
@ -64,6 +67,7 @@ time.
* lib/types/alias.inc
* lib/types/automountType.inc
* lib/types/bind.inc
* lib/types/bindDyndbType.inc
* lib/types/customType.inc
* lib/types/gon.inc
* lib/types/kopanoAddressListType.inc

View file

@ -1,6 +1,9 @@
June 2025 9.2
- TAK support added
- Active Directory: allow to restore deleted entries in tree view (415)
- Fixed bugs:
-> Unix: profile editor for users not working (418)
-> Custom fields: problems with deleting facsimileTelephoneNumber (419)
13.03.2025 9.1
- Usability improvements (347, 348, 360, 403)

View file

@ -17,6 +17,8 @@ time.
* lib/modules/automount.inc
* lib/modules/bindDLZ.inc
* lib/modules/bindDLZXfr.inc
* lib/modules/bindDyndbRecord.inc
* lib/modules/bindDyndbZone.inc
* lib/modules/customBaseType.inc
* lib/modules/customFields.inc
* lib/modules/customScripts.inc
@ -56,6 +58,7 @@ time.
* lib/modules/rfc2307bisAutomount.inc
* lib/modules/rfc2307bisPosixGroup.inc
* lib/modules/selfRegistration.inc
* lib/modules/simpleSecurityObject.inc
* lib/modules/sudoRole.inc
* lib/modules/uidObject.inc
* lib/modules/webauthn.inc
@ -64,6 +67,7 @@ time.
* lib/types/alias.inc
* lib/types/automountType.inc
* lib/types/bind.inc
* lib/types/bindDyndbType.inc
* lib/types/customType.inc
* lib/types/gon.inc
* lib/types/kopanoAddressListType.inc

View file

@ -467,7 +467,7 @@
<entry>dhcp.schema</entry>
<entry>docs/schema/dhcp.schema</entry>
<entry>Part of LAM installation: docs/schema/dhcp.schema</entry>
<entry>The LDAP suffix should be set to your dhcpServer
entry.</entry>
@ -486,7 +486,7 @@
<entry>schema.ldif</entry>
<entry>part of bind-dyndb-ldap</entry>
<entry>Part of bind-dyndb-ldap</entry>
<entry>LAM Pro only</entry>
</row>
@ -505,7 +505,7 @@
<entry>dlz.schema</entry>
<entry>part of <ulink url="http://bind-dlz.sourceforge.net/">Bind
<entry>Part of <ulink url="http://bind-dlz.sourceforge.net/">Bind
DLZ patch</ulink></entry>
<entry>LAM Pro only</entry>
@ -821,6 +821,24 @@
<entry>LAM Pro only, requires DDS extension on LDAP server
side</entry>
</row>
<row>
<entry><inlinemediaobject>
<imageobject>
<imagedata fileref="images/schema_tak.png" width="16px"/>
</imageobject>
</inlinemediaobject></entry>
<entry>TAK</entry>
<entry>takUser</entry>
<entry>tak-*.ldif</entry>
<entry>Part of LAM installation: docs/schema/tak-*.ldif</entry>
<entry/>
</row>
</tbody>
</tgroup>
</table>

View file

@ -2510,6 +2510,56 @@ AuthorizedKeysCommandUser root</literallayout>
<graphic fileref="images/mod_lastBind3.png"/>
</screenshot>
</section>
<section>
<title>TAK</title>
<para>The <ulink url="https://www.civtak.org/">TAK</ulink> module
supports the Team Awareness Kit or Tactical Assault Kit (TAK) with the
Android Team Awareness Kit (ATAK).</para>
<para>You can define callsigns, team roles and colors for users.</para>
<para><emphasis role="bold">LDAP schema</emphasis></para>
<para>The module expects that TAK users use the object class "takUser"
and the attributes "takCallsign", "takRole" and "takColor". You can find
matching schema files in /usr/share/ldap-account-manager/docs/schema
(DEB/RPM) or docs/schema (tar.bz2). Please see the beginning of the
files for installation instructions.</para>
<itemizedlist>
<listitem>
<para>OpenLDAP: tak-OpenLDAP.ldif</para>
</listitem>
<listitem>
<para>Samba 4: tak-Samba4-attributes.ldif and
tak-Samba4-objectClass.ldif</para>
</listitem>
<listitem>
<para>Windows (AD): tak-Windows.ldif</para>
</listitem>
</itemizedlist>
<para><emphasis role="bold">Configuration</emphasis></para>
<para>Add the TAK module for users in your server profile:</para>
<screenshot>
<graphic fileref="images/mod_tak1.png"/>
</screenshot>
<para>Now you can manage the TAK attributes for users.</para>
<para>LAM Pro users can add these attributes to the self-service profile
if needed.</para>
<screenshot>
<graphic fileref="images/mod_tak2.png"/>
</screenshot>
</section>
</section>
<section>

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,33 @@
#
# LDAP schema for LAM TAK functionality
#
# This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
# Copyright (C) 2025 Roland Gruber
#
#
# OID bases:
# 1.3.6.1.4.1.34955 Roland Gruber Softwareentwicklung
# 1.3.6.1.4.1.34955.1 attributes
# 1.3.6.1.4.1.34955.2 object classes
#
# Installation:
# ldapadd -x -W -H ldap://localhost -D "cn=admin,dc=company,dc=com" -f tak-OpenLDAP.ldif
#
# Please replace "localhost" with your LDAP server and "cn=admin,dc=company,dc=com" with your LDAP admin user (usually starts with cn=admin or cn=manager).
#
# In some cases you might need to import directly on the OpenLDAP server as root:
# ldapadd -Y EXTERNAL -H ldapi:/// -f tak-OpenLDAP.ldif
#
# Version: 1
#
# Changelog:
# 1: initial release (LAM 9.2)
#
dn: cn=tak,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: tak
olcAttributeTypes: ( 1.3.6.1.4.1.34955.1.100 NAME 'takCallsign' DESC 'TAK callsign' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
olcAttributeTypes: ( 1.3.6.1.4.1.34955.1.101 NAME 'takRole' DESC 'TAK team role' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
olcAttributeTypes: ( 1.3.6.1.4.1.34955.1.102 NAME 'takColor' DESC 'TAK team color' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
olcObjectClasses: ( 1.3.6.1.4.1.34955.2.10 NAME 'takUser' DESC 'TAK user' SUP top AUXILIARY MAY ( takCallsign $ takRole $ takColor ) MUST ( cn ) )

View file

@ -0,0 +1,58 @@
#
# LDAP schema for LAM TAK functionality
#
# This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
# Copyright (C) 2025 Roland Gruber
#
#
# OID bases:
# 1.3.6.1.4.1.34955 Roland Gruber Softwareentwicklung
# 1.3.6.1.4.1.34955.1 attributes
# 1.3.6.1.4.1.34955.2 object classes
#
# Please replace DOMAIN_TOP_DN with your LDAP suffix (e.g. dc=samba4,dc=test).
# This file must be installed first.
#
# Installation: ldbmodify -H /var/lib/samba/private/sam.ldb tak-Samba4-attributes.ldif --option="dsdb:schema update allowed"=true
#
#
# Version: 1
# 1: initial release (LAM 9.2)
#
dn: CN=takCallsign,CN=Schema,CN=Configuration,DOMAIN_TOP_DN
objectClass: top
objectClass: attributeSchema
attributeID: 1.3.6.1.4.1.34955.1.100
attributeSyntax: 2.5.5.12
oMSyntax: 64
isSingleValued: TRUE
rangeLower: 4
cn: takCallsign
name: takCallsign
lDAPDisplayName: takCallsign
description: TAK callsign
dn: CN=takRole,CN=Schema,CN=Configuration,DOMAIN_TOP_DN
objectClass: top
objectClass: attributeSchema
attributeID: 1.3.6.1.4.1.34955.1.101
attributeSyntax: 2.5.5.12
oMSyntax: 64
isSingleValued: TRUE
cn: takRole
name: takRole
lDAPDisplayName: takRole
description: TAK team role
dn: CN=takColor,CN=Schema,CN=Configuration,DOMAIN_TOP_DN
objectClass: top
objectClass: attributeSchema
attributeID: 1.3.6.1.4.1.34955.1.102
attributeSyntax: 2.5.5.12
oMSyntax: 64
isSingleValued: TRUE
cn: takColor
name: takColor
LDAPDisplayName: takColor
Description: TAK team color

View file

@ -0,0 +1,36 @@
#
# LDAP schema for LAM TAK functionality
#
# This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
# Copyright (C) 2025 Roland Gruber
#
#
# OID bases:
# 1.3.6.1.4.1.34955 Roland Gruber Softwareentwicklung
# 1.3.6.1.4.1.34955.1 attributes
# 1.3.6.1.4.1.34955.2 object classes
#
# Please replace DOMAIN_TOP_DN with your LDAP suffix (e.g. dc=samba4,dc=test).
# This file must be installed second.
#
# Installation: ldbmodify -H /var/lib/samba/private/sam.ldb tak-Samba4-objectClass.ldif --option="dsdb:schema update allowed"=true
#
#
# Version: 1
# 1: initial release (LAM 9.2)
#
dn: CN=takUser,CN=Schema,CN=Configuration,DOMAIN_TOP_DN
objectClass: top
objectClass: classSchema
governsID: 1.3.6.1.4.1.34955.2.10
cn: takUser
lDAPDisplayName: takUser
subClassOf: top
objectClassCategory: 3
mustContain: cn
mayContain: takCallsign
mayContain: takRole
mayContain: takColor
description: TAK user
possSuperiors: top

View file

@ -0,0 +1,100 @@
#
# LDAP schema for LAM TAK functionality
#
# This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
# Copyright (C) 2025 Roland Gruber
#
#
# OID bases:
# 1.3.6.1.4.1.34955 Roland Gruber Softwareentwicklung
# 1.3.6.1.4.1.34955.1 attributes
# 1.3.6.1.4.1.34955.2 object classes
#
# Please replace DOMAIN_TOP_DN with your LDAP suffix (e.g. dc=windows,dc=test).
#
# Installation: ldifde -v -i -f tak-Windows.ldif
#
#
# Version: 1
# 1: initial release (LAM 9.2)
#
dn: CN=takCallsign,CN=Schema,CN=Configuration,DOMAIN_TOP_DN
changetype: add
objectClass: top
objectClass: attributeSchema
attributeID: 1.3.6.1.4.1.34955.1.100
attributeSyntax: 2.5.5.12
oMSyntax: 64
isSingleValued: TRUE
rangeLower: 4
cn: takCallsign
name: takCallsign
lDAPDisplayName: takCallsign
description: TAK callsign
dn: CN=takRole,CN=Schema,CN=Configuration,DOMAIN_TOP_DN
changetype: add
objectClass: top
objectClass: attributeSchema
attributeID: 1.3.6.1.4.1.34955.1.101
attributeSyntax: 2.5.5.12
oMSyntax: 64
isSingleValued: TRUE
cn: takRole
name: takRole
lDAPDisplayName: takRole
description: TAK team role
dn: CN=takColor,CN=Schema,CN=Configuration,DOMAIN_TOP_DN
changetype: add
objectClass: top
objectClass: attributeSchema
attributeID: 1.3.6.1.4.1.34955.1.102
attributeSyntax: 2.5.5.12
oMSyntax: 64
isSingleValued: TRUE
cn: takColor
name: takColor
LDAPDisplayName: takColor
Description: TAK team color
dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-
dn: CN=takUser,CN=Schema,CN=Configuration,DOMAIN_TOP_DN
changetype: add
objectClass: top
objectClass: classSchema
governsID: 1.3.6.1.4.1.34955.2.10
cn: takUser
lDAPDisplayName: takUser
subClassOf: top
objectClassCategory: 3
mustContain: cn
mayContain: takCallsign
mayContain: takRole
mayContain: takColor
description: TAK user
possSuperiors: top
dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-
dn: CN=User,CN=Schema,CN=Configuration,DOMAIN_TOP_DN
changetype: modify
add: auxiliaryClass
auxiliaryClass: takUser
-
dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-

View file

@ -3,7 +3,7 @@
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
Copyright (C) 2003 - 2006 Tilo Lutz
2009 - 2024 Roland Gruber
2009 - 2025 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
@ -77,6 +77,18 @@ function in_array_ignore_case($needle, $haystack) {
return false;
}
/**
* Checks if two arrays have the same content.
*
* @param array $array1 array 1
* @param array $array2 array 2
* @return bool same content
*/
function areArrayContentsEqual(array $array1, array $array2): bool {
$intersect = array_intersect($array1, $array2);
return ((count($array1) === count($array2)) && (count($intersect) === count($array1)));
}
/**
* Sorts an array in natural order by its keys.
*

View file

@ -9,7 +9,7 @@ use LAM\PDF\PDFImage;
/*
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
Copyright (C) 2003 - 2024 Roland Gruber
Copyright (C) 2003 - 2025 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
@ -1034,7 +1034,7 @@ abstract class baseModule {
if (!empty($regex)) {
$this->checkUploadRegex($regexIDs, $rawAccounts[$position][$ids[$colName]], $message, $position, $errors);
}
$partialAccounts[$position][$attrName] = trim($rawAccounts[$position][$ids[$colName]]);
$partialAccounts[$position][$attrName] = [trim($rawAccounts[$position][$ids[$colName]])];
}
// multi-value
else {
@ -1799,7 +1799,7 @@ abstract class baseModule {
* @param array $container return value of checkSelfServiceOptions()
* @param String $name attribute name
* @param array $attributes LDAP attributes
* @param string $fields input fields
* @param array $fields input fields
* @param array $readOnlyFields list of read-only fields
* @param String $validationID validation ID for get_preg()
* @param array $validationMessage validation message data (defaults to $this->messages[$name][0])
@ -1822,13 +1822,11 @@ abstract class baseModule {
$container['add'][$ldapAttrName] = [$_POST[$fieldName]];
}
}
elseif ($requiredMessage !== null) {
$container['messages'][] = $requiredMessage;
}
elseif (isset($attributes[$ldapAttrName])) {
if ($requiredMessage !== null) {
$container['messages'][] = $requiredMessage;
}
else {
$container['del'][$ldapAttrName] = $attributes[$ldapAttrName];
}
$container['del'][$ldapAttrName] = $attributes[$ldapAttrName];
}
}
}
@ -1954,8 +1952,7 @@ abstract class baseModule {
$container['messages'][] = $requiredMessage;
}
$valuesOld = $attributes[$ldapAttrName] ?? [];
$intersect = array_intersect($valuesOld, $valuesNew);
if ((count($valuesOld) !== count($valuesNew)) || (count($intersect) !== count($valuesOld))) {
if (!areArrayContentsEqual($valuesOld, $valuesNew)) {
$container['mod'][$ldapAttrName] = $valuesNew;
}
}

View file

@ -1589,87 +1589,49 @@ class accountContainer {
*/
function save_module_attributes($attributes, $orig) {
$return = [];
$toadd = [];
$tomodify = [];
$torem = [];
$notchanged = [];
// get list of all attributes
$attr_names = array_keys($attributes);
$orig_names = array_keys($orig);
$toModify = [];
$notChanged = [];
// cleanup
foreach ($attributes as $name => &$values) {
// remove empty values
$values = array_values($values);
for ($i = 0; $i < count($values); $i++) {
if ($values[$i] === '') {
unset($values[$i]);
}
}
$values = array_values($values);
// remove empty list of values
if (sizeof($values) === 0) {
unset($attributes[$name]);
}
}
// find deleted attributes (in $orig but no longer in $attributes)
foreach ($orig_names as $value) {
if (!isset($attributes[$value])) {
$torem[$value] = $orig[$value];
foreach ($orig as $name => $value) {
if (!isset($attributes[$name]) || (sizeof($attributes[$name]) === 0)) {
$toModify[$name] = [];
}
}
// find changed attributes
foreach ($attr_names as $name) {
// find deleted attributes
if (isset($orig[$name]) && is_array($orig[$name])) {
foreach ($orig[$name] as $value) {
if (is_array($attributes[$name])) {
if (!in_array($value, $attributes[$name], true)
&& ($value !== null)
&& ($value !== '')) {
$torem[$name][] = $value;
}
}
elseif (($value !== null) && ($value !== '')) {
$torem[$name][] = $value;
}
}
foreach ($attributes as $name => $value) {
// new attributes
if (!isset($orig[$name])) {
$toModify[$name] = $value;
}
// find new attributes
if (isset($attributes[$name]) && is_array($attributes[$name])) {
foreach ($attributes[$name] as $value) {
if (isset($orig[$name]) && is_array($orig[$name])) {
if (!in_array($value, $orig[$name], true)
&& ($value !== null)
&& ($value !== '')) {
$toadd[$name][] = $value;
}
}
elseif (($value !== null) && ($value !== '')) {
$toadd[$name][] = $value;
}
}
// changed attributes
elseif (!areArrayContentsEqual($value, $orig[$name])) {
$toModify[$name] = $value;
}
// find unchanged attributes
if (isset($orig[$name]) && is_array($orig[$name]) && is_array($attributes[$name])) {
foreach ($attributes[$name] as $value) {
if (($value !== null) && ($value !== '') && in_array($value, $orig[$name], true)) {
$notchanged[$name][] = $value;
}
}
// unchanged attributes
else {
$notChanged[$name] = $value;
}
}
// create modify with add and remove
$attributes2 = array_keys($toadd);
for ($i = 0; $i < count($attributes2); $i++) {
if (isset($torem[$attributes2[$i]]) && ($toadd[$attributes2[$i]] !== []) && (count($torem[$attributes2[$i]]) > 0)) {
// found attribute which should be modified
$tomodify[$attributes2[$i]] = $toadd[$attributes2[$i]];
// merge unchanged values
if (isset($notchanged[$attributes2[$i]])) {
$tomodify[$attributes2[$i]] = array_merge($tomodify[$attributes2[$i]], $notchanged[$attributes2[$i]]);
unset($notchanged[$attributes2[$i]]);
}
// remove old add and remove commands
unset($toadd[$attributes2[$i]]);
unset($torem[$attributes2[$i]]);
}
if ($toModify !== []) {
$return[$this->dn_orig]['modify'] = $toModify;
}
if ($toadd !== []) {
$return[$this->dn_orig]['add'] = $toadd;
}
if ($torem !== []) {
$return[$this->dn_orig]['remove'] = $torem;
}
if ($tomodify !== []) {
$return[$this->dn_orig]['modify'] = $tomodify;
}
if ($notchanged !== []) {
$return[$this->dn_orig]['notchanged'] = $notchanged;
if ($notChanged !== []) {
$return[$this->dn_orig]['notchanged'] = $notChanged;
}
return $return;
}

View file

@ -894,28 +894,6 @@ class inetOrgPerson extends baseModule implements passwordService, AccountStatus
return [];
}
$return = parent::save_attributes();
// postalAddress, registeredAddress, facsimileTelephoneNumber and jpegPhoto need special removing
if (isset($return[$this->getAccountContainer()->dn_orig]['remove']['postalAddress'])) {
$return[$this->getAccountContainer()->dn_orig]['modify']['postalAddress'] = $this->attributes['postalAddress'];
unset($return[$this->getAccountContainer()->dn_orig]['remove']['postalAddress']);
}
if (isset($return[$this->getAccountContainer()->dn_orig]['remove']['registeredAddress'])) {
$return[$this->getAccountContainer()->dn_orig]['modify']['registeredAddress'] = $this->attributes['registeredAddress'];
unset($return[$this->getAccountContainer()->dn_orig]['remove']['registeredAddress']);
}
if (isset($return[$this->getAccountContainer()->dn_orig]['remove']['facsimileTelephoneNumber'])) {
$return[$this->getAccountContainer()->dn_orig]['modify']['facsimileTelephoneNumber'] = $this->attributes['facsimileTelephoneNumber'];
unset($return[$this->getAccountContainer()->dn_orig]['remove']['facsimileTelephoneNumber']);
}
if (isset($return[$this->getAccountContainer()->dn_orig]['add']['facsimileTelephoneNumber'])
&& isset($this->orig['facsimileTelephoneNumber']) && (count($this->orig['facsimileTelephoneNumber']) > 0)) {
$return[$this->getAccountContainer()->dn_orig]['modify']['facsimileTelephoneNumber'] = $this->attributes['facsimileTelephoneNumber'];
unset($return[$this->getAccountContainer()->dn_orig]['add']['facsimileTelephoneNumber']);
}
if (isset($return[$this->getAccountContainer()->dn_orig]['remove']['jpegPhoto'])) {
$return[$this->getAccountContainer()->dn_orig]['modify']['jpegPhoto'] = [];
unset($return[$this->getAccountContainer()->dn_orig]['remove']['jpegPhoto']);
}
// add information about clear text password
if ($this->clearTextPassword != null) {
$return[$this->getAccountContainer()->dn_orig]['info']['userPasswordClearText'][0] = $this->clearTextPassword;
@ -2730,19 +2708,19 @@ class inetOrgPerson extends baseModule implements passwordService, AccountStatus
$attributes, $readOnlyFields, false, false, 'givenName');
$this->addSimpleSelfServiceTextField($return, 'lastName', _('Last name'), $fields,
$attributes, $readOnlyFields, true, false, 'sn');
$this->addSimpleSelfServiceTextField($return, 'mail', _('Email address'), $fields,
$this->addMultiValueSelfServiceTextField($return, 'mail', _('Email address'), $fields,
$attributes, $readOnlyFields);
$this->addMultiValueSelfServiceTextField($return, 'labeledURI', _('Web site'), $fields,
$attributes, $readOnlyFields, false, false, 'labeledURI');
$this->addSimpleSelfServiceTextField($return, 'telephoneNumber', _('Telephone number'), $fields,
$this->addMultiValueSelfServiceTextField($return, 'telephoneNumber', _('Telephone number'), $fields,
$attributes, $readOnlyFields, false, false, 'telephoneNumber');
$this->addSimpleSelfServiceTextField($return, 'homePhone', _('Home telephone number'), $fields,
$this->addMultiValueSelfServiceTextField($return, 'homePhone', _('Home telephone number'), $fields,
$attributes, $readOnlyFields, false, false, 'homePhone');
$this->addSimpleSelfServiceTextField($return, 'mobile', _('Mobile telephone number'), $fields,
$this->addMultiValueSelfServiceTextField($return, 'mobile', _('Mobile telephone number'), $fields,
$attributes, $readOnlyFields);
$this->addSimpleSelfServiceTextField($return, 'faxNumber', _('Fax number'), $fields,
$this->addMultiValueSelfServiceTextField($return, 'faxNumber', _('Fax number'), $fields,
$attributes, $readOnlyFields, false, false, 'facsimileTelephoneNumber');
$this->addSimpleSelfServiceTextField($return, 'pager', _('Pager'), $fields,
$this->addMultiValueSelfServiceTextField($return, 'pager', _('Pager'), $fields,
$attributes, $readOnlyFields);
$this->addMultiValueSelfServiceTextField($return, 'street', _('Street'), $fields,
$attributes, $readOnlyFields);
@ -3200,22 +3178,22 @@ class inetOrgPerson extends baseModule implements passwordService, AccountStatus
$this->checkSimpleSelfServiceTextField($return, 'lastName', $attributes, $fields,
$readOnlyFields, 'realname', $this->messages['lastname'][0], $this->messages['lastname'][0],
'sn');
$this->checkSimpleSelfServiceTextField($return, 'mail', $attributes, $fields,
$this->checkMultiValueSelfServiceTextField($return, 'mail', $attributes, $fields,
$readOnlyFields, 'email', $this->messages['mail'][0]);
$this->checkMultiValueSelfServiceTextField($return, 'labeledURI', $attributes, $fields,
$readOnlyFields, null, null, null, 'labeledURI');
$this->checkSimpleSelfServiceTextField($return, 'telephoneNumber', $attributes, $fields,
$this->checkMultiValueSelfServiceTextField($return, 'telephoneNumber', $attributes, $fields,
$readOnlyFields, 'telephone', $this->messages['telephoneNumber'][0], null,
'telephoneNumber');
$this->checkSimpleSelfServiceTextField($return, 'homePhone', $attributes, $fields,
$this->checkMultiValueSelfServiceTextField($return, 'homePhone', $attributes, $fields,
$readOnlyFields, 'telephone', $this->messages['homePhone'][0], null,
'homePhone');
$this->checkSimpleSelfServiceTextField($return, 'faxNumber', $attributes, $fields,
$this->checkMultiValueSelfServiceTextField($return, 'faxNumber', $attributes, $fields,
$readOnlyFields, 'telephone', $this->messages['facsimileTelephoneNumber'][0], null,
'facsimileTelephoneNumber');
$this->checkSimpleSelfServiceTextField($return, 'mobile', $attributes, $fields,
$this->checkMultiValueSelfServiceTextField($return, 'mobile', $attributes, $fields,
$readOnlyFields, 'telephone', $this->messages['mobile'][0]);
$this->checkSimpleSelfServiceTextField($return, 'pager', $attributes, $fields,
$this->checkMultiValueSelfServiceTextField($return, 'pager', $attributes, $fields,
$readOnlyFields, 'telephone', $this->messages['pager'][0]);
$this->checkMultiValueSelfServiceTextField($return, 'street', $attributes, $fields,
$readOnlyFields, 'street', $this->messages['street'][0]);

View file

@ -8,7 +8,7 @@ use LAM\TYPES\ConfiguredType;
/*
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
Copyright (C) 2009 - 2024 Roland Gruber
Copyright (C) 2009 - 2025 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
@ -159,32 +159,6 @@ class nisnetgroup extends baseModule {
$this->messages['domain'][0] = ['ERROR', _('Domain name'), _('Domain name is invalid!')];
}
/**
* Returns a list of modifications which have to be made to the LDAP account.
*
* @return array list of modifications
* <br>This function returns an array with 3 entries:
* <br>array( DN1 ('add' => array($attr), 'remove' => array($attr), 'modify' => array($attr)), DN2 .... )
* <br>DN is the DN to change. It may be possible to change several DNs (e.g. create a new user and add him to some groups via attribute memberUid)
* <br>"add" are attributes which have to be added to LDAP entry
* <br>"remove" are attributes which have to be removed from LDAP entry
* <br>"modify" are attributes which have to been modified in LDAP entry
* <br>"info" are values with informational value (e.g. to be used later by pre/postModify actions)
*/
function save_attributes() {
$return = $this->getAccountContainer()->save_module_attributes($this->attributes, $this->orig);
// nisNetgroupTriple needs special changing
if (isset($return[$this->getAccountContainer()->dn_orig]['remove']['nisNetgroupTriple'])) {
$return[$this->getAccountContainer()->dn_orig]['modify']['nisNetgroupTriple'] = $this->attributes['nisNetgroupTriple'];
unset($return[$this->getAccountContainer()->dn_orig]['remove']['nisNetgroupTriple']);
}
if (isset($return[$this->getAccountContainer()->dn_orig]['add']['nisNetgroupTriple'])) {
$return[$this->getAccountContainer()->dn_orig]['modify']['nisNetgroupTriple'] = $this->attributes['nisNetgroupTriple'];
unset($return[$this->getAccountContainer()->dn_orig]['add']['nisNetgroupTriple']);
}
return $return;
}
/**
* Returns the HTML meta data for the main account page.
*

View file

@ -1382,10 +1382,13 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP
/**
* Returns if groups with same name can be created.
*
* @param ?string $typeId type ID
* @return bool creation is possible
*/
private function allowsToCreateGroupWithSameName(): bool {
$typeId = $this->getAccountContainer()->get_type()->getId();
private function allowsToCreateGroupWithSameName(?string $typeId = null): bool {
if ($typeId === null) {
$typeId = $this->getAccountContainer()->get_type()->getId();
}
if (($this->get_scope() !== 'user')
|| $this->isBooleanConfigOptionSet('posixAccount_' . $typeId . '_hideCreateGroup')) {
return false;
@ -2174,7 +2177,7 @@ class posixAccount extends baseModule implements passwordService, AccountStatusP
if ($this->get_scope() == 'user') {
// primary Unix group
$primaryGroups = $groups;
$allowToCreateGroupWithUserName = $this->allowsToCreateGroupWithSameName();
$allowToCreateGroupWithUserName = $this->allowsToCreateGroupWithSameName($typeId);
if ($allowToCreateGroupWithUserName) {
$primaryGroups = [_('Create group with same name') => self::CREATE_GROUP_WITH_SAME_NAME] + $primaryGroups;
}

327
lam/lib/modules/takUser.inc Normal file
View file

@ -0,0 +1,327 @@
<?php
/*
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
Copyright (C) 2025 Mark Halliday
2025 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
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
use LAM\TYPES\ConfiguredType;
/**
* Manages the object class "takUser" for users.
*
* @package modules
*
* @author Mark Halliday
* @author Roland Gruber
*/
class takUser extends baseModule {
/**
* Returns true if this module can manage accounts of the current type, otherwise false.
*
* @return boolean true if module fits
*/
public function can_manage() {
return $this->get_scope() === 'user';
}
/** possible roles */
private const ROLE_TYPES = ['Team Member', 'Team Leader', 'HQ', 'RTO', 'K9', 'Medic', 'Forward Observer', 'Sniper'];
/** possible colors */
private const COLOR_TYPES = ['Blue', 'Brown', 'Cyan', 'Dark Blue', 'Dark Green', 'Green', 'Magenta', 'Maroon', 'Orange', 'Purple', 'Red', 'Teal', 'White', 'Yellow'];
/**
* {@inheritDoc}
*/
public function get_metaData() {
$return = [];
// icon
$return['icon'] = 'tak.png';
// alias name
$return["alias"] = _("TAK");
// LDAP filter
$return["ldap_filter"] = ['or' => "(objectClass=takUser)"];
// RDN attribute
$return["RDN"] = ["cn" => "normal"];
// module dependencies
$return['dependencies'] = ['depends' => [], 'conflicts' => []];
// managed object classes
$return['objectClasses'] = ['takUser'];
// managed attributes
$return['attributes'] = ['takcallsign', 'takrole', 'takcolor'
];
// help Entries
$return['help'] = [
'takcallsign' => [
"Headline" => _("Callsign"), 'attr' => 'takCallsign',
"Text" => _("The user's callsign to be displayed to other TAK users. It must be unique.")
],
'takrole' => [
"Headline" => _("Team role"), 'attr' => 'takRole',
"Text" => _("The user's role to be displayed to other TAK users.")
],
'takcolor' => [
"Headline" => _("Team color"), 'attr' => 'takColor',
"Text" => _("The user's team color to be displayed to other TAK users.")
],
];
// upload fields
$return['upload_columns'] = [
[
'name' => 'takuser_takcallsign',
'description' => _('Callsign'),
'help' => 'takcallsign',
'example' => 'UK-ORG-01',
'required' => true,
'unique' => true
],
[
'name' => 'takuser_takrole',
'description' => _('Team role'),
'help' => 'takrole',
'default' => 'Team Member',
'values' => implode(", ", self::ROLE_TYPES),
'example' => 'Team Member',
'required' => true
],
[
'name' => 'takuser_takcolor',
'description' => _('Team color'),
'help' => 'takcolor',
'default' => 'Cyan',
'values' => implode(", ", self::COLOR_TYPES),
'example' => 'Cyan',
'required' => true
],
];
// available PDF fields
$return['PDF_fields'] = [
'takcallsign' => _('Callsign'),
'takrole' => _('Team role'),
'takcolor' => _('Team color'),
];
// profile options
$profileContainer = new htmlResponsiveRow();
$profileContainer->add(new htmlResponsiveInputField(_('Callsign'), 'takuser_takcallsign', null, 'takusercallsign'));
$profileContainer->add(new htmlResponsiveInputField(_('Team Role'), 'takuser_takrole', null, 'takuserrole'));
$profileContainer->add(new htmlResponsiveInputField(_('Team Color'), 'takuser_takcolor', null, 'takusercolor'));
$return['profile_options'] = $profileContainer;
// self-service field settings
$return['selfServiceFieldSettings'] = [
'takcallsign' => _('Callsign'),
'takrole' => _('Team role'),
'takcolor' => _('Team color'),
];
return $return;
}
/**
* {@inheritDoc}
*/
public function load_Messages() {
$this->messages['takcallsign'][0] = ['ERROR', _('Callsign'), _('Callsign contains invalid characters. Valid characters are: a-z, A-Z, 0-9 and .-_')];
$this->messages['takcallsign'][1] = ['ERROR', _('A TAK login with this callsign already exists. Please choose a different callsign.')];
$this->messages['takcallsign'][2] = ['ERROR', _('Callsign'), _('This field is required.')];
$this->messages['takcallsign'][3] = ['ERROR', _('Account %s:') . ' takuser_takcallsign', _('Callsign contains invalid characters. Valid characters are: a-z, A-Z, 0-9 and .-_')];
$this->messages['takrole'][0] = ['ERROR', _('Account %s:') . ' takuser_takrole', _('Please enter a valid option:') . ' ' . implode(', ', self::ROLE_TYPES)];
$this->messages['takcolor'][0] = ['ERROR', _('Account %s:') . ' takuser_takcolor', _('Please enter a valid option:') . ' ' . implode(', ', self::COLOR_TYPES)];
}
/**
* {@inheritDoc}
*/
public function display_html_attributes() {
$return = new htmlResponsiveRow();
// takCallsign
$this->addSimpleInputTextField($return, 'takcallsign', _('Callsign'), true);
// takRole
$takRole = $this->attributes['takrole'][0] ?? '';
$return->add(new htmlResponsiveSelect('takrole', self::ROLE_TYPES, [$takRole], _('Team role'), 'takrole'));
//takColor
$takColor = $this->attributes['takcolor'][0] ?? 'Cyan';
$return->add(new htmlResponsiveSelect('takcolor', self::COLOR_TYPES, [$takColor], _('Team color'), 'takcolor'));
return $return;
}
/**
* {@inheritDoc}
*/
public function process_attributes() {
$return = [];
$this->attributes['takcallsign'][0] = $_POST['takcallsign'];
$this->attributes['takrole'][0] = $_POST['takrole'];
$this->attributes['takcolor'][0] = $_POST['takcolor'];
// check if callsign is filled
if ($_POST['takcallsign'] == '') {
$return[] = $this->messages['takcallsign'][2];
}
elseif (!get_preg($_POST['takcallsign'], 'username')) {
$return[] = $this->messages['takcallsign'][0];
}
// check if callsign is unique
elseif (empty($this->orig['takcallsign'][0]) || ($this->attributes['takcallsign'][0] !== $this->orig['takcallsign'][0])) {
$suffix = $this->getAccountContainer()->get_type()->getSuffix();
$search = searchLDAP($suffix, 'takcallsign=' . $_POST['takcallsign'], ['dn']);
if (!empty($search)) {
$return[] = $this->messages['takcallsign'][1];
}
}
return $return;
}
/**
* {@inheritDoc}
* @see baseModule::build_uploadAccounts()
*/
public function build_uploadAccounts($rawAccounts, $ids, &$partialAccounts, $selectedModules, &$type) {
$errors = [];
for ($i = 0; $i < count($rawAccounts); $i++) {
// add object class
if (!in_array('takUser', $partialAccounts[$i]['objectClass'])) {
$partialAccounts[$i]['objectClass'][] = 'takUser';
}
$this->mapSimpleUploadField($rawAccounts, $ids, $partialAccounts, $i, 'takuser_takcallsign', 'takcallsign', 'username', $this->messages['takcallsign'][3], $errors);
$this->mapSimpleUploadField($rawAccounts, $ids, $partialAccounts, $i, 'takuser_takrole', 'takrole', null, null, $errors);
if (!in_array($partialAccounts[$i]['takrole'][0], self::ROLE_TYPES)) {
$errors[] = array_merge($this->messages['takrole'][0], [[$i]]);
}
$this->mapSimpleUploadField($rawAccounts, $ids, $partialAccounts, $i, 'takuser_takcolor', 'takcolor', null, null, $errors);
if (!in_array($partialAccounts[$i]['takcolor'][0], self::COLOR_TYPES)) {
$errors[] = array_merge($this->messages['takcolor'][0], [[$i]]);
}
}
return $errors;
}
/**
* {@inheritDoc}
* @see baseModule::get_pdfEntries()
*/
public function get_pdfEntries($pdfKeys, $typeId) {
$return = [];
$this->addSimplePDFField($return, 'takcallsign', _('Callsign'));
$this->addSimplePDFField($return, 'takrole', _('Team role'));
$this->addSimplePDFField($return, 'takcolor', _('Team color'));
return $return;
}
/**
* @inheritDoc
*/
public function load_profile($profile) {
// profile mappings in meta data
parent::load_profile($profile);
// special profile options
if (!empty($profile['takuser_takcallsign'][0])) {
$this->attributes['takcallsign'][0] = $profile['takuser_takcallsign'][0];
}
if (!empty($profile['takuser_takrole'][0])) {
$this->attributes['takrole'][0] = $profile['takuser_takrole'][0];
}
if (!empty($profile['takuser_takcolor'][0])) {
$this->attributes['takcolor'][0] = $profile['takuser_takcolor'][0];
}
}
/**
* @inheritDoc
*/
public function getSelfServiceOptions($fields, $attributes, $passwordChangeOnly, $readOnlyFields) {
$return = [];
if ($passwordChangeOnly) {
return $return; // only password fields as long no LDAP content can be read
}
if (!in_array_ignore_case('takUser', $attributes['objectClass'])) {
return $return;
}
$this->addSimpleSelfServiceTextField($return, 'takcallsign', _('Callsign'), $fields, $attributes, $readOnlyFields, true);
if (in_array('takrole', $fields)) {
$role = '';
if (isset($attributes['takrole'][0])) {
$role = $attributes['takrole'][0];
}
if (in_array('takrole', $readOnlyFields)) {
$field = new htmlOutputText($role);
}
else {
$selected = [];
if (!empty($attributes['takrole'][0])) {
$selected = [$attributes['takrole'][0]];
}
$field = new htmlSelect('takUser_takrole', self::ROLE_TYPES, $selected);
}
$return['takrole'] = new htmlResponsiveRow(
new htmlLabel('takUser_takrole', $this->getSelfServiceLabel('takrole', _('Team role'))), $field
);
}
if (in_array('takcolor', $fields)) {
$color = '';
if (isset($attributes['takcolor'][0])) {
$color = $attributes['takcolor'][0];
}
if (in_array('takcolor', $readOnlyFields)) {
$field = new htmlOutputText($color);
}
else {
$selected = [];
if (!empty($attributes['takcolor'][0])) {
$selected = [$attributes['takcolor'][0]];
}
$field = new htmlSelect('takUser_takcolor', self::COLOR_TYPES, $selected);
}
$return['takcolor'] = new htmlResponsiveRow(
new htmlLabel('takUser_takcolor', $this->getSelfServiceLabel('takcolor', _('Team color'))), $field
);
}
return $return;
}
/**
* @inheritDoc
*/
public function checkSelfServiceOptions($fields, $attributes, $passwordChangeOnly, $readOnlyFields) {
$return = ['messages' => [], 'add' => [], 'del' => [], 'mod' => [], 'info' => []];
if ($passwordChangeOnly) {
return $return; // skip processing if only a password change is done
}
if (!in_array_ignore_case('takUser', $attributes['objectClass'])) {
return $return;
}
$this->checkSimpleSelfServiceTextField($return, 'takcallsign', $attributes, $fields, $readOnlyFields, 'username', $this->messages['takcallsign'][0], $this->messages['takcallsign'][2]);
$this->checkSimpleSelfServiceTextField($return, 'takrole', $attributes, $fields, $readOnlyFields);
$this->checkSimpleSelfServiceTextField($return, 'takcolor', $attributes, $fields, $readOnlyFields);
return $return;
}
/**
* @inheritDoc
*/
public function getListAttributeDescriptions(ConfiguredType $type): array {
return [
"takcallsign" => _("Callsign"),
"takrole" => _("Team role"),
"takcolor" => _("Team color"),
];
}
}

View file

@ -13,7 +13,7 @@ use LamTemporaryFilesManager;
/*
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
Copyright (C) 2004 - 2024 Roland Gruber
Copyright (C) 2004 - 2025 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
@ -155,7 +155,7 @@ if ($_FILES['inputfile'] && ($_FILES['inputfile']['size'] > 0)) {
$checkcolumns = [];
$columns = [];
foreach ($uploadColumns as $uploadColumn) {
$columns = array_merge($columns, $uploadColumns);
$columns = array_merge($columns, $uploadColumn);
}
foreach ($columns as $column) {
if (isset($column['required']) && ($column['required'] === true)) {

View file

@ -2,7 +2,7 @@
use PHPUnit\Framework\TestCase;
/*
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
Copyright (C) 2018 - 2024 Roland Gruber
Copyright (C) 2018 - 2025 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
@ -325,4 +325,13 @@ class AccountTest extends TestCase {
$this->assertFalse(get_preg('ab?c:80', 'hostAndPort'));
}
function testAreArrayContentsEqual() {
$this->assertTrue(areArrayContentsEqual([], []));
$this->assertFalse(areArrayContentsEqual(['1'], []));
$this->assertFalse(areArrayContentsEqual([], ['1']));
$this->assertTrue(areArrayContentsEqual(['a', 'b', 'c'], ['c', 'b', 'a']));
$this->assertFalse(areArrayContentsEqual(['a', 'b', 'c'], ['a', 'c', 'd']));
$this->assertFalse(areArrayContentsEqual(['a', 'b', 'c'], ['a', 'c']));
}
}

View file

@ -27,7 +27,6 @@ parameters:
- '#Call to an undefined method object::.*#'
- '#Parameter \#2 \$string of function explode expects string, .* given.#'
- '#Parameter \#2 \$result of function ldap_.* expects LDAP\\Result, array\|LDAP\\Result given.#'
- '#Cannot access an offset on mixed.#'
- '#Cannot access offset .* on mixed.#'
- '#Cannot access offset .* on array\|int.#'
- '#Cannot access an offset on array\|Countable.#'
@ -47,4 +46,3 @@ parameters:
- '#Binary operation .* between .* and .* results in an error.#'
- '#Parameter \#. .* of (function|method) .* expects .*, mixed given.#'
- '#Cannot access property .* on mixed#'
- '#Cannot use \+\+ on mixed.#'