ldap-rest 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/abstract/plugin.js +1 -1
  3. package/dist/bin/index.js +1 -1
  4. package/dist/config/args.js +1 -1
  5. package/dist/config/schema.js +1 -0
  6. package/dist/lib/expressFormatedResponses.js +1 -1
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/lib/utils.js +1 -1
  9. package/dist/plugins/auth/authzScope.js +1 -0
  10. package/dist/plugins/auth/llng.js +1 -1
  11. package/dist/plugins/ldap/accountLifecycle.js +1 -0
  12. package/dist/plugins/ldap/bulkImport.js +1 -1
  13. package/dist/plugins/ldap/departmentSync.js +1 -1
  14. package/dist/plugins/ldap/enterpriseRules.js +1 -0
  15. package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
  16. package/dist/plugins/ldap/flatGeneric.js +1 -1
  17. package/dist/plugins/ldap/groups.js +1 -1
  18. package/dist/plugins/ldap/organizations.js +1 -1
  19. package/dist/plugins/scim/groups.js +1 -1
  20. package/dist/plugins/twake/calendar.js +1 -0
  21. package/dist/plugins/twake/calendarResources.js +1 -1
  22. package/dist/plugins/twake/james.js +1 -1
  23. package/dist/src/abstract/ldapFlat.d.ts +159 -1
  24. package/dist/src/abstract/plugin.d.ts +13 -0
  25. package/dist/src/config/args.d.ts +2 -0
  26. package/dist/src/config/schema.d.ts +382 -12
  27. package/dist/src/lib/expressFormatedResponses.d.ts +14 -0
  28. package/dist/src/lib/ldapActions.d.ts +33 -6
  29. package/dist/src/lib/utils.d.ts +65 -0
  30. package/dist/src/plugins/auth/authzScope.d.ts +53 -0
  31. package/dist/src/plugins/auth/llng.d.ts +30 -1
  32. package/dist/src/plugins/ldap/accountLifecycle.d.ts +56 -0
  33. package/dist/src/plugins/ldap/departmentSync.d.ts +43 -0
  34. package/dist/src/plugins/ldap/enterpriseRules.d.ts +436 -0
  35. package/dist/src/plugins/ldap/flatGeneric.d.ts +29 -0
  36. package/dist/src/plugins/ldap/groups.d.ts +22 -0
  37. package/dist/src/plugins/ldap/organizations.d.ts +25 -4
  38. package/dist/src/plugins/twake/calendar.d.ts +113 -0
  39. package/dist/src/plugins/twake/calendarResources.d.ts +9 -58
  40. package/docs/README.md +1 -0
  41. package/docs/client-development/api/openapi.md +1 -1
  42. package/docs/client-development/browser/libraries.md +1 -1
  43. package/docs/plugin-development/dependencies.md +10 -7
  44. package/docs/presentations/en.md +1 -1
  45. package/docs/presentations/fr.md +1 -1
  46. package/docs/usage/README.md +1 -0
  47. package/docs/usage/configuration.md +35 -15
  48. package/docs/usage/directory-audit.md +105 -0
  49. package/docs/usage/plugins/README.md +6 -6
  50. package/docs/usage/plugins/auth/README.md +6 -5
  51. package/docs/usage/plugins/auth/authz-scope.md +103 -0
  52. package/docs/usage/plugins/auth/llng.md +17 -0
  53. package/docs/usage/plugins/integrations/README.md +5 -5
  54. package/docs/usage/plugins/integrations/{calendar-resources.md → calendar.md} +70 -13
  55. package/docs/usage/plugins/ldap/README.md +13 -11
  56. package/docs/usage/plugins/ldap/account-lifecycle.md +129 -0
  57. package/docs/usage/plugins/ldap/bulk-import.md +8 -1
  58. package/docs/usage/plugins/ldap/enterprise-rules.md +167 -0
  59. package/docs/usage/plugins/ldap/flat-generic.md +172 -0
  60. package/docs/usage/plugins/ldap/groups.md +10 -4
  61. package/docs/usage/plugins/ldap/organizations.md +59 -33
  62. package/docs/usage/upgrading.md +322 -0
  63. package/package.json +50 -5
  64. package/static/schemas/example/README.md +36 -0
  65. package/static/schemas/example/groups.json +94 -0
  66. package/static/schemas/example/nomenclature/domains.json +49 -0
  67. package/static/schemas/example/organizations.json +106 -0
  68. package/static/schemas/example/positions.json +35 -0
  69. package/static/schemas/example/users.json +246 -0
  70. package/static/schemas/twake/groups.json +81 -15
  71. package/static/schemas/twake/nomenclature/twakeAccountStatus.json +26 -0
  72. package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +22 -0
  73. package/static/schemas/twake/nomenclature/twakeDomain.json +60 -0
  74. package/static/schemas/twake/nomenclature/twakeListType.json +22 -0
  75. package/static/schemas/twake/nomenclature/twakeMailboxType.json +22 -0
  76. package/static/schemas/twake/nomenclature/twakeTitle.json +42 -0
  77. package/static/schemas/twake/organizations.json +92 -12
  78. package/static/schemas/twake/positions.json +22 -4
  79. package/static/schemas/twake/users.json +269 -32
