diff --git a/lam-packaging/debian/copyright b/lam-packaging/debian/copyright index cab1f9509..a9b92c1c3 100644 --- a/lam-packaging/debian/copyright +++ b/lam-packaging/debian/copyright @@ -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 diff --git a/lam/HISTORY b/lam/HISTORY index 661b9993f..1b6a16b84 100644 --- a/lam/HISTORY +++ b/lam/HISTORY @@ -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) diff --git a/lam/copyright b/lam/copyright index 6b0822d04..d10c3adbe 100644 --- a/lam/copyright +++ b/lam/copyright @@ -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 @@ -433,7 +437,7 @@ E: THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -F: +F: 3-Clause BSD License Redistribution and use in source and binary forms, with or without diff --git a/lam/docs/manual-sources/appendix-schema.xml b/lam/docs/manual-sources/appendix-schema.xml index d22cab80b..c369ed91e 100644 --- a/lam/docs/manual-sources/appendix-schema.xml +++ b/lam/docs/manual-sources/appendix-schema.xml @@ -467,7 +467,7 @@ dhcp.schema - docs/schema/dhcp.schema + Part of LAM installation: docs/schema/dhcp.schema The LDAP suffix should be set to your dhcpServer entry. @@ -486,7 +486,7 @@ schema.ldif - part of bind-dyndb-ldap + Part of bind-dyndb-ldap LAM Pro only @@ -505,7 +505,7 @@ dlz.schema - part of Bind + Part of Bind DLZ patch LAM Pro only @@ -821,6 +821,24 @@ LAM Pro only, requires DDS extension on LDAP server side + + + + + + + + + TAK + + takUser + + tak-*.ldif + + Part of LAM installation: docs/schema/tak-*.ldif + + + diff --git a/lam/docs/manual-sources/chapter-modules.xml b/lam/docs/manual-sources/chapter-modules.xml index cca247743..e03e61617 100644 --- a/lam/docs/manual-sources/chapter-modules.xml +++ b/lam/docs/manual-sources/chapter-modules.xml @@ -2510,6 +2510,56 @@ AuthorizedKeysCommandUser root + +
+ TAK + + The TAK module + supports the Team Awareness Kit or Tactical Assault Kit (TAK) with the + Android Team Awareness Kit (ATAK). + + You can define callsigns, team roles and colors for users. + + LDAP schema + + 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. + + + + OpenLDAP: tak-OpenLDAP.ldif + + + + Samba 4: tak-Samba4-attributes.ldif and + tak-Samba4-objectClass.ldif + + + + Windows (AD): tak-Windows.ldif + + + + Configuration + + Add the TAK module for users in your server profile: + + + + + + Now you can manage the TAK attributes for users. + + LAM Pro users can add these attributes to the self-service profile + if needed. + + + + +
diff --git a/lam/docs/manual-sources/images/mod_tak1.png b/lam/docs/manual-sources/images/mod_tak1.png new file mode 100644 index 000000000..5caf6d0a4 Binary files /dev/null and b/lam/docs/manual-sources/images/mod_tak1.png differ diff --git a/lam/docs/manual-sources/images/mod_tak2.png b/lam/docs/manual-sources/images/mod_tak2.png new file mode 100644 index 000000000..a5e97b7c0 Binary files /dev/null and b/lam/docs/manual-sources/images/mod_tak2.png differ diff --git a/lam/docs/manual-sources/images/schema_tak.png b/lam/docs/manual-sources/images/schema_tak.png new file mode 100644 index 000000000..0bb41fb6b Binary files /dev/null and b/lam/docs/manual-sources/images/schema_tak.png differ diff --git a/lam/docs/schema/tak-OpenLDAP.ldif b/lam/docs/schema/tak-OpenLDAP.ldif new file mode 100644 index 000000000..a9d69372c --- /dev/null +++ b/lam/docs/schema/tak-OpenLDAP.ldif @@ -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 ) ) diff --git a/lam/docs/schema/tak-Samba4-attributes.ldif b/lam/docs/schema/tak-Samba4-attributes.ldif new file mode 100644 index 000000000..38b93523a --- /dev/null +++ b/lam/docs/schema/tak-Samba4-attributes.ldif @@ -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 diff --git a/lam/docs/schema/tak-Samba4-objectClass.ldif b/lam/docs/schema/tak-Samba4-objectClass.ldif new file mode 100644 index 000000000..25d165988 --- /dev/null +++ b/lam/docs/schema/tak-Samba4-objectClass.ldif @@ -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 diff --git a/lam/docs/schema/tak-Windows.ldif b/lam/docs/schema/tak-Windows.ldif new file mode 100644 index 000000000..0fba52bed --- /dev/null +++ b/lam/docs/schema/tak-Windows.ldif @@ -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 +- diff --git a/lam/lib/account.inc b/lam/lib/account.inc index 3afa4f5c4..0a3ff74fe 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -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. * diff --git a/lam/lib/baseModule.inc b/lam/lib/baseModule.inc index 662042a85..6e6e47e52 100644 --- a/lam/lib/baseModule.inc +++ b/lam/lib/baseModule.inc @@ -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; } } diff --git a/lam/lib/modules.inc b/lam/lib/modules.inc index 02d6053d4..70e4b7ef4 100644 --- a/lam/lib/modules.inc +++ b/lam/lib/modules.inc @@ -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; } diff --git a/lam/lib/modules/inetOrgPerson.inc b/lam/lib/modules/inetOrgPerson.inc index 4d9e5d462..dcfdeb232 100644 --- a/lam/lib/modules/inetOrgPerson.inc +++ b/lam/lib/modules/inetOrgPerson.inc @@ -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]); diff --git a/lam/lib/modules/nisnetgroup.inc b/lam/lib/modules/nisnetgroup.inc index 66d3b45ed..4722d907b 100644 --- a/lam/lib/modules/nisnetgroup.inc +++ b/lam/lib/modules/nisnetgroup.inc @@ -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 - *
This function returns an array with 3 entries: - *
array( DN1 ('add' => array($attr), 'remove' => array($attr), 'modify' => array($attr)), DN2 .... ) - *
DN is the DN to change. It may be possible to change several DNs (e.g. create a new user and add him to some groups via attribute memberUid) - *
"add" are attributes which have to be added to LDAP entry - *
"remove" are attributes which have to be removed from LDAP entry - *
"modify" are attributes which have to been modified in LDAP entry - *
"info" are values with informational value (e.g. to be used later by pre/postModify actions) - */ - 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. * diff --git a/lam/lib/modules/posixAccount.inc b/lam/lib/modules/posixAccount.inc index d9f128979..043246e50 100644 --- a/lam/lib/modules/posixAccount.inc +++ b/lam/lib/modules/posixAccount.inc @@ -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; } diff --git a/lam/lib/modules/takUser.inc b/lam/lib/modules/takUser.inc new file mode 100644 index 000000000..ede66a210 --- /dev/null +++ b/lam/lib/modules/takUser.inc @@ -0,0 +1,327 @@ +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"), + ]; + } + +} diff --git a/lam/templates/upload/massBuildAccounts.php b/lam/templates/upload/massBuildAccounts.php index 8e437f837..71af045f7 100644 --- a/lam/templates/upload/massBuildAccounts.php +++ b/lam/templates/upload/massBuildAccounts.php @@ -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)) { diff --git a/lam/tests/lib/AccountTest.php b/lam/tests/lib/AccountTest.php index c3bcc63e4..ac1a95d5d 100644 --- a/lam/tests/lib/AccountTest.php +++ b/lam/tests/lib/AccountTest.php @@ -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'])); + } + } diff --git a/phpstan.neon b/phpstan.neon index 5e35a2ff9..9f05bfdc6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -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.#'