ldap-rest 0.17.1 → 0.19.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 (73) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/abstract/twakePlugin.js +1 -1
  3. package/dist/config/args.js +1 -1
  4. package/dist/config/schema.js +1 -1
  5. package/dist/lib/ldapActions.js +1 -1
  6. package/dist/plugins/ldap/enterpriseRules.js +1 -1
  7. package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
  8. package/dist/plugins/ldap/groups.js +1 -1
  9. package/dist/plugins/ldap/organizations.js +1 -1
  10. package/dist/plugins/twake/james.js +1 -1
  11. package/dist/src/abstract/ldapFlat.d.ts +9 -11
  12. package/dist/src/browser/ldap-group-editor/types.d.ts +2 -1
  13. package/dist/src/browser/ldap-resource-editor/types.d.ts +3 -2
  14. package/dist/src/browser/ldap-unit-editor/types.d.ts +2 -1
  15. package/dist/src/browser/ldap-user-editor/types.d.ts +3 -2
  16. package/dist/src/browser/shared/types/index.d.ts +7 -1
  17. package/dist/src/browser/shared/utils/schema.d.ts +20 -1
  18. package/dist/src/config/args.d.ts +1 -0
  19. package/dist/src/config/schema.d.ts +27 -4
  20. package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +11 -0
  21. package/dist/src/plugins/ldap/organizations.d.ts +0 -8
  22. package/dist/src/plugins/twake/james.d.ts +63 -75
  23. package/docs/client-development/api/reference.md +1 -1
  24. package/docs/client-development/api/rest-api.md +13 -1
  25. package/docs/client-development/schemas/README.md +18 -13
  26. package/docs/plugin-development/dependencies.md +1 -3
  27. package/docs/presentations/en.md +0 -28
  28. package/docs/presentations/fr.md +0 -28
  29. package/docs/usage/configuration.md +13 -12
  30. package/docs/usage/plugins/integrations/james-mail.md +92 -129
  31. package/docs/usage/plugins/integrations/james-mailboxes.md +70 -83
  32. package/docs/usage/plugins/ldap/external-users.md +23 -2
  33. package/docs/usage/plugins/ldap/flat-generic.md +23 -5
  34. package/docs/usage/plugins/utilities/weblogs.md +1 -1
  35. package/docs/usage/upgrading.md +48 -0
  36. package/package.json +1 -1
  37. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js +2 -1
  38. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js.map +1 -1
  39. package/static/browser/ldap-user-editor/api/UserApiClient.js +2 -1
  40. package/static/browser/ldap-user-editor/api/UserApiClient.js.map +1 -1
  41. package/static/browser/ldap-user-editor/components/UserEditor.js +11 -8
  42. package/static/browser/ldap-user-editor/components/UserEditor.js.map +1 -1
  43. package/static/browser/ldap-user-editor/components/UserList.js +3 -1
  44. package/static/browser/ldap-user-editor/components/UserList.js.map +1 -1
  45. package/static/browser/ldap-user-editor.esm.js +52 -10
  46. package/static/browser/ldap-user-editor.esm.js.map +1 -1
  47. package/static/browser/ldap-user-editor.js +52 -10
  48. package/static/browser/ldap-user-editor.js.map +1 -1
  49. package/static/browser/ldap-user-editor.min.js +1 -1
  50. package/static/browser/ldap-user-editor.min.js.map +1 -1
  51. package/static/browser/shared/index.js +1 -1
  52. package/static/browser/shared/types/index.js.map +1 -1
  53. package/static/browser/shared/utils/form.js +2 -1
  54. package/static/browser/shared/utils/form.js.map +1 -1
  55. package/static/browser/shared/utils/schema.js +33 -1
  56. package/static/browser/shared/utils/schema.js.map +1 -1
  57. package/static/browser/shared.esm.js +153 -121
  58. package/static/browser/shared.esm.js.map +1 -1
  59. package/static/browser/shared.js +154 -120
  60. package/static/browser/shared.js.map +1 -1
  61. package/static/browser/shared.min.js +1 -1
  62. package/static/browser/shared.min.js.map +1 -1
  63. package/static/schemas/standard/groups.json +1 -1
  64. package/static/schemas/twake/groups.json +53 -18
  65. package/static/schemas/twake/nomenclature/twakeAccountStatus.json +24 -8
  66. package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +22 -7
  67. package/static/schemas/twake/nomenclature/twakeDomain.json +21 -8
  68. package/static/schemas/twake/nomenclature/twakeListType.json +22 -7
  69. package/static/schemas/twake/nomenclature/twakeMailboxType.json +22 -7
  70. package/static/schemas/twake/nomenclature/twakeTitle.json +32 -12
  71. package/static/schemas/twake/organizations.json +29 -13
  72. package/static/schemas/twake/positions.json +13 -5
  73. package/static/schemas/twake/users.json +103 -38