@@ -1 +1 @@
1
- import t from"fs";import{wantJson as e,jsonBody as i,created as s,tryMethod as r}from"../lib/expressFormatedResponses.js";import{transformSchemas as a,asyncHandler as n,escapeRegex as o,getParentDn as h,escapeDnValue as d,validateDnValue as c,launchHooksChained as l,launchHooks as u,getCompiledRegex as f,escapeLdapFilter as m}from"../lib/utils.js";import{NotFoundError as b,BadRequestError as p,ConflictError as g}from"../lib/errors.js";import w from"./plugin.js";class LdapFlat extends w{constructor(e,i){if(super(e),this.ldap=e.ldap,this.base=i.base,this.mainAttribute=i.mainAttribute,this.objectClass=i.objectClass,this.defaultAttributes=i.defaultAttributes||{},this.singularName=i.singularName,this.pluralName=i.pluralName,this.hookPrefix=i.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(a(e,this.config)),this.logger.info(`${this.singularName} schema loaded from ${i.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${i.schemaPath}: ${t}`)}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,n(async(t,i)=>{if(!e(t,i))return;const s={};if(t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute){if(!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(t.query.attribute))throw new p("Invalid LDAP attribute name");const e=m(t.query.match);s.filter=`(${t.query.attribute}=*${e}*)`}t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(","));const r=await this.listEntries(s);i.json(r)})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,n(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,n(async(t,e)=>this.apiMove(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const t=this.resolveDn(s),e=await this.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new b(`${this.singularName} not found`);i.json(e.searchEntries[0])}catch(t){if(t.code&&32===t.code)throw new b(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const r=i(t,e,this.mainAttribute);if(!r)return;const a=r[this.mainAttribute],n={...r};delete n[this.mainAttribute],delete n.dn,await this.addEntry(a,n,t);const o=await this.searchEntriesByName(a,!1);return s(e,o[a])}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await r(i,this.deleteEntry.bind(this),s)}async apiModify(t,e){const s=i(t,e);if(!s)return;const a=decodeURIComponent(t.params.id);await r(e,this.modifyEntry.bind(this),a,s)}async apiMove(t,s){if(!e(t,s))return;const r=i(t,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(t.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new p("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,t);s.json({success:!0,...o})}resolveDn(t){if(new RegExp(`^${o(this.mainAttribute)}=`,"i").test(t)){const e=h(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new p(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${d(t)},${this.base}`}async addEntry(t,e={},i){let s;new RegExp(`^${this.mainAttribute}=`,"i").test(t)?(s=this.resolveDn(t),t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(c(t,this.mainAttribute),s=this.resolveDn(t)),await this.validateNewEntry(s,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let r,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,a]=await l(this.registeredHooks[`${this.hookPrefix}add`],[s,a]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{r=await this.ldap.add(s,a,i)}catch(t){throw this.logger.error("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return u(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e){let i=this.resolveDn(t);const s=this.opNumber();if([i,e]=await l(this.registeredHooks[`${this.hookPrefix}modify`],[i,e,s]),e.add&&e.add[this.mainAttribute])throw new g(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new p(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new p(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new p(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(i,e);const r=await this.ldap.modify(i,e);return u(this.registeredHooks[`${this.hookPrefix}modifydone`],[i,e,s]),r}async renameEntry(t,e){/,/.test(t)||c(t,this.mainAttribute),/,/.test(e)||c(e,this.mainAttribute);let i=this.resolveDn(t),s=this.resolveDn(e);[i,s]=await l(this.registeredHooks[`${this.hookPrefix}rename`],[i,s]);const r=await this.ldap.rename(i,s);return u(this.registeredHooks[`${this.hookPrefix}renamedone`],[i,s]),r}async deleteEntry(t){let e=this.resolveDn(t);e=await l(this.registeredHooks[`${this.hookPrefix}delete`],e);const i=await this.ldap.delete(e);return u(this.registeredHooks[`${this.hookPrefix}deletedone`],e),i}async moveEntry(t,e,i){const s=this.resolveDn(t),r=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[r]||!this.schema?.attributes[a])throw new p(`Schema for ${this.singularName} does not support move operation (missing ${r} or ${a})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[r]},s,i);if(!n.searchEntries||0===n.searchEntries.length)throw new b(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await l(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const s=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!s.searchEntries||0===s.searchEntries.length)throw new b(`Organization ${t} not found`);const r=s.searchEntries[0];if(r[i]){const t=r[i];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e}){const i={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(i.attributes=e);const s=await this.ldap.search(i,this.base),r={};for await(const t of s)t.searchEntries.forEach(t=>{if(t[this.mainAttribute]){const e=t[this.mainAttribute];let i;i=Array.isArray(e)?"string"==typeof e[0]?e[0]:String(e[0]):"string"==typeof e?e:String(e),r[i]=t}});return r}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=e?`(${this.mainAttribute}=*${t}*)`:`(${this.mainAttribute}=${t})`;return await this.listEntries({filter:s,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new p(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const s=JSON.stringify(e.default);if(s!==JSON.stringify(i))throw new p(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new p(`Invalid value for attribute "${t}"`);if(e.required&&!i)throw new p(`Attribute "${t}" is required`)}for(const[t,i]of Object.entries(this.schema.attributes))if(i.required&&!e[t])throw new p(`Attribute "${t}" is required`);return!0}async validateChanges(t,e){if(!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new p(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new p(`Invalid value for attribute "${t}"`);if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new p(`Invalid value for attribute "${t}"`);if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new p(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if("pointer"===i.type){if("string"!=typeof e)throw new p(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>f(`,?${o(t)}$`,"i").test(s)))throw new p(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new p(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new p(`Field ${t} points to invalid or non-existent DN: ${s}`)}}if(i.test){const t="string"==typeof i.test?f(i.test):i.test;return Array.isArray(e)?e.every(e=>t.test(e)):t.test(e)}return!0}}export{LdapFlat as default};
1
+ import t from"fs";import{wantJson as e,jsonBody as i,created as s,tryMethod as r,multiStatus as a}from"../lib/expressFormatedResponses.js";import{transformSchemas as n,getCompiledRegex as o,escapeDnValue as h,asyncHandler as d,escapeRegex as c,validateDnValue as u,isDummyMemberDn as l,normalizeDn as f,getParentDn as m,launchHooksChained as b,launchHooks as p,escapeLdapFilter as g,isDnInBranch as w,substringSearchFilter as y}from"../lib/utils.js";import{neverReturnAttributes as $,schemaAttribute as A,modifiedAttributeNames as v,missingRequiredAttribute as E,checkDnValues as x}from"../config/schema.js";import{BadRequestError as N,ConflictError as j,NotFoundError as k,HttpError as D}from"../lib/errors.js";import O from"./plugin.js";class LdapFlat extends O{constructor(e,i){if(super(e),this.ldap=e.ldap,this.base=i.base,this.mainAttribute=i.mainAttribute,this.objectClass=i.objectClass,this.defaultAttributes=i.defaultAttributes||{},this.singularName=i.singularName,this.pluralName=i.pluralName,this.hookPrefix=i.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(n(e,this.config)),this.logger.info(`${this.singularName} schema loaded from ${i.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${i.schemaPath}: ${t}`)}}clientForbiddenAttributes(){const t=new Set;if(!this.schema)return t;for(const[e,i]of Object.entries(this.schema.attributes))(e!==this.mainAttribute||i.generatedFrom)&&(i.generated||i.readOnly)&&t.add(e.toLowerCase());return t}hiddenAttributes(){return $([this.schema])}project(t){const e=this.hiddenAttributes();if(0===e.size)return t;const i={};for(const[s,r]of Object.entries(t))e.has(s.split(";")[0].toLowerCase())||(i[s]=r);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,s]of Object.entries(t))e[i]=this.project(s);return e}rejectForbiddenInput(t){const e=this.clientForbiddenAttributes();if(0!==e.size)for(const i of t){if("dn"===i)continue;if(!e.has(i.split(";")[0].toLowerCase()))continue;const t=A(this.schema,i)?.[1];throw new N(t?.readOnly?`Attribute "${i}" is read-only and cannot be set`:`Attribute "${i}" is computed by the server and cannot be set`)}}async generateMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const s=t[i.attribute],r=Array.isArray(s)?s[0]:s;if(null==r||""===r)throw new N(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let a=String(r);if(i.extract){const t=o(i.extract).exec(a);t&&(a=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(a=a.toLowerCase()),i.strip&&(a=a.replace(o(i.strip,"g"),"")),!a)throw new N(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);if(!await this.rdnExists(a))return a;if("suffix"!==i.onCollision)throw new j(`${this.singularName} "${a}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const e=`${a}-${t}`;if(!await this.rdnExists(e))return e}throw new j(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}async rdnExists(t){try{const e=await this.ldap.search({paged:!1,scope:"base"},`${this.mainAttribute}=${h(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}invalidValueMessage(t){const e=this.schema?.attributes[t],i=e?.hint||e?.items?.hint;return i?`Invalid value for attribute "${t}": ${i}`:`Invalid value for attribute "${t}"`}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,d(async(t,i)=>{if(!e(t,i))return;const s={};t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(s.filter=y(t.query.match,t.query.attribute)),t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(","));const r=await this.listEntries(s);i.json(this.projectList(r))})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,d(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,d(async(t,e)=>this.apiMove(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,d(async(t,e)=>this.apiRename(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const t=this.resolveDn(s),e=await this.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new k(`${this.singularName} not found`);i.json(this.project(e.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new k(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const r=this.schema?.attributes[this.mainAttribute],a=Boolean(r?.generated&&r.generatedFrom),n=a?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o=a?await this.generateMainAttribute(n):n[this.mainAttribute],h={...n};delete h[this.mainAttribute],delete h.dn,await this.addEntry(o,h,t);const d=await this.searchEntriesByName(o,!1);return s(e,this.project(d[o]))}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await r(i,this.deleteEntry.bind(this),s)}async apiModify(t,e){const s=i(t,e);if(!s)return;this.rejectForbiddenInput(v(s));const a=decodeURIComponent(t.params.id);await r(e,this.modifyEntry.bind(this),a,s)}async apiMove(t,s){if(!e(t,s))return;const r=i(t,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(t.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new N("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,t);s.json({success:!0,...o})}async apiRename(t,s){if(!e(t,s))return;const r=i(t,s,"newId");if(!r)return;const n=r.newId;if("string"!=typeof n)throw new N("Missing or invalid newId in request body");if(this.rejectForbiddenInput(Object.keys(r).filter(t=>"newId"!==t)),new RegExp(`^${c(this.mainAttribute)}=`,"i").test(n))throw new N(`newId must be a ${this.mainAttribute} value, not a DN`);try{u(n,this.mainAttribute)}catch(t){throw new N(t instanceof Error?t.message:"Invalid newId")}if(!await this._validateOneChange(this.mainAttribute,n))throw new N(this.invalidValueMessage(this.mainAttribute));const o=decodeURIComponent(t.params.id),h=this.resolveDn(o),d=this.resolveDn(n),m=this.config.group_dummy_user;if(m&&(l(h,m)||l(d,m)))throw new j(`${m} is the configured placeholder member and cannot be renamed`);const b=f(h)===f(d),p=await this.entryExists(h),g=b?p:await this.entryExists(d),w=this.requirePlugin("ldapEnterpriseRules");let y=[];if(b){if(!p)throw new k(`${this.singularName} not found`)}else if(p){if(g)throw new j(`${this.singularName} "${n}" already exists`);y=await(w?.findReferences(h))||[],await this.renameEntry(h,d,t)}else{if(!g)throw new k(`${this.singularName} not found`);y=await(w?.findReferences(h))||[]}const $=w&&y.length>0?await w.rewriteReferences(y,h,d,t):{updated:0,failed:[]},A={success:0===$.failed.length,dn:d,referencesUpdated:$.updated};0!==$.failed.length?a(s,{...A,referencesFailed:$.failed}):s.json(A)}async entryExists(t){try{const e=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(new RegExp(`^${c(this.mainAttribute)}=`,"i").test(t)){const e=m(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new N(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${h(t)},${this.base}`}async addEntry(t,e={},i){let s;new RegExp(`^${this.mainAttribute}=`,"i").test(t)?(s=this.resolveDn(t),t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(u(t,this.mainAttribute),s=this.resolveDn(t)),await this.validateNewEntry(s,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let r,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,a]=await b(this.registeredHooks[`${this.hookPrefix}add`],[s,a]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{r=await this.ldap.add(s,a,i)}catch(t){const e=t instanceof D&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),t instanceof D)throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return p(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e){let i=this.resolveDn(t);const s=this.opNumber();if([i,e]=await b(this.registeredHooks[`${this.hookPrefix}modify`],[i,e,s]),e.add&&e.add[this.mainAttribute])throw new j(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new N(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new N(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new N(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(i,e);const r=await this.ldap.modify(i,e);return p(this.registeredHooks[`${this.hookPrefix}modifydone`],[i,e,s]),r}async renameEntry(t,e,i){/,/.test(t)||u(t,this.mainAttribute),/,/.test(e)||u(e,this.mainAttribute);let s=this.resolveDn(t),r=this.resolveDn(e);[s,r]=await b(this.registeredHooks[`${this.hookPrefix}rename`],[s,r]);const a=await(i?this.ldap.forRequest(i).rename(s,r):this.ldap.rename(s,r));return p(this.registeredHooks[`${this.hookPrefix}renamedone`],[s,r]),a}async deleteEntry(t){let e=this.resolveDn(t);e=await b(this.registeredHooks[`${this.hookPrefix}delete`],e);const i=await this.ldap.delete(e);return p(this.registeredHooks[`${this.hookPrefix}deletedone`],e),i}async moveEntry(t,e,i){const s=this.resolveDn(t),r=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[r]||!this.schema?.attributes[a])throw new N(`Schema for ${this.singularName} does not support move operation (missing ${r} or ${a})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[r]},s,i);if(!n.searchEntries||0===n.searchEntries.length)throw new k(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await b(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const s=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!s.searchEntries||0===s.searchEntries.length)throw new k(`Organization ${t} not found`);const r=s.searchEntries[0];if(r[i]){const t=r[i];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){if(e instanceof k)throw e;if(32===e.code)throw new k(`Organization ${t} not found`);throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e}){const i={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(i.attributes=e);const s=await this.ldap.search(i,this.base),r={};for await(const t of s)t.searchEntries.forEach(t=>{if(t[this.mainAttribute]){const e=t[this.mainAttribute];let i;i=Array.isArray(e)?"string"==typeof e[0]?e[0]:String(e[0]):"string"==typeof e?e:String(e),r[i]=t}});return r}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=g(t),r=e?`(${this.mainAttribute}=*${s}*)`:`(${this.mainAttribute}=${s})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new N(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const s=JSON.stringify(e.default);if(s!==JSON.stringify(i))throw new N(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.required&&!i&&!e.generated)throw new N(`Attribute "${t}" is required`)}const i=E(this.schema,e,this.server.loadedPlugins);if(i)throw new N(`Attribute "${i}" is required`);return!0}async validateChanges(t,e){if(!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new N(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new N(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if("pointer"===i.type){if("string"!=typeof e)throw new N(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>w(s,t)))throw new N(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new N(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new N(`Field ${t} points to invalid or non-existent DN: ${s}`)}}"pointer"!==i.type&&await x(t,i,e,async t=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0);const s=i.test??i.items?.test;if(s){const t="string"==typeof s?o(s):s;return function(t){return(Array.isArray(t)?t:[t]).map(t=>String(t))}(e).every(e=>t.test(e))}return!0}}export{LdapFlat as default};
@@ -1 +1 @@
1
- export{asyncHandler,escapeDnValue,escapeLdapFilter,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DmPlugin{constructor(r){this.registeredHooks={},this.server=r,this.config=r.config,this.logger=r.logger,this.registeredHooks=r.hooks}opNumber(){return this.server.operationSequence++}requirePlugin(r){const e=this.server.loadedPlugins[r];return e||(this.missingPluginsWarned||(this.missingPluginsWarned=new Set),this.missingPluginsWarned.has(r)||(this.missingPluginsWarned.add(r),this.logger.warn(`${this.name}: required plugin '${r}' is not loaded — its features will be skipped`)),null)}}export{DmPlugin as default};
1
+ import{neverReturnAttributes as r,withoutAttributes as e}from"../config/schema.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DmPlugin{constructor(r){this.registeredHooks={},this.server=r,this.config=r.config,this.logger=r.logger,this.registeredHooks=r.hooks}opNumber(){return this.server.operationSequence++}hideNeverReturn(s){const i=[],isSchema=r=>"object"==typeof r?.attributes;for(const r of Object.values(this.server.loadedPlugins)){const e=r;if(isSchema(e.schema)&&i.push(e.schema),Array.isArray(e.instances))for(const r of e.instances)isSchema(r.schema)&&i.push(r.schema)}const o=r(i);if(0===o.size||!s||"object"!=typeof s)return s;const project=r=>r&&"object"==typeof r&&!Array.isArray(r)?e(r,o):r;return Array.isArray(s)?s.map(project):"dn"in s?e(s,o):Object.fromEntries(Object.entries(s).map(([r,e])=>[r,project(e)]))}requirePlugin(r){const e=this.server.loadedPlugins[r];return e||(this.missingPluginsWarned||(this.missingPluginsWarned=new Set),this.missingPluginsWarned.has(r)||(this.missingPluginsWarned.add(r),this.logger.warn(`${this.name}: required plugin '${r}' is not loaded — its features will be skipped`)),null)}}export{DmPlugin as default};
package/dist/bin/index.js CHANGED
@@ -1 +1 @@
1
- import{fileURLToPath as e}from"url";import{join as t,dirname as r}from"path";import i from"express";import s from"body-parser";import{parseConfig as o}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as h}from"../logger/winston.js";import{setLogger as l}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as u,claimedPrefixes as d}from"../lib/auth/base.js";import g from"../plugins/priority.json.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,launchHooks,launchHooksChained,normalizeDn,parseDn,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this.authenticators=[],this._authState={dispatcherMounted:!1},this._errorMiddlewareSetup=!1,this.config=o(n),this.app=i(),this.app.use(s.json()),this.app.use(s.urlencoded({extended:!0})),this.logger=h(this.config),this.ldap=new a(this),l(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const t=this.config.plugin.find(e=>e.includes("configApi"));let r=this.config.plugin.filter(e=>!e.includes("configApi"));const i=(async()=>{for(const e of g)r.includes(e)&&(r=r.filter(t=>t!==e),await this.loadPlugin(e))})();e.push(i),e.push(i.then(async()=>{const e=r.map(e=>this.loadPlugin(e));await Promise.all(e)})),t&&e.push(Promise.all(e).then(()=>this.loadPlugin(t)))}this.ready=new Promise((t,r)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),t()}).catch(e=>r(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),t())})}routerStack(){const e=this.app;return(e.router??e._router)?.stack}withConfig(e){return Object.assign(Object.create(Object.getPrototypeOf(this)),this,{config:e})}mountAuthDispatcher(e){const t=e.roles||[];t.includes("protect")||t.includes("logging")||this.mountAuthDispatcherNow()}mountAuthDispatcherNow(){this._authState.dispatcherMounted||(this._authState.dispatcherMounted=!0,this.app.use((e,t,r)=>{this.dispatchAuth(e,t,r)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}dispatchAuth(e,t,r){let i=-1,s=[];for(const t of this.authenticators)for(const r of t.pathPrefixes)u(r,e.path)&&(r.length>i?(i=r.length,s=[t]):r.length!==i||s.includes(t)||s.push(t));if(i<0&&(s=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===s.length)return r();const run=i=>{if(i>=s.length)return r();s[i].authenticate(e,t,()=>run(i+1))};run(0)}claimedAuthPrefixes(e){return d(this.loadedPlugins,e)}warnUnauthenticatedRoutes(){const e=Object.values(this.loadedPlugins).filter(e=>e.roles?.includes("auth"));if(0===e.length)return[];const t=[];for(const r of e){if(!r.pathPrefixes||0===r.pathPrefixes.length)return[];t.push(...r.pathPrefixes)}const r=this.routerStack(),i=new Set;for(const e of r||[]){const t=e.route?.path;"string"==typeof t?i.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&i.add(e))}const s=[...i].filter(e=>!t.some(t=>e===t||e.startsWith(`${t}/`)));return s.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication: ${s.sort().join(", ")}`),s.sort()}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.routerStack();if(e){const t=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==t&&e.splice(t,1)}}this._errorMiddleware=(e,t,r,i)=>{let s="statusCode"in e?e.statusCode:500,o=e.message;if(500===s&&/\[authz-forbidden\]/.test(e.message)?(s=403,o="Token does not have permission on this branch"):403===s&&/\[authz-forbidden\]/.test(e.message)&&(o="Token does not have permission on this branch"),s>=400&&s<500)return this.logger.warn(`Client error ${s} in request ${t.method} ${t.path}: ${o}`),r.headersSent?void 0:r.status(s).json({error:o});this.logger.error(`Server error ${s} in request ${t.method} ${t.path}: ${e.message}`,{stack:e.stack,url:t.url,method:t.method}),r.headersSent||r.status(s).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,t)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:t})}),new Promise((e,t)=>{this.server=this.app.listen(this.config.port,r=>{r?(this.logger.error(`Error starting server: ${r}`),t(r)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(i){let s,o;if(/:/.test(i)){let e=i.substring(i.indexOf(":")+1);if(i=i.substring(0,i.indexOf(":")),/:/.test(e)){s=e.substring(0,e.indexOf(":")),s||(s=void 0),e=e.substring(e.indexOf(":")+1);try{o=JSON.parse(e),"object"!=typeof o?(this.logger.error(`Overrides for plugin ${i} are not valid: ${e}`),o=void 0):this.logger.debug(`Overrides for plugin ${s||i}: ${e}`)}catch(t){this.logger.error(`Failed to parse overrides for plugin ${i}: ${t}, using ${e}`),o=void 0}}else s=e,s||(s=void 0)}else s=void 0;return this.logger.debug(`Loading plugin ${i}`),i.startsWith("core/")&&(i=i.replace("core/",t(r(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((e,t)=>{import(i).then(async r=>{let n;if(r&&r.default&&(r=r.default),o){const e={...this.config,...o};n=new r(this.withConfig(e))}else n=new r(this);if(!n)return t(new Error(`Unable to load ${i}`));e(await this.registerPlugin(i,n,s)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(e=>t(new Error(`Failed to load plugin ${i}: ${e}`)))})}async registerPlugin(e,t,r){if(t.name||(t.name=e),r&&(t.name=r),this.loadedPlugins[t.name])return this.logger.warn(`Plugin ${e} not registered: the name "${t.name}" is already taken. This instance is dropped and its routes will not exist. To load the same plugin twice, give each one a name: --plugin '${e}:${t.name}2:{…}'`),!1;if(this.logger.debug(`Registering plugin ${e} as ${t.name}`),t.dependencies)for(const e in t.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${t.name} depends on ${e}, loading it first`),await this.loadPlugin(t.dependencies[e]));if(t.api&&(this.mountAuthDispatcher(t),this.logger.debug(`Plugin ${t.name} has API, registering it`),await t.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),t.hooks)for(const e in t.hooks){this.logger.debug(`Plugin ${t.name} has hook ${e}, registering it`);const r=t.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!r||"function"!=typeof r)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);this.hooks[e].push(r)}return this.loadedPlugins[t.name]=t,!0}}export{DM};
1
+ import{fileURLToPath as e}from"url";import{join as t,dirname as r}from"path";import i from"express";import s from"body-parser";import{parseConfig as o}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as h}from"../logger/winston.js";import{setLogger as l}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as u,claimedPrefixes as d}from"../lib/auth/base.js";import g from"../plugins/priority.json.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,isDummyMemberDn,launchHooks,launchHooksChained,normalizeDn,parseDn,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this.authenticators=[],this._authState={dispatcherMounted:!1},this._errorMiddlewareSetup=!1,this.config=o(n),this.app=i(),this.app.use(s.json()),this.app.use(s.urlencoded({extended:!0})),this.logger=h(this.config),this.ldap=new a(this),l(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const t=this.config.plugin.find(e=>e.includes("configApi"));let r=this.config.plugin.filter(e=>!e.includes("configApi"));const i=(async()=>{for(const e of g)r.includes(e)&&(r=r.filter(t=>t!==e),await this.loadPlugin(e))})();e.push(i),e.push(i.then(async()=>{const e=r.map(e=>this.loadPlugin(e));await Promise.all(e)})),t&&e.push(Promise.all(e).then(()=>this.loadPlugin(t)))}this.ready=new Promise((t,r)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),t()}).catch(e=>r(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),t())})}routerStack(){const e=this.app;return(e.router??e._router)?.stack}withConfig(e){return Object.assign(Object.create(Object.getPrototypeOf(this)),this,{config:e})}mountAuthDispatcher(e){const t=e.roles||[];t.includes("protect")||t.includes("logging")||this.mountAuthDispatcherNow()}mountAuthDispatcherNow(){this._authState.dispatcherMounted||(this._authState.dispatcherMounted=!0,this.app.use((e,t,r)=>{this.dispatchAuth(e,t,r)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}dispatchAuth(e,t,r){let i=-1,s=[];for(const t of this.authenticators)for(const r of t.pathPrefixes)u(r,e.path)&&(r.length>i?(i=r.length,s=[t]):r.length!==i||s.includes(t)||s.push(t));if(i<0&&(s=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===s.length)return r();const run=i=>{if(i>=s.length)return r();s[i].authenticate(e,t,()=>run(i+1))};run(0)}claimedAuthPrefixes(e){return d(this.loadedPlugins,e)}warnUnauthenticatedRoutes(){const e=Object.values(this.loadedPlugins).filter(e=>e.roles?.includes("auth"));if(0===e.length)return[];const t=[];for(const r of e){if(!r.pathPrefixes||0===r.pathPrefixes.length)return[];t.push(...r.pathPrefixes)}const r=this.routerStack(),i=new Set;for(const e of r||[]){const t=e.route?.path;"string"==typeof t?i.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&i.add(e))}const s=[...i].filter(e=>!t.some(t=>e===t||e.startsWith(`${t}/`)));return s.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication: ${s.sort().join(", ")}`),s.sort()}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.routerStack();if(e){const t=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==t&&e.splice(t,1)}}this._errorMiddleware=(e,t,r,i)=>{let s="statusCode"in e?e.statusCode:500,o=e.message;if(500===s&&/\[authz-forbidden\]/.test(e.message)?(s=403,o="Token does not have permission on this branch"):403===s&&/\[authz-forbidden\]/.test(e.message)&&(o="Token does not have permission on this branch"),s>=400&&s<500)return this.logger.warn(`Client error ${s} in request ${t.method} ${t.path}: ${o}`),r.headersSent?void 0:r.status(s).json({error:o});this.logger.error(`Server error ${s} in request ${t.method} ${t.path}: ${e.message}`,{stack:e.stack,url:t.url,method:t.method}),r.headersSent||r.status(s).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,t)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:t})}),new Promise((e,t)=>{this.server=this.app.listen(this.config.port,r=>{r?(this.logger.error(`Error starting server: ${r}`),t(r)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(i){let s,o;if(/:/.test(i)){let e=i.substring(i.indexOf(":")+1);if(i=i.substring(0,i.indexOf(":")),/:/.test(e)){s=e.substring(0,e.indexOf(":")),s||(s=void 0),e=e.substring(e.indexOf(":")+1);try{o=JSON.parse(e),"object"!=typeof o?(this.logger.error(`Overrides for plugin ${i} are not valid: ${e}`),o=void 0):this.logger.debug(`Overrides for plugin ${s||i}: ${e}`)}catch(t){this.logger.error(`Failed to parse overrides for plugin ${i}: ${t}, using ${e}`),o=void 0}}else s=e,s||(s=void 0)}else s=void 0;return this.logger.debug(`Loading plugin ${i}`),i.startsWith("core/")&&(i=i.replace("core/",t(r(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((e,t)=>{import(i).then(async r=>{let n;if(r&&r.default&&(r=r.default),o){const e={...this.config,...o};n=new r(this.withConfig(e))}else n=new r(this);if(!n)return t(new Error(`Unable to load ${i}`));e(await this.registerPlugin(i,n,s)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(e=>t(new Error(`Failed to load plugin ${i}: ${e}`)))})}async registerPlugin(e,t,r){if(t.name||(t.name=e),r&&(t.name=r),this.loadedPlugins[t.name])return this.logger.warn(`Plugin ${e} not registered: the name "${t.name}" is already taken. This instance is dropped and its routes will not exist. To load the same plugin twice, give each one a name: --plugin '${e}:${t.name}2:{…}'`),!1;if(this.logger.debug(`Registering plugin ${e} as ${t.name}`),t.dependencies)for(const e in t.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${t.name} depends on ${e}, loading it first`),await this.loadPlugin(t.dependencies[e]));if(t.api&&(this.mountAuthDispatcher(t),this.logger.debug(`Plugin ${t.name} has API, registering it`),await t.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),t.hooks)for(const e in t.hooks){this.logger.debug(`Plugin ${t.name} has hook ${e}, registering it`);const r=t.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!r||"function"!=typeof r)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);this.hooks[e].push(r)}return this.loadedPlugins[t.name]=t,!0}}export{DM};
@@ -1 +1 @@
1
- import{fileURLToPath as _}from"url";import{join as a,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",a(e(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",a(e(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",a(e(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
1
+ import{fileURLToPath as _}from"url";import{join as a,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",a(e(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",a(e(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",a(e(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
@@ -0,0 +1 @@
1
+ import{BadRequestError as t}from"../lib/errors.js";import{isDnInBranch as e,getCompiledRegex as r}from"../lib/utils.js";function hasRole(t,e){return!!t?.role&&(Array.isArray(t.role)?t.role.includes(e):t.role===e)}function roleAttribute(t,e){if(t)for(const[r,n]of Object.entries(t.attributes))if(hasRole(n,e))return r}function schemaAttribute(t,e){if(!t)return;const r=e.split(";")[0],n=t.attributes[r];if(n)return[r,n];const i=r.toLowerCase();for(const[e,r]of Object.entries(t.attributes))if(e.toLowerCase()===i)return[e,r]}function modifiedAttributeNames(t){return[...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete.map(String):Object.keys(t.delete||{})]}function neverReturnAttributes(t){const e=new Set;for(const r of t)if(r)for(const[t,n]of Object.entries(r.attributes))n.neverReturn&&e.add(t.toLowerCase());return e}function withoutAttributes(t,e){if(0===e.size)return t;const r={};for(const[n,i]of Object.entries(t))e.has(n.split(";")[0].toLowerCase())||(r[n]=i);return r}async function checkDnValues(r,n,i,o){const s="pointer"===n.type,a=s?n.branch:n.items?.branch,u=s||"pointer"===n.items?.type;if(!u&&!a?.length)return;if(s&&"string"!=typeof i)throw new t(`Field ${r} must be a string (DN pointer)`);const c=(Array.isArray(i)?i:[i]).map(t=>String(t));for(const n of c){if(a?.length&&!a.some(t=>e(n,t)))throw new t(`Field ${r} must point to a DN within allowed branches: ${a.join(", ")}`);let i=!1;if(u){try{i=await o(n)}catch{i=!1}if(!i)throw new t(`Field ${r} points to invalid or non-existent DN: ${n}`)}}}function matchesPattern(t,e){const n=t.test??t.items?.test;if(!n)return!0;const i="string"==typeof n?r(n):n;return(Array.isArray(e)?e:[e]).every(t=>i.test(String(t)))}function assertClientMaySet(e,r,n){if(e)for(const i of r){if("dn"===i)continue;const r=schemaAttribute(e,i);if(!r)continue;const[o,s]=r;if((!n||o.toLowerCase()!==n.toLowerCase()||s.generatedFrom)&&(s.generated||s.readOnly))throw new t(s.readOnly?`Attribute "${i}" is read-only and cannot be set`:`Attribute "${i}" is computed by the server and cannot be set`)}}function missingRequiredAttribute(t,e,r){if(!t)return;const filledByAPlugin=(e,n)=>Object.values(r).some(r=>{const i=r.fillsGeneratedAttribute;return"function"==typeof i&&i.call(r,e,n,t)});for(const[r,n]of Object.entries(t.attributes))if(n.required&&!e[r]&&(!n.generated||!n.generatedFrom&&!filledByAPlugin(r,n)))return r}export{assertClientMaySet,checkDnValues,hasRole,matchesPattern,missingRequiredAttribute,modifiedAttributeNames,neverReturnAttributes,roleAttribute,schemaAttribute,withoutAttributes};
@@ -1 +1 @@
1
- let r;const setLogger=t=>{r=t},getLogger=()=>r,tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(t,e,n)=>{r?.warn(`Client error ${t}: ${n}`),e.status(t).json({error:n})},ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),forbidden=(r,t="Forbidden")=>_rejectResponse(403,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(t,e)=>{const n=e instanceof Error?e.message:String(e);let o=e instanceof Error&&"statusCode"in e?e.statusCode:500;if(500===o&&/\[authz-forbidden\]/.test(n)&&(o=403),o>=400&&o<500){const e=403===o&&/\[authz-forbidden\]/.test(n)?"Token does not have permission on this branch":n;return r.warn(`Client error ${o}: ${e}`),void t.status(o).json({error:e})}e instanceof Error?r.error(e.message,e):"string"==typeof e?r.error(e):r.error("Server error",e),t.status(o).json({error:"check logs"})},wantJson=(r,t)=>!1!==r.accepts("json")||(badRequest(t),!1),jsonBody=(r,t,...e)=>{try{if(!wantJson(r,t))return!1;const n=r.body;if(e.length>0)for(let r=0;r<e.length;r++)if(null==n[e[r]])return badRequest(t,"Bad content"),!1;return n}catch(r){return badRequest(t,"Bad content"),!1}};export{badRequest,created,forbidden,getLogger,jsonBody,notFound,ok,serverError,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
1
+ let r;const setLogger=t=>{r=t},getLogger=()=>r,tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(t,e,n)=>{r?.warn(`Client error ${t}: ${n}`),e.status(t).json({error:n})},ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},multiStatus=(r,t)=>{r.status(207).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),forbidden=(r,t="Forbidden")=>_rejectResponse(403,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(t,e)=>{const n=e instanceof Error?e.message:String(e);let s=e instanceof Error&&"statusCode"in e?e.statusCode:500;if(500===s&&/\[authz-forbidden\]/.test(n)&&(s=403),s>=400&&s<500){const e=403===s&&/\[authz-forbidden\]/.test(n)?"Token does not have permission on this branch":n;return r.warn(`Client error ${s}: ${e}`),void t.status(s).json({error:e})}e instanceof Error?r.error(e.message,e):"string"==typeof e?r.error(e):r.error("Server error",e),t.status(s).json({error:"check logs"})},wantJson=(r,t)=>!1!==r.accepts("json")||(badRequest(t),!1),jsonBody=(r,t,...e)=>{try{if(!wantJson(r,t))return!1;const n=r.body;if(e.length>0)for(let r=0;r<e.length;r++)if(null==n[e[r]])return badRequest(t,"Bad content"),!1;return n}catch(r){return badRequest(t,"Bad content"),!1}};export{badRequest,created,forbidden,getLogger,jsonBody,multiStatus,notFound,ok,serverError,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
@@ -1 +1 @@
1
- import{Client as e,Attribute as t,Change as i}from"ldapts";import{LRUCache as n}from"lru-cache";import o from"p-limit";import{launchHooksChained as s,launchHooks as r,escapeDnValue as a}from"./utils.js";const c={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),n=t?.code;return"number"==typeof n&&(i.code=n),i}class ldapActions{constructor(e){this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=o(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=1e3*(this.config.ldap_cache_ttl||300);if(this.searchCache=new n({max:i,ttl:s,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(`LDAP search cache initialized: max=${i}, ttl=${s/1e3}s`),this.attrSignatureCache=new n({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");e.config.ldap_base?this.base=e.config.ldap_base:(this.base=e.config.ldap_dn.split(",",2)[1],this.logger.warn(`LDAP base is not defined, using "${this.base}"`)),this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const t=[];for(let i=0;i<this.ldapUrls.length;i++){const n=(this.currentUrlIndex+i)%this.ldapUrls.length,o=this.ldapUrls[n];try{this.logger.debug(`Attempting connection to ${o}`);const t={...this.options,url:o,tlsOptions:o.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new e(t);return await i.bind(this.dn,this.pwd),n!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${o}`),this.currentUrlIndex=n),i}catch(e){this.logger.warn(`Failed to connect to ${o}: ${String(e)}`),t.push(e)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${t.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let i=this.availableConnections.length-1;i>=0;i--){const n=this.availableConnections[i];if(e-n.createdAt>this.connectionTtl){t.push(n),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(n);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return`${e}:${t.scope||"sub"}:${n}:${i}`}invalidateCache(e){for(const t of this.searchCache.keys())t.startsWith(e)&&this.searchCache.delete(t)}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i)=>this.modify(t,i,e),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}async search(e,t=this.base,i){let n={...c,...e};if(n=await s(this.parent.hooks.ldapsearchopts,n),[t,n]=await s(this.parent.hooks.ldapsearchrequest,[t,n,i]),!n.paged&&"base"===n.scope){const e=this.getCacheKey(t,n),i=this.searchCache.get(e);if(i)return this.logger.debug(`LDAP search cache hit: ${e}`),i}const o=await this.acquireConnection();try{let e=n.paged?o.client.searchPaginated(t,n):o.client.search(t,n);if(e=await s(this.parent.hooks.ldapsearchresult,e),!n.paged&&"base"===n.scope&&e instanceof Promise){const i=await e,o=this.getCacheKey(t,n);return this.searchCache.set(o,i),this.logger.debug(`LDAP search cached: ${o}`),i}return n.paged?this.wrapPaginatedSearch(e,o):e}finally{n.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,i,n){e=this.setDn(e),i.objectClass&&0!==i.objectClass.length||!this.config.user_class||(i.objectClass=this.config.user_class);const o={};for(const[e,t]of Object.entries(i))Buffer.isBuffer(t)?o[e]=t.toString():Array.isArray(t)&&t.length>0&&Buffer.isBuffer(t[0])?o[e]=t.map(e=>e.toString()):o[e]=t;[e,i]=await s(this.parent.hooks.ldapaddrequest,[e,o,n]);const a=[];for(const[e,i]of Object.entries(o)){const n=Array.isArray(i)?i:[i];a.push(new t({type:e,values:n}))}const c=await this.acquireConnection();try{return await c.client.add(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapadddone,[e,i]).catch(e=>{this.logger.error(`Hook ldapadddone failed: ${String(e)}`)}),!0}catch(e){throw ldapError("LDAP add error",e)}finally{this.releaseConnection(c)}}async modify(e,n,o){e=this.setDn(e);const a=[],c=this.opNumber();if([e,n]=await s(this.parent.hooks.ldapmodifyrequest,[e,n,c,o]),n.add)for(const[e,o]of Object.entries(n.add))a.push(new i({operation:"add",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(n.replace)for(const[e,o]of Object.entries(n.replace))a.push(new i({operation:"replace",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(n.delete)if(Array.isArray(n.delete))for(const e of n.delete)e&&a.push(new i({operation:"delete",modification:new t({type:e,values:[]})}));else for(const[e,o]of Object.entries(n.delete)){const n=new i({operation:"delete",modification:new t(o?{type:e,values:Array.isArray(o)?o:[o]}:{type:e})});a.push(n)}if(0===a.length){return Object.keys(n.add||{}).length>0||Object.keys(n.replace||{}).length>0||(Array.isArray(n.delete)?n.delete.length>0:Object.keys(n.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),r(this.parent.hooks.ldapmodifydone,[e,{},c]).catch(e=>{this.logger.error(`Hook ldapmodifydone failed: ${String(e)}`)}),!1}{const t=await this.acquireConnection();try{return await t.client.modify(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapmodifydone,[e,n,c]).catch(e=>{this.logger.error(`Hook ldapmodifydone failed: ${String(e)}`)}),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(a)}`),ldapError("LDAP modify error",t)}finally{this.releaseConnection(t)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t),[e,t]=await s(this.parent.hooks.ldaprenamerequest,[e,t,i]);const n=await this.acquireConnection();try{return await n.client.modifyDN(e,t),r(this.parent.hooks.ldaprenamedone,[e,t]).catch(e=>{this.logger.error(`Hook ldaprenamedone failed: ${String(e)}`)}),!0}catch(e){throw ldapError("LDAP rename error",e)}finally{this.releaseConnection(n)}}async move(e,t){e=this.setDn(e),t=this.setDn(t);const i=await this.acquireConnection();try{return await i.client.modifyDN(e,t),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw ldapError("LDAP move error",e)}finally{this.releaseConnection(i)}}async delete(e,t){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),[e]=await s(this.parent?.hooks.ldapdeleterequest,[e,t]);const i=await this.acquireConnection();try{for(const t of e){try{await i.client.del(t),this.invalidateCache(t)}catch(e){throw ldapError("LDAP delete error",e)}r(this.parent.hooks.ldapdeletedone,t).catch(e=>{this.logger.error(`Hook ldapdeletedone failed: ${String(e)}`)})}return!0}finally{this.releaseConnection(i)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${a(e)},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
1
+ import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as n}from"ldapts";import{LRUCache as o}from"lru-cache";import s from"p-limit";import{launchHooksChained as a,launchHooks as r,escapeDnValue as c}from"./utils.js";import{ConflictError as l}from"./errors.js";const h={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),n=t?.code;return"number"==typeof n&&(i.code=n),i}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]});class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=s(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,n=this.config.ldap_cache_ttl,a="string"==typeof n?parseInt(n,10)||0:n??0,r=1e3*Math.max(a,0);if(this.cacheEnabled=r>0,this.searchCache=new o({max:i,ttl:this.cacheEnabled?r:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${r/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new o({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");e.config.ldap_base?this.base=e.config.ldap_base:(this.base=e.config.ldap_dn.split(",",2)[1],this.logger.warn(`LDAP base is not defined, using "${this.base}"`)),this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const e=[];for(let i=0;i<this.ldapUrls.length;i++){const n=(this.currentUrlIndex+i)%this.ldapUrls.length,o=this.ldapUrls[n];try{this.logger.debug(`Attempting connection to ${o}`);const e={...this.options,url:o,tlsOptions:o.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await i.bind(this.dn,this.pwd),n!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${o}`),this.currentUrlIndex=n),i}catch(t){this.logger.warn(`Failed to connect to ${o}: ${String(t)}`),e.push(t)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${e.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let i=this.availableConnections.length-1;i>=0;i--){const n=this.availableConnections[i];if(e-n.createdAt>this.connectionTtl){t.push(n),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(n);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),n=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",o="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",o,i,n,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}invalidateCache(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),i=`,${t}`,n=[];for(const e of this.searchCache.keys()){const o=foldDn(e.split("\0")[0]);(o===t||o.endsWith(i))&&n.push(e)}for(const e of n)this.searchCache.delete(e)}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i)=>this.modify(t,i,e),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}async search(e,t=this.base,i){let n={...h,...e};n=await a(this.parent.hooks.ldapsearchopts,n),[t,n]=await a(this.parent.hooks.ldapsearchrequest,[t,n,i]);const o=this.cacheEnabled&&!n.paged&&"base"===n.scope,s=o?this.getCacheKey(t,n):"",r=this.cacheGeneration;if(o){const e=this.searchCache.get(s);if(e)return this.logger.debug(`LDAP search cache hit: ${digestKey(s)}`),cloneSearchResult(e)}const c=await this.acquireConnection();try{let e=n.paged?c.client.searchPaginated(t,n):c.client.search(t,n);if(e=await a(this.parent.hooks.ldapsearchresult,e),o){const t=e;return r===this.cacheGeneration?(this.searchCache.set(s,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(s)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(s)}`),t}return n.paged?this.wrapPaginatedSearch(e,c):e}finally{n.paged||this.releaseConnection(c)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,t,n){e=this.setDn(e),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const o={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?o[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?o[e]=i.map(e=>e.toString()):o[e]=i;[e,t]=await a(this.parent.hooks.ldapaddrequest,[e,o,n]);const s=[];for(const[e,t]of Object.entries(o)){const n=Array.isArray(t)?t:[t];s.push(new i({type:e,values:n}))}const c=await this.acquireConnection();try{return this.invalidateCache(e),await c.client.add(e,s),this.invalidateCache(e),r(this.parent.hooks.ldapadddone,[e,t]).catch(e=>{this.logger.error(`Hook ldapadddone failed: ${String(e)}`)}),!0}catch(t){throw function(e,t){const i=ldapError("LDAP add error",t);if(68!==i.code)return i;const n=new l(`Entry ${e} already exists`);return n.code=68,n}(e,t)}finally{this.releaseConnection(c)}}async modify(e,t,o){e=this.setDn(e);const s=[],c=this.opNumber();if([e,t]=await a(this.parent.hooks.ldapmodifyrequest,[e,t,c,o]),t.add)for(const[e,o]of Object.entries(t.add))s.push(new n({operation:"add",modification:new i({type:e,values:Array.isArray(o)?o:[o]})}));if(t.replace)for(const[e,o]of Object.entries(t.replace))s.push(new n({operation:"replace",modification:new i({type:e,values:Array.isArray(o)?o:[o]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&s.push(new n({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,o]of Object.entries(t.delete)){const t=new n({operation:"delete",modification:new i(o?{type:e,values:Array.isArray(o)?o:[o]}:{type:e})});s.push(t)}if(0===s.length){return Object.keys(t.add||{}).length>0||Object.keys(t.replace||{}).length>0||(Array.isArray(t.delete)?t.delete.length>0:Object.keys(t.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),r(this.parent.hooks.ldapmodifydone,[e,{},c]).catch(e=>{this.logger.error(`Hook ldapmodifydone failed: ${String(e)}`)}),!1}{const i=await this.acquireConnection();try{return this.invalidateCache(e),await i.client.modify(e,s),this.invalidateCache(e),r(this.parent.hooks.ldapmodifydone,[e,t,c]).catch(e=>{this.logger.error(`Hook ldapmodifydone failed: ${String(e)}`)}),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(s)}`),ldapError("LDAP modify error",t)}finally{this.releaseConnection(i)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t),[e,t]=await a(this.parent.hooks.ldaprenamerequest,[e,t,i]);const n=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await n.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),r(this.parent.hooks.ldaprenamedone,[e,t]).catch(e=>{this.logger.error(`Hook ldaprenamedone failed: ${String(e)}`)}),!0}catch(e){throw ldapError("LDAP rename error",e)}finally{this.releaseConnection(n)}}async move(e,t){e=this.setDn(e),t=this.setDn(t);const i=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await i.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw ldapError("LDAP move error",e)}finally{this.releaseConnection(i)}}async delete(e,t){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),[e]=await a(this.parent?.hooks.ldapdeleterequest,[e,t]);const i=await this.acquireConnection();try{for(const t of e){try{this.invalidateCache(t),await i.client.del(t),this.invalidateCache(t)}catch(e){throw ldapError("LDAP delete error",e)}r(this.parent.hooks.ldapdeletedone,t).catch(e=>{this.logger.error(`Hook ldapdeletedone failed: ${String(e)}`)})}return!0}finally{this.releaseConnection(i)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${c(e)},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
package/dist/lib/utils.js CHANGED
@@ -1 +1 @@
1
- import{BadRequestError as e}from"./errors.js";import{getLogger as t}from"./expressFormatedResponses.js";const n=t(),r=new Map;function getCompiledRegex(e,t){const n=t?`${e}:${t}`:e;let i=r.get(n);return i||(i=new RegExp(e,t),r.set(n,i)),i}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const launchHooks=async(e,...t)=>{if(e)for(const r of e)if(r)try{await r(...t)}catch(e){n.error("Hook error",e)}},launchHooksChained=async(e,t)=>{if(e)for(const n of e)n&&(t=await n(t));return t},transformSchemas=(e,t)=>e.toString().replace(/__(\S+)__/g,(e,n)=>{if(!n||"string"!=typeof n)return e;const r=n.trim().toLowerCase();return t[r]?"object"!=typeof t[r]?t[r]:JSON.stringify(t[r]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function escapeDnValue(e){return e.replace(/\\/g,"\\\\").replace(/,/g,"\\,").replace(/\+/g,"\\+").replace(/"/g,'\\"').replace(/</g,"\\<").replace(/>/g,"\\>").replace(/;/g,"\\;").replace(/=/g,"\\=").replace(/\0/g,"\\00").replace(/^\s/,"\\ ").replace(/\s$/,"\\ ").replace(/^#/,"\\#")}function unescapeDnValue(t){if("string"!=typeof t)throw new e("DN value must be a string");let n="",r=0;for(;r<t.length;)if("\\"===t[r]&&r+1<t.length){const e=t[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<t.length){const e=t.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){n+=String.fromCharCode(parseInt(e,16)),r+=3;continue}}n+=e,r+=2}else n+=t[r],r++;return n}function validateDnValue(e,t){if(null==e||"string"!=typeof e)throw new Error(`${t} must be a string`);if(0===e.trim().length)throw new Error(`${t} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${t} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${t} contains invalid invisible characters`)}function parseDn(t){if("string"!=typeof t)throw new e("DN must be a string");const n=[];let r="",i=!1;for(let e=0;e<t.length;e++){const o=t[e];i?(r+=o,i=!1):"\\"===o?(r+=o,i=!0):","===o?(n.push(r.trim()),r=""):r+=o}return r&&n.push(r.trim()),n}function getParentDn(e){const t=parseDn(e);return t.length<=1?e:t.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function isChildOf(e,t){const n=e.toLowerCase(),r=t.toLowerCase();if(!n.endsWith(r))return!1;if(n.length===r.length)return!1;return n.substring(0,n.length-r.length).endsWith(",")}function normalizeDn(e){return parseDn(e).map(e=>e.split("+").map(e=>{const t=e.indexOf("=");return(-1===t?e.trim():`${e.slice(0,t).trim()}=${e.slice(t+1).trim()}`).toLowerCase()}).sort().join("+")).filter(e=>e.length>0).join(",")}function isDnInBranch(e,t){const n=normalizeDn(t).split(",").filter(Boolean);if(0===n.length)return!1;const r=normalizeDn(e).split(",").filter(Boolean);if(r.length<n.length)return!1;return r.slice(r.length-n.length).every((e,t)=>e===n[t])}const asyncHandler=e=>(t,n,r)=>{Promise.resolve(e(t,n,r)).catch(r)};export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,launchHooks,launchHooksChained,normalizeDn,parseDn,transformSchemas,unescapeDnValue,validateDnValue};
1
+ import{BadRequestError as e}from"./errors.js";import{getLogger as t}from"./expressFormatedResponses.js";const n=t(),r=new Map;function getCompiledRegex(e,t){const n=t?`${e}:${t}`:e;let i=r.get(n);return i||(i=new RegExp(e,t),r.set(n,i)),i}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const launchHooks=async(e,...t)=>{if(e)for(const r of e)if(r)try{await r(...t)}catch(e){n.error("Hook error",e)}},launchHooksChained=async(e,t)=>{if(e)for(const n of e)n&&(t=await n(t));return t},transformSchemas=(e,t)=>e.toString().replace(/__(\S+)__/g,(e,n)=>{if(!n||"string"!=typeof n)return e;const r=n.trim().toLowerCase();return t[r]?"object"!=typeof t[r]?t[r]:JSON.stringify(t[r]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function substringSearchFilter(t,n){const r=/^[a-zA-Z][a-zA-Z0-9-]*$/,i=n.split(",").map(e=>e.trim()).filter(Boolean);if(0===i.length||!i.every(e=>r.test(e)))throw new e("Invalid LDAP attribute name");const o=escapeLdapFilter(t),a=i.map(e=>`(${e}=*${o}*)`);return 1===a.length?a[0]:`(|${a.join("")})`}function escapeDnValue(e){return e.replace(/\\/g,"\\\\").replace(/,/g,"\\,").replace(/\+/g,"\\+").replace(/"/g,'\\"').replace(/</g,"\\<").replace(/>/g,"\\>").replace(/;/g,"\\;").replace(/=/g,"\\=").replace(/\0/g,"\\00").replace(/^\s/,"\\ ").replace(/\s$/,"\\ ").replace(/^#/,"\\#")}function unescapeDnValue(t){if("string"!=typeof t)throw new e("DN value must be a string");let n="",r=0;for(;r<t.length;)if("\\"===t[r]&&r+1<t.length){const e=t[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<t.length){const e=t.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){n+=String.fromCharCode(parseInt(e,16)),r+=3;continue}}n+=e,r+=2}else n+=t[r],r++;return n}function validateDnValue(e,t){if(null==e||"string"!=typeof e)throw new Error(`${t} must be a string`);if(0===e.trim().length)throw new Error(`${t} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${t} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${t} contains invalid invisible characters`)}function parseDn(t){if("string"!=typeof t)throw new e("DN must be a string");const n=[];let r="",i=!1;for(let e=0;e<t.length;e++){const o=t[e];i?(r+=o,i=!1):"\\"===o?(r+=o,i=!0):","===o?(n.push(r.trim()),r=""):r+=o}return r&&n.push(r.trim()),n}function getParentDn(e){const t=parseDn(e);return t.length<=1?e:t.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function rdnValue(e){const t=/^[^=]+=((?:\\.|[^,])*)/.exec(e);return t?t[1].replace(/\\(.)/g,"$1"):""}function isChildOf(e,t){const n=e.toLowerCase(),r=t.toLowerCase();if(!n.endsWith(r))return!1;if(n.length===r.length)return!1;return n.substring(0,n.length-r.length).endsWith(",")}function normalizeDn(e){return parseDn(e).map(e=>e.split("+").map(e=>{const t=e.indexOf("=");return(-1===t?e.trim():`${e.slice(0,t).trim()}=${e.slice(t+1).trim()}`).toLowerCase()}).sort().join("+")).filter(e=>e.length>0).join(",")}function isDummyMemberDn(e,t){if(!t||"string"!=typeof e)return!1;if(e===t)return!0;try{return normalizeDn(e)===normalizeDn(t)}catch{return!1}}function isDnInBranch(e,t){const n=normalizeDn(t).split(",").filter(Boolean);if(0===n.length)return!1;const r=normalizeDn(e).split(",").filter(Boolean);if(r.length<n.length)return!1;return r.slice(r.length-n.length).every((e,t)=>e===n[t])}const asyncHandler=e=>(t,n,r)=>{Promise.resolve(e(t,n,r)).catch(r)};export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,isDummyMemberDn,launchHooks,launchHooksChained,normalizeDn,parseDn,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue};
@@ -0,0 +1 @@
1
+ import e from"../../abstract/plugin.js";import{asyncHandler as t}from"../../lib/utils.js";import{UnauthorizedError as s}from"../../lib/errors.js";import{roleAttribute as a}from"../../config/schema.js";class AuthzScope extends e{constructor(){super(...arguments),this.name="authzScope",this.roles=["api"]}api(e){e.get(`${this.config.api_prefix}/v1/authz/scope`,t(async(e,t)=>this.scope(e,t)))}authz(){return Object.values(this.server.loadedPlugins).find(e=>{if(!e.roles?.includes("authz"))return!1;const t=e;return"function"==typeof t.resolveUser&&"function"==typeof t.getAuthorizedBranches&&"function"==typeof t.getUserPermissions})}entities(){const e=(this.config.ldap_organization_link_attribute||"").toLowerCase(),linkable=t=>Boolean(e)&&(!t?.attributes||Object.keys(t.attributes).some(t=>t.toLowerCase()===e)),t=[],s=this.server.loadedPlugins.ldapFlatGeneric;for(const e of s?.instances||[])t.push({name:e.pluralName,base:e.base,linkable:linkable(e.schema)});const a=this.server.loadedPlugins.ldapGroups;return a?.base&&t.push({name:"groups",base:a.base,linkable:linkable(a.schema)}),this.server.loadedPlugins.ldapOrganizations&&this.config.ldap_top_organization&&t.push({name:"organizations",base:this.config.ldap_top_organization,linkable:!1}),t}async scope(e,t){const a=this.authz();if(!a)return void t.json({user:e.user??null,unrestricted:!0,branches:[],entities:this.entities().map(({name:e,base:t})=>({name:e,base:t,create:!0}))});if(!e.user)throw new s("No authenticated user");const i=await a.resolveUser(e.user);if(!i)throw new s(`Unknown user ${e.user}`);const r=await a.getAuthorizedBranches(i),n=[],o=new Map,permissionsOn=async e=>{let t=o.get(e);return t||(t=await a.getUserPermissions(i,e),o.set(e,t)),t};for(const e of r){const t=await permissionsOn(e);n.push({dn:e,...t,...await this.branchLabel(e)})}const c=r.some(e=>!0===o.get(e)?.write),u=this.config.ldap_top_organization,h=[];for(const{name:e,base:t,linkable:s}of this.entities()){const a=u&&t===u?!0===(await permissionsOn(u)).write:s?c:!0===(await permissionsOn(t)).write;h.push({name:e,base:t,create:a})}t.json({user:i,unrestricted:!1,branches:n,entities:h})}async branchLabel(e){const t=this.server.loadedPlugins.ldapOrganizations,s=a(t?.schema,"organizationPath")||this.config.ldap_organization_path_attribute;try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:["ou","o","cn",...s?[s]:[]]},e),a=t?.searchEntries?.[0];if(!a)return{};const first=e=>{const t=Array.isArray(e)?e[0]:e;return"string"==typeof t?t||void 0:Buffer.isBuffer(t)&&t.toString("utf8")||void 0};return{name:first(a.ou)||first(a.o)||first(a.cn),path:s?first(a[s]):void 0}}catch(e){return{}}}}export{AuthzScope as default};
@@ -1 +1 @@
1
- import*as e from"lemonldap-ng-handler";import t from"../../lib/auth/base.js";class AuthLLNG extends t{constructor(){super(...arguments),this.name="authLemonldapNg",this.roles=["auth"]}authMethod(t,s,a){e.run(t,s,()=>{t.user=t.headers["Lm-Remote-User"],a()})}}export{AuthLLNG as default};
1
+ import e from"../../lib/auth/base.js";const t=new WeakMap;class AuthLLNG extends e{constructor(){super(...arguments),this.name="authLemonldapNg",this.roles=["auth"]}async api(e){let n;try{n=await this.loadHandler()}catch(e){throw new Error(`${this.name}: requires the optional dependency "lemonldap-ng-handler", which is not installed. Install it to use this plugin, or remove it from the configuration. (${e instanceof Error?e.message:String(e)})`)}const a=this.config.llng_ini??"",i=t.get(n);if(void 0!==i&&i!==a)throw new Error(`${this.name}: lemonldap-ng-handler is already initialized from "${i}"; it keeps its state at the module level, so a second instance of this plugin cannot also use "${a}". Load one instance of this plugin, or point every instance at the same lemonldap-ng.ini.`);try{await n.init({configStorage:{confFile:a}})}catch(e){throw new Error(`${this.name}: cannot initialize the LemonLDAP::NG handler from ${a}. Check that the file exists, that its [configuration] section is reachable and that [node-handler] lists this server in nodeVhosts. (${e instanceof Error?e.message:String(e)})`)}t.set(n,a),this.handler=n,super.api(e)}async loadHandler(){return import("lemonldap-ng-handler")}authMethod(e,t,n){if(!this.handler)throw new Error(`${this.name}: lemonldap-ng-handler is not loaded`);this.handler.run(e,t,()=>{e.user=e.headers["Lm-Remote-User"],n()})}}export{AuthLLNG as default};
@@ -0,0 +1 @@
1
+ import{randomBytes as e}from"crypto";import t from"../../abstract/plugin.js";import{roleAttribute as s}from"../../config/schema.js";import{asyncHandler as o}from"../../lib/utils.js";import{jsonBody as a}from"../../lib/expressFormatedResponses.js";import{BadRequestError as n,NotFoundError as r}from"../../lib/errors.js";const c="abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%*-_=+";function generatePassword(t=16){const s=67*Math.floor(256/67);let o="";for(;o.length<t;)for(const a of e(t))if(!(a>=s)&&(o+=c[a%67],o.length===t))break;return o}class LdapAccountLifecycle extends t{constructor(){super(...arguments),this.name="ldapAccountLifecycle",this.roles=["api"],this.dependencies={ldapFlatGeneric:"core/ldap/flatGeneric"}}api(e){const t=this.server.loadedPlugins.ldapFlatGeneric;if(!t?.instances?.length)return void this.logger.warn("accountLifecycle: no flat entity is loaded, no endpoint registered");const a=`${this.config.api_prefix}/v1/ldap`;for(const n of t.instances){const t=s(n.schema,"accountStatus"),r=t?n.schema?.attributes[t]?.states:void 0;t&&r&&Object.keys(r).length>0&&(e.post(`${a}/${n.pluralName}/:id/status`,o(async(e,s)=>this.setStatus(n,t,r,e,s))),this.logger.info(`accountLifecycle: ${n.pluralName} states = ${Object.keys(r).join(", ")}`));const c=s(n.schema,"password");c&&e.post(`${a}/${n.pluralName}/:id/password`,o(async(e,t)=>this.resetPassword(n,c,e,t)))}}async setStatus(e,t,s,o,r){const c=a(o,r,"state");if(!c)return;if(!Object.prototype.hasOwnProperty.call(s,c.state))throw new n(`Unknown state "${c.state}" for ${e.singularName}; known states: ${Object.keys(s).join(", ")}`);const i=s[c.state],d=decodeURIComponent(o.params.id);await this.modify(e,d,{replace:{[t]:i}}),r.json({success:!0,state:c.state})}async resetPassword(e,t,o,r){const c=a(o,r);if(!c)return;const i=c.password;if(void 0!==i&&("string"!=typeof i||""===i))throw new n("Field password must be a non-empty string; omit it to have one generated");const d=void 0===i,l=d?generatePassword():i,p={[t]:l},f=s(e.schema,"passwordReset"),m=!1!==c.forceChange;if(f){const t=e.schema?.attributes[f],s=t?.states,o=m?s?.required??"TRUE":s?.cleared??"FALSE";p[f]=o}const u=decodeURIComponent(o.params.id);await this.modify(e,u,{replace:p}),r.json({success:!0,generated:d,forceChange:Boolean(f)&&m,...d?{password:l}:{}})}async modify(e,t,s){try{await e.modifyEntry(t,s)}catch(t){if(32===t.code)throw new r(`${e.singularName} not found`);throw t}}}export{LdapAccountLifecycle as default,generatePassword};
@@ -1 +1 @@
1
- import t from"fs";import e from"multer";import{parse as r}from"csv-parse/sync";import i from"../../abstract/plugin.js";import{badRequest as s,serverError as o}from"../../lib/expressFormatedResponses.js";import{validateDnValue as n,escapeDnValue as a}from"../../lib/utils.js";class LdapBulkImport extends i{constructor(r){super(r),this.name="ldapBulkImport",this.roles=["configurable"],this.resources=new Map;const i=parseInt(this.config.bulk_import_max_file_size,10)||10485760;this.upload=e({storage:e.memoryStorage(),limits:{fileSize:i},fileFilter:(t,e,r)=>{"text/csv"===e.mimetype||e.originalname.endsWith(".csv")?r(null,!0):r(new Error("Only CSV files are allowed"))}});const s=this.config.bulk_import_schemas;if(!s)return void this.logger.warn("No bulk import schemas configured");const o=s.split(",").map(t=>t.trim());for(const e of o){const[r,i]=e.split(":").map(t=>t.trim());if(r&&i)try{const e=t.readFileSync(i,"utf-8"),s=JSON.parse(e);this.resources.set(r,{name:r,schema:s,base:s.base||this.config.ldap_base,mainAttribute:s.mainAttribute||"cn"}),this.logger.info(`Loaded bulk import resource: ${r} from ${i}`)}catch(t){this.logger.error(`Failed to load schema for ${r}: ${t.message}`)}else this.logger.warn(`Invalid schema entry: ${e}`)}}api(t){for(const[e,r]of this.resources)t.get(`${this.config.api_prefix}/v1/ldap/bulk-import/${e}/template.csv`,(t,e)=>this.getTemplate(t,e,r)),t.post(`${this.config.api_prefix}/v1/ldap/bulk-import/${e}`,this.upload.single("file"),async(t,e)=>this.bulkImport(t,e,r))}getTemplate(t,e,r){const i=this.getEditableAttributes(r.schema);i.push("organizationDn"),e.setHeader("Content-Type","text/csv; charset=utf-8"),e.setHeader("Content-Disposition",`attachment; filename="${r.name}-template.csv"`),e.send(i.join(",")+"\n")}async bulkImport(t,e,i){if(!t.file)return s(e,"No file uploaded");const n="true"===t.body.dryRun||!0===t.body.dryRun,a="true"===t.body.updateExisting||!0===t.body.updateExisting,c="false"!==t.body.continueOnError&&!1!==t.body.continueOnError,l=Date.now(),p={success:!0,total:0,created:0,updated:0,skipped:0,failed:0,errors:[],details:{duration:"",linesProcessed:0}};try{const s=t.file.buffer.toString("utf-8"),o=r(s,{columns:!0,skip_empty_lines:!0,trim:!0});p.total=o.length;for(let e=0;e<o.length;e++){const r=e+2,s=o[e];try{const{dn:e,entry:r}=await this.processRecord(s,i),o=await this.entryExists(e);if(o&&!a){p.skipped++;continue}if(n){p.created++;continue}o&&a?(await this.server.ldap.modify(e,{replace:r},t),p.updated++):(await this.server.ldap.add(e,r,t),p.created++)}catch(t){if(p.failed++,p.errors.push({line:r,identifier:s[i.mainAttribute],error:t.message}),!c)throw t}}p.details.linesProcessed=o.length,p.details.duration=`${((Date.now()-l)/1e3).toFixed(1)}s`,e.json(p)}catch(t){return o(e,t)}}async processRecord(t,e){const r={},i=e.schema,s=t.organizationDn;delete t.organizationDn;for(const[t,e]of Object.entries(i.properties||{}))if(!0===e.fixed){const i=e.default||e.const;void 0!==i&&(r[t]=i)}for(const[e,i]of Object.entries(t))i&&""!==i.trim()&&(r[e]=i.includes(";")?i.split(";").map(t=>t.trim()):i);if(s){const t=this.findAttributeByRole(i,"organizationLink");t&&(r[t]=s);const e=this.findAttributeByRole(i,"organizationPath");if(e){const t=(await this.fetchOrganization(s))[e];r[e]=Array.isArray(t)?t[0]:t}}for(const[t,e]of Object.entries(i.properties||{}))if(!0===e.required&&!r[t])throw new Error(`Missing required attribute: ${t}`);const o=e.mainAttribute,c=r[o];if(!c)throw new Error(`Missing main attribute: ${o}`);const l=Array.isArray(c)?String(c[0]):String(c);n(l,o);return{dn:`${o}=${a(l)},${e.base}`,entry:r}}getEditableAttributes(t){const e=[];for(const[r,i]of Object.entries(t.properties||{}))!0!==i.fixed&&(i.role?.includes("organizationLink")||i.role?.includes("organizationPath")||e.push(r));return e}findAttributeByRole(t,e){for(const[r,i]of Object.entries(t.properties||{}))if(i.role?.includes(e))return r;return null}async fetchOrganization(t){const e=await this.server.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new Error(`Organization not found: ${t}`);return e.searchEntries[0]}async entryExists(t){try{return(await this.server.ldap.search({paged:!1,scope:"base"},t)).searchEntries.length>0}catch(t){return!1}}getConfigApiData(){const t=this.config.api_prefix||"/api",e=[];return this.resources.forEach((r,i)=>{e.push({name:i,mainAttribute:r.mainAttribute,base:r.base,maxFileSize:parseInt(this.config.bulk_import_max_file_size,10)||10485760,batchSize:parseInt(this.config.bulk_import_batch_size,10)||100,endpoints:{template:`${t}/v1/ldap/bulk-import/${i}/template.csv`,import:`${t}/v1/ldap/bulk-import/${i}`}})}),{enabled:!0,resources:e}}}export{LdapBulkImport as default};
1
+ import t from"fs";import e from"multer";import{parse as r}from"csv-parse/sync";import i from"../../abstract/plugin.js";import{badRequest as s,serverError as o}from"../../lib/expressFormatedResponses.js";import{validateDnValue as n,escapeDnValue as a}from"../../lib/utils.js";const c=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)(;[A-Za-z0-9-]+)*$/;class LdapBulkImport extends i{constructor(r){super(r),this.name="ldapBulkImport",this.roles=["configurable"],this.resources=new Map;const i=parseInt(this.config.bulk_import_max_file_size,10)||10485760;this.upload=e({storage:e.memoryStorage(),limits:{fileSize:i},fileFilter:(t,e,r)=>{"text/csv"===e.mimetype||e.originalname.endsWith(".csv")?r(null,!0):r(new Error("Only CSV files are allowed"))}});const s=this.config.bulk_import_schemas;if(!s)return void this.logger.warn("No bulk import schemas configured");const o=s.split(",").map(t=>t.trim());for(const e of o){const[r,i]=e.split(":").map(t=>t.trim());if(r&&i)try{const e=t.readFileSync(i,"utf-8"),s=JSON.parse(e);this.resources.set(r,{name:r,schema:s,base:s.base||this.config.ldap_base,mainAttribute:s.mainAttribute||"cn"}),this.logger.info(`Loaded bulk import resource: ${r} from ${i}`)}catch(t){this.logger.error(`Failed to load schema for ${r}: ${t.message}`)}else this.logger.warn(`Invalid schema entry: ${e}`)}}api(t){for(const[e,r]of this.resources)t.get(`${this.config.api_prefix}/v1/ldap/bulk-import/${e}/template.csv`,(t,e)=>this.getTemplate(t,e,r)),t.post(`${this.config.api_prefix}/v1/ldap/bulk-import/${e}`,this.upload.single("file"),async(t,e)=>this.bulkImport(t,e,r))}getTemplate(t,e,r){const i=this.getEditableAttributes(r.schema);i.push("organizationDn"),e.setHeader("Content-Type","text/csv; charset=utf-8"),e.setHeader("Content-Disposition",`attachment; filename="${r.name}-template.csv"`),e.send(i.join(",")+"\n")}async bulkImport(t,e,i){if(!t.file)return s(e,"No file uploaded");const n="true"===t.body.dryRun||!0===t.body.dryRun,a="true"===t.body.updateExisting||!0===t.body.updateExisting,c="false"!==t.body.continueOnError&&!1!==t.body.continueOnError,l=Date.now(),p={success:!0,total:0,created:0,updated:0,skipped:0,failed:0,errors:[],details:{duration:"",linesProcessed:0}};try{const s=t.file.buffer.toString("utf-8"),o=r(s,{columns:!0,skip_empty_lines:!0,trim:!0});p.total=o.length;for(let e=0;e<o.length;e++){const r=e+2,s=o[e];try{const{dn:e,entry:r}=await this.processRecord(s,i),o=await this.entryExists(e);if(o&&!a){p.skipped++;continue}if(n){p.created++;continue}o&&a?(await this.server.ldap.modify(e,{replace:r},t),p.updated++):(await this.server.ldap.add(e,r,t),p.created++)}catch(t){if(p.failed++,p.errors.push({line:r,identifier:s[i.mainAttribute],error:t.message}),!c)throw t}}p.details.linesProcessed=o.length,p.details.duration=`${((Date.now()-l)/1e3).toFixed(1)}s`,e.json(p)}catch(t){return o(e,t)}}async processRecord(t,e){const r={},i=e.schema,s=t.organizationDn;delete t.organizationDn;for(const[t,e]of Object.entries(i.properties||{}))if(!0===e.fixed){const i=e.default||e.const;void 0!==i&&(r[t]=i)}for(const[e,i]of Object.entries(t))if(i&&""!==i.trim()){if(!c.test(e))throw new Error(`Invalid column name: ${e}`);r[e]=i.includes(";")?i.split(";").map(t=>t.trim()):i}if(s){const t=this.findAttributeByRole(i,"organizationLink");t&&(r[t]=s);const e=this.findAttributeByRole(i,"organizationPath");if(e){const t=(await this.fetchOrganization(s))[e];r[e]=Array.isArray(t)?t[0]:t}}for(const[t,e]of Object.entries(i.properties||{}))if(!0===e.required&&!r[t])throw new Error(`Missing required attribute: ${t}`);const o=e.mainAttribute,l=r[o];if(!l)throw new Error(`Missing main attribute: ${o}`);const p=Array.isArray(l)?String(l[0]):String(l);n(p,o);return{dn:`${o}=${a(p)},${e.base}`,entry:r}}getEditableAttributes(t){const e=[];for(const[r,i]of Object.entries(t.properties||{}))!0!==i.fixed&&(i.role?.includes("organizationLink")||i.role?.includes("organizationPath")||e.push(r));return e}findAttributeByRole(t,e){for(const[r,i]of Object.entries(t.properties||{}))if(i.role?.includes(e))return r;return null}async fetchOrganization(t){const e=await this.server.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new Error(`Organization not found: ${t}`);return e.searchEntries[0]}async entryExists(t){try{return(await this.server.ldap.search({paged:!1,scope:"base"},t)).searchEntries.length>0}catch(t){return!1}}getConfigApiData(){const t=this.config.api_prefix||"/api",e=[];return this.resources.forEach((r,i)=>{e.push({name:i,mainAttribute:r.mainAttribute,base:r.base,maxFileSize:parseInt(this.config.bulk_import_max_file_size,10)||10485760,batchSize:parseInt(this.config.bulk_import_batch_size,10)||100,endpoints:{template:`${t}/v1/ldap/bulk-import/${i}/template.csv`,import:`${t}/v1/ldap/bulk-import/${i}`}})}),{enabled:!0,resources:e}}}export{LdapBulkImport as default};
@@ -1 +1 @@
1
- import t from"../../abstract/plugin.js";class LdapDepartmentSync extends t{constructor(t){super(t),this.name="ldapDepartmentSync",this.roles=["consistency"],this.hooks={ldaprenamedone:async([t,e])=>{if(/^ou=/.test(t)){this.logger.info(`Organization renamed from ${t} to ${e}, synchronizing linked resources...`);try{const r=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,"");await this.updateLinkedResources(t,e,r),await this.updateDescendantReferences(t,e,r)}catch(t){this.logger.error(`Error synchronizing department links after rename: ${t}`)}}}},this.linkAttr=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",this.pathAttr=this.config.ldap_organization_path_attribute||"twakeDepartmentPath"}async updateLinkedResources(t,e,r){const a=`(${this.linkAttr}=${t})`;this.logger.debug(`Searching for resources directly linked to ${t}: ${a}`);const i=await this.server.ldap.search({paged:!0,filter:a,attributes:[this.linkAttr,this.pathAttr]},r);let n=0;for await(const t of i)for(const r of t.searchEntries){const t=String(r.dn);try{const r=await this.getDepartmentPath(e);await this.server.ldap.modify(t,{replace:{[this.linkAttr]:e,[this.pathAttr]:r}}),n++,this.logger.debug(`Updated ${t}: ${this.linkAttr}=${e}, ${this.pathAttr}=${r}`)}catch(e){this.logger.error(`Failed to update ${t} after organization rename: ${e}`)}}this.logger.info(`Updated ${n} resources directly linked to the renamed organization`)}async updateDescendantReferences(t,e,r){const a=`(${this.linkAttr}=*)`;this.logger.debug(`Searching for resources linked to descendants of ${t}: ${a}`);const i=await this.server.ldap.search({paged:!0,filter:a,attributes:[this.linkAttr,this.pathAttr]},r);let n=0;for await(const r of i)for(const a of r.searchEntries){const r=String(a.dn),i=a[this.linkAttr],s=Array.isArray(i)?String(i[0]):String(i);if(s!==t&&s.endsWith(`,${t}`))try{const a=s.replace(`,${t}`,`,${e}`),i=await this.getDepartmentPath(a);await this.server.ldap.modify(r,{replace:{[this.linkAttr]:a,[this.pathAttr]:i}}),n++,this.logger.debug(`Updated descendant link ${r}: ${this.linkAttr}=${a}, ${this.pathAttr}=${i}`)}catch(t){this.logger.error(`Failed to update descendant reference ${r}: ${t}`)}}this.logger.info(`Updated ${n} resources linked to descendants of the renamed organization`)}async getDepartmentPath(t){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.pathAttr,"ou","o"]},t);if(!e.searchEntries||0===e.searchEntries.length)throw new Error(`Organization ${t} not found`);const r=e.searchEntries[0];if(r[this.pathAttr]){const t=r[this.pathAttr];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${this.pathAttr} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}}export{LdapDepartmentSync as default};
1
+ import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{isDnInBranch as a,parseDn as r,normalizeDn as n,getParentDn as i,rdnValue as s}from"../../lib/utils.js";class LdapDepartmentSync extends t{constructor(t){super(t),this.name="ldapDepartmentSync",this.roles=["consistency"],this.hooks={ldaprenamedone:async([t,e])=>{if(/^ou=/.test(t)){this.logger.info(`Organization renamed from ${t} to ${e}, synchronizing linked resources...`);try{const a=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,"");await this.updateOrganizationPaths(e);for(const r of this.linkedAttributes())await this.updateLinkedResources(t,e,a,r),await this.updateDescendantReferences(t,e,a,r)}catch(t){this.logger.error(`Error synchronizing department links after rename: ${t}`)}}}},this.linkAttr=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",this.pathAttr=this.config.ldap_organization_path_attribute||"twakeDepartmentPath"}organizationPathAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"organizationPath")||this.pathAttr}linkedAttributes(){const t=[],a=this.server.loadedPlugins.ldapFlatGeneric;for(const e of a?.instances||[])t.push(e.schema);const r=this.server.loadedPlugins.ldapGroups;t.push(r?.schema);const n=[];for(const a of t){const t=e(a,"organizationLink");t&&n.push({link:t,path:e(a,"organizationPath")||this.pathAttr})}n.push({link:this.linkAttr,path:this.pathAttr});const i=new Set;return n.filter(({link:t,path:e})=>{const a=`${t.toLowerCase()} ${e.toLowerCase()}`;return!i.has(a)&&(i.add(a),!0)})}async updateOrganizationPaths(t){const e=this.organizationPathAttribute(),o=this.config.ldap_top_organization;if(!o||!a(t,o))return;const h=this.config.ldap_organization_path_separator||" / ",c=await this.server.ldap.search({paged:!0,scope:"sub",filter:`(${e}=*)`,attributes:[e]},t),d=[];for await(const t of c)for(const a of t.searchEntries){const t=String(a.dn);if(!/^ou=/i.test(t))continue;const r=a[e];d.push({dn:t,path:String(Array.isArray(r)?r[0]:r)})}d.sort((t,e)=>r(t.dn).length-r(e.dn).length);const l=n(o),g=new Map;let p=0;for(const{dn:t,path:a}of d){const r=i(t),o=n(r);let c;if(o!==l&&(c=g.get(o)??await this.readPath(r)??void 0,void 0===c)){this.logger.warn(`Organization ${r} has no ${e}: the path of ${t} is left as it is`);continue}const d=s(t),u=c?`${c}${h}${d}`:d;if(g.set(n(t),u),u!==a)try{await this.server.ldap.modify(t,{replace:{[e]:u}}),p++}catch(e){this.logger.error(`Failed to update the path of ${t} after organization rename: ${e}`)}}this.logger.info(`Updated the path of ${p} organizations below ${t}`)}async readPath(t){const e=this.organizationPathAttribute();try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e]},t),r=a.searchEntries[0]?.[e];return void 0===r?null:String(Array.isArray(r)?r[0]:r)}catch{return null}}async updateLinkedResources(t,e,a,r){const n=`(${r.link}=${t})`;this.logger.debug(`Searching for resources directly linked to ${t}: ${n}`);const i=await this.server.ldap.search({paged:!0,filter:n,attributes:[r.link,r.path]},a);let s=0;for await(const t of i)for(const a of t.searchEntries){const t=String(a.dn);try{const a=await this.getDepartmentPath(e);await this.server.ldap.modify(t,{replace:{[r.link]:e,[r.path]:a}}),s++,this.logger.debug(`Updated ${t}: ${r.link}=${e}, ${r.path}=${a}`)}catch(e){this.logger.error(`Failed to update ${t} after organization rename: ${e}`)}}this.logger.info(`Updated ${s} resources directly linked to the renamed organization`)}async updateDescendantReferences(t,e,a,r){const n=`(${r.link}=*)`;this.logger.debug(`Searching for resources linked to descendants of ${t}: ${n}`);const i=await this.server.ldap.search({paged:!0,filter:n,attributes:[r.link,r.path]},a);let s=0;for await(const a of i)for(const n of a.searchEntries){const a=String(n.dn),i=n[r.link],o=Array.isArray(i)?String(i[0]):String(i);if(o!==t&&o.endsWith(`,${t}`))try{const n=o.replace(`,${t}`,`,${e}`),i=await this.getDepartmentPath(n);await this.server.ldap.modify(a,{replace:{[r.link]:n,[r.path]:i}}),s++,this.logger.debug(`Updated descendant link ${a}: ${r.link}=${n}, ${r.path}=${i}`)}catch(t){this.logger.error(`Failed to update descendant reference ${a}: ${t}`)}}this.logger.info(`Updated ${s} resources linked to descendants of the renamed organization`)}async getDepartmentPath(t){const e=this.organizationPathAttribute();try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e,"ou","o"]},t);if(!a.searchEntries||0===a.searchEntries.length)throw new Error(`Organization ${t} not found`);const r=a.searchEntries[0];if(r[e]){const t=r[e];return Array.isArray(t)?String(t[0]):String(t)}const n=r.ou||r.o;if(n){return`/${Array.isArray(n)?String(n[0]):String(n)}`}return this.logger.warn(`Organization ${t} has no ${e} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}}export{LdapDepartmentSync as default};
@@ -0,0 +1 @@
1
+ import e from"../../abstract/plugin.js";import{roleAttribute as t,hasRole as n}from"../../config/schema.js";import{normalizeDn as r,isDnInBranch as i,getParentDn as a,rdnValue as s,escapeLdapFilter as o,isDummyMemberDn as c}from"../../lib/utils.js";import{BadRequestError as u,ConflictError as l}from"../../lib/errors.js";function valueList(e){return null==e?[]:Array.isArray(e)?e.map(e=>String(e)):[String(e)]}function entryValues(e,t){const n=t.toLowerCase();for(const[t,r]of Object.entries(e))if(t.split(";")[0].toLowerCase()===n)return valueList(r);return[]}function asDn(e){try{return r(e)}catch{return e}}const d={B:1,KB:1e3,MB:1e6,GB:1e9,TB:1e12};function parseByteSize(e){if("number"==typeof e)return Math.round(e);const t=String(e).trim(),n=/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)?$/i.exec(t);if(!n)throw new u(`"${t}" is not a valid size`);const r=(n[2]||"B").toUpperCase();return Math.round(parseFloat(n[1])*d[r])}function fillsGeneratedAttribute(e,n,r){return!(void 0===n.default||!n.generated&&!n.normalize)||t(r,"organizationPath")===e}function parseDirectoryDate(e){const t=String(e).trim(),n=/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})?(\d{2})?(?:[.,](\d+))?(Z|[+-]\d{2}(?::?\d{2})?)?$/.exec(t);if(!n){const e=new Date(t);return isNaN(e.getTime())?null:e}const[,r,i,a,s,o,c,u,l]=n,[d,f,h,m,p,g]=[r,i,a,s,o,c].map(e=>void 0===e?0:Number(e));if(f<1||f>12||m>23||p>59||g>60)return null;let b=Date.UTC(d,f-1,h,m,p,g);const y=new Date(b);if(y.getUTCMonth()!==f-1||y.getUTCDate()!==h)return null;if(void 0!==u){const e=void 0!==c?1e3:void 0!==o?6e4:36e5;b+=Math.round(Number(`0.${u}`)*e)}if(l&&"Z"!==l){const e=l.slice(1).replace(":",""),t=Number(e.slice(0,2)),n=e.length>2?Number(e.slice(2,4)):0;if(t>23||n>59)return null;b-=("-"===l[0]?-1:1)*(60*t+n)*6e4}return new Date(b)}function startOfUtcDay(e=new Date){return Date.UTC(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate())}function mailInDomains(e,t,n=!1){if(0===t.length)return!0;if(t.includes("*"))return!0;const r=e.lastIndexOf("@");if(r<0)return!1;const i=e.slice(r+1).toLowerCase();return t.some(e=>{const t=e.toLowerCase();return i===t||n&&i.endsWith(`.${t}`)})}class LdapEnterpriseRules extends e{constructor(e){super(e),this.name="ldapEnterpriseRules",this.roles=["consistency"],this.hooks={ldapaddrequest:async([e,t,n])=>{const r=this.resolveEntity(e);return r&&(await this.runChecks(r,e,t,null),await this.runTransformations(r,e,t,!0)),void 0!==n?[e,t,n]:[e,t]},ldapmodifyrequest:async([e,n,r,i])=>{const a=this.resolveEntity(e);if(!a)return[e,n,r,i];const s={...n.add||{},...n.replace||{}};if(0===Object.keys(s).length)return[e,n,r,i];const o=await this.readEntry(e),c=t(a.schema,"organizationLink"),u={...s};if(c&&c in s&&o)for(const[e,t]of Object.entries(a.schema.attributes))t.mailDomainScope&&(e in u||void 0!==o[e]&&(u[e]=o[e]));await this.runChecks(a,e,u,o),await this.runTransformations(a,e,s,!1);for(const[e,t]of Object.entries(s))n.replace&&e in n.replace?n.replace[e]=t:n.add&&e in n.add?n.add[e]=t:(n.replace=n.replace||{},n.replace[e]=t);return[e,n,r,i]},ldapdeleterequest:async([e,t])=>{for(const t of Array.isArray(e)?e:[e])await this.guardDelete(t);return[e,t]}},this.dummyMember=this.config.group_dummy_user,this.topOrganization=this.config.ldap_top_organization,this.logger.info("Enterprise directory rules enabled")}fillsGeneratedAttribute(e,t,n){return fillsGeneratedAttribute(e,t,n)}bindings(){const e=[],t=this.server.loadedPlugins.ldapFlatGeneric;for(const n of t?.instances||[])n.schema&&e.push({base:n.base,subtree:!1,schema:n.schema,label:n.singularName});const n=this.server.loadedPlugins.ldapGroups;n?.base&&n.schema&&e.push({base:n.base,subtree:!0,schema:n.schema,label:"group"});const r=this.server.loadedPlugins.ldapOrganizations;return r?.schema&&this.topOrganization&&e.push({base:this.topOrganization,subtree:!0,schema:r.schema,label:"organization"}),e}resolveEntity(e){const t=r(e);let n,s=-1;for(const o of this.bindings()){const c=r(o.base);if(t===c)continue;if(!i(e,o.base))continue;if(!o.subtree&&r(a(e))!==c)continue;const u=c.split(",").length;u>s&&(n=o,s=u)}return n}async runChecks(e,t,r,i){for(const[a,s]of Object.entries(r)){const o=e.schema.attributes[a];if(!o)continue;const c=valueList(s);0!==c.length&&(o.unique&&await this.checkUnique(e,a,o,c,i?t:null),o.mailDomainScope&&await this.checkMailDomains(e,o,c,r,i),n(o,"accountExpiry")&&this.checkNotInThePast(a,o,c))}}async runTransformations(e,n,r,i){for(const[t,n]of Object.entries(e.schema.attributes)){if(i&&void 0===r[t]&&void 0!==n.default&&(n.generated||n.normalize)&&(r[t]=Array.isArray(n.default)?n.default.map(e=>String(e)):String(n.default),"pointer"===n.type||"pointer"===n.items?.type))for(const e of valueList(r[t]))if(!await this.readEntry(e))throw new u(`Schema default for "${t}" points to a non-existent entry: ${e}`);if("byteSize"===n.normalize&&void 0!==r[t]){const e=valueList(r[t]);try{r[t]=Array.isArray(r[t])?e.map(e=>String(parseByteSize(e))):String(parseByteSize(e[0]))}catch{throw new u(`Invalid value for attribute "${t}"${n.hint?`: ${n.hint}`:""}`)}}}const o=t(e.schema,"organizationPath");if(!o)return;if(!e.schema.attributes[o]?.generated&&void 0!==r[o])return;const c=t(e.schema,"organizationLink");if(c){const e=valueList(r[c])[0];if(!e)return;const t=await this.organizationPath(e);if(void 0===t)throw new u(`Organization ${e} has no ${o}, so "${o}" cannot be computed`);return void(r[o]=t)}if(!i)return;const l=valueList(r[t(e.schema,"identifier")||""])[0]||s(n);if(!l)return;const d=a(n),f=(this.topOrganization||this.config.ldap_base||"").toLowerCase(),h=!f||d.toLowerCase().endsWith(`,${f}`)?await this.organizationPath(d):void 0,m=this.config.ldap_organization_path_separator||" / ";r[o]=h?`${h}${m}${l}`:l}async checkUnique(e,t,n,r,i){const a="object"==typeof n.unique?n.unique:{},s=[t,...a.attributes||[]],c=function(e,t){const contains=(e,t)=>t===e||t.endsWith(`,${e}`),n=[];for(const r of[e,...t||[]]){const e=r.toLowerCase();if(!n.some(t=>contains(t.toLowerCase(),e))){for(let t=n.length-1;t>=0;t--)contains(e,n[t].toLowerCase())&&n.splice(t,1);n.push(r)}}return n}(e.base,a.branches);for(const n of r){if(void 0!==a.sentinel&&n===a.sentinel)continue;const r=s.map(e=>`(${e}=${o(n)})`).join("");let u=s.length>1?`(|${r})`:r;a.filter&&(u=`(&${a.filter}${u})`);for(const r of c){const a=await this.server.ldap.search({paged:!1,scope:"sub",filter:u,attributes:["dn"]},r);for(const r of a.searchEntries||[]){const a=String(r.dn);if(!i||a.toLowerCase()!==i.toLowerCase())throw this.logger.warn(`${t}="${n}" refused for ${i||"a new entry"}: already held by ${a}`),new l(`${e.label}: "${n}" is already used (${t} must be unique)`)}}}}async checkMailDomains(e,t,n,r,i){const a="directory"===t.mailDomainScope?await this.directoryDomains():await this.organizationDomains(e,r,i);if(0!==a.length)for(const e of n)if(!mailInDomains(e,a,t.allowSubdomains))throw new l(`The mail domain of "${e}" is not one of the authorised domains (${a.join(", ")})`)}async organizationDomains(e,n,r){const i=t(e.schema,"organizationLink");if(!i)return[];const s=valueList(n[i])[0]||valueList(r?.[i])[0]||void 0;if(!s)return[];const o=this.bindings().find(e=>"organization"===e.label)?.schema,c=t(o,"domainLink")||this.config.enterprise_domain_link_attribute;if(!c)return[];const u=[],l=new Set;let d=s;const f=(this.topOrganization||this.config.ldap_base||"").toLowerCase(),h=this.domainNameAttribute();for(;d&&!l.has(d.toLowerCase());){l.add(d.toLowerCase());const e=await this.readEntry(d);for(const t of valueList(e?.[c])){const e=await this.readEntry(t),n=valueList(e?.[h]);0===n.length&&this.logger.warn(`Organization ${d} links to domain ${t}, which has no "${h}": no mail domain restriction comes from it`),u.push(...n)}if(d.toLowerCase()===f)break;const t=a(d);d=t===d?void 0:t}return u}domainNameAttribute(){for(const e of this.bindings()){const n=t(e.schema,"domainName");if(n)return n}return this.config.enterprise_domain_name_attribute||"associatedDomain"}async directoryDomains(){const e=this.config.ldap_base;if(!e)return[];const t=this.domainNameAttribute(),n=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(${t}=*)`,attributes:[t]},e),r=[];for(const e of n.searchEntries||[])r.push(...valueList(e[t]));return r}checkNotInThePast(e,t,n){const r=startOfUtcDay();for(const i of n){const n=parseDirectoryDate(i);if(!n)throw new u(`Invalid value for attribute "${e}"${t.hint?`: ${t.hint}`:""}`);if(n.getTime()<r)throw new u(`Attribute "${e}" must not be earlier than today`)}}async organizationPath(e){const n=this.bindings().find(e=>"organization"===e.label)?.schema,r=t(n,"organizationPath")||this.config.ldap_organization_path_attribute;if(!r)return;const i=await this.readEntry(e);return i?valueList(i[r])[0]:void 0}async guardDelete(e){const t=this.resolveEntity(e);if(t)for(const[n,r]of Object.entries(t.schema.attributes)){if("nonEmpty"!==r.deleteGuard)continue;const i=await this.readEntry(e),a=valueList(i?.[n]).filter(e=>!c(e,this.dummyMember));if(a.length>0)throw new l(`Cannot delete ${t.label} ${e}: it still has ${a.length} member(s)`)}await this.guardReferences(e)}async guardReferences(e){for(const t of this.referringAttributes(e)){if(!t.pointer)continue;if("restrict"!==t.attr.referentialIntegrity)continue;const n=(await this.searchReferrers(t,e))[0];if(n)throw this.logger.warn(`Delete of ${e} refused: still referenced by ${String(n.dn)} (${t.name})`),new l(`Cannot delete ${e}: still referenced by another entry (${t.name})`)}}referringAttributes(e){const t=a(e),r=[];for(const e of this.bindings())for(const[a,s]of Object.entries(e.schema.attributes)){const o="pointer"===s.type?s:s.items,c=n(s,"members")||n(s,"owners");if(!o&&!c)continue;const u=o?.branch||[];u.length>0&&!u.some(e=>i(t,e))||r.push({binding:e,name:a,attr:s,pointer:o,multiValued:"array"===s.type})}return r}async searchReferrers(e,t){return(await this.server.ldap.search({paged:!1,scope:"sub",filter:`(${e.name}=${o(t)})`,attributes:["dn",e.name]},e.binding.base)).searchEntries||[]}async findReferences(e){const t=[],n=new Set;return await Promise.all(this.referringAttributes(e).map(r=>this.server.ldap.queryLimit(async()=>{for(const i of await this.searchReferrers(r,e)){const e=String(i.dn),a=`${asDn(e)} ${r.name.toLowerCase()}`;n.has(a)||(n.add(a),t.push({dn:e,attribute:r.name,values:entryValues(i,r.name),multiValued:r.multiValued}))}}))),t}static referenceKey(e){return`${asDn(e.dn)} ${e.attribute.toLowerCase()}`}async rewriteReferences(e,t,n,r){const i=asDn(t),a=asDn(n);if(i===a)return{updated:0,failed:[]};const s=r?this.server.ldap.forRequest(r):{modify:(e,t)=>this.server.ldap.modify(e,t)},o=new Map;for(const e of await this.findReferences(t))o.set(LdapEnterpriseRules.referenceKey(e),e);let c=e.filter(e=>!o.has(LdapEnterpriseRules.referenceKey(e))).length;const u=new Map;return await Promise.all([...o.values()].map(e=>this.server.ldap.queryLimit(async()=>{try{await this.applyRewrite(s,e,i,a,n),c++}catch(r){this.logger.error(`Rename ${t} -> ${n}: could not rewrite ${e.attribute} of ${e.dn}: ${String(r)}`),u.set(e.attribute,(u.get(e.attribute)||0)+1)}}))),{updated:c,failed:[...u].map(([e,t])=>({attribute:e,count:t}))}}async applyRewrite(e,t,n,r,i){const a=this.rewrite(t,n,r,i);if(!a)return;try{return void await e.modify(t.dn,a)}catch(e){const n=e.code;if(20!==n&&16!==n)throw e;this.logger.debug(`Rewrite of ${t.attribute} on ${t.dn} met a concurrent one (code ${n}); reading it back`)}const s={...t,values:await this.freshValues(t.dn,t.attribute)},o=this.rewrite(s,n,r,i);o&&await e.modify(s.dn,o)}async freshValues(e,t){this.server.ldap.invalidateCache(e);const n=await this.server.ldap.search({paged:!1,scope:"base",attributes:[t]},e),r=n.searchEntries?.[0];return r?entryValues(r,t):[]}rewrite(e,t,n,r){const i=e.attribute;if(t===n)return;const a=e.values.filter(e=>asDn(e)===t);if(0===a.length)return;const s=e.values.some(e=>asDn(e)===n);if(!e.multiValued){const n=[];for(const i of e.values){const e=asDn(i)===t?r:i;n.some(t=>asDn(t)===asDn(e))||n.push(e)}return{replace:{[i]:n}}}return s?{delete:{[i]:a}}:{delete:{[i]:a},add:{[i]:r}}}async readEntry(e){try{const t=await this.server.ldap.search({paged:!1,scope:"base"},e);return t?.searchEntries?.[0]||null}catch(e){return null}}}export{LdapEnterpriseRules as default,fillsGeneratedAttribute,mailInDomains,parseByteSize,parseDirectoryDate,startOfUtcDay};
@@ -1 +1 @@
1
- import e from"../../abstract/plugin.js";import{launchHooksChained as r,launchHooks as s}from"../../lib/utils.js";class ExternalUsersInGroups extends e{constructor(e){super(e),this.name="externalUsersInGroups",this.roles=["consistency"],this.dependencies={ldapGroups:"core/ldap/groups"},this.hooks={ldapgroupvalidatemembers:async([e,a])=>(await Promise.all(a.map(e=>e.replace(/\s/g,"")).map(e=>this.server.ldap.queryLimit(async()=>{if(!new RegExp(`mail=([^,]+),${this.config.external_members_branch}$`).test(e))return!1;try{return await this.ldap.search({paged:!1,scope:"base"},e),!0}catch{const a=e.replace(/^mail=([^,]+),.*$/,"$1");if(!a)throw new Error(`Malformed member ${e}`);if(this.config.mail_domain&&Array.isArray(this.config.mail_domain)){const e=a.split("@")[1];if(e&&this.config.mail_domain.includes(e))throw new Error(`Cannot create external user with managed domain: ${a}`)}let t={objectClass:this.config.external_branch_class||this.config.user_class,mail:a,[this.config.ldap_groups_main_attribute]:a,sn:a};[e,t]=await r(this.registeredHooks.externaluserentry,[e,t]);try{return await this.ldap.add(e,t),s(this.registeredHooks.externaluseradded,e,t),!0}catch(r){throw new Error(`Unable to insert ${e}: ${r instanceof Error?r.message:String(r)}`)}}}))),[e,a])},this.ldap=e.ldap}}export{ExternalUsersInGroups as default};
1
+ import r from"../../abstract/plugin.js";import{HttpError as e}from"../../lib/errors.js";import{launchHooksChained as s,launchHooks as a}from"../../lib/utils.js";class ExternalUsersInGroups extends r{constructor(r){super(r),this.name="externalUsersInGroups",this.roles=["consistency"],this.dependencies={ldapGroups:"core/ldap/groups"},this.hooks={ldapgroupvalidatemembers:async([r,t])=>(await Promise.all(t.map(r=>r.replace(/\s/g,"")).map(r=>this.server.ldap.queryLimit(async()=>{if(!new RegExp(`mail=([^,]+),${this.config.external_members_branch}$`).test(r))return!1;try{return await this.ldap.search({paged:!1,scope:"base"},r),!0}catch{const t=r.replace(/^mail=([^,]+),.*$/,"$1");if(!t)throw new Error(`Malformed member ${r}`);if(this.config.mail_domain&&Array.isArray(this.config.mail_domain)){const r=t.split("@")[1];if(r&&this.config.mail_domain.includes(r))throw new Error(`Cannot create external user with managed domain: ${t}`)}let i={objectClass:this.config.external_branch_class||this.config.user_class,mail:t,[this.config.ldap_groups_main_attribute]:t,sn:t};[r,i]=await s(this.registeredHooks.externaluserentry,[r,i]);try{return await this.ldap.add(r,i),a(this.registeredHooks.externaluseradded,r,i),!0}catch(s){if(s instanceof e)throw s;throw new Error(`Unable to insert ${r}: ${s instanceof Error?s.message:String(s)}`)}}}))),[r,t])},this.ldap=r.ldap}}export{ExternalUsersInGroups as default};
@@ -1 +1 @@
1
- import t from"fs";import a from"../../abstract/plugin.js";import e from"../../abstract/ldapFlat.js";import{transformSchemas as s}from"../../lib/utils.js";class LdapFlatInstance extends e{constructor(t,a){super(t,a),this.name="ldapFlatInstance",this.roles=["api"],this.dependencies={departmentSync:"core/ldap/departmentSync"}}}class LdapFlatGeneric extends a{constructor(a){super(a),this.name="ldapFlatGeneric",this.roles=["configurable"],this.instances=[],this.schemaPaths=[];const e=this.config.ldap_flat_schema||[];0!==e.length?e.forEach(e=>{try{const i=t.readFileSync(e,"utf8"),n=JSON.parse(s(i,this.config));if(!n.entity)throw new Error(`Schema ${e} is missing "entity" metadata section`);const r=["name","mainAttribute","objectClass","singularName","pluralName","base"];for(const t of r)if(!n.entity[t])throw new Error(`Schema ${e} is missing entity.${t}`);let l=n.entity.base;l=l.replace(/\{([^}]+)\}/g,(t,a)=>{const e=a,s=this.config[e];return"string"==typeof s?s:t});const c=new LdapFlatInstance(a,{base:l,mainAttribute:n.entity.mainAttribute,objectClass:n.entity.objectClass,defaultAttributes:n.entity.defaultAttributes||{},schemaPath:e,singularName:n.entity.singularName,pluralName:n.entity.pluralName,hookPrefix:`ldap${n.entity.name}`});c.name=`ldapFlat:${n.entity.name}`,this.instances.push(c),this.schemaPaths.push(e),this.logger.info(`Created ldapFlat instance for "${n.entity.name}" (${n.entity.pluralName})`)}catch(t){this.logger.error(`Failed to load schema ${e}: ${t}`)}}):this.logger.warn("No schemas provided for ldapFlatGeneric plugin")}api(t){this.instances.forEach(a=>{a.api(t)})}getConfigApiData(){const t=this.config.api_prefix||"/api",a=this.config.static_name||"static";return{flatResources:this.instances.map((e,s)=>{let i;if(this.server.loadedPlugins.static&&this.schemaPaths[s]){const t=this.schemaPaths[s],e=t.indexOf("/schemas/");if(-1!==e){const s=t.substring(e);i=`/${a}${s}`}}return{name:e.name.replace("ldapFlat:",""),singularName:e.singularName,pluralName:e.pluralName,mainAttribute:e.mainAttribute,objectClass:e.objectClass,base:e.base,schema:e.schema||{strict:!1,attributes:{}},schemaUrl:i,endpoints:{list:`${t}/v1/ldap/${e.pluralName}`,get:`${t}/v1/ldap/${e.pluralName}/:id`,create:`${t}/v1/ldap/${e.pluralName}`,update:`${t}/v1/ldap/${e.pluralName}/:id`,delete:`${t}/v1/ldap/${e.pluralName}/:id`}}})}}}export{LdapFlatGeneric as default};
1
+ import e from"fs";import t from"../../abstract/plugin.js";import a from"../../abstract/ldapFlat.js";import{transformSchemas as i}from"../../lib/utils.js";const l=[{plugin:"ldapGroups",modules:["ldap/groups"],collection:"groups"},{plugin:"ldapOrganizations",modules:["ldap/organizations","ldap/organization"],collection:"organizations"},{plugin:"ldapRaw",modules:["ldap/raw"],collection:"raw"},{plugin:"ldapBulkImport",modules:["ldap/bulkImport"],collection:"bulk-import"}];function pluginModule(e){return e.split(":")[0].replace(/\.[cm]?js$/,"").split("/").slice(-2).join("/")}class LdapFlatInstance extends a{constructor(e,t){super(e,t),this.name="ldapFlatInstance",this.roles=["api"],this.dependencies={departmentSync:"core/ldap/departmentSync"}}}class LdapFlatGeneric extends t{constructor(t){super(t),this.name="ldapFlatGeneric",this.roles=["configurable"],this.instances=[],this.schemaPaths=[];const a=this.config.ldap_flat_schema||[],s=new Map,n=new Map;if(0===a.length)return void this.logger.warn("No schemas provided for ldapFlatGeneric plugin");const r=(this.config.plugin||[]).map(pluginModule),o=new Map;for(const{plugin:e,modules:t,collection:a}of l)(this.server.loadedPlugins[e]||t.some(e=>r.includes(e)))&&o.set(a,`plugin ${e}`);a.forEach(a=>{try{const l=e.readFileSync(a,"utf8"),r=JSON.parse(i(l,this.config));if(!r.entity)throw new Error(`Schema ${a} is missing "entity" metadata section`);const p=["name","mainAttribute","objectClass","singularName","pluralName","base"];for(const e of p)if(!r.entity[e])throw new Error(`Schema ${a} is missing entity.${e}`);const c=s.get(r.entity.name);if(c)throw new Error(`entity "${r.entity.name}" is already declared by ${c}`);const u=n.get(r.entity.pluralName)||o.get(r.entity.pluralName);if(u)throw new Error(`entity "${r.entity.name}" wants the URL of "${r.entity.pluralName}", already served by ${u}`);let m=r.entity.base;m=m.replace(/\{([^}]+)\}/g,(e,t)=>{const a=t,i=this.config[a];return"string"==typeof i?i:e});const d=new LdapFlatInstance(t,{base:m,mainAttribute:r.entity.mainAttribute,objectClass:r.entity.objectClass,defaultAttributes:r.entity.defaultAttributes||{},schemaPath:a,singularName:r.entity.singularName,pluralName:r.entity.pluralName,hookPrefix:`ldap${r.entity.name}`});d.label=r.entity.label,d.singularLabel=r.entity.singularLabel,d.valueLabels=r.entity.valueLabels,d.name=`ldapFlat:${r.entity.name}`,this.instances.push(d),this.schemaPaths.push(a),s.set(r.entity.name,a),n.set(r.entity.pluralName,a),this.logger.info(`Created ldapFlat instance for "${r.entity.name}" (${r.entity.pluralName})`)}catch(e){this.logger.error(`Failed to load schema ${a}: ${e}`)}})}api(e){this.instances.forEach(t=>{t.api(e)})}getConfigApiData(){const e=this.config.api_prefix||"/api",t=this.config.static_name||"static";return{flatResources:this.instances.map((a,i)=>{let l;if(this.server.loadedPlugins.static&&this.schemaPaths[i]){const e=this.schemaPaths[i],a=e.indexOf("/schemas/");if(-1!==a){const i=e.substring(a);l=`/${t}${i}`}}return{name:a.name.replace("ldapFlat:",""),singularName:a.singularName,pluralName:a.pluralName,label:a.label,singularLabel:a.singularLabel,valueLabels:a.valueLabels,mainAttribute:a.mainAttribute,objectClass:a.objectClass,base:a.base,schema:a.schema||{strict:!1,attributes:{}},schemaUrl:l,endpoints:{list:`${e}/v1/ldap/${a.pluralName}`,get:`${e}/v1/ldap/${a.pluralName}/:id`,create:`${e}/v1/ldap/${a.pluralName}`,update:`${e}/v1/ldap/${a.pluralName}/:id`,delete:`${e}/v1/ldap/${a.pluralName}/:id`,rename:`${e}/v1/ldap/${a.pluralName}/:id/rename`}}})}}}export{LdapFlatGeneric as default,l as ldapPluginCollections};
@@ -1 +1 @@
1
- import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{transformSchemas as o,asyncHandler as n,escapeDnValue as d,validateDnValue as h,launchHooksChained as c,launchHooks as p,getCompiledRegex as l,escapeRegex as m,escapeLdapFilter as u}from"../../lib/utils.js";import{NotFoundError as f,BadRequestError as g}from"../../lib/errors.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async([e,t])=>{let r=e;return Array.isArray(r)||(r=[r]),this.logger.debug(`User deletion detected, removing from groups: ${r.join(", ")}`),await Promise.all(r.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)}),[e,t]}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(o(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new g("Invalid match query");if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new g("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups(i);return a(t,s)})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,n(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new g("cn is required");const a=i(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new g("member must be a string or string array");await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,n(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new g("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new g("targetOrgDn must be a string");await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"newCn");if(o){if("string"!=typeof o.newCn)throw new g("newCn must be a string");await s(t,this.renameGroup.bind(this),a,o.newCn)}}))}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${d(i)},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)throw new f("Group not found");t.json(r.searchEntries[0])}catch(e){if(e.code&&32===e.code)throw new f("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;const o=a[this.cn],n=a.member?a.member:[],d=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,d)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new g("cn is required");await s(t,this.deleteGroup.bind(this),i)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new g("cn is required");const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(h(e,this.cn),i=`${this.cn}=${d(e)},${this.base}`),await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});const s=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||s.includes("twakeGroup")||s.includes("twakeStaticGroup")||s.push("twakeGroup");let a,o={objectClass:s,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&o.member.push(...t),[i,o]=await c(this.registeredHooks.ldapgroupadd,[i,o]);try{a=await this.ldap.add(i,o)}catch(e){throw new Error(`Failed to add group ${i}: ${e}`)}return p(this.registeredHooks.ldapgroupadddone,[i,o]),a}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;const i=this.opNumber();if([r,t]=await c(this.registeredHooks.ldapgroupmodify,[r,t,i]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(r,t);const s=await this.ldap.modify(r,t);return p(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){/,/.test(e)||h(e,this.cn),/,/.test(t)||h(t,this.cn);let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${d(t)},${this.base}`;[r,i]=await c(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return p(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;t=await c(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return p(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await c(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;if([e,t]=await c(this.registeredHooks.ldapgroupdeletemember,[e,t]),t===this.config.group_dummy_user)throw new Error("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${e}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r))))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let h;try{h=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===h.searchEntries.length)throw new Error(`Target organization ${t} not found`);const c=h.searchEntries[0][s];if(!c)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:c}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)e.searchEntries.map(e=>{const t=e[this.cn];t&&(i[t]=e),Array.isArray(i[t].member)||(i[t].member=[i[t].member]),i[t].member=i[t].member.filter(e=>e!==this.config.group_dummy_user)});return i=await c(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=t?`(${this.cn}=*${e}*)`:`(${this.cn}=${e})`;return await this.listGroups({filter:i,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await c(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await c(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new Error(`Invalid value for field ${e}`);for(const[e,r]of Object.entries(this.schema.attributes))if(r.required&&null==t[e])throw new Error(`Missing required field ${e}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await c(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new Error(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new Error(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new Error(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test){const i="string"==typeof r.items.test?l(r.items.test):r.items.test;for(let s of t){if(typeof s!==r.items.type)throw new Error(`Field ${e} must be of type ${r.items.type}`);if("string"!=typeof s&&(s=s.toString()),!i.test(s))throw new Error(`Field ${e} has invalid value ${s}`)}}}else if("pointer"===r.type){if("string"!=typeof t)throw new Error(`Field ${e} must be a string (DN pointer)`);const i=t;if(r.branch&&r.branch.length>0){if(!r.branch.some(e=>l(`,?${m(e)}$`,"i").test(i)))throw new Error(`Field ${e} must point to a DN within allowed branches: ${r.branch.join(", ")}`)}try{const t=await this.ldap.search({paged:!1,scope:"base"},i);if(!t||!t.searchEntries||0===t.searchEntries.length)throw new Error(`Field ${e} points to non-existent DN: ${i}`)}catch(t){throw new Error(`Field ${e} points to invalid or non-existent DN: ${i}: ${t}`)}if(r.test){if(!("string"==typeof r.test?l(r.test):r.test).test(i))throw new Error(`Field ${e} has invalid value ${i}`)}}else{if(typeof t!==r.type)return!1;if("string"!=typeof t&&(t=t.toString()),r.test){if(!("string"==typeof r.test?l(r.test):r.test).test(t))throw new Error(`Field ${e} has invalid value ${t}`)}}return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
1
+ import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{transformSchemas as o,asyncHandler as n,isDummyMemberDn as h,escapeDnValue as c,validateDnValue as d,launchHooksChained as p,launchHooks as m,substringSearchFilter as l,escapeLdapFilter as u}from"../../lib/utils.js";import{NotFoundError as f,BadRequestError as g,HttpError as b}from"../../lib/errors.js";import{assertClientMaySet as w,modifiedAttributeNames as y,missingRequiredAttribute as $,checkDnValues as v,matchesPattern as _}from"../../config/schema.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async([e,t])=>{let r=e;return Array.isArray(r)||(r=[r]),this.logger.debug(`User deletion detected, removing from groups: ${r.join(", ")}`),await Promise.all(r.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)}),[e,t]}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(o(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new g("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=l(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new g("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups(i);return a(t,this.hideNeverReturn(s))})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,n(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new g("cn is required");const a=i(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new g("member must be a string or string array");await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,n(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new g("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new g("targetOrgDn must be a string");await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"newCn");if(o){if("string"!=typeof o.newCn)throw new g("newCn must be a string");await s(t,this.renameGroup.bind(this),a,o.newCn)}}))}isDummyMember(e){return h(e,this.config.group_dummy_user)}withoutDummyMember(e){const t=e.member;if(void 0===t)return e;const r=Array.isArray(t)?t:[t];return{...e,member:r.filter(e=>!this.isDummyMember(e))}}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${c(i)},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)throw new f("Group not found");t.json(this.hideNeverReturn(this.withoutDummyMember(r.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new f("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;w(this.schema,Object.keys(a),this.cn);const o=a[this.cn],n=a.member?a.member:[],h=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,h)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new g("cn is required");await s(t,this.deleteGroup.bind(this),i)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new g("cn is required");w(this.schema,y(r));const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(d(e,this.cn),i=`${this.cn}=${c(e)},${this.base}`),await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});const s=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||s.includes("twakeGroup")||s.includes("twakeStaticGroup")||s.push("twakeGroup");let a,o={objectClass:s,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&o.member.push(...t),[i,o]=await p(this.registeredHooks.ldapgroupadd,[i,o]);try{a=await this.ldap.add(i,o)}catch(e){if(e instanceof b)throw e;throw new Error(`Failed to add group ${i}: ${e}`)}return m(this.registeredHooks.ldapgroupadddone,[i,o]),a}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;const i=this.opNumber();if([r,t]=await p(this.registeredHooks.ldapgroupmodify,[r,t,i]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(r,t);const s=await this.ldap.modify(r,t);return m(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){/,/.test(e)||d(e,this.cn),/,/.test(t)||d(t,this.cn);let r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${c(t)},${this.base}`;[r,i]=await p(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return m(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;t=await p(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return m(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await p(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{if(e instanceof b)throw e;throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;if([e,t]=await p(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new g("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{if(e instanceof b)throw e;throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${e}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r))))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let h;try{h=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===h.searchEntries.length)throw new Error(`Target organization ${t} not found`);const d=h.searchEntries[0][s];if(!d)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:d}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)for(const t of e.searchEntries){const e=t[this.cn];e&&(i[e]=this.withoutDummyMember(t))}return i=await p(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=t?`(${this.cn}=*${e}*)`:`(${this.cn}=${e})`;return await this.listGroups({filter:i,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await p(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await p(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new g(`Invalid value for field ${e}`);const r=$(this.schema,t,this.server.loadedPlugins);if(r)throw new g(`Missing required field ${r}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await p(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new g(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new g(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new g(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test&&"pointer"!==r.items.type)for(const i of t)if(typeof i!==r.items.type)throw new g(`Field ${e} must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await v(e,r,t,async e=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!_(r,t))throw new g(`Field ${e} has invalid value ${String(t)}`);return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};