From c835cbe4018b9198127b5e2729e932fd54d78b1e Mon Sep 17 00:00:00 2001 From: Pflum <59760697+Pflum@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:30:58 +0200 Subject: [PATCH 01/11] Add virtual groups for roles in groups --- ctldap.js | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/ctldap.js b/ctldap.js index daa7db6..d3b9c5f 100644 --- a/ctldap.js +++ b/ctldap.js @@ -164,7 +164,12 @@ async function fetchMemberships(site) { searchParams: {"with_deleted": false} }); logDebug(site, "fetchMemberships done"); - return result['data']; + const memberships = []; + result['data'].forEach((m) => { + m.groupId = 'g' + m.groupId + memberships.push(m); + }); + return memberships; } /** @@ -195,15 +200,28 @@ async function fetchGroups(site) { const groupMap = {}; const sgmKeys = Object.keys(site.specialGroupMappings); data.forEach((g) => { + g['roles'].forEach((r) => { + // Create new Group for each role + const s = {}; + s.dn = site.compatTransform(site.fnGroupDn(g['name'] + ' ' + r['name'])); + s.id = 'r' + r.id; + s.name = g['name'] + ' ' + r['name']; + s.information = g.information; + const info = s['information']; + s.specialClasses = sgmKeys.filter((k) => info[k]) + groupMap[s['id']] = s; + }); + // Strip some irrelevant information delete g['settings']; - delete g['roles']; // Pre-compute the "distinguished name" of this group for LDAP g.dn = site.compatTransform(site.fnGroupDn(g['name'])); + g.id = 'g' + g.id; const info = g['information']; g.specialClasses = sgmKeys.filter((k) => info[k]) groupMap[g['id']] = g; }); + logTrace(site, () => `Return Group: ${JSON.stringify(groupMap)}`) return groupMap; } @@ -229,6 +247,24 @@ async function fetchAll(site) { const [personMap, groupMap, memberships, groupTypes] = await Promise.all([ fetchPersons(site), fetchGroups(site), fetchMemberships(site), fetchGroupTypes(site) ]); + + memberships.forEach((m) => { + const n = structuredClone(m); + + Object.entries(groupMap).forEach(([key, g]) => { + if (m.groupId == g.id) { + g.roles.forEach((r) => { + if (m.groupTypeRoleId == r.groupTypeRoleId) { + n.groupId = 'r' + r.id + memberships.push(n) + } + }); + } + }) + }); + + logTrace(site, () => `Return Membership: ${JSON.stringify(memberships)}`) + // Create membership mappings const g2p = {}, p2g = {}; memberships.forEach((m) => { @@ -341,7 +377,7 @@ function requestGroups(req, _res, next) { cn, displayname: g['name'], id, - nsUniqueId: `g${id}`, + nsUniqueId: `${id}`, objectClass: objectClasses, uniqueMember: (g2p[id] || []).map((pid) => personMap[pid].dn) } From 2bf908a71bfe5c395d30139a410b4f3c550ca406 Mon Sep 17 00:00:00 2001 From: Pflum <59760697+Pflum@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:30:58 +0200 Subject: [PATCH 02/11] Add config option and disable feature by default --- ctldap-config.js | 1 + ctldap.js | 59 ++++++++++++++++++++++++++---------------------- ctldap.yml | 2 ++ 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/ctldap-config.js b/ctldap-config.js index 4fd12ff..6062ab6 100644 --- a/ctldap-config.js +++ b/ctldap-config.js @@ -32,6 +32,7 @@ export class CtldapConfig { this.dnLowerCase = CtldapConfig.asOptionalBool(config.dnLowerCase); this.emailLowerCase = CtldapConfig.asOptionalBool(config.emailLowerCase); this.emailsUnique = CtldapConfig.asOptionalBool(config.emailsUnique); + this.virtualRoleGroups = CtldapConfig.asOptionalBool(config.virtualRoleGroups); this.ldapCertFilename = config.ldapCertFilename; this.ldapKeyFilename = config.ldapKeyFilename; this.ldapBaseDn = config.ldapBaseDn; diff --git a/ctldap.js b/ctldap.js index d3b9c5f..1bdde09 100644 --- a/ctldap.js +++ b/ctldap.js @@ -200,17 +200,20 @@ async function fetchGroups(site) { const groupMap = {}; const sgmKeys = Object.keys(site.specialGroupMappings); data.forEach((g) => { - g['roles'].forEach((r) => { - // Create new Group for each role - const s = {}; - s.dn = site.compatTransform(site.fnGroupDn(g['name'] + ' ' + r['name'])); - s.id = 'r' + r.id; - s.name = g['name'] + ' ' + r['name']; - s.information = g.information; - const info = s['information']; - s.specialClasses = sgmKeys.filter((k) => info[k]) - groupMap[s['id']] = s; - }); + + if (site.virtualRoleGroups || ((site.virtualRoleGroups === undefined) && config.virtualRoleGroups)) { + g['roles'].forEach((r) => { + // Create new Group for each role + const s = {}; + s.dn = site.compatTransform(site.fnGroupDn(g['name'] + ' ' + r['name'])); + s.id = 'r' + r.id; + s.name = g['name'] + ' ' + r['name']; + s.information = g.information; + const info = s['information']; + s.specialClasses = sgmKeys.filter((k) => info[k]) + groupMap[s['id']] = s; + }); + } // Strip some irrelevant information delete g['settings']; @@ -248,20 +251,22 @@ async function fetchAll(site) { fetchPersons(site), fetchGroups(site), fetchMemberships(site), fetchGroupTypes(site) ]); - memberships.forEach((m) => { - const n = structuredClone(m); - - Object.entries(groupMap).forEach(([key, g]) => { - if (m.groupId == g.id) { - g.roles.forEach((r) => { - if (m.groupTypeRoleId == r.groupTypeRoleId) { - n.groupId = 'r' + r.id - memberships.push(n) - } - }); - } - }) - }); + if (site.virtualRoleGroups || ((site.virtualRoleGroups === undefined) && config.virtualRoleGroups)) { + memberships.forEach((m) => { + const n = structuredClone(m); + + Object.entries(groupMap).forEach(([key, g]) => { + if (m.groupId == g.id) { + g.roles.forEach((r) => { + if (m.groupTypeRoleId == r.groupTypeRoleId) { + n.groupId = 'r' + r.id + memberships.push(n) + } + }); + } + }) + }); + } logTrace(site, () => `Return Membership: ${JSON.stringify(memberships)}`) @@ -376,8 +381,8 @@ function requestGroups(req, _res, next) { attributes: { cn, displayname: g['name'], - id, - nsUniqueId: `${id}`, + id: id.replace(/^g/g, ''), + nsUniqueId: id, objectClass: objectClasses, uniqueMember: (g2p[id] || []).map((pid) => personMap[pid].dn) } diff --git a/ctldap.yml b/ctldap.yml index 3bfadac..b72cb78 100644 --- a/ctldap.yml +++ b/ctldap.yml @@ -14,6 +14,8 @@ config: dnLowerCase: ${IS_DN_LOWER_CASE:true} # This is required for clients that need lowercase email addresses, e.g. Seafile emailLowerCase: ${IS_EMAIL_LOWER_CASE:true} + # Create aditional virtual groups for every role in group, e.g. for group "event a" with the roles "head" and "participant" the additional group "event a head" and "event a participant" + virtualRoleGroups: ${IS_VIRTUAL_ROLE_GROUPS:false} # LDAP admin user, can be a "virtual" root user or a ChurchTools username (virtual root is recommended!) ldapUser: ${LDAP_USER:root} From b8323c5a10a5999327390ccd96dbb755926927d1 Mon Sep 17 00:00:00 2001 From: Pflum <59760697+Pflum@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:45:53 +0200 Subject: [PATCH 03/11] Fix typo and write better comment --- ctldap.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctldap.yml b/ctldap.yml index b72cb78..3fd65cc 100644 --- a/ctldap.yml +++ b/ctldap.yml @@ -14,7 +14,7 @@ config: dnLowerCase: ${IS_DN_LOWER_CASE:true} # This is required for clients that need lowercase email addresses, e.g. Seafile emailLowerCase: ${IS_EMAIL_LOWER_CASE:true} - # Create aditional virtual groups for every role in group, e.g. for group "event a" with the roles "head" and "participant" the additional group "event a head" and "event a participant" + # Create additional virtual groups for each role within a group. For example, for the group 'Event A' with the roles 'Head' and 'Participant', create the additional groups 'Event A Head' and 'Event A Participant'. virtualRoleGroups: ${IS_VIRTUAL_ROLE_GROUPS:false} # LDAP admin user, can be a "virtual" root user or a ChurchTools username (virtual root is recommended!) From ee6bae60187f9de0b7167f0b50643acf5db1757c Mon Sep 17 00:00:00 2001 From: Pflum <59760697+Pflum@users.noreply.github.com> Date: Tue, 5 May 2026 15:48:54 +0200 Subject: [PATCH 04/11] Improve logging --- ctldap.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ctldap.js b/ctldap.js index 1bdde09..7184c75 100644 --- a/ctldap.js +++ b/ctldap.js @@ -163,7 +163,7 @@ async function fetchMemberships(site) { const result = await site.api.get('groups/members', { searchParams: {"with_deleted": false} }); - logDebug(site, "fetchMemberships done"); + logDebug(site, "fetchMemberships done, found " + result['data'].length + " memberships"); const memberships = []; result['data'].forEach((m) => { m.groupId = 'g' + m.groupId @@ -179,7 +179,7 @@ async function fetchMemberships(site) { */ async function fetchPersons(site) { const data = await fetchAllPaginated(site, 'persons', { limit: 500 }); - logDebug(site, "fetchPersons done"); + logDebug(site, "fetchPersons done, found " + data.length + " persons"); const personMap = {}; data.forEach((p) => { if (p['invitationStatus'] === "accepted") { @@ -196,7 +196,7 @@ async function fetchPersons(site) { */ async function fetchGroups(site) { const data = await fetchAllPaginated(site, 'groups', { limit: 100 }); - logDebug(site, "fetchGroups done"); + logDebug(site, "fetchGroups done, found " + data.length + " groups"); const groupMap = {}; const sgmKeys = Object.keys(site.specialGroupMappings); data.forEach((g) => { @@ -224,7 +224,6 @@ async function fetchGroups(site) { g.specialClasses = sgmKeys.filter((k) => info[k]) groupMap[g['id']] = g; }); - logTrace(site, () => `Return Group: ${JSON.stringify(groupMap)}`) return groupMap; } @@ -268,8 +267,6 @@ async function fetchAll(site) { }); } - logTrace(site, () => `Return Membership: ${JSON.stringify(memberships)}`) - // Create membership mappings const g2p = {}, p2g = {}; memberships.forEach((m) => { From 7051ffe760aae2d03d6882dd96fb41c9362b8d68 Mon Sep 17 00:00:00 2001 From: Pflum <59760697+Pflum@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:10:51 +0200 Subject: [PATCH 05/11] Add default username if no username is set in ct --- ctldap.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ctldap.js b/ctldap.js index 7184c75..cbd78bc 100644 --- a/ctldap.js +++ b/ctldap.js @@ -183,8 +183,15 @@ async function fetchPersons(site) { const personMap = {}; data.forEach((p) => { if (p['invitationStatus'] === "accepted") { - personMap[p['id']] = p; + if (p['cmsUserId'] === ''){ + p.cmsUserId = (p['firstName'] + '.' + p['lastName']).toLowerCase() + .replace('ö', 'oe') + .replace('ä', 'ae') + .replace('ü', 'ue') + .replace('ß', 'ss'); + } p.dn = site.compatTransform(site.fnUserDn(p['cmsUserId'])); + personMap[p['id']] = p; } }); return personMap; From 5475ba3a4f4fb1eeb54665552bd1d8d843fbc756 Mon Sep 17 00:00:00 2001 From: Pflum <59760697+Pflum@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:42:12 +0200 Subject: [PATCH 06/11] Add custom user attributes mapping --- ctldap-config.js | 4 +++- ctldap-site.js | 1 + ctldap.js | 8 +++++++- ctldap.yml | 6 ++++++ package.json | 1 + 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ctldap-config.js b/ctldap-config.js index 6062ab6..5d8af31 100644 --- a/ctldap-config.js +++ b/ctldap-config.js @@ -29,6 +29,7 @@ export class CtldapConfig { this.ctUri = config.ctUri; this.apiToken = config.apiToken; this.specialGroupMappings = config.specialGroupMappings || {}; + this.specialUserAttributes = config.specialUserAttributes || {}; this.dnLowerCase = CtldapConfig.asOptionalBool(config.dnLowerCase); this.emailLowerCase = CtldapConfig.asOptionalBool(config.emailLowerCase); this.emailsUnique = CtldapConfig.asOptionalBool(config.emailsUnique); @@ -45,7 +46,8 @@ export class CtldapConfig { ldapPassword: config.ldapPassword, ctUri: config.ctUri, apiToken: config.apiToken, - specialGroupMappings: config.specialGroupMappings + specialGroupMappings: config.specialGroupMappings, + specialUserAttributes: config.specialUserAttributes } } this.sites = Object.keys(sites).map((siteName) => new CtldapSite(this, siteName, sites[siteName])); diff --git a/ctldap-site.js b/ctldap-site.js index c696ed3..dc429e5 100644 --- a/ctldap-site.js +++ b/ctldap-site.js @@ -24,6 +24,7 @@ export class CtldapSite { this.ldapUser = site.ldapUser || config.ldapUser; this.ldapPassword = site.ldapPassword; this.specialGroupMappings = site.specialGroupMappings; + this.specialUserAttributes = site.specialUserAttributes; this.dnLowerCase = CtldapConfig.asOptionalBool(site.dnLowerCase); this.emailLowerCase = CtldapConfig.asOptionalBool(site.emailLowerCase); this.emailsUnique = CtldapConfig.asOptionalBool(site.emailsUnique); diff --git a/ctldap.js b/ctldap.js index cbd78bc..214862f 100644 --- a/ctldap.js +++ b/ctldap.js @@ -7,6 +7,7 @@ * @licence GNU/GPL v3.0 */ import fs from "fs"; +import jp from "jsonpath"; import ldapjs from "ldapjs"; import { CtldapConfig } from "./ctldap-config.js"; import { patchLdapjsFilters } from "./ldapjs-filter-overrides.js"; @@ -311,6 +312,10 @@ function requestUsers(req, _res, next) { let newCache = Object.entries(personMap).map(([id, p]) => { const cn = p['cmsUserId']; const email = site.compatTransformEmail(p['email']); + const extraattributes ={}; + for (const [key, value] of Object.entries(site.specialUserAttributes)) { + extraattributes[key] = jp.query(p, value); + } return { dn: p.dn, attributes: { @@ -336,7 +341,8 @@ function requestUsers(req, _res, next) { .flatMap((gid) => groupMap[gid].specialClasses) .map((key) => site.specialGroupMappings[key]['personClass']) ], - memberOf: (p2g[id] || []).map((gid) => groupMap[gid].dn) + memberOf: (p2g[id] || []).map((gid) => groupMap[gid].dn), + ...extraattributes } }; }); diff --git a/ctldap.yml b/ctldap.yml index 3fd65cc..ef01340 100644 --- a/ctldap.yml +++ b/ctldap.yml @@ -45,6 +45,12 @@ config: groupClass: NextCloudGroup personClass: NextCloudUser + # This map can be used to add special attributes to the user. They can also get dynamicly from the user + # by jsonpath (https://github.com/dchester/jsonpath#jsonpath-syntax) to the array returned by the API. + # To get an example you can access https://mysite.church.tools/api/persons?limit=5 + #specialUserAttributes: + # age: '$.age' + # To use SSL/TLS, provide file names for x509 certificate and key here # Use this command to create a private key and a certificate: # openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 diff --git a/package.json b/package.json index 25959b3..64ababd 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@node-rs/argon2": "^1.5.2", "@node-rs/bcrypt": "^1.7.3", "got": "^13.0.0", + "jsonpath": "^1.3.0", "ldap-escape": "^2.0.6", "ldapjs": "^3.0.5", "tough-cookie": "^4.1.3", From f566226f6a90484dcb0acf5ba13cd8cf9e06d4d1 Mon Sep 17 00:00:00 2001 From: Pflum <59760697+Pflum@users.noreply.github.com> Date: Tue, 5 May 2026 11:48:57 +0200 Subject: [PATCH 07/11] Fix custom user attributes mapping --- ctldap-config.js | 16 ++++++++-------- ctldap.js | 11 +++++++---- ctldap.yml | 4 +++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/ctldap-config.js b/ctldap-config.js index 5d8af31..455257f 100644 --- a/ctldap-config.js +++ b/ctldap-config.js @@ -40,14 +40,14 @@ export class CtldapConfig { // Configure sites const sites = yaml.sites || {}; // If ldapBaseDn is set, create a site from the global config properties. - if (config.ldapBaseDn) { - sites[config.ldapBaseDn] = { - ldapUser: config.ldapUser, - ldapPassword: config.ldapPassword, - ctUri: config.ctUri, - apiToken: config.apiToken, - specialGroupMappings: config.specialGroupMappings, - specialUserAttributes: config.specialUserAttributes + if (this.ldapBaseDn) { + sites[this.ldapBaseDn] = { + ldapUser: this.ldapUser, + ldapPassword: this.ldapPassword, + ctUri: this.ctUri, + apiToken: this.apiToken, + specialGroupMappings: this.specialGroupMappings, + specialUserAttributes: this.specialUserAttributes } } this.sites = Object.keys(sites).map((siteName) => new CtldapSite(this, siteName, sites[siteName])); diff --git a/ctldap.js b/ctldap.js index 214862f..21839ba 100644 --- a/ctldap.js +++ b/ctldap.js @@ -312,10 +312,13 @@ function requestUsers(req, _res, next) { let newCache = Object.entries(personMap).map(([id, p]) => { const cn = p['cmsUserId']; const email = site.compatTransformEmail(p['email']); - const extraattributes ={}; - for (const [key, value] of Object.entries(site.specialUserAttributes)) { - extraattributes[key] = jp.query(p, value); - } + const extraattributes = {}; + Object.entries(site.specialUserAttributes).forEach(([key, value]) => { + const uvalue = jp.query(p, value); + if (uvalue.length !== 0 && uvalue[0] !== null) { + extraattributes[key] = uvalue; + } + }); return { dn: p.dn, attributes: { diff --git a/ctldap.yml b/ctldap.yml index ef01340..3e78350 100644 --- a/ctldap.yml +++ b/ctldap.yml @@ -77,4 +77,6 @@ sites: # specialGroupMappings: # nextcloud: # groupClass: NextCloudGroup -# personClass: NextCloudUser \ No newline at end of file +# personClass: NextCloudUser +# specialUserAttributes: +# age: '$.age' \ No newline at end of file From 70cb1fe20fe754e529630eb1a562eb6a6ea812d8 Mon Sep 17 00:00:00 2001 From: Pflum <59760697+Pflum@users.noreply.github.com> Date: Tue, 5 May 2026 11:49:29 +0200 Subject: [PATCH 08/11] Add skip empty groups --- ctldap-config.js | 1 + ctldap.js | 8 ++++++++ ctldap.yml | 2 ++ 3 files changed, 11 insertions(+) diff --git a/ctldap-config.js b/ctldap-config.js index 455257f..7b2ac43 100644 --- a/ctldap-config.js +++ b/ctldap-config.js @@ -34,6 +34,7 @@ export class CtldapConfig { this.emailLowerCase = CtldapConfig.asOptionalBool(config.emailLowerCase); this.emailsUnique = CtldapConfig.asOptionalBool(config.emailsUnique); this.virtualRoleGroups = CtldapConfig.asOptionalBool(config.virtualRoleGroups); + this.skipEmptyGroups = CtldapConfig.asOptionalBool(config.skipEmptyGroups); this.ldapCertFilename = config.ldapCertFilename; this.ldapKeyFilename = config.ldapKeyFilename; this.ldapBaseDn = config.ldapBaseDn; diff --git a/ctldap.js b/ctldap.js index 21839ba..cd4af9c 100644 --- a/ctldap.js +++ b/ctldap.js @@ -295,6 +295,14 @@ async function fetchAll(site) { } } }); + if (site.skipEmptyGroups || ((site.skipEmptyGroups === undefined) && config.skipEmptyGroups)) { + Object.entries(groupMap).forEach(([key, g]) => { + if (!(key in g2p)) { + logDebug(site, "Removed empty group: " + g.dn); + delete groupMap[key] + } + }); + } return { groupTypes, g2p, p2g, personMap, groupMap }; }); } diff --git a/ctldap.yml b/ctldap.yml index 3e78350..6506ac9 100644 --- a/ctldap.yml +++ b/ctldap.yml @@ -16,6 +16,8 @@ config: emailLowerCase: ${IS_EMAIL_LOWER_CASE:true} # Create additional virtual groups for each role within a group. For example, for the group 'Event A' with the roles 'Head' and 'Participant', create the additional groups 'Event A Head' and 'Event A Participant'. virtualRoleGroups: ${IS_VIRTUAL_ROLE_GROUPS:false} + # Hide groups without any member. + skipEmptyGroups: ${IS_SKIP_EMPTY_GROUPS:false} # LDAP admin user, can be a "virtual" root user or a ChurchTools username (virtual root is recommended!) ldapUser: ${LDAP_USER:root} From 6291cc930189405d205b0374c36615c5741cc2f7 Mon Sep 17 00:00:00 2001 From: Pflum <59760697+Pflum@users.noreply.github.com> Date: Tue, 5 May 2026 14:06:17 +0200 Subject: [PATCH 09/11] Make invite filter configureable --- ctldap-config.js | 1 + ctldap.js | 3 ++- ctldap.yml | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ctldap-config.js b/ctldap-config.js index 7b2ac43..fb2c264 100644 --- a/ctldap-config.js +++ b/ctldap-config.js @@ -33,6 +33,7 @@ export class CtldapConfig { this.dnLowerCase = CtldapConfig.asOptionalBool(config.dnLowerCase); this.emailLowerCase = CtldapConfig.asOptionalBool(config.emailLowerCase); this.emailsUnique = CtldapConfig.asOptionalBool(config.emailsUnique); + this.filterInvitedPersons = CtldapConfig.asOptionalBool(config.filterInvitedPersons); this.virtualRoleGroups = CtldapConfig.asOptionalBool(config.virtualRoleGroups); this.skipEmptyGroups = CtldapConfig.asOptionalBool(config.skipEmptyGroups); this.ldapCertFilename = config.ldapCertFilename; diff --git a/ctldap.js b/ctldap.js index cd4af9c..f885f6c 100644 --- a/ctldap.js +++ b/ctldap.js @@ -183,7 +183,8 @@ async function fetchPersons(site) { logDebug(site, "fetchPersons done, found " + data.length + " persons"); const personMap = {}; data.forEach((p) => { - if (p['invitationStatus'] === "accepted") { + const filterInvitedPersons = (site.filterInvitedPersons || ((site.filterInvitedPersons === undefined) && config.filterInvitedPersons)); + if ((!filterInvitedPersons) || (filterInvitedPersons && p['invitationStatus'] === "accepted")) { if (p['cmsUserId'] === ''){ p.cmsUserId = (p['firstName'] + '.' + p['lastName']).toLowerCase() .replace('ö', 'oe') diff --git a/ctldap.yml b/ctldap.yml index 6506ac9..7c3167c 100644 --- a/ctldap.yml +++ b/ctldap.yml @@ -14,6 +14,8 @@ config: dnLowerCase: ${IS_DN_LOWER_CASE:true} # This is required for clients that need lowercase email addresses, e.g. Seafile emailLowerCase: ${IS_EMAIL_LOWER_CASE:true} + # Hide persons with an invitation status not accepted + filterInvitedPersons: ${IS_FILTER_INVATED_PERSONS:true} # Create additional virtual groups for each role within a group. For example, for the group 'Event A' with the roles 'Head' and 'Participant', create the additional groups 'Event A Head' and 'Event A Participant'. virtualRoleGroups: ${IS_VIRTUAL_ROLE_GROUPS:false} # Hide groups without any member. From b72236d19e903bf421c337ae296b45479fea92d5 Mon Sep 17 00:00:00 2001 From: Pflum <59760697+Pflum@users.noreply.github.com> Date: Fri, 8 May 2026 08:46:26 +0200 Subject: [PATCH 10/11] Fix typo in filterInvitedPersons variable --- ctldap.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ctldap.yml b/ctldap.yml index 7c3167c..ef2ea41 100644 --- a/ctldap.yml +++ b/ctldap.yml @@ -15,7 +15,7 @@ config: # This is required for clients that need lowercase email addresses, e.g. Seafile emailLowerCase: ${IS_EMAIL_LOWER_CASE:true} # Hide persons with an invitation status not accepted - filterInvitedPersons: ${IS_FILTER_INVATED_PERSONS:true} + filterInvitedPersons: ${IS_FILTER_INVITED_PERSONS:true} # Create additional virtual groups for each role within a group. For example, for the group 'Event A' with the roles 'Head' and 'Participant', create the additional groups 'Event A Head' and 'Event A Participant'. virtualRoleGroups: ${IS_VIRTUAL_ROLE_GROUPS:false} # Hide groups without any member. @@ -83,4 +83,4 @@ sites: # groupClass: NextCloudGroup # personClass: NextCloudUser # specialUserAttributes: -# age: '$.age' \ No newline at end of file +# age: '$.age' From be840af9c5d10964632af3adf06bfc4b7088d32f Mon Sep 17 00:00:00 2001 From: Pflum <59760697+Pflum@users.noreply.github.com> Date: Wed, 13 May 2026 09:22:10 +0200 Subject: [PATCH 11/11] Move defaults to site to apply for all sites --- ctldap-config.js | 4 ++-- ctldap-site.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ctldap-config.js b/ctldap-config.js index fb2c264..5212a7e 100644 --- a/ctldap-config.js +++ b/ctldap-config.js @@ -28,8 +28,8 @@ export class CtldapConfig { this.ldapPassword = config.ldapPassword; this.ctUri = config.ctUri; this.apiToken = config.apiToken; - this.specialGroupMappings = config.specialGroupMappings || {}; - this.specialUserAttributes = config.specialUserAttributes || {}; + this.specialGroupMappings = config.specialGroupMappings; + this.specialUserAttributes = config.specialUserAttributes; this.dnLowerCase = CtldapConfig.asOptionalBool(config.dnLowerCase); this.emailLowerCase = CtldapConfig.asOptionalBool(config.emailLowerCase); this.emailsUnique = CtldapConfig.asOptionalBool(config.emailsUnique); diff --git a/ctldap-site.js b/ctldap-site.js index dc429e5..c61e33d 100644 --- a/ctldap-site.js +++ b/ctldap-site.js @@ -23,8 +23,8 @@ export class CtldapSite { // Take ldapUser from main config if not specified for site. this.ldapUser = site.ldapUser || config.ldapUser; this.ldapPassword = site.ldapPassword; - this.specialGroupMappings = site.specialGroupMappings; - this.specialUserAttributes = site.specialUserAttributes; + this.specialGroupMappings = site.specialGroupMappings || {}; + this.specialUserAttributes = site.specialUserAttributes || {}; this.dnLowerCase = CtldapConfig.asOptionalBool(site.dnLowerCase); this.emailLowerCase = CtldapConfig.asOptionalBool(site.emailLowerCase); this.emailsUnique = CtldapConfig.asOptionalBool(site.emailsUnique);