@@ -1 +1 @@
1
- import{wantJson as t,jsonBody as e,created as i,tryMethod as s,multiStatus as r}from"../lib/expressFormatedResponses.js";import{getCompiledRegex as a,escapeDnValue as n,asyncHandler as o,escapeRegex as h,validateDnValue as d,isDummyMemberDn as c,normalizeDn as u,getParentDn as l,launchHooksChained as f,launchHooks as m,escapeLdapFilter as b,isDnInBranch as p,substringSearchFilter as w}from"../lib/utils.js";import{neverReturnAttributes as g,schemaAttribute as y,modifiedAttributeNames as $,missingRequiredAttribute as A,checkDnValues as v}from"../config/schema.js";import{BadRequestError as E,ConflictError as x,NotFoundError as N,HttpError as j,UnprocessableEntityError as C}from"../lib/errors.js";import{extractLdapCode as k}from"../lib/ldapCodes.js";import{loadSchemaFile as D}from"../lib/schemaFile.js";import O from"./plugin.js";class LdapFlat extends O{constructor(t,e){if(super(t),this.ldap=t.ldap,this.base=e.base,this.mainAttribute=e.mainAttribute,this.objectClass=e.objectClass,this.defaultAttributes=e.defaultAttributes||{},this.singularName=e.singularName,this.pluralName=e.pluralName,this.hookPrefix=e.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(this.ldap.declareObjectClasses(this.base,this.objectClass),e.schemaPath)try{this.schema=D(e.schemaPath,{config:this.config}),this.logger.info(`${this.singularName} schema loaded from ${e.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${e.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 g([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=y(this.schema,i)?.[1];throw new E(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 E(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let n=String(r);if(i.extract){const t=a(i.extract).exec(n);t&&(n=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(n=n.toLowerCase()),i.strip&&(n=n.replace(a(i.strip,"g"),"")),!n)throw new E(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);if(!await this.rdnExists(n))return n;if("suffix"!==i.onCollision)throw new x(`${this.singularName} "${n}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const e=`${n}-${t}`;if(!await this.rdnExists(e))return e}throw new x(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${n(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(e){e.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,o(async(e,i)=>{if(!t(e,i))return;const s={};let r,a;if(e.query.match&&"string"==typeof e.query.match&&e.query.attribute&&"string"==typeof e.query.attribute&&(s.filter=w(e.query.match,e.query.attribute)),e.query.attributes&&"string"==typeof e.query.attributes&&(s.attributes=e.query.attributes.split(",")),void 0!==e.query.limit){const t=e.query.limit;if(r="string"==typeof t&&/^\d+$/.test(t)?+t:NaN,!Number.isSafeInteger(r)||r<1)throw new E('Query parameter "limit" must be a positive integer')}try{a=await this.listEntriesLimited({...s,limit:r,req:e})}catch(t){if(void 0!==r||4!==k(t))throw t;throw new C("Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`.")}a.truncated&&i.set("X-Result-Truncated","true"),i.json(this.projectList(a.entries))})),e.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,o(async(t,e)=>this.apiGet(t,e))),e.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,o(async(t,e)=>this.apiAdd(t,e))),e.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,o(async(t,e)=>this.apiDelete(t,e))),e.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,o(async(t,e)=>this.apiModify(t,e))),e.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,o(async(t,e)=>this.apiMove(t,e))),e.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,o(async(t,e)=>this.apiRename(t,e)))}async apiGet(e,i){if(!t(e,i))return;const s=decodeURIComponent(e.params.id);try{const t=this.resolveDn(s),r=await this.ldap.search({paged:!1,scope:"base"},t,e);if(0===r.searchEntries.length)throw new N(`${this.singularName} not found`);i.json(this.project(r.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new N(`${this.singularName} not found`);throw t}}async apiAdd(t,s){const r=this.schema?.attributes[this.mainAttribute],a=Boolean(r?.generated&&r.generatedFrom),n=a?e(t,s):e(t,s,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 i(s,this.project(d[o]))}async apiDelete(e,i){if(!t(e,i))return;const r=decodeURIComponent(e.params.id);await s(i,this.deleteEntry.bind(this),r,e)}async apiModify(t,i){const r=e(t,i);if(!r)return;this.rejectForbiddenInput($(r));const a=decodeURIComponent(t.params.id);await s(i,this.modifyEntry.bind(this),a,r,t)}async apiMove(i,s){if(!t(i,s))return;const r=e(i,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(i.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new E("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,i);s.json({success:!0,...o})}async apiRename(i,s){if(!t(i,s))return;const a=e(i,s,"newId");if(!a)return;const n=a.newId;if("string"!=typeof n)throw new E("Missing or invalid newId in request body");if(this.rejectForbiddenInput(Object.keys(a).filter(t=>"newId"!==t)),new RegExp(`^${h(this.mainAttribute)}=`,"i").test(n))throw new E(`newId must be a ${this.mainAttribute} value, not a DN`);try{d(n,this.mainAttribute)}catch(t){throw new E(t instanceof Error?t.message:"Invalid newId")}if(!await this._validateOneChange(this.mainAttribute,n))throw new E(this.invalidValueMessage(this.mainAttribute));const o=decodeURIComponent(i.params.id),l=this.resolveDn(o),f=this.resolveDn(n),m=this.config.group_dummy_user;if(m&&(c(l,m)||c(f,m)))throw new x(`${m} is the configured placeholder member and cannot be renamed`);const b=u(l)===u(f),p=await this.entryExists(l),w=b?p:await this.entryExists(f),g=this.requirePlugin("ldapEnterpriseRules");let y=[];if(b){if(!p)throw new N(`${this.singularName} not found`)}else if(p){if(w)throw new x(`${this.singularName} "${n}" already exists`);y=await(g?.findReferences(l))||[],await this.renameEntry(l,f,i)}else{if(!w)throw new N(`${this.singularName} not found`);y=await(g?.findReferences(l))||[]}const $=g&&y.length>0?await g.rewriteReferences(y,l,f,i):{updated:0,failed:[]},A={success:0===$.failed.length,dn:f,referencesUpdated:$.updated};0!==$.failed.length?r(s,{...A,referencesFailed:$.failed}):s.json(A)}async entryExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(new RegExp(`^${h(this.mainAttribute)}=`,"i").test(t)){const e=l(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new E(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${n(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")):(d(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 f(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 j&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),t instanceof j)throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return m(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e,i){let s=this.resolveDn(t);const r=this.opNumber();if([s,e]=await f(this.registeredHooks[`${this.hookPrefix}modify`],[s,e,r]),e.add&&e.add[this.mainAttribute])throw new x(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new E(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new E(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new E(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(s,e);const a=await this.ldap.modify(s,e,i);return m(this.registeredHooks[`${this.hookPrefix}modifydone`],[s,e,r]),a}async renameEntry(t,e,i){/,/.test(t)||d(t,this.mainAttribute),/,/.test(e)||d(e,this.mainAttribute);let s=this.resolveDn(t),r=this.resolveDn(e);[s,r]=await f(this.registeredHooks[`${this.hookPrefix}rename`],[s,r]);const a=await(i?this.ldap.forRequest(i).rename(s,r):this.ldap.rename(s,r));return m(this.registeredHooks[`${this.hookPrefix}renamedone`],[s,r]),a}async deleteEntry(t,e){let i=this.resolveDn(t);i=await f(this.registeredHooks[`${this.hookPrefix}delete`],i);const s=await this.ldap.delete(i,e);return m(this.registeredHooks[`${this.hookPrefix}deletedone`],i),s}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 E(`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 N(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await f(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h,i),{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 N(`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 N)throw e;if(32===e.code)throw new N(`Organization ${t} not found`);throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries(t){return(await this.listEntriesLimited(t)).entries}async listEntriesLimited({filter:t,attributes:e,req:i,limit:s}){const r={paged:void 0===s||{pageSize:Math.min(s+2,100)},filter:t||"(objectClass=*)"};if(e&&e.length>0){const t=this.mainAttribute.toLowerCase();r.attributes=e.some(e=>e.toLowerCase()===t)?e:[...e,this.mainAttribute]}const a=function(t,e){const i={},s=new Set;return{entries:i,add(r){for(const a of r){const r=a[t],n=Array.isArray(r)?r[0]:r;if(null==n||""===n)continue;const o=String(n);if(!s.has(o)){if(void 0!==e&&s.size>=e)return!1;s.add(o)}i[o]=a}return!0}}}(this.mainAttribute,s);try{const t=await this.ldap.search(r,this.base,i);for await(const e of t)if(!a.add(e.searchEntries))return{entries:a.entries,truncated:!0};return{entries:a.entries,truncated:!1}}catch(t){if(void 0===s||4!==k(t))throw t}try{const t=await this.ldap.search({...r,paged:!1,sizeLimit:Math.min(s+1,2147483647)},this.base,i);a.add(t.searchEntries)}catch(t){if(4!==k(t))throw t}return{entries:a.entries,truncated:!0}}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=b(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 E(`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 E(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new E(this.invalidValueMessage(t));if(e.required&&!i&&!e.generated)throw new E(`Attribute "${t}" is required`)}const i=A(this.schema,e,this.server.loadedPlugins);if(i)throw new E(`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 E(`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 E(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 E(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 E(`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 E(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>p(s,t)))throw new E(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.system.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new E(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new E(`Field ${t} points to invalid or non-existent DN: ${s}`)}}"pointer"!==i.type&&await v(t,i,e,async t=>(await this.ldap.system.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?a(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
+ import{wantJson as t,jsonBody as e,created as i,tryMethod as s,multiStatus as r}from"../lib/expressFormatedResponses.js";import{getCompiledRegex as a,escapeDnValue as n,asyncHandler as o,escapeRegex as h,validateDnValue as c,isDummyMemberDn as d,normalizeDn as u,getParentDn as l,launchHooksChained as f,launchHooks as m,escapeLdapFilter as b,isDnInBranch as p,substringSearchFilter as w}from"../lib/utils.js";import{neverReturnAttributes as g,schemaAttribute as y,invalidValueMessage as $,modifiedAttributeNames as A,missingRequiredAttribute as v,checkDnValues as E}from"../config/schema.js";import{BadRequestError as x,ConflictError as N,NotFoundError as j,HttpError as C,UnprocessableEntityError as k}from"../lib/errors.js";import{extractLdapCode as O}from"../lib/ldapCodes.js";import{loadSchemaFile as D}from"../lib/schemaFile.js";import P from"./plugin.js";class LdapFlat extends P{constructor(t,e){if(super(t),this.ldap=t.ldap,this.base=e.base,this.mainAttribute=e.mainAttribute,this.objectClass=e.objectClass,this.defaultAttributes=e.defaultAttributes||{},this.singularName=e.singularName,this.pluralName=e.pluralName,this.hookPrefix=e.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(this.ldap.declareObjectClasses(this.base,this.objectClass),e.schemaPath)try{this.schema=D(e.schemaPath,{config:this.config}),this.logger.info(`${this.singularName} schema loaded from ${e.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${e.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 g([this.schema])}entryAttributes(){return["*",...Object.keys(this.schema?.attributes??{})]}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=y(this.schema,i)?.[1];throw new x(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 x(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let n=String(r);if(i.extract){const t=a(i.extract).exec(n);t&&(n=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(n=n.toLowerCase()),i.strip&&(n=n.replace(a(i.strip,"g"),"")),!n)throw new x(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);if(!await this.rdnExists(n))return n;if("suffix"!==i.onCollision)throw new N(`${this.singularName} "${n}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const e=`${n}-${t}`;if(!await this.rdnExists(e))return e}throw new N(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${n(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}invalidValueMessage(t){return $(t,this.schema?.attributes[t])}api(e){e.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,o(async(e,i)=>{if(!t(e,i))return;const s={};let r,a;if(e.query.match&&"string"==typeof e.query.match&&e.query.attribute&&"string"==typeof e.query.attribute&&(s.filter=w(e.query.match,e.query.attribute)),e.query.attributes&&"string"==typeof e.query.attributes&&(s.attributes=e.query.attributes.split(",")),void 0!==e.query.limit){const t=e.query.limit;if(r="string"==typeof t&&/^\d+$/.test(t)?+t:NaN,!Number.isSafeInteger(r)||r<1)throw new x('Query parameter "limit" must be a positive integer')}try{a=await this.listEntriesLimited({...s,limit:r,req:e})}catch(t){if(void 0!==r||4!==O(t))throw t;throw new k("Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`.")}a.truncated&&i.set("X-Result-Truncated","true"),i.json(this.projectList(a.entries))})),e.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,o(async(t,e)=>this.apiGet(t,e))),e.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,o(async(t,e)=>this.apiAdd(t,e))),e.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,o(async(t,e)=>this.apiDelete(t,e))),e.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,o(async(t,e)=>this.apiModify(t,e))),e.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,o(async(t,e)=>this.apiMove(t,e))),e.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,o(async(t,e)=>this.apiRename(t,e)))}async apiGet(e,i){if(!t(e,i))return;const s=decodeURIComponent(e.params.id);try{const t=this.resolveDn(s),r=await this.ldap.search({paged:!1,scope:"base",attributes:this.entryAttributes()},t,e);if(0===r.searchEntries.length)throw new j(`${this.singularName} not found`);i.json(this.project(r.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new j(`${this.singularName} not found`);throw t}}async apiAdd(t,s){const r=this.schema?.attributes[this.mainAttribute],a=Boolean(r?.generated&&r.generatedFrom),n=a?e(t,s):e(t,s,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 c=await this.searchEntriesByName(o,!1);return i(s,this.project(c[o]))}async apiDelete(e,i){if(!t(e,i))return;const r=decodeURIComponent(e.params.id);await s(i,this.deleteEntry.bind(this),r,e)}async apiModify(t,i){const r=e(t,i);if(!r)return;this.rejectForbiddenInput(A(r));const a=decodeURIComponent(t.params.id);await s(i,this.modifyEntry.bind(this),a,r,t)}async apiMove(i,s){if(!t(i,s))return;const r=e(i,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(i.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new x("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,i);s.json({success:!0,...o})}async apiRename(i,s){if(!t(i,s))return;const a=e(i,s,"newId");if(!a)return;const n=a.newId;if("string"!=typeof n)throw new x("Missing or invalid newId in request body");if(this.rejectForbiddenInput(Object.keys(a).filter(t=>"newId"!==t)),new RegExp(`^${h(this.mainAttribute)}=`,"i").test(n))throw new x(`newId must be a ${this.mainAttribute} value, not a DN`);try{c(n,this.mainAttribute)}catch(t){throw new x(t instanceof Error?t.message:"Invalid newId")}if(!await this._validateOneChange(this.mainAttribute,n))throw new x(this.invalidValueMessage(this.mainAttribute));const o=decodeURIComponent(i.params.id),l=this.resolveDn(o),f=this.resolveDn(n),m=this.config.group_dummy_user;if(m&&(d(l,m)||d(f,m)))throw new N(`${m} is the configured placeholder member and cannot be renamed`);const b=u(l)===u(f),p=await this.entryExists(l),w=b?p:await this.entryExists(f),g=this.requirePlugin("ldapEnterpriseRules");let y=[];if(b){if(!p)throw new j(`${this.singularName} not found`)}else if(p){if(w)throw new N(`${this.singularName} "${n}" already exists`);y=await(g?.findReferences(l))||[],await this.renameEntry(l,f,i)}else{if(!w)throw new j(`${this.singularName} not found`);y=await(g?.findReferences(l))||[]}const $=g&&y.length>0?await g.rewriteReferences(y,l,f,i):{updated:0,failed:[]},A={success:0===$.failed.length,dn:f,referencesUpdated:$.updated};0!==$.failed.length?r(s,{...A,referencesFailed:$.failed}):s.json(A)}async entryExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(new RegExp(`^${h(this.mainAttribute)}=`,"i").test(t)){const e=l(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new x(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${n(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 f(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 C&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),t instanceof C)throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return m(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e,i){let s=this.resolveDn(t);const r=this.opNumber();if([s,e]=await f(this.registeredHooks[`${this.hookPrefix}modify`],[s,e,r]),e.add&&e.add[this.mainAttribute])throw new N(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new x(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new x(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new x(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(s,e);const a=await this.ldap.modify(s,e,i);return m(this.registeredHooks[`${this.hookPrefix}modifydone`],[s,e,r]),a}async renameEntry(t,e,i){/,/.test(t)||c(t,this.mainAttribute),/,/.test(e)||c(e,this.mainAttribute);let s=this.resolveDn(t),r=this.resolveDn(e);[s,r]=await f(this.registeredHooks[`${this.hookPrefix}rename`],[s,r]);const a=await(i?this.ldap.forRequest(i).rename(s,r):this.ldap.rename(s,r));return m(this.registeredHooks[`${this.hookPrefix}renamedone`],[s,r]),a}async deleteEntry(t,e){let i=this.resolveDn(t);i=await f(this.registeredHooks[`${this.hookPrefix}delete`],i);const s=await this.ldap.delete(i,e);return m(this.registeredHooks[`${this.hookPrefix}deletedone`],i),s}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 x(`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 j(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await f(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h,i),{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 j(`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 j)throw e;if(32===e.code)throw new j(`Organization ${t} not found`);throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries(t){return(await this.listEntriesLimited(t)).entries}async listEntriesLimited({filter:t,attributes:e,req:i,limit:s}){const r={paged:void 0===s||{pageSize:Math.min(s+2,100)},filter:t||"(objectClass=*)"};if(e&&e.length>0){const t=this.mainAttribute.toLowerCase();r.attributes=e.some(e=>e.toLowerCase()===t)?e:[...e,this.mainAttribute]}const a=function(t,e){const i={},s=new Set;return{entries:i,add(r){for(const a of r){const r=a[t],n=Array.isArray(r)?r[0]:r;if(null==n||""===n)continue;const o=String(n);if(!s.has(o)){if(void 0!==e&&s.size>=e)return!1;s.add(o)}i[o]=a}return!0}}}(this.mainAttribute,s);try{const t=await this.ldap.search(r,this.base,i);for await(const e of t)if(!a.add(e.searchEntries))return{entries:a.entries,truncated:!0};return{entries:a.entries,truncated:!1}}catch(t){if(void 0===s||4!==O(t))throw t}try{const t=await this.ldap.search({...r,paged:!1,sizeLimit:Math.min(s+1,2147483647)},this.base,i);a.add(t.searchEntries)}catch(t){if(4!==O(t))throw t}return{entries:a.entries,truncated:!0}}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=b(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 x(`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 x(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new x(this.invalidValueMessage(t));if(e.required&&!i&&!e.generated)throw new x(`Attribute "${t}" is required`)}const i=v(this.schema,e,this.server.loadedPlugins);if(i)throw new x(`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 x(`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 x(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 x(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 x(`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 x(`Attribute "${t}" must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>p(s,t)))throw new x(`Attribute "${t}" must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.system.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new x(`Attribute "${t}" points to non-existent DN: ${s}`)}catch(e){throw new x(`Attribute "${t}" points to invalid or non-existent DN: ${s}`)}}"pointer"!==i.type&&await E(t,i,e,async t=>(await this.ldap.system.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?a(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
- import t from"node-fetch";import e from"p-limit";import r from"./plugin.js";class TwakePlugin extends r{constructor(t,r,s,i){if(super(t),this.retryAttempts=1,this.retryDelay=0,this.webadminUrl=this.config[r]||"",this.webadminToken=this.config[s]||"",!this.webadminUrl)throw new Error(`Twake plugin: ${r} is required`);this.webadminToken||this.logger.warn(`Twake plugin: No authentication token configured (${s}) - requests will be unauthenticated`);const a="number"==typeof this.config[i]?this.config[i]:10;this.requestLimit=e(a),this.logger.info(`Twake plugin HTTP request concurrency limit set to ${a}`),this.mailAttr=this.config.mail_attribute||"mail",this.displayNameAttr=this.config.display_name_attribute||"displayName"}createHeaders(t){const e={};return this.webadminToken&&(e.Authorization=`Bearer ${this.webadminToken}`),t&&(e["Content-Type"]=t),e}async callWebAdminApi(e,r,s,i,a,n,o=[]){const u={plugin:this.name,event:e,result:"error",dn:i,...n},l={method:s,headers:this.createHeaders(a?"application/json":void 0)};a&&(l.body=a);for(let s=1;;s++){try{const i=await this.requestLimit(()=>t(r,l));if(i.ok)this.logger.info({...u,result:"success",http_status:i.status,url:r});else{if(this.shouldRetry(i.status,o,s)){this.logger.warn({...u,result:"retry",attempt:s,http_status:i.status,http_status_text:i.statusText,url:r}),await this.waitBeforeRetry(s);continue}this.shouldIgnoreError(i.status,e)?this.logger.debug({...u,result:"ignored",http_status:i.status,http_status_text:i.statusText,url:r}):this.logger.error({...u,...s>1?{attempts:s}:{},http_status:i.status,http_status_text:i.statusText,url:r})}}catch(t){this.logger.error({...u,error:t,url:r})}return}}shouldRetry(t,e,r){return r<this.retryAttempts&&e.includes(t)}waitBeforeRetry(t){return new Promise(e=>setTimeout(e,this.retryDelay*t))}shouldIgnoreError(t,e){return 409===t&&e.includes("Alias")}attributeToString(t){return t?Array.isArray(t)?t.length>0?String(t[0]):null:String(t):null}async ldapGetAttributes(t,e){try{const r=e&&e.length>0?e:void 0,s=await this.server.ldap.search({paged:!1,scope:"base",attributes:r},t);return s.searchEntries&&s.searchEntries.length>0?s.searchEntries[0]:null}catch(e){return this.logger.debug(`Could not fetch attributes from DN ${t}: ${e}`),null}}getAliases(t){if(!t)return[];return(Array.isArray(t)?t:[t]).map(t=>Buffer.isBuffer(t)?t.toString("utf-8"):String(t)).map(t=>this.normalizeAlias(t))}normalizeAlias(t){return t.toLowerCase().startsWith("smtp:")?t.substring(5):t}extractMailDomain(t){const e=t.split("@");return 2===e.length?e[1]:null}extractDomainFromDn(t){const e=t.match(/dc=([^,]+)/gi);return e?e.map(t=>t.substring(3)).join("."):"example.com"}}export{TwakePlugin,TwakePlugin as default};
1
+ import t from"node-fetch";import e from"p-limit";import r from"./plugin.js";class TwakePlugin extends r{constructor(t,r,s,i){if(super(t),this.retryAttempts=1,this.retryDelay=0,this.webadminUrl=this.config[r]||"",this.webadminToken=this.config[s]||"",!this.webadminUrl)throw new Error(`Twake plugin: ${r} is required`);this.webadminToken||this.logger.warn(`Twake plugin: No authentication token configured (${s}) - requests will be unauthenticated`);const a="number"==typeof this.config[i]?this.config[i]:10;this.requestLimit=e(a),this.logger.info(`Twake plugin HTTP request concurrency limit set to ${a}`),this.mailAttr=this.config.mail_attribute||"mail",this.displayNameAttr=this.config.display_name_attribute||"displayName"}createHeaders(t){const e={};return this.webadminToken&&(e.Authorization=`Bearer ${this.webadminToken}`),t&&(e["Content-Type"]=t),e}async callWebAdminApi(e,r,s,i,a,n,o=[]){const u={plugin:this.name,event:e,result:"error",dn:i,...n},l={method:s,headers:this.createHeaders(a?"application/json":void 0)};a&&(l.body=a);for(let s=1;;s++){try{const i=await this.requestLimit(()=>t(r,l));if(i.ok)this.logger.info({...u,result:"success",http_status:i.status,url:r});else{if(this.shouldRetry(i.status,o,s)){this.logger.warn({...u,result:"retry",attempt:s,http_status:i.status,http_status_text:i.statusText,url:r}),await this.waitBeforeRetry(s);continue}this.shouldIgnoreError(i.status,e)?this.logger.debug({...u,result:"ignored",http_status:i.status,http_status_text:i.statusText,url:r}):this.logger.error({...u,...s>1?{attempts:s}:{},http_status:i.status,http_status_text:i.statusText,url:r})}}catch(t){this.logger.error({...u,error:t,url:r})}return}}shouldRetry(t,e,r){return r<this.retryAttempts&&e.includes(t)}waitBeforeRetry(t){return new Promise(e=>setTimeout(e,this.retryDelay*t))}shouldIgnoreError(t,e){return 409===t&&/alias/i.test(e)}attributeToString(t){return t?Array.isArray(t)?t.length>0?String(t[0]):null:String(t):null}async ldapGetAttributes(t,e){try{const r=e&&e.length>0?e:void 0,s=await this.server.ldap.search({paged:!1,scope:"base",attributes:r},t);return s.searchEntries&&s.searchEntries.length>0?s.searchEntries[0]:null}catch(e){return this.logger.debug(`Could not fetch attributes from DN ${t}: ${e}`),null}}getAliases(t){if(!t)return[];return(Array.isArray(t)?t:[t]).map(t=>Buffer.isBuffer(t)?t.toString("utf-8"):String(t)).map(t=>this.normalizeAlias(t))}normalizeAlias(t){return t.toLowerCase().startsWith("smtp:")?t.substring(5):t}extractMailDomain(t){const e=t.split("@");return 2===e.length?e[1]:null}extractDomainFromDn(t){const e=t.match(/dc=([^,]+)/gi);return e?e.map(t=>t.substring(3)).join("."):"example.com"}}export{TwakePlugin,TwakePlugin as default};
@@ -1 +1 @@
1
- import{fileURLToPath as e}from"url";import{join as _,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--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","identifiers",!0],["--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","mailQuotaSize"],["--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","identifiers"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes","identifiers"],["--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",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"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","identifiers"],["--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","identifiers"],["--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-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases","dns"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes","identifiers"],["--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-retry-attempts","DM_JAMES_RETRY_ATTEMPTS",5,"number"],["--james-retry-delay","DM_JAMES_RETRY_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches","dns"],["--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"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-space-base","DM_TWAKE_SPACE_BASE",""],["--twake-space-class","DM_TWAKE_SPACE_CLASS",["top","twakeSpace"],"array",null,"identifiers"],["--twake-space-display-name-attribute","DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-space-admin-attribute","DM_TWAKE_SPACE_ADMIN_ATTRIBUTE","twakeSpaceAdmin"],["--twake-space-editor-attribute","DM_TWAKE_SPACE_EDITOR_ATTRIBUTE","twakeSpaceEditor"],["--twake-space-viewer-attribute","DM_TWAKE_SPACE_VIEWER_ATTRIBUTE","twakeSpaceViewer"],["--twake-space-user-role-attribute","DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE",""],["--twake-space-exchange","DM_TWAKE_SPACE_EXCHANGE","space"],["--twake-space-sync-queue","DM_TWAKE_SPACE_SYNC_QUEUE","twake.space.sync.requested.ldap-rest"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array",null,"dns"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array",null,"identifiers"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array",null,"identifiers"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array",null,"dns"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array",null,"dns"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--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","identifiers"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes","identifiers"],["--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"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps","phrases"],["--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","phrases"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--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-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array",null,"identifiers"],["--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-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--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","identifiers"],["--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","identifiers"],["--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","identifiers"],["--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 e}from"url";import{join as _,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--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","identifiers",!0],["--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","mailQuotaSize"],["--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","identifiers"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes","identifiers"],["--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",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"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","identifiers"],["--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","identifiers"],["--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-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases","dns"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes","identifiers"],["--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-retry-attempts","DM_JAMES_RETRY_ATTEMPTS",5,"number"],["--james-retry-delay","DM_JAMES_RETRY_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches","dns"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--james-sync-forwards","DM_JAMES_SYNC_FORWARDS",!1,"boolean"],["--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"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-space-base","DM_TWAKE_SPACE_BASE",""],["--twake-space-class","DM_TWAKE_SPACE_CLASS",["top","twakeSpace"],"array",null,"identifiers"],["--twake-space-display-name-attribute","DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-space-admin-attribute","DM_TWAKE_SPACE_ADMIN_ATTRIBUTE","twakeSpaceAdmin"],["--twake-space-editor-attribute","DM_TWAKE_SPACE_EDITOR_ATTRIBUTE","twakeSpaceEditor"],["--twake-space-viewer-attribute","DM_TWAKE_SPACE_VIEWER_ATTRIBUTE","twakeSpaceViewer"],["--twake-space-user-role-attribute","DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE",""],["--twake-space-exchange","DM_TWAKE_SPACE_EXCHANGE","space"],["--twake-space-sync-queue","DM_TWAKE_SPACE_SYNC_QUEUE","twake.space.sync.requested.ldap-rest"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array",null,"dns"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array",null,"identifiers"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array",null,"identifiers"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array",null,"dns"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array",null,"dns"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--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","identifiers"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes","identifiers"],["--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"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps","phrases"],["--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","phrases"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--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-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array",null,"identifiers"],["--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-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--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","identifiers"],["--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","identifiers"],["--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","identifiers"],["--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 +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
+ import{BadRequestError as t}from"../lib/errors.js";import{isDnInBranch as e,getCompiledRegex as r}from"../lib/utils.js";function localizedText(t,e="en"){if(null==t)return;if("string"==typeof t)return t||void 0;const r=e.split("-")[0].toLowerCase();for(const n of[e,r,"en"])if(t[n])return t[n];return Object.values(t).find(Boolean)}function invalidValueMessage(t,e){const r=localizedText(e?.hint||e?.items?.hint);return`Invalid value for attribute "${t}"${r?`: ${r}`:""}`}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(`Attribute "${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(`Attribute "${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(`Attribute "${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,invalidValueMessage,localizedText,matchesPattern,missingRequiredAttribute,modifiedAttributeNames,neverReturnAttributes,roleAttribute,schemaAttribute,withoutAttributes};
@@ -1 +1 @@
1
- import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as s,Control as n}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{normalizeDn as o,isDnInBranch as c,launchHooksChained as l,launchHooks as h,escapeDnValue as d}from"./utils.js";import{changeContext as p}from"./changeContext.js";import{runOperation as u,outsideOperation as f}from"./operation.js";import{NotFoundError as g,ConflictError as y,BadRequestError as m}from"./errors.js";import{extractLdapCode as w,isSchemaRefusal as b}from"./ldapCodes.js";import{SchemaIndex as C,parseSchema as A}from"./ldapSchema.js";const v={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),s=w(t);return void 0===s?i:withCode(i,s)}function withCode(e,t){return e.code=t,e}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]});function ldapRefusal(e,t,i){return withCode(new m(`${e}: ${String(i)}`),t)}function ldapWriteError(e,t,i){const s=w(i);return b(s)?ldapRefusal(`The directory refused this write to ${t}`,s,i):ldapError(e,i)}function ldapRenameError(e,t,i,s){const n=w(s);return 68===n?withCode(new y(`Entry ${i} already exists`),n):b(n)?ldapRefusal(`The directory refused to rename ${t} to ${i}`,n,s):ldapError(e,s)}class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.declaredClasses=new Map,this.schemaUnavailable=!1,this.classWarnings=new a({max:1e4}),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=r(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=this.config.ldap_cache_ttl,n="string"==typeof s?parseInt(s,10)||0:s??0,o=1e3*Math.max(n,0);if(this.cacheEnabled=o>0,this.searchCache=new a({max:i,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new a({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");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_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 s=(this.currentUrlIndex+i)%this.ldapUrls.length,n=this.ldapUrls[s];try{this.logger.debug(`Attempting connection to ${n}`);const e={...this.options,url:n,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await i.bind(this.dn,this.pwd),s!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${n}`),this.currentUrlIndex=s),i}catch(t){this.logger.warn(`Failed to connect to ${n}: ${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 s=this.availableConnections[i];if(e-s.createdAt>this.connectionTtl){t.push(s),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(s);-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),s=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",n,i,s,!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}`,s=[];for(const e of this.searchCache.keys()){const n=foldDn(e.split("\0")[0]);(n===t||n.endsWith(i))&&s.push(e)}for(const e of s)this.searchCache.delete(e)}get system(){return this}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i,s)=>this.modify(t,i,e,s),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}declareObjectClasses(e,t,i=!1){const s=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);if(!e||0===s.length)return;const n=JSON.stringify([o(e),s.map(e=>e.toLowerCase()).sort(),i]);this.declaredClasses.set(n,{base:e,classes:s,includeBase:i})}declaredFor(e){let t=0,i=[];for(const s of this.declaredClasses.values()){if(!c(e,s.base))continue;const n=o(s.base);if(!s.includeBase&&o(e)===n)continue;const a=n.split(",").length;a>t&&(t=a,i=[]),a===t&&i.push(...s.classes)}const s=new Set;return i.filter(e=>{const t=e.toLowerCase();return!s.has(t)&&(s.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t){if(!1===this.config.ldap_flat_auto_repair)return[];const i=this.declaredFor(e);if(0===i.length)return[];const s=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let n;try{n=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>n.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:i}=n.resolveAttributes(e);return new Set([...t,...i].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),a=[...valued(t.add),...valued(t.replace)],r=i.filter(e=>{const t=n.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const i=allowedBy([e]);return a.some(e=>i.has(attributeKey(e)))});if(0===r.length)return[];let o;try{o=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...n.resolveAttributes(r).must]},e)).searchEntries[0]}catch{return[]}if(!o)return[];const c=o.objectClass??o.objectclass,l=(Array.isArray(c)?c:c?[c]:[]).map(String),h=allowedBy(l);let d=a.filter(e=>n.getAttributeType(e)&&!h.has(attributeKey(e)));if(0===d.length)return[];const p=new Set(s.map(attributeKey)),u=new Set([...valued(o).filter(e=>!p.has(attributeKey(e))),...a].map(attributeKey)),{auxiliary:f,structural:g}=n.missingClasses(i,l),y=[];for(const t of f){const i=t.names[0]??t.oid,s=allowedBy([i]),a=d.filter(e=>s.has(attributeKey(e)));if(0===a.length)continue;const r=n.resolveAttributes([i]).must.filter(e=>!u.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${i} for ${a.join(", ")}, and cannot be given it without ${r.join(", ")}`):(y.push(i),d=d.filter(e=>!a.includes(e)))}const m=g.filter(e=>{const t=allowedBy([e]);return d.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${d.join(", ")}: its schema declares ${m.join(", ")}, a structural class an existing entry cannot be given`),y.length>0&&this.logger.info(`${e}: adding ${y.join(", ")}, which the change needs and its schema declares`),y}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,i=Array.isArray(t)?t[0]:t;if(i)return String(i)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new g(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new g(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},i=new C(A({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:i,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${i.schema.objectClasses.length} object classes, ${i.schema.attributeTypes.length} attribute types`),i})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}filterForCaller(e,t,i){if(!this.parent.hooks.ldapsearchfilter)return e;const s=this.parent.hooks.ldapsearchfilter,one=async e=>{const[n]=await l(s,[e,t,i]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,i){let s={...v,...e};s=await l(this.parent.hooks.ldapsearchopts,s),[t,s]=await l(this.parent.hooks.ldapsearchrequest,[t,s,i]);const n=this.cacheEnabled&&!s.paged&&"base"===s.scope,a=n?this.getCacheKey(t,s):"",r=this.cacheGeneration;if(n){const e=this.searchCache.get(a);if(e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),i,s)}const o=await this.acquireConnection();try{let e=s.paged?o.client.searchPaginated(t,s):o.client.search(t,s);if(e=await l(this.parent.hooks.ldapsearchresult,e),n){const t=e;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(t,i,s)}return s.paged?this.filterForCaller(this.wrapPaginatedSearch(e,o),i,s):this.filterForCaller(e,i,s)}finally{s.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,t,s){e=this.setDn(e),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const n={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?n[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?n[e]=i.map(e=>e.toString()):n[e]=i;[e,t]=await l(this.parent.hooks.ldapaddrequest,[e,n,s]);const a=[];for(const[e,s]of Object.entries(t)){const t=Array.isArray(s)?s:[s];a.push(new i({type:e,values:t}))}const r=await this.acquireConnection();try{this.invalidateCache(e),await r.client.add(e,a),this.invalidateCache(e),h(this.parent.hooks.ldapadddone,[e,t],p(s))}catch(t){throw function(e,t){const i=w(t);return 68===i?withCode(new y(`Entry ${e} already exists`),i):20===i?ldapRefusal(`The directory refused this write to ${e}`,i,t):ldapWriteError("LDAP add error",e,t)}(e,t)}finally{this.releaseConnection(r)}return await h(this.parent.hooks.ldapaddafter,[e,t],p(s)),!0}async modify(e,t,i,s={}){const n=this.opNumber(),a=[];try{return await u(n,()=>this.applyModify(e,t,i,s,n,a))}finally{this.endOperation(a,this.parent.hooks.ldapmodifyend,n)}}async applyModify(e,t,a,r,o,c){e=this.setDn(e);const d=[];if([e,t]=await l(this.parent.hooks.ldapmodifyrequest,[e,t,o,a]),t.add)for(const[e,n]of Object.entries(t.add))d.push(new s({operation:"add",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.replace)for(const[e,n]of Object.entries(t.replace))d.push(new s({operation:"replace",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&d.push(new s({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,n]of Object.entries(t.delete)){const t=new s({operation:"delete",modification:new i(n?{type:e,values:Array.isArray(n)?n:[n]}:{type:e})});d.push(t)}if(0===d.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`),c.push(h(this.parent.hooks.ldapmodifydone,[e,{},o],r.context??p(a))),!1}{const l=await this.classRepair(e,t),u=l.length>0?new s({operation:"add",modification:new i({type:"objectClass",values:l})}):void 0;u&&d.push(u);const f=await this.acquireConnection();try{this.invalidateCache(e);const i=r.permissive?[new n("1.2.840.113556.1.4.1413",{critical:!1})]:void 0;try{await f.client.modify(e,d,i)}catch(t){if(!u||20!==t?.code)throw t;d.splice(d.indexOf(u),1),await f.client.modify(e,d,i)}return this.invalidateCache(e),c.push(h(this.parent.hooks.ldapmodifydone,[e,t,o],r.context??p(a))),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(d)}`),function(e,t){const i=w(t);return 65!==i&&20!==i?ldapWriteError("LDAP modify error",e,t):withCode(new y(`Entry ${e} cannot hold this change: ${String(t)}`),i)}(e,t)}finally{this.releaseConnection(f)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=this.opNumber(),n=[];try{return await u(s,()=>this.applyRename(e,t,i,n))}finally{this.endOperation(n,this.parent.hooks.ldaprenameend,s)}}async applyRename(e,t,i,s){[e,t]=await l(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),s.push(h(this.parent.hooks.ldaprenamedone,[e,t],p(i))),!0}catch(i){throw ldapRenameError("LDAP rename error",e,t,i)}finally{this.releaseConnection(n)}}async move(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),f(()=>h(this.parent.hooks.ldaprenamedone,[e,t],p(i))),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(i){throw ldapRenameError("LDAP move error",e,t,i)}finally{this.releaseConnection(s)}}async delete(e,t,i={}){const s=(Array.isArray(e)?e:[e]).map(e=>this.setDn(e)),n=this.opNumber(),a=[];try{return await u(n,()=>this.applyDelete(s,t,i,a))}finally{this.endOperation(a,this.parent?.hooks.ldapdeleteend,n)}}async applyDelete(e,t,i,s){[e]=await l(this.parent?.hooks.ldapdeleterequest,[e,t]);const n=await this.acquireConnection();try{for(const a of e){try{this.invalidateCache(a),await n.client.del(a),this.invalidateCache(a)}catch(e){throw ldapError("LDAP delete error",e)}s.push(h(this.parent.hooks.ldapdeletedone,a,i.context??p(t)))}return!0}finally{this.releaseConnection(n)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${d(e)},${this.base}`,e}endOperation(e,t,...i){t?.length&&Promise.all(e).then(()=>h(t,...i))}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 s,Control as n}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{normalizeDn as o,isDnInBranch as c,launchHooksChained as l,launchHooks as h,escapeDnValue as d}from"./utils.js";import{changeContext as p}from"./changeContext.js";import{runOperation as u,outsideOperation as f}from"./operation.js";import{NotFoundError as g,BadRequestError as y,ConflictError as m}from"./errors.js";import{extractLdapCode as w,isSchemaRefusal as b}from"./ldapCodes.js";import{SchemaIndex as C,parseSchema as A}from"./ldapSchema.js";const v={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),s=w(t);return void 0===s?i:withCode(i,s)}function withCode(e,t){return e.code=t,e}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]});function ldapRefusal(e,t,i){return withCode(new y(`${e}: ${String(i)}`),t)}function assertHasValue(e,t){if(Array.isArray(t)&&0===t.length)throw new y(`Attribute "${e}" has no value`)}function ldapWriteError(e,t,i){const s=w(i);return b(s)?ldapRefusal(`The directory refused this write to ${t}`,s,i):ldapError(e,i)}function ldapRenameError(e,t,i,s){const n=w(s);return 68===n?withCode(new m(`Entry ${i} already exists`),n):b(n)?ldapRefusal(`The directory refused to rename ${t} to ${i}`,n,s):ldapError(e,s)}class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.declaredClasses=new Map,this.schemaUnavailable=!1,this.classWarnings=new a({max:1e4}),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=r(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=this.config.ldap_cache_ttl,n="string"==typeof s?parseInt(s,10)||0:s??0,o=1e3*Math.max(n,0);if(this.cacheEnabled=o>0,this.searchCache=new a({max:i,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new a({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");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_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 s=(this.currentUrlIndex+i)%this.ldapUrls.length,n=this.ldapUrls[s];try{this.logger.debug(`Attempting connection to ${n}`);const e={...this.options,url:n,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await i.bind(this.dn,this.pwd),s!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${n}`),this.currentUrlIndex=s),i}catch(t){this.logger.warn(`Failed to connect to ${n}: ${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 s=this.availableConnections[i];if(e-s.createdAt>this.connectionTtl){t.push(s),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(s);-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),s=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",n,i,s,!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}`,s=[];for(const e of this.searchCache.keys()){const n=foldDn(e.split("\0")[0]);(n===t||n.endsWith(i))&&s.push(e)}for(const e of s)this.searchCache.delete(e)}get system(){return this}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i,s)=>this.modify(t,i,e,s),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}declareObjectClasses(e,t,i=!1){const s=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);if(!e||0===s.length)return;const n=JSON.stringify([o(e),s.map(e=>e.toLowerCase()).sort(),i]);this.declaredClasses.set(n,{base:e,classes:s,includeBase:i})}declaredFor(e){let t=0,i=[];for(const s of this.declaredClasses.values()){if(!c(e,s.base))continue;const n=o(s.base);if(!s.includeBase&&o(e)===n)continue;const a=n.split(",").length;a>t&&(t=a,i=[]),a===t&&i.push(...s.classes)}const s=new Set;return i.filter(e=>{const t=e.toLowerCase();return!s.has(t)&&(s.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t){if(!1===this.config.ldap_flat_auto_repair)return[];const i=this.declaredFor(e);if(0===i.length)return[];const s=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let n;try{n=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>n.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:i}=n.resolveAttributes(e);return new Set([...t,...i].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),a=[...valued(t.add),...valued(t.replace)],r=i.filter(e=>{const t=n.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const i=allowedBy([e]);return a.some(e=>i.has(attributeKey(e)))});if(0===r.length)return[];let o;try{o=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...n.resolveAttributes(r).must]},e)).searchEntries[0]}catch{return[]}if(!o)return[];const c=o.objectClass??o.objectclass,l=(Array.isArray(c)?c:c?[c]:[]).map(String),h=allowedBy(l);let d=a.filter(e=>n.getAttributeType(e)&&!h.has(attributeKey(e)));if(0===d.length)return[];const p=new Set(s.map(attributeKey)),u=new Set([...valued(o).filter(e=>!p.has(attributeKey(e))),...a].map(attributeKey)),{auxiliary:f,structural:g}=n.missingClasses(i,l),y=[];for(const t of f){const i=t.names[0]??t.oid,s=allowedBy([i]),a=d.filter(e=>s.has(attributeKey(e)));if(0===a.length)continue;const r=n.resolveAttributes([i]).must.filter(e=>!u.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${i} for ${a.join(", ")}, and cannot be given it without ${r.join(", ")}`):(y.push(i),d=d.filter(e=>!a.includes(e)))}const m=g.filter(e=>{const t=allowedBy([e]);return d.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${d.join(", ")}: its schema declares ${m.join(", ")}, a structural class an existing entry cannot be given`),y.length>0&&this.logger.info(`${e}: adding ${y.join(", ")}, which the change needs and its schema declares`),y}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,i=Array.isArray(t)?t[0]:t;if(i)return String(i)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new g(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new g(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},i=new C(A({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:i,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${i.schema.objectClasses.length} object classes, ${i.schema.attributeTypes.length} attribute types`),i})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}filterForCaller(e,t,i){if(!this.parent.hooks.ldapsearchfilter)return e;const s=this.parent.hooks.ldapsearchfilter,one=async e=>{const[n]=await l(s,[e,t,i]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,i){let s={...v,...e};s=await l(this.parent.hooks.ldapsearchopts,s),[t,s]=await l(this.parent.hooks.ldapsearchrequest,[t,s,i]);const n=this.cacheEnabled&&!s.paged&&"base"===s.scope,a=n?this.getCacheKey(t,s):"",r=this.cacheGeneration;if(n){const e=this.searchCache.get(a);if(e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),i,s)}const o=await this.acquireConnection();try{let e=s.paged?o.client.searchPaginated(t,s):o.client.search(t,s);if(e=await l(this.parent.hooks.ldapsearchresult,e),n){const t=e;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(t,i,s)}return s.paged?this.filterForCaller(this.wrapPaginatedSearch(e,o),i,s):this.filterForCaller(e,i,s)}finally{s.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,t,s){e=this.setDn(e),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const n={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?n[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?n[e]=i.map(e=>e.toString()):n[e]=i;[e,t]=await l(this.parent.hooks.ldapaddrequest,[e,n,s]);const a=[];for(const[e,s]of Object.entries(t)){assertHasValue(e,s);const t=Array.isArray(s)?s:[s];a.push(new i({type:e,values:t}))}const r=await this.acquireConnection();try{this.invalidateCache(e),await r.client.add(e,a),this.invalidateCache(e),h(this.parent.hooks.ldapadddone,[e,t],p(s))}catch(t){throw function(e,t){const i=w(t);return 68===i?withCode(new m(`Entry ${e} already exists`),i):20===i?ldapRefusal(`The directory refused this write to ${e}`,i,t):ldapWriteError("LDAP add error",e,t)}(e,t)}finally{this.releaseConnection(r)}return await h(this.parent.hooks.ldapaddafter,[e,t],p(s)),!0}async modify(e,t,i,s={}){const n=this.opNumber(),a=[];try{return await u(n,()=>this.applyModify(e,t,i,s,n,a))}finally{this.endOperation(a,this.parent.hooks.ldapmodifyend,n)}}async applyModify(e,t,a,r,o,c){e=this.setDn(e);const d=[];if([e,t]=await l(this.parent.hooks.ldapmodifyrequest,[e,t,o,a]),t.add)for(const[e,n]of Object.entries(t.add))assertHasValue(e,n),d.push(new s({operation:"add",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.replace)for(const[e,n]of Object.entries(t.replace))d.push(new s({operation:"replace",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&d.push(new s({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,n]of Object.entries(t.delete)){const t=new s({operation:"delete",modification:new i(n?{type:e,values:Array.isArray(n)?n:[n]}:{type:e})});d.push(t)}if(0===d.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`),c.push(h(this.parent.hooks.ldapmodifydone,[e,{},o],r.context??p(a))),!1}{const l=await this.classRepair(e,t),u=l.length>0?new s({operation:"add",modification:new i({type:"objectClass",values:l})}):void 0;u&&d.push(u);const f=await this.acquireConnection();try{this.invalidateCache(e);const i=r.permissive?[new n("1.2.840.113556.1.4.1413",{critical:!1})]:void 0;try{await f.client.modify(e,d,i)}catch(t){if(!u||20!==t?.code)throw t;d.splice(d.indexOf(u),1),await f.client.modify(e,d,i)}return this.invalidateCache(e),c.push(h(this.parent.hooks.ldapmodifydone,[e,t,o],r.context??p(a))),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(d)}`),function(e,t){const i=w(t);return 65!==i&&20!==i?ldapWriteError("LDAP modify error",e,t):withCode(new m(`Entry ${e} cannot hold this change: ${String(t)}`),i)}(e,t)}finally{this.releaseConnection(f)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=this.opNumber(),n=[];try{return await u(s,()=>this.applyRename(e,t,i,n))}finally{this.endOperation(n,this.parent.hooks.ldaprenameend,s)}}async applyRename(e,t,i,s){[e,t]=await l(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),s.push(h(this.parent.hooks.ldaprenamedone,[e,t],p(i))),!0}catch(i){throw ldapRenameError("LDAP rename error",e,t,i)}finally{this.releaseConnection(n)}}async move(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),f(()=>h(this.parent.hooks.ldaprenamedone,[e,t],p(i))),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(i){throw ldapRenameError("LDAP move error",e,t,i)}finally{this.releaseConnection(s)}}async delete(e,t,i={}){const s=(Array.isArray(e)?e:[e]).map(e=>this.setDn(e)),n=this.opNumber(),a=[];try{return await u(n,()=>this.applyDelete(s,t,i,a))}finally{this.endOperation(a,this.parent?.hooks.ldapdeleteend,n)}}async applyDelete(e,t,i,s){[e]=await l(this.parent?.hooks.ldapdeleterequest,[e,t]);const n=await this.acquireConnection();try{for(const a of e){try{this.invalidateCache(a),await n.client.del(a),this.invalidateCache(a)}catch(e){throw ldapError("LDAP delete error",e)}s.push(h(this.parent.hooks.ldapdeletedone,a,i.context??p(t)))}return!0}finally{this.releaseConnection(n)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${d(e)},${this.base}`,e}endOperation(e,t,...i){t?.length&&Promise.all(e).then(()=>h(t,...i))}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
@@ -1 +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
+ import e from"../../abstract/plugin.js";import{roleAttribute as t,hasRole as n,invalidValueMessage as r}from"../../config/schema.js";import{normalizeDn as i,isDnInBranch as a,getParentDn as s,rdnValue as o,escapeLdapFilter as c,isDummyMemberDn as u}from"../../lib/utils.js";import{BadRequestError as l,ConflictError as d}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 i(e)}catch{return e}}const f={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 l(`"${t}" is not a valid size`);const r=(n[2]||"B").toUpperCase();return Math.round(parseFloat(n[1])*f[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=i(e);let n,r=-1;for(const o of this.bindings()){const c=i(o.base);if(t===c)continue;if(!a(e,o.base))continue;if(!o.subtree&&i(s(e))!==c)continue;const u=c.split(",").length;u>r&&(n=o,r=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,i,a){for(const[t,n]of Object.entries(e.schema.attributes)){if(a&&void 0===i[t]&&void 0!==n.default&&(n.generated||n.normalize)&&(i[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(i[t]))if(!await this.readEntry(e))throw new l(`Schema default for "${t}" points to a non-existent entry: ${e}`);if("byteSize"===n.normalize&&void 0!==i[t]){const e=valueList(i[t]);try{i[t]=Array.isArray(i[t])?e.map(e=>String(parseByteSize(e))):String(parseByteSize(e[0]))}catch{throw new l(r(t,n))}}}const c=t(e.schema,"organizationPath");if(!c)return;if(!e.schema.attributes[c]?.generated&&void 0!==i[c])return;const u=t(e.schema,"organizationLink");if(u){const e=valueList(i[u])[0];if(!e)return;const t=await this.organizationPath(e);if(void 0===t)throw new l(`Organization ${e} has no ${c}, so "${c}" cannot be computed`);return void(i[c]=t)}if(!a)return;const d=valueList(i[t(e.schema,"identifier")||""])[0]||o(n);if(!d)return;const f=s(n),h=(this.topOrganization||this.config.ldap_base||"").toLowerCase(),m=!h||f.toLowerCase().endsWith(`,${h}`)?await this.organizationPath(f):void 0,p=this.config.ldap_organization_path_separator||" / ";i[c]=m?`${m}${p}${d}`:d}async checkUnique(e,t,n,r,i){const a="object"==typeof n.unique?n.unique:{},s=[t,...a.attributes||[]],o=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}=${c(n)})`).join("");let u=s.length>1?`(|${r})`:r;a.filter&&(u=`(&${a.filter}${u})`);for(const r of o){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 d(`${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 d(`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 a=valueList(n[i])[0]||valueList(r?.[i])[0]||void 0;if(!a)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=a;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=s(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 i=startOfUtcDay();for(const a of n){const n=parseDirectoryDate(a);if(!n)throw new l(r(e,t));if(n.getTime()<i)throw new l(`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=>!u(e,this.dummyMember));if(a.length>0)throw new d(`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 d(`Cannot delete ${e}: still referenced by another entry (${t.name})`)}}referringAttributes(e){const t=s(e),r=[];for(const e of this.bindings())for(const[i,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=>a(t,e))||r.push({binding:e,name:i,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}=${c(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 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
+ import r from"../../abstract/plugin.js";import{HttpError as e}from"../../lib/errors.js";import{launchHooksChained as s,launchHooks as t,parseDn as a,normalizeDn as n,unescapeDnValue as i}from"../../lib/utils.js";class ExternalUsersInGroups extends r{constructor(r){super(r),this.name="externalUsersInGroups",this.roles=["consistency","configurable"],this.dependencies={ldapGroups:"core/ldap/groups"},this.hooks={ldapgroupvalidatemembers:async([r,a])=>(await Promise.all(a.map(r=>this.server.ldap.queryLimit(async()=>{const a=this.externalAddress(r);if(null===a)return!1;try{return await this.ldap.search({paged:!1,scope:"base"},r),!0}catch{if(!a)throw new Error(`Malformed member ${r}`);const n=a.split("@")[1]?.toLowerCase();if(n&&this.managedDomains().includes(n))throw new Error(`Cannot create external user with managed domain: ${a}`);let i={objectClass:this.config.external_branch_class||this.config.user_class,mail:a,[this.config.ldap_groups_main_attribute]:a,sn:a};[r,i]=await s(this.registeredHooks.externaluserentry,[r,i]);try{return await this.ldap.add(r,i),t(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,a])},this.ldap=r.ldap}externalAddress(r){const e=this.config.external_members_branch;if(!e)return null;const[s,...t]=a(r);if(!s||n(t.join(","))!==n(e))return null;const o=s.indexOf("=");if(o<0||"mail"!==s.slice(0,o).trim().toLowerCase())return null;const l=s.slice(o+1).trim();return/(^|[^\\])(\\\\)*\+/.test(l)?null:i(l)}managedDomains(){const r=this.config.mail_domain;return Array.isArray(r)?r.map(r=>String(r).toLowerCase()):[]}getConfigApiData(){return{enabled:!0,branch:this.config.external_members_branch,rdnAttribute:"mail",managedDomains:this.managedDomains()}}}export{ExternalUsersInGroups as default};
@@ -1 +1 @@
1
- import e from"../../abstract/plugin.js";import{wantJson as t,jsonBody as r,tryMethod as i,ok as s}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as a}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as o,schemaUrl as n}from"../../lib/schemaFile.js";import{asyncHandler as d,isDummyMemberDn as h,escapeDnValue as c,validateDnValue as p,launchHooksChained as m,launchHooks as l,escapeLdapFilter as u,organizationLink as f,substringSearchFilter as g}from"../../lib/utils.js";import{NotFoundError as b,BadRequestError as w,HttpError as y}from"../../lib/errors.js";import{assertClientMaySet as $,modifiedAttributeNames as v,missingRequiredAttribute as _,checkDnValues as E,matchesPattern as G}from"../../config/schema.js";class LdapGroups extends e{constructor(e){if(super(e),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeletedone:async e=>{const t=Array.isArray(e)?e:[e];this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)})}},this.ldap=e.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");if(this.config.ldap_group_base&&e.ldap.declareObjectClasses(this.base,this.config.group_class||[]),this.config.group_schema){const e=this.config.group_schema;o(e,{config:this.config}).then(e=>{this.schema=this.adaptSchema(e),this.logger.debug("Group schema loaded")}).catch(t=>this.logger.error(`Failed to load group schema from ${e}: ${t}`))}}adaptSchema(e){return e}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,d(async(e,r)=>{if(!t(e,r))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new w("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=g(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new w("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 a=await this.listGroups({...i,req:e});return s(r,this.hideNeverReturn(a))})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,d(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,d(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,d(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,d(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,d(async(e,t)=>{const s=decodeURIComponent(e.params.cn);if(!s)throw new w("cn is required");const a=r(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new w("member must be a string or string array");await i(t,this.addMember.bind(this),s,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,d(async(e,r)=>{if(!t(e,r))return;const s=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!s||!a)throw new w("cn and member are required");await i(r,this.deleteMember.bind(this),s,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,d(async(e,s)=>{if(!t(e,s))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new w("cn is required");const o=r(e,s,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new w("targetOrgDn must be a string");await i(s,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,d(async(e,s)=>{if(!t(e,s))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new w("cn is required");const o=r(e,s,"newCn");if(o){if("string"!=typeof o.newCn)throw new w("newCn must be a string");await i(s,this.renameGroup.bind(this),a,o.newCn,e)}}))}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,r){if(!t(e,r))return;const i=decodeURIComponent(e.params.cn);try{const t=/,/.test(i)?i:`${this.cn}=${c(i)},${this.base}`,s=await this.ldap.search({paged:!1,scope:"base"},t,e);if(0===s.searchEntries.length)throw new b("Group not found");r.json(this.hideNeverReturn(this.withoutDummyMember(s.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new b("Group not found");throw e}}async apiAdd(e,t,...s){const a=r(e,t,...s);if(!a)return;$(this.schema,Object.keys(a),this.cn);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 i(t,this.addGroup.bind(this),o,n,d,e)}async apiDelete(e,r){if(!t(e,r))return;const s=decodeURIComponent(e.params.cn);if(!s)throw new w("cn is required");await i(r,this.deleteGroup.bind(this),s,e)}async apiModify(e,t){const s=r(e,t);if(!s)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new w("cn is required");$(this.schema,v(s));const o=Object.fromEntries(Object.entries(s).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await i(t,this.modifyGroup.bind(this),a,o,e)}async addGroup(e,t=[],r={},i){let s;new RegExp(`^${this.cn}=`).test(e)?(s=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(p(e,this.cn),s=`${this.cn}=${c(e)},${this.base}`),await this.validateMembers(s,t),await this.validateNewGroup(s,{objectClass:this.config.group_class,cn:e,member:t,...r});const a=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||a.includes("twakeGroup")||a.includes("twakeStaticGroup")||a.push("twakeGroup");let o,n={objectClass:a,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&n.member.push(...t),[s,n]=await m(this.registeredHooks.ldapgroupadd,[s,n]);try{o=await this.ldap.add(s,n,i)}catch(e){if(e instanceof y)throw e;throw new Error(`Failed to add group ${s}: ${e}`)}return l(this.registeredHooks.ldapgroupadddone,[s,n]),o}async modifyGroup(e,t,r){let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;const s=this.opNumber();if([i,t]=await m(this.registeredHooks.ldapgroupmodify,[i,t,s]),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(i,t);const a=await this.ldap.modify(i,t,r);return l(this.registeredHooks.ldapgroupmodifydone,[i,t,s]),a}async renameGroup(e,t,r){/,/.test(e)||p(e,this.cn),/,/.test(t)||p(t,this.cn);let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,s=/,/.test(t)?t:`${this.cn}=${c(t)},${this.base}`;[i,s]=await m(this.registeredHooks.ldapgrouprename,[i,s]);const a=await this.ldap.rename(i,s,r);return l(this.registeredHooks.ldapgrouprenamedone,[i,s]),a}async deleteGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;r=await m(this.registeredHooks.ldapgroupdelete,r);const i=await this.ldap.delete(r,t);return l(this.registeredHooks.ldapgroupdeletedone,r),i}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await m(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{if(e instanceof y)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 m(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new w("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{if(e instanceof y)throw e;throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${u(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=>{if(65===a(r)&&this.config.group_dummy_user)return this.ldap.modify(t.dn,{add:{member:this.config.group_dummy_user},delete:{member:e}});throw r}).catch(r=>{const i=a(r);16!==i&&32!==i&&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],d=f(n[i]);if(!d)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(d===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 p=h.searchEntries[0][s];if(!p)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:p}},r),this.logger.info(`Group ${a} moved from ${d} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t,req:r}={}){const i=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base,r).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let s={};for await(const e of i)for(const t of e.searchEntries){const e=t[this.cn];if(Array.isArray(e)&&0===e.length)continue;e&&(s[e]=this.withoutDummyMember(t))}return s=await m(this.registeredHooks._ldapgrouplist,s),s}async searchGroupsByName(e,t=!1,r=[this.cn,"member"],i){const s=u(e),a=t?`(${this.cn}=*${s}*)`:`(${this.cn}=${s})`;return await this.listGroups({filter:a,attributes:r,req:i})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await m(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 m(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new w(`Invalid value for field ${e}`);const r=_(this.schema,t,this.server.loadedPlugins);if(r)throw new w(`Missing required field ${r}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await m(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 w(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new w(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new w(`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 w(`Field ${e} must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await E(e,r,t,async e=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!G(r,t))throw new w(`Field ${e} has invalid value ${String(t)}`);return!0}getConfigApiData(){const e=this.config.api_prefix||"/api",t=this.server.loadedPlugins.static&&this.config.group_schema?n(this.config,this.config.group_schema):void 0;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"../../abstract/plugin.js";import{wantJson as t,jsonBody as r,tryMethod as i,ok as s}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as a}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as o,schemaUrl as n}from"../../lib/schemaFile.js";import{asyncHandler as h,isDummyMemberDn as c,escapeDnValue as d,validateDnValue as p,launchHooksChained as m,launchHooks as u,escapeLdapFilter as l,organizationLink as f,substringSearchFilter as g}from"../../lib/utils.js";import{NotFoundError as b,BadRequestError as w,HttpError as y}from"../../lib/errors.js";import{assertClientMaySet as $,modifiedAttributeNames as v,invalidValueMessage as _,missingRequiredAttribute as E,checkDnValues as A,matchesPattern as G}from"../../config/schema.js";class LdapGroups extends e{constructor(e){if(super(e),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeletedone:async e=>{const t=Array.isArray(e)?e:[e];this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)})}},this.ldap=e.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");if(this.config.ldap_group_base&&e.ldap.declareObjectClasses(this.base,this.config.group_class||[]),this.config.group_schema){const e=this.config.group_schema;o(e,{config:this.config}).then(e=>{this.schema=this.adaptSchema(e),this.logger.debug("Group schema loaded")}).catch(t=>this.logger.error(`Failed to load group schema from ${e}: ${t}`))}}adaptSchema(e){return e}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,r)=>{if(!t(e,r))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new w("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=g(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new w("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=l(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const a=await this.listGroups({...i,req:e});return s(r,this.hideNeverReturn(a))})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,h(async(e,t)=>{const s=decodeURIComponent(e.params.cn);if(!s)throw new w("cn is required");const a=r(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new w("member must be a string or string array");await i(t,this.addMember.bind(this),s,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,h(async(e,r)=>{if(!t(e,r))return;const s=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!s||!a)throw new w("cn and member are required");await i(r,this.deleteMember.bind(this),s,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,h(async(e,s)=>{if(!t(e,s))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new w("cn is required");const o=r(e,s,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new w("targetOrgDn must be a string");await i(s,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,h(async(e,s)=>{if(!t(e,s))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new w("cn is required");const o=r(e,s,"newCn");if(o){if("string"!=typeof o.newCn)throw new w("newCn must be a string");await i(s,this.renameGroup.bind(this),a,o.newCn,e)}}))}isDummyMember(e){return c(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,r){if(!t(e,r))return;const i=decodeURIComponent(e.params.cn);try{const t=/,/.test(i)?i:`${this.cn}=${d(i)},${this.base}`,s=await this.ldap.search({paged:!1,scope:"base"},t,e);if(0===s.searchEntries.length)throw new b("Group not found");r.json(this.hideNeverReturn(this.withoutDummyMember(s.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new b("Group not found");throw e}}async apiAdd(e,t,...s){const a=r(e,t,...s);if(!a)return;$(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 i(t,this.addGroup.bind(this),o,n,h,e)}async apiDelete(e,r){if(!t(e,r))return;const s=decodeURIComponent(e.params.cn);if(!s)throw new w("cn is required");await i(r,this.deleteGroup.bind(this),s,e)}async apiModify(e,t){const s=r(e,t);if(!s)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new w("cn is required");$(this.schema,v(s));const o=Object.fromEntries(Object.entries(s).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await i(t,this.modifyGroup.bind(this),a,o,e)}async addGroup(e,t=[],r={},i){let s;new RegExp(`^${this.cn}=`).test(e)?(s=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(p(e,this.cn),s=`${this.cn}=${d(e)},${this.base}`),await this.validateMembers(s,t),await this.validateNewGroup(s,{objectClass:this.config.group_class,cn:e,member:t,...r});const a=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||a.includes("twakeGroup")||a.includes("twakeStaticGroup")||a.push("twakeGroup");let o,n={objectClass:a,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&n.member.push(...t),[s,n]=await m(this.registeredHooks.ldapgroupadd,[s,n]);try{o=await this.ldap.add(s,n,i)}catch(e){if(e instanceof y)throw e;throw new Error(`Failed to add group ${s}: ${e}`)}return u(this.registeredHooks.ldapgroupadddone,[s,n]),o}async modifyGroup(e,t,r){let i=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;const s=this.opNumber();if([i,t]=await m(this.registeredHooks.ldapgroupmodify,[i,t,s]),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(i,t);const a=await this.ldap.modify(i,t,r);return u(this.registeredHooks.ldapgroupmodifydone,[i,t,s]),a}async renameGroup(e,t,r){/,/.test(e)||p(e,this.cn),/,/.test(t)||p(t,this.cn);let i=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`,s=/,/.test(t)?t:`${this.cn}=${d(t)},${this.base}`;[i,s]=await m(this.registeredHooks.ldapgrouprename,[i,s]);const a=await this.ldap.rename(i,s,r);return u(this.registeredHooks.ldapgrouprenamedone,[i,s]),a}async deleteGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;r=await m(this.registeredHooks.ldapgroupdelete,r);const i=await this.ldap.delete(r,t);return u(this.registeredHooks.ldapgroupdeletedone,r),i}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await m(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{if(e instanceof y)throw 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 m(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new w("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{if(e instanceof y)throw e;throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${l(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=>{if(65===a(r)&&this.config.group_dummy_user)return this.ldap.modify(t.dn,{add:{member:this.config.group_dummy_user},delete:{member:e}});throw r}).catch(r=>{const i=a(r);16!==i&&32!==i&&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],h=f(n[i]);if(!h)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(h===t)throw new Error("Group is already in the target organization");let c;try{c=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===c.searchEntries.length)throw new Error(`Target organization ${t} not found`);const p=c.searchEntries[0][s];if(!p)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:p}},r),this.logger.info(`Group ${a} moved from ${h} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t,req:r}={}){const i=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base,r).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let s={};for await(const e of i)for(const t of e.searchEntries){const e=t[this.cn];if(Array.isArray(e)&&0===e.length)continue;e&&(s[e]=this.withoutDummyMember(t))}return s=await m(this.registeredHooks._ldapgrouplist,s),s}async searchGroupsByName(e,t=!1,r=[this.cn,"member"],i){const s=l(e),a=t?`(${this.cn}=*${s}*)`:`(${this.cn}=${s})`;return await this.listGroups({filter:a,attributes:r,req:i})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await m(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){const r=a(t);if(32===r)throw new w(`Member ${e} not found`);if(34===r)throw new w(`Member ${e} is not a valid DN`);throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){if(e instanceof y)throw e;throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await m(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new w(_(e,this.schema.attributes[e]));const r=E(this.schema,t,this.server.loadedPlugins);if(r)throw new w(`Attribute "${r}" is required`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await m(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))if(!await this._validateOneChange(e,r))throw new w(_(e,this.schema.attributes[e]));if(t.replace)for(const[e,r]of Object.entries(t.replace))if(!await this._validateOneChange(e,r))throw new w(_(e,this.schema.attributes[e]));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 w(`Attribute "${e}" is not allowed`);return!0}if(null==t||Array.isArray(t)&&0===t.length){if(r.required)throw new w(`Attribute "${e}" is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new w(`Attribute "${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 w(`Attribute "${e}" must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await A(e,r,t,async e=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!G(r,t))throw new w(_(e,r));return!0}getConfigApiData(){const e=this.config.api_prefix||"/api",t=this.server.loadedPlugins.static&&this.config.group_schema?n(this.config,this.config.group_schema):void 0;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 +1 @@
1
- import t from"../../abstract/plugin.js";import{tryMethodData as i,jsonBody as a,tryMethod as e,wantJson as n}from"../../lib/expressFormatedResponses.js";import{asyncHandler as s,validateDnValue as r,escapeDnValue as o,getRdn as h,isDnInBranch as c,rdnValue as d,normalizeDn as l,getParentDn as g,escapeLdapFilter as p,launchHooksChained as f,isChildOf as u,substringSearchFilter as w}from"../../lib/utils.js";import{BadRequestError as m,NotFoundError as z,ConflictError as y,HttpError as b}from"../../lib/errors.js";import{extractLdapCode as $}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as _,schemaUrl as v}from"../../lib/schemaFile.js";import{assertClientMaySet as O,modifiedAttributeNames as C,missingRequiredAttribute as E,checkDnValues as A,matchesPattern as L}from"../../config/schema.js";class LdapOrganizations extends t{constructor(t){if(super(t),this.name="ldapOrganizations",this.roles=["api","consistency","configurable"],this.hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i,t),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a,e])=>{let n,s={},r={};const checkIsOu=async()=>{if(void 0!==n)return n;if(i.replace?.objectClass)n=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)n=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);n=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return n};if(i.delete){const t=(Array.isArray(i.delete)?i.delete.map(String):Object.keys(i.delete)).map(t=>t.split(";")[0].toLowerCase()),a=t.includes(this.linkAttr.toLowerCase()),e=t.includes(this.pathAttr.toLowerCase());if(a||e){if(!await checkIsOu()&&a)throw new m("An organization link cannot be deleted");if(e)throw new m("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(s={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(s={...s,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(s).length>0){await checkIsOu()||await this.checkDeptLink(s)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r,t)}return[t,i,a,e]},ldapdeleterequest:async([t,i])=>{const a=Array.isArray(t)?t:[t];for(const t of a)/^ou=/.test(t)&&await this.isEmptyOrganization(t);return[t,i]},ldaprenamerequest:([t,i,a])=>[t,i,a]},this.sizeLimitWarned=new Set,!this.config.ldap_top_organization)throw new Error("Missing --ldap-top-organization");if(this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,t.ldap.declareObjectClasses(this.config.ldap_top_organization,this.config.ldap_organization_class||[],!0),this.config.organization_schema){const t=this.config.organization_schema;_(t,{config:this.config}).then(t=>{this.schema=t,this.logger.debug("Organization schema loaded")}).catch(i=>this.logger.error(`Failed to load organization schema from ${t}: ${i}`))}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/organizations/top`,async(t,a)=>{await i(a,async()=>this.hideNeverReturn(await this.getOrganisationTop(t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,a)=>{const e=decodeURIComponent(t.params.dn);await i(a,async()=>this.hideNeverReturn(await this.getOrganisationByDn(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,async(t,a)=>{const e=decodeURIComponent(t.params.dn);await i(a,async()=>this.hideNeverReturn(await this.getOrganisationSubnodes(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,s(async(t,a)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new m('query parameter "q" is required');await i(a,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(e,n,t)))})),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/search`,s(async(t,a)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new m('query parameter "q" is required');await i(a,async()=>this.hideNeverReturn(await this.searchOrganisationTree(e,n,t)))})),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,s(async(t,i)=>this.apiAdd(t,i))),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,s(async(t,i)=>this.apiModify(t,i))),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,s(async(t,i)=>this.apiDelete(t,i))),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,s(async(t,i)=>this.apiMove(t,i)))}async apiAdd(t,i){const n=a(t,i,"ou");if(!n)return;O(this.schema,Object.keys(n),"ou"),r(n.ou,"ou");const s=n.parentDn||this.config.ldap_top_organization,h=`ou=${o(n.ou)},${s}`,c={objectClass:this.config.ldap_organization_class,ou:n.ou,...Object.fromEntries(Object.entries(n).filter(([t])=>"ou"!==t&&"parentDn"!==t))};await e(i,this.addOrganization.bind(this),h,c,t)}async apiModify(t,i){const n=a(t,i);if(!n)return;const s=decodeURIComponent(t.params.dn);if(!s)throw new m("dn is required");O(this.schema,C(n)),await e(i,this.modifyOrganization.bind(this),s,n,t)}async apiDelete(t,i){if(!n(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new m("dn is required");await e(i,this.deleteOrganization.bind(this),a,t)}async apiMove(t,e){if(!n(t,e))return;const s=a(t,e,"targetOrgDn");if(!s)return;const r=decodeURIComponent(t.params.dn);if(!r)throw new m("dn is required");const{targetOrgDn:o}=s;if(!o||"string"!=typeof o)throw new m("Missing or invalid targetOrgDn in request body");await i(e,this.moveOrganization.bind(this),r,o,t)}async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new z(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new m(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t,i){if(t[this.pathAttr]){const a=t[this.pathAttr],e=Array.isArray(a)?a[0]:a,n=this.config.ldap_organization_path_separator||" / ",s=this.config.ldap_top_organization||"",r=i&&s&&/^ou=/i.test(h(i))&&c(i,s)?d(i):void 0,o=t.ou,f=(Array.isArray(o)?o[0]:o)||r;let u=e;if((this.isOu(t)||void 0!==r)&&f){const t=s?d(s):"";if(t&&e.startsWith(f+n)&&e.endsWith(n+t))return;if(e===f){const t=s?l(s):"";if(!i||!t||!c(i,s)||l(i)===t||l(g(i))===t)return;throw new m(`Organization path "${e}" names no parent, but ${i} is not directly under ${s}`)}if(!e.endsWith(n+f))throw new m(`Organization path must end with its own name, preceded by separator "${n}"`);u=e.slice(0,e.length-n.length-f.length)}const w=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${p(u)})`,attributes:["dn"]},this.config.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new m(`Invalid organization path ${e}: no organization holds "${u}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${p(t)})`})).searchEntries.length>0)throw new y(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=[].concat(t.objectClass).map(t=>String(t).toLowerCase());return this.config.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.config.ldap_top_organization)throw new m("No top organization configured");const i=await this.server.ldap.search({paged:!1,scope:"base"},this.config.ldap_top_organization,t);if(1!==i.searchEntries.length)throw new z("Top organization not found");const[,a]=await f(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){let a;try{a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i)}catch(i){if(32===$(i))throw new z(`Organization ${t} not found`);throw i}if(1!==a.searchEntries.length)throw new z(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.config.ldap_organization_max_subnodes||50,n=i?.query?.objectClass;n&&"organizationalUnit"!==n||a.push(...await this.childOrganizations(t,"(objectClass=organizationalUnit)",e,i));const s=this.config.ldap_top_organization,r=g(s);let o=`(${this.config.ldap_organization_link_attribute}=${p(t)})`;return n&&(o=`(&${o}(objectClass=${p(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`),a.push(...await this.linkedEntities(r,o,e,t,i)),a}warnSizeLimit(t,i){this.sizeLimitWarned.has(t)?this.server.logger.debug(i):(this.sizeLimitWarned.size<LdapOrganizations.SIZE_LIMIT_WARNED_MAX&&this.sizeLimitWarned.add(t),this.server.logger.warn(i))}static moreIndicator(t,i,a,e){return{dn:t,cn:[i],objectClass:["moreIndicator"],_isMoreIndicator:"true",...void 0===e?{}:{_totalCount:e.toString()},_displayedCount:a.toString()}}async childOrganizations(t,i,a,e){try{const a=await this.server.ldap.search({paged:!0,scope:"one",filter:i},t,e),n=[];for await(const t of a)n.push(...t.searchEntries);return this.server.logger.debug(`Found ${n.length} sub-OUs for ${t}`),n}catch(i){const a=$(i);if(32===a)return this.server.logger.debug(`No sub-OUs for ${t}: the node holds none`),[];if(4!==a)throw i}const n=(await this.server.ldap.search({paged:!1,scope:"one",filter:i,sizeLimit:a+1},t,e)).searchEntries.slice(0,a);return this.warnSizeLimit(`children:${t}`,`Organization ${t} holds more child organizations than the directory will list in one answer, so ${n.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to list them all.`),[...n,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",n.length)]}async linkedEntities(t,i,a,e,n){const s=[];let r=0;try{const e=await this.server.ldap.search({paged:!0,filter:i},t,n);for await(const t of e){r+=t.searchEntries.length;const i=a-s.length;i>0&&s.push(...t.searchEntries.slice(0,i))}}catch(s){if(4!==$(s))throw s;const r=await this.server.ldap.search({paged:!1,filter:i,sizeLimit:a+1},t,n);this.warnSizeLimit(`linked:${e}`,`More entries are attached to ${e} than the directory will list in one answer. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to count them all.`);const o=r.searchEntries.slice(0,a);return[...o,LdapOrganizations.moreIndicator(`more-${e}`,"... more elements than the directory will list",o.length)]}return this.server.logger.debug(`Found ${r} linked entities for ${e}, returning ${s.length}`),r<=a?s:[...s,LdapOrganizations.moreIndicator(`more-${e}`,`... ${r-a} more ${r-a===1?"element":"elements"}`,a,r)]}async addOrganization(t,i,a){return await this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i,a){return await this.validateChanges(t,i),await this.server.ldap.modify(t,i,a)}async validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!await this._validateOneChange(t,a))throw new m(this.invalidValueMessage(t));const a=E(this.schema,i,this.server.loadedPlugins);if(a)throw new m(`Missing required field ${a}`);return!0}invalidValueMessage(t){const i=this.schema?.attributes[t],a=i?.hint||i?.items?.hint;return a?`Invalid value for field ${t}: ${a}`:`Invalid value for field ${t}`}async validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!await this._validateOneChange(t,a))throw new m(this.invalidValueMessage(t));if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!await this._validateOneChange(t,a))throw new m(this.invalidValueMessage(t));return!0}async _validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new m(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new m(`Field ${t} is required`);return!0}if("string"===a.type&&Array.isArray(i))throw new m(`Field ${t} must be a single value`);return await A(t,a,i,t=>this.entryExists(t)),L(a,i)}async entryExists(t){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){let e;try{e=await this.server.ldap.search({paged:!1,scope:"base"},i)}catch(t){throw new z(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!e.searchEntries||0===e.searchEntries.length)throw new z(`Target organization ${i} not found`);if(!this.isOu(e.searchEntries[0]))throw new m(`Target ${i} is not an organizational unit`);const n=`${h(t)},${i}`;let s;try{s=await this.server.ldap.search({paged:!1,scope:"base"},t)}catch(t){throw new z(`Source organization not found: ${t}`)}if(!s.searchEntries||0===s.searchEntries.length)throw new z(`Source organization ${t} not found`);if(n===t)throw new m("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||u(i,t))throw new m("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,n,a),this.logger.info(`Moved organization from ${t} to ${n}`)}catch(t){if(t instanceof b)throw t;throw new Error(`Failed to move organization: ${t}`)}return{newDn:n}}async deleteOrganization(t,i){return await this.server.ldap.delete(t,i)}async searchOrganisationSubnodes(t,i,a){const e=[],n=this.config.ldap_organization_max_subnodes||50;e.push(...await this.childOrganizations(t,`(&(objectClass=organizationalUnit)${w(i,"ou,description")})`,n,a));const s=this.config.ldap_top_organization,r=g(s),o=p(t),h=this.config.display_name_attribute||"displayName",c=`(&(${this.config.ldap_organization_link_attribute}=${o})${w(i,`uid,cn,mail,sn,givenName,${h}`)})`;return this.server.logger.debug(`Searching for linked entities with filter: ${c} in ${r}`),e.push(...await this.linkedEntities(r,c,n,t,a)),e}async searchOrganisationTree(t,i,a){const e=this.config.ldap_organization_max_subnodes||50,n=this.pathAttr?`ou,description,${this.pathAttr}`:"ou,description",s=`(&(objectClass=organizationalUnit)${w(i,n)})`,r=[];let o=0;try{const i=await this.server.ldap.search({paged:!0,scope:"sub",filter:s},t,a);for await(const t of i){o+=t.searchEntries.length;const i=e-r.length;i>0&&r.push(...t.searchEntries.slice(0,i))}}catch(i){const n=$(i);if(32===n)return this.server.logger.debug(`No organizations under ${t}: no such node`),[];if(4!==n)throw i;const r=(await this.server.ldap.search({paged:!1,scope:"sub",filter:s,sizeLimit:e+1},t,a)).searchEntries.slice(0,e);return this.warnSizeLimit(`search:${t}`,`A search under ${t} matched more organizations than the directory will list in one answer, so ${r.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to search them all.`),[...r,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",r.length)]}if(o<=e)return r;const h=o-e;return[...r,LdapOrganizations.moreIndicator(`more-organizations-${t}`,`... ${h} more ${1===h?"organization":"organizations"}, narrow the search`,e,o)]}getConfigApiData(){const t=this.config.api_prefix||"/api",i=this.server.loadedPlugins.static&&this.config.organization_schema?v(this.config,this.config.organization_schema):void 0;return{enabled:!0,topOrganization:this.config.ldap_top_organization||"",organizationClass:this.config.ldap_organization_class||["top","organizationalUnit"],linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.config.ldap_organization_path_separator||" / ",maxSubnodes:this.config.ldap_organization_max_subnodes||50,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations/top`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,search:`${t}/v1/ldap/organizations/:dn/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}LdapOrganizations.SIZE_LIMIT_WARNED_MAX=500;export{LdapOrganizations as default};
1
+ import t from"../../abstract/plugin.js";import{tryMethodData as i,jsonBody as a,tryMethod as e,wantJson as n}from"../../lib/expressFormatedResponses.js";import{asyncHandler as r,validateDnValue as s,escapeDnValue as o,getRdn as h,isDnInBranch as c,rdnValue as d,normalizeDn as l,getParentDn as g,escapeLdapFilter as p,launchHooksChained as u,isChildOf as f,substringSearchFilter as w}from"../../lib/utils.js";import{BadRequestError as m,NotFoundError as z,ConflictError as y,HttpError as b}from"../../lib/errors.js";import{extractLdapCode as $}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as _,schemaUrl as v}from"../../lib/schemaFile.js";import{assertClientMaySet as O,modifiedAttributeNames as A,missingRequiredAttribute as C,invalidValueMessage as E,checkDnValues as L,matchesPattern as k}from"../../config/schema.js";class LdapOrganizations extends t{constructor(t){if(super(t),this.name="ldapOrganizations",this.roles=["api","consistency","configurable"],this.hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i,t),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a,e])=>{let n,r={},s={};const checkIsOu=async()=>{if(void 0!==n)return n;if(i.replace?.objectClass)n=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)n=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);n=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return n};if(i.delete){const t=(Array.isArray(i.delete)?i.delete.map(String):Object.keys(i.delete)).map(t=>t.split(";")[0].toLowerCase()),a=t.includes(this.linkAttr.toLowerCase()),e=t.includes(this.pathAttr.toLowerCase());if(a||e){if(!await checkIsOu()&&a)throw new m("An organization link cannot be deleted");if(e)throw new m("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(r={...i.replace}),i.replace[this.pathAttr]&&(s={...i.replace})),i.add&&(i.add[this.linkAttr]&&(r={...r,...i.add}),i.add[this.pathAttr]&&(s={...s,...i.add})),Object.keys(r).length>0){await checkIsOu()||await this.checkDeptLink(r)}if(Object.keys(s).length>0){await checkIsOu()&&await this.checkDeptPath(s,t)}return[t,i,a,e]},ldapdeleterequest:async([t,i])=>{const a=Array.isArray(t)?t:[t];for(const t of a)/^ou=/.test(t)&&await this.isEmptyOrganization(t);return[t,i]},ldaprenamerequest:([t,i,a])=>[t,i,a]},this.sizeLimitWarned=new Set,!this.config.ldap_top_organization)throw new Error("Missing --ldap-top-organization");if(this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,t.ldap.declareObjectClasses(this.config.ldap_top_organization,this.config.ldap_organization_class||[],!0),this.config.organization_schema){const t=this.config.organization_schema;_(t,{config:this.config}).then(t=>{this.schema=t,this.logger.debug("Organization schema loaded")}).catch(i=>this.logger.error(`Failed to load organization schema from ${t}: ${i}`))}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/organizations/top`,async(t,a)=>{await i(a,async()=>this.hideNeverReturn(await this.getOrganisationTop(t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,a)=>{const e=decodeURIComponent(t.params.dn);await i(a,async()=>this.hideNeverReturn(await this.getOrganisationByDn(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,async(t,a)=>{const e=decodeURIComponent(t.params.dn);await i(a,async()=>this.hideNeverReturn(await this.getOrganisationSubnodes(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,r(async(t,a)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new m('query parameter "q" is required');await i(a,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(e,n,t)))})),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/search`,r(async(t,a)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new m('query parameter "q" is required');await i(a,async()=>this.hideNeverReturn(await this.searchOrganisationTree(e,n,t)))})),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,r(async(t,i)=>this.apiAdd(t,i))),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,r(async(t,i)=>this.apiModify(t,i))),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,r(async(t,i)=>this.apiDelete(t,i))),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,r(async(t,i)=>this.apiMove(t,i)))}async apiAdd(t,i){const n=a(t,i,"ou");if(!n)return;O(this.schema,Object.keys(n),"ou"),s(n.ou,"ou");const r=n.parentDn||this.config.ldap_top_organization,h=`ou=${o(n.ou)},${r}`,c={objectClass:this.config.ldap_organization_class,ou:n.ou,...Object.fromEntries(Object.entries(n).filter(([t])=>"ou"!==t&&"parentDn"!==t))};await e(i,this.addOrganization.bind(this),h,c,t)}async apiModify(t,i){const n=a(t,i);if(!n)return;const r=decodeURIComponent(t.params.dn);if(!r)throw new m("dn is required");O(this.schema,A(n)),await e(i,this.modifyOrganization.bind(this),r,n,t)}async apiDelete(t,i){if(!n(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new m("dn is required");await e(i,this.deleteOrganization.bind(this),a,t)}async apiMove(t,e){if(!n(t,e))return;const r=a(t,e,"targetOrgDn");if(!r)return;const s=decodeURIComponent(t.params.dn);if(!s)throw new m("dn is required");const{targetOrgDn:o}=r;if(!o||"string"!=typeof o)throw new m("Missing or invalid targetOrgDn in request body");await i(e,this.moveOrganization.bind(this),s,o,t)}async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new z(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new m(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t,i){if(t[this.pathAttr]){const a=t[this.pathAttr],e=Array.isArray(a)?a[0]:a,n=this.config.ldap_organization_path_separator||" / ",r=this.config.ldap_top_organization||"",s=i&&r&&/^ou=/i.test(h(i))&&c(i,r)?d(i):void 0,o=t.ou,u=(Array.isArray(o)?o[0]:o)||s;let f=e;if((this.isOu(t)||void 0!==s)&&u){const t=r?d(r):"";if(t&&e.startsWith(u+n)&&e.endsWith(n+t))return;if(e===u){const t=r?l(r):"";if(!i||!t||!c(i,r)||l(i)===t||l(g(i))===t)return;throw new m(`Organization path "${e}" names no parent, but ${i} is not directly under ${r}`)}if(!e.endsWith(n+u))throw new m(`Organization path must end with its own name, preceded by separator "${n}"`);f=e.slice(0,e.length-n.length-u.length)}const w=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${p(f)})`,attributes:["dn"]},this.config.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new m(`Invalid organization path ${e}: no organization holds "${f}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${p(t)})`})).searchEntries.length>0)throw new y(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=[].concat(t.objectClass).map(t=>String(t).toLowerCase());return this.config.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.config.ldap_top_organization)throw new m("No top organization configured");const i=await this.server.ldap.search({paged:!1,scope:"base"},this.config.ldap_top_organization,t);if(1!==i.searchEntries.length)throw new z("Top organization not found");const[,a]=await u(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){let a;try{a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i)}catch(i){if(32===$(i))throw new z(`Organization ${t} not found`);throw i}if(1!==a.searchEntries.length)throw new z(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.config.ldap_organization_max_subnodes||50,n=i?.query?.objectClass;n&&"organizationalUnit"!==n||a.push(...await this.childOrganizations(t,"(objectClass=organizationalUnit)",e,i));const r=this.config.ldap_top_organization,s=g(r);let o=`(${this.config.ldap_organization_link_attribute}=${p(t)})`;return n&&(o=`(&${o}(objectClass=${p(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${s}`),a.push(...await this.linkedEntities(s,o,e,t,i)),a}warnSizeLimit(t,i){this.sizeLimitWarned.has(t)?this.server.logger.debug(i):(this.sizeLimitWarned.size<LdapOrganizations.SIZE_LIMIT_WARNED_MAX&&this.sizeLimitWarned.add(t),this.server.logger.warn(i))}static moreIndicator(t,i,a,e){return{dn:t,cn:[i],objectClass:["moreIndicator"],_isMoreIndicator:"true",...void 0===e?{}:{_totalCount:e.toString()},_displayedCount:a.toString()}}async childOrganizations(t,i,a,e){try{const a=await this.server.ldap.search({paged:!0,scope:"one",filter:i},t,e),n=[];for await(const t of a)n.push(...t.searchEntries);return this.server.logger.debug(`Found ${n.length} sub-OUs for ${t}`),n}catch(i){const a=$(i);if(32===a)return this.server.logger.debug(`No sub-OUs for ${t}: the node holds none`),[];if(4!==a)throw i}const n=(await this.server.ldap.search({paged:!1,scope:"one",filter:i,sizeLimit:a+1},t,e)).searchEntries.slice(0,a);return this.warnSizeLimit(`children:${t}`,`Organization ${t} holds more child organizations than the directory will list in one answer, so ${n.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to list them all.`),[...n,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",n.length)]}async linkedEntities(t,i,a,e,n){const r=[];let s=0;try{const e=await this.server.ldap.search({paged:!0,filter:i},t,n);for await(const t of e){s+=t.searchEntries.length;const i=a-r.length;i>0&&r.push(...t.searchEntries.slice(0,i))}}catch(r){if(4!==$(r))throw r;const s=await this.server.ldap.search({paged:!1,filter:i,sizeLimit:a+1},t,n);this.warnSizeLimit(`linked:${e}`,`More entries are attached to ${e} than the directory will list in one answer. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to count them all.`);const o=s.searchEntries.slice(0,a);return[...o,LdapOrganizations.moreIndicator(`more-${e}`,"... more elements than the directory will list",o.length)]}return this.server.logger.debug(`Found ${s} linked entities for ${e}, returning ${r.length}`),s<=a?r:[...r,LdapOrganizations.moreIndicator(`more-${e}`,`... ${s-a} more ${s-a===1?"element":"elements"}`,a,s)]}async addOrganization(t,i,a){return await this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i,a){return await this.validateChanges(t,i),await this.server.ldap.modify(t,i,a)}async validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!await this._validateOneChange(t,a))throw new m(this.invalidValueMessage(t));const a=C(this.schema,i,this.server.loadedPlugins);if(a)throw new m(`Attribute "${a}" is required`);return!0}invalidValueMessage(t){return E(t,this.schema?.attributes[t])}async validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!await this._validateOneChange(t,a))throw new m(this.invalidValueMessage(t));if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!await this._validateOneChange(t,a))throw new m(this.invalidValueMessage(t));return!0}async _validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new m(`Attribute "${t}" is not allowed`);return!0}if(null==i||Array.isArray(i)&&0===i.length){if(a.required)throw new m(`Attribute "${t}" is required`);return!0}if("string"===a.type&&Array.isArray(i))throw new m(`Attribute "${t}" must be a single value`);return await L(t,a,i,t=>this.entryExists(t)),k(a,i)}async entryExists(t){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){let e;try{e=await this.server.ldap.search({paged:!1,scope:"base"},i)}catch(t){throw new z(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!e.searchEntries||0===e.searchEntries.length)throw new z(`Target organization ${i} not found`);if(!this.isOu(e.searchEntries[0]))throw new m(`Target ${i} is not an organizational unit`);const n=`${h(t)},${i}`;let r;try{r=await this.server.ldap.search({paged:!1,scope:"base"},t)}catch(t){throw new z(`Source organization not found: ${t}`)}if(!r.searchEntries||0===r.searchEntries.length)throw new z(`Source organization ${t} not found`);if(n===t)throw new m("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||f(i,t))throw new m("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,n,a),this.logger.info(`Moved organization from ${t} to ${n}`)}catch(t){if(t instanceof b)throw t;throw new Error(`Failed to move organization: ${t}`)}return{newDn:n}}async deleteOrganization(t,i){return await this.server.ldap.delete(t,i)}async searchOrganisationSubnodes(t,i,a){const e=[],n=this.config.ldap_organization_max_subnodes||50;e.push(...await this.childOrganizations(t,`(&(objectClass=organizationalUnit)${w(i,"ou,description")})`,n,a));const r=this.config.ldap_top_organization,s=g(r),o=p(t),h=this.config.display_name_attribute||"displayName",c=`(&(${this.config.ldap_organization_link_attribute}=${o})${w(i,`uid,cn,mail,sn,givenName,${h}`)})`;return this.server.logger.debug(`Searching for linked entities with filter: ${c} in ${s}`),e.push(...await this.linkedEntities(s,c,n,t,a)),e}async searchOrganisationTree(t,i,a){const e=this.config.ldap_organization_max_subnodes||50,n=this.pathAttr?`ou,description,${this.pathAttr}`:"ou,description",r=`(&(objectClass=organizationalUnit)${w(i,n)})`,s=[];let o=0;try{const i=await this.server.ldap.search({paged:!0,scope:"sub",filter:r},t,a);for await(const t of i){o+=t.searchEntries.length;const i=e-s.length;i>0&&s.push(...t.searchEntries.slice(0,i))}}catch(i){const n=$(i);if(32===n)return this.server.logger.debug(`No organizations under ${t}: no such node`),[];if(4!==n)throw i;const s=(await this.server.ldap.search({paged:!1,scope:"sub",filter:r,sizeLimit:e+1},t,a)).searchEntries.slice(0,e);return this.warnSizeLimit(`search:${t}`,`A search under ${t} matched more organizations than the directory will list in one answer, so ${s.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to search them all.`),[...s,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",s.length)]}if(o<=e)return s;const h=o-e;return[...s,LdapOrganizations.moreIndicator(`more-organizations-${t}`,`... ${h} more ${1===h?"organization":"organizations"}, narrow the search`,e,o)]}getConfigApiData(){const t=this.config.api_prefix||"/api",i=this.server.loadedPlugins.static&&this.config.organization_schema?v(this.config,this.config.organization_schema):void 0;return{enabled:!0,topOrganization:this.config.ldap_top_organization||"",organizationClass:this.config.ldap_organization_class||["top","organizationalUnit"],linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.config.ldap_organization_path_separator||" / ",maxSubnodes:this.config.ldap_organization_max_subnodes||50,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations/top`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,search:`${t}/v1/ldap/organizations/:dn/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}LdapOrganizations.SIZE_LIMIT_WARNED_MAX=500;export{LdapOrganizations as default};