ldap-rest 0.2.2 → 0.3.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 (38) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/bin/index.js +1 -1
  3. package/dist/config/args.js +1 -1
  4. package/dist/lib/expressFormatedResponses.js +1 -1
  5. package/dist/plugins/auth/authzDynamic.js +1 -0
  6. package/dist/plugins/auth/authzDynamicHash.js +1 -0
  7. package/dist/plugins/scim/baseResolver.js +1 -0
  8. package/dist/plugins/scim/bulk.js +1 -0
  9. package/dist/plugins/scim/discovery.js +1 -0
  10. package/dist/plugins/scim/errors.js +1 -0
  11. package/dist/plugins/scim/filter.js +1 -0
  12. package/dist/plugins/scim/groups.js +1 -0
  13. package/dist/plugins/scim/mapping.js +1 -0
  14. package/dist/plugins/scim/patch.js +1 -0
  15. package/dist/plugins/scim/scim.js +1 -0
  16. package/dist/plugins/scim/types.js +1 -0
  17. package/dist/plugins/scim/users.js +1 -0
  18. package/dist/src/abstract/ldapFlat.d.ts +20 -0
  19. package/dist/src/config/args.d.ts +24 -0
  20. package/dist/src/plugins/auth/authzDynamic.d.ts +117 -0
  21. package/dist/src/plugins/auth/authzDynamicHash.d.ts +27 -0
  22. package/dist/src/plugins/scim/baseResolver.d.ts +23 -0
  23. package/dist/src/plugins/scim/bulk.d.ts +43 -0
  24. package/dist/src/plugins/scim/discovery.d.ts +27 -0
  25. package/dist/src/plugins/scim/errors.d.ts +37 -0
  26. package/dist/src/plugins/scim/filter.d.ts +13 -0
  27. package/dist/src/plugins/scim/groups.d.ts +61 -0
  28. package/dist/src/plugins/scim/mapping.d.ts +52 -0
  29. package/dist/src/plugins/scim/patch.d.ts +39 -0
  30. package/dist/src/plugins/scim/scim.d.ts +23 -0
  31. package/dist/src/plugins/scim/types.d.ts +222 -0
  32. package/dist/src/plugins/scim/users.d.ts +58 -0
  33. package/docs/usage/plugins/auth/authz-dynamic.md +183 -0
  34. package/docs/usage/plugins/integrations/scim.md +567 -0
  35. package/package.json +65 -1
  36. package/static/schemas/scim/Group.json +31 -0
  37. package/static/schemas/scim/User.json +147 -0
  38. package/static/schemas/scim/default-mapping.json +48 -0
@@ -1 +1 @@
1
- import t from"fs";import{wantJson as e,jsonBody as i,created as s,tryMethod as a}from"../lib/expressFormatedResponses.js";import{transformSchemas as r,asyncHandler as n,escapeDnValue as o,validateDnValue as h,launchHooksChained as d,launchHooks as c,getCompiledRegex as u,escapeRegex as l,escapeLdapFilter as f}from"../lib/utils.js";import{NotFoundError as b,BadRequestError as m,ConflictError as p}from"../lib/errors.js";import g from"./plugin.js";class LdapFlat extends g{constructor(e,i){if(super(e),this.ldap=e.ldap,this.base=i.base,this.mainAttribute=i.mainAttribute,this.objectClass=i.objectClass,this.defaultAttributes=i.defaultAttributes||{},this.singularName=i.singularName,this.pluralName=i.pluralName,this.hookPrefix=i.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(r(e,this.config)),this.logger.info(`${this.singularName} schema loaded from ${i.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${i.schemaPath}: ${t}`)}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,n(async(t,i)=>{if(!e(t,i))return;const s={};if(t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute){if(!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(t.query.attribute))throw new m("Invalid LDAP attribute name");const e=f(t.query.match);s.filter=`(${t.query.attribute}=*${e}*)`}t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(","));const a=await this.listEntries(s);i.json(a)})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,n(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,n(async(t,e)=>this.apiMove(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const t=/,/.test(s)?s:`${this.mainAttribute}=${o(s)},${this.base}`,e=await this.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new b(`${this.singularName} not found`);i.json(e.searchEntries[0])}catch(t){if(t.code&&32===t.code)throw new b(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const a=i(t,e,this.mainAttribute);if(!a)return;const r=a[this.mainAttribute],n={...a};delete n[this.mainAttribute],delete n.dn,await this.addEntry(r,n,t);const o=await this.searchEntriesByName(r,!1);return s(e,o[r])}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await a(i,this.deleteEntry.bind(this),s)}async apiModify(t,e){const s=i(t,e);if(!s)return;const r=decodeURIComponent(t.params.id);await a(e,this.modifyEntry.bind(this),r,s)}async apiMove(t,s){if(!e(t,s))return;const a=i(t,s,"targetOrgDn");if(!a)return;const r=decodeURIComponent(t.params.id),{targetOrgDn:n}=a;if(!n||"string"!=typeof n)throw new m("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(r,n,t);s.json({success:!0,...o})}async addEntry(t,e={},i){let s;if(new RegExp(`^${this.mainAttribute}=`).test(t)){s=t;const e=`,${this.base}`;if(!s.endsWith(e))throw new m(`DN must be in the flat branch "${this.base}". Provided DN "${s}" does not end with "${e}"`);t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")}else h(t,this.mainAttribute),s=`${this.mainAttribute}=${o(t)},${this.base}`;await this.validateNewEntry(s,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let a,r={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,r]=await d(this.registeredHooks[`${this.hookPrefix}add`],[s,r]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(r,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(r))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{a=await this.ldap.add(s,r,i)}catch(t){throw this.logger.error("LDAP add failed:",{dn:s,entry:JSON.stringify(r,null,2),error:t}),new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return c(this.registeredHooks[`${this.hookPrefix}adddone`],[s,r]),a}async modifyEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${o(t)},${this.base}`;const s=this.opNumber();if([i,e]=await d(this.registeredHooks[`${this.hookPrefix}modify`],[i,e,s]),e.add&&e.add[this.mainAttribute])throw new p(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new m(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new m(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new m(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(i,e);const a=await this.ldap.modify(i,e);return c(this.registeredHooks[`${this.hookPrefix}modifydone`],[i,e,s]),a}async renameEntry(t,e){/,/.test(t)||h(t,this.mainAttribute),/,/.test(e)||h(e,this.mainAttribute);let i=/,/.test(t)?t:`${this.mainAttribute}=${o(t)},${this.base}`,s=/,/.test(e)?e:`${this.mainAttribute}=${o(e)},${this.base}`;[i,s]=await d(this.registeredHooks[`${this.hookPrefix}rename`],[i,s]);const a=await this.ldap.rename(i,s);return c(this.registeredHooks[`${this.hookPrefix}renamedone`],[i,s]),a}async deleteEntry(t){let e=/,/.test(t)?t:`${this.mainAttribute}=${o(t)},${this.base}`;e=await d(this.registeredHooks[`${this.hookPrefix}delete`],e);const i=await this.ldap.delete(e);return c(this.registeredHooks[`${this.hookPrefix}deletedone`],e),i}async moveEntry(t,e,i){const s=/,/.test(t)?t:`${this.mainAttribute}=${o(t)},${this.base}`,a=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",r=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[a]||!this.schema?.attributes[r])throw new m(`Schema for ${this.singularName} does not support move operation (missing ${a} or ${r})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[a]},s,i);if(!n.searchEntries||0===n.searchEntries.length)throw new b(`Entry ${s} not found`);const h=await this.getDepartmentPath(e,i);[,e]=await d(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const c={replace:{[a]:e,[r]:h}};return await this.modifyEntry(t,c),{departmentPath:h,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const s=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!s.searchEntries||0===s.searchEntries.length)throw new b(`Organization ${t} not found`);const a=s.searchEntries[0];if(a[i]){const t=a[i];return Array.isArray(t)?String(t[0]):String(t)}const r=a.ou||a.o;if(r){return`/${Array.isArray(r)?String(r[0]):String(r)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e}){const i={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(i.attributes=e);const s=await this.ldap.search(i,this.base),a={};for await(const t of s)t.searchEntries.forEach(t=>{if(t[this.mainAttribute]){const e=t[this.mainAttribute];let i;i=Array.isArray(e)?"string"==typeof e[0]?e[0]:String(e[0]):"string"==typeof e?e:String(e),a[i]=t}});return a}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=e?`(${this.mainAttribute}=*${t}*)`:`(${this.mainAttribute}=${t})`;return await this.listEntries({filter:s,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new m(`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 m(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new m(`Invalid value for attribute "${t}"`);if(e.required&&!i)throw new m(`Attribute "${t}" is required`)}for(const[t,i]of Object.entries(this.schema.attributes))if(i.required&&!e[t])throw new m(`Attribute "${t}" is required`);return!0}async validateChanges(t,e){if(!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new m(`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 m(`Invalid value for attribute "${t}"`);if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new m(`Invalid value for attribute "${t}"`);if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new m(`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 m(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>u(`,?${l(t)}$`,"i").test(s)))throw new m(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new m(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new m(`Field ${t} points to invalid or non-existent DN: ${s}`)}}if(i.test){const t="string"==typeof i.test?u(i.test):i.test;return Array.isArray(e)?e.every(e=>t.test(e)):t.test(e)}return!0}}export{LdapFlat as default};
1
+ import t from"fs";import{wantJson as e,jsonBody as i,created as s,tryMethod as r}from"../lib/expressFormatedResponses.js";import{transformSchemas as a,asyncHandler as n,escapeRegex as o,getParentDn as h,escapeDnValue as d,validateDnValue as c,launchHooksChained as l,launchHooks as u,getCompiledRegex as f,escapeLdapFilter as m}from"../lib/utils.js";import{NotFoundError as b,BadRequestError as p,ConflictError as g}from"../lib/errors.js";import w from"./plugin.js";class LdapFlat extends w{constructor(e,i){if(super(e),this.ldap=e.ldap,this.base=i.base,this.mainAttribute=i.mainAttribute,this.objectClass=i.objectClass,this.defaultAttributes=i.defaultAttributes||{},this.singularName=i.singularName,this.pluralName=i.pluralName,this.hookPrefix=i.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(a(e,this.config)),this.logger.info(`${this.singularName} schema loaded from ${i.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${i.schemaPath}: ${t}`)}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,n(async(t,i)=>{if(!e(t,i))return;const s={};if(t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute){if(!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(t.query.attribute))throw new p("Invalid LDAP attribute name");const e=m(t.query.match);s.filter=`(${t.query.attribute}=*${e}*)`}t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(","));const r=await this.listEntries(s);i.json(r)})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,n(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,n(async(t,e)=>this.apiMove(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const t=this.resolveDn(s),e=await this.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new b(`${this.singularName} not found`);i.json(e.searchEntries[0])}catch(t){if(t.code&&32===t.code)throw new b(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const r=i(t,e,this.mainAttribute);if(!r)return;const a=r[this.mainAttribute],n={...r};delete n[this.mainAttribute],delete n.dn,await this.addEntry(a,n,t);const o=await this.searchEntriesByName(a,!1);return s(e,o[a])}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await r(i,this.deleteEntry.bind(this),s)}async apiModify(t,e){const s=i(t,e);if(!s)return;const a=decodeURIComponent(t.params.id);await r(e,this.modifyEntry.bind(this),a,s)}async apiMove(t,s){if(!e(t,s))return;const r=i(t,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(t.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new p("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,t);s.json({success:!0,...o})}resolveDn(t){if(new RegExp(`^${o(this.mainAttribute)}=`,"i").test(t)){const e=h(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new p(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${d(t)},${this.base}`}async addEntry(t,e={},i){let s;new RegExp(`^${this.mainAttribute}=`,"i").test(t)?(s=this.resolveDn(t),t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(c(t,this.mainAttribute),s=this.resolveDn(t)),await this.validateNewEntry(s,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let r,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,a]=await l(this.registeredHooks[`${this.hookPrefix}add`],[s,a]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{r=await this.ldap.add(s,a,i)}catch(t){throw this.logger.error("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return u(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e){let i=this.resolveDn(t);const s=this.opNumber();if([i,e]=await l(this.registeredHooks[`${this.hookPrefix}modify`],[i,e,s]),e.add&&e.add[this.mainAttribute])throw new g(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new p(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new p(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new p(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(i,e);const r=await this.ldap.modify(i,e);return u(this.registeredHooks[`${this.hookPrefix}modifydone`],[i,e,s]),r}async renameEntry(t,e){/,/.test(t)||c(t,this.mainAttribute),/,/.test(e)||c(e,this.mainAttribute);let i=this.resolveDn(t),s=this.resolveDn(e);[i,s]=await l(this.registeredHooks[`${this.hookPrefix}rename`],[i,s]);const r=await this.ldap.rename(i,s);return u(this.registeredHooks[`${this.hookPrefix}renamedone`],[i,s]),r}async deleteEntry(t){let e=this.resolveDn(t);e=await l(this.registeredHooks[`${this.hookPrefix}delete`],e);const i=await this.ldap.delete(e);return u(this.registeredHooks[`${this.hookPrefix}deletedone`],e),i}async moveEntry(t,e,i){const s=this.resolveDn(t),r=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[r]||!this.schema?.attributes[a])throw new p(`Schema for ${this.singularName} does not support move operation (missing ${r} or ${a})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[r]},s,i);if(!n.searchEntries||0===n.searchEntries.length)throw new b(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await l(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const s=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!s.searchEntries||0===s.searchEntries.length)throw new b(`Organization ${t} not found`);const r=s.searchEntries[0];if(r[i]){const t=r[i];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e}){const i={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(i.attributes=e);const s=await this.ldap.search(i,this.base),r={};for await(const t of s)t.searchEntries.forEach(t=>{if(t[this.mainAttribute]){const e=t[this.mainAttribute];let i;i=Array.isArray(e)?"string"==typeof e[0]?e[0]:String(e[0]):"string"==typeof e?e:String(e),r[i]=t}});return r}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=e?`(${this.mainAttribute}=*${t}*)`:`(${this.mainAttribute}=${t})`;return await this.listEntries({filter:s,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new p(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const s=JSON.stringify(e.default);if(s!==JSON.stringify(i))throw new p(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new p(`Invalid value for attribute "${t}"`);if(e.required&&!i)throw new p(`Attribute "${t}" is required`)}for(const[t,i]of Object.entries(this.schema.attributes))if(i.required&&!e[t])throw new p(`Attribute "${t}" is required`);return!0}async validateChanges(t,e){if(!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new p(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new p(`Invalid value for attribute "${t}"`);if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new p(`Invalid value for attribute "${t}"`);if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new p(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if("pointer"===i.type){if("string"!=typeof e)throw new p(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>f(`,?${o(t)}$`,"i").test(s)))throw new p(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new p(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new p(`Field ${t} points to invalid or non-existent DN: ${s}`)}}if(i.test){const t="string"==typeof i.test?f(i.test):i.test;return Array.isArray(e)?e.every(e=>t.test(e)):t.test(e)}return!0}}export{LdapFlat as default};
package/dist/bin/index.js CHANGED
@@ -1 +1 @@
1
- import{fileURLToPath as e}from"url";import{join as r,dirname as i}from"path";import s from"express";import o from"body-parser";import{parseConfig as t}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as d}from"../lib/expressFormatedResponses.js";import g from"../plugins/priority.json.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this._errorMiddlewareSetup=!1,this.config=t(n),this.app=s(),this.app.use(o.json()),this.app.use(o.urlencoded({extended:!0})),this.logger=l(this.config),this.ldap=new a(this),d(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const r=this.config.plugin.find(e=>e.includes("configApi"));let i=this.config.plugin.filter(e=>!e.includes("configApi"));const s=(async()=>{for(const e of g)i.includes(e)&&(i=i.filter(r=>r!==e),await this.loadPlugin(e))})();e.push(s),e.push(s.then(async()=>{const e=i.map(e=>this.loadPlugin(e));await Promise.all(e)})),r&&e.push(Promise.all(e).then(()=>this.loadPlugin(r)))}this.ready=new Promise((r,i)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),r()}).catch(e=>i(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),r())})}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.app._router?.stack;if(e){const r=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==r&&e.splice(r,1)}}this._errorMiddleware=(e,r,i,s)=>{const o="statusCode"in e?e.statusCode:500;if(o>=400&&o<500)return this.logger.warn(`Client error ${o} in request ${r.method} ${r.path}: ${e.message}`),i.headersSent?void 0:i.status(o).json({error:e.message});this.logger.error(`Server error ${o} in request ${r.method} ${r.path}: ${e.message}`,{stack:e.stack,url:r.url,method:r.method}),i.headersSent||i.status(o).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,r)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:r})}),new Promise((e,r)=>{this.server=this.app.listen(this.config.port,i=>{i?(this.logger.error(`Error starting server: ${i}`),r(i)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(s){let o,t;if(/:/.test(s)){let e=s.substring(s.indexOf(":")+1);if(s=s.substring(0,s.indexOf(":")),/:/.test(e)){o=e.substring(0,e.indexOf(":")),o||(o=void 0),e=e.substring(e.indexOf(":")+1);try{t=JSON.parse(e),"object"!=typeof t?(this.logger.error(`Overrides for plugin ${s} are not valid: ${e}`),t=void 0):this.logger.debug(`Overrides for plugin ${o||s}: ${e}`)}catch(r){this.logger.error(`Failed to parse overrides for plugin ${s}: ${r}, using ${e}`),t=void 0}}else o=e,o||(o=void 0)}else o=void 0;return this.logger.debug(`Loading plugin ${s}`),s.startsWith("core/")&&(s=s.replace("core/",r(i(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((e,r)=>{import(s).then(async i=>{let n;if(i&&i.default&&(i=i.default),t){const e={...this.config,...t};n=new i({...this,config:e})}else n=new i(this);if(!n)return r(new Error(`Unable to load ${s}`));e(await this.registerPlugin(s,n,o)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(e=>r(new Error(`Failed to load plugin ${s}: ${e}`)))})}async registerPlugin(e,r,i){if(r.name||(r.name=e),i&&(r.name=i),this.loadedPlugins[r.name])return this.logger.info(`Plugin ${e} already loaded as ${r.name}`),!1;if(this.logger.debug(`Registering plugin ${e} as ${r.name}`),r.dependencies)for(const e in r.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${r.name} depends on ${e}, loading it first`),await this.loadPlugin(r.dependencies[e]));if(r.api&&(this.logger.debug(`Plugin ${r.name} has API, registering it`),await r.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),r.hooks)for(const e in r.hooks){this.logger.debug(`Plugin ${r.name} has hook ${e}, registering it`);const i=r.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!i||"function"!=typeof i)throw new Error(`Plugin ${r.name}: hook ${e} is invalid`);this.hooks[e].push(i)}return this.loadedPlugins[r.name]=r,!0}}export{DM};
1
+ import{fileURLToPath as e}from"url";import{join as r,dirname as i}from"path";import s from"express";import o from"body-parser";import{parseConfig as t}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as d}from"../lib/expressFormatedResponses.js";import g from"../plugins/priority.json.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this._errorMiddlewareSetup=!1,this.config=t(n),this.app=s(),this.app.use(o.json()),this.app.use(o.urlencoded({extended:!0})),this.logger=l(this.config),this.ldap=new a(this),d(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const r=this.config.plugin.find(e=>e.includes("configApi"));let i=this.config.plugin.filter(e=>!e.includes("configApi"));const s=(async()=>{for(const e of g)i.includes(e)&&(i=i.filter(r=>r!==e),await this.loadPlugin(e))})();e.push(s),e.push(s.then(async()=>{const e=i.map(e=>this.loadPlugin(e));await Promise.all(e)})),r&&e.push(Promise.all(e).then(()=>this.loadPlugin(r)))}this.ready=new Promise((r,i)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),r()}).catch(e=>i(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),r())})}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.app._router?.stack;if(e){const r=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==r&&e.splice(r,1)}}this._errorMiddleware=(e,r,i,s)=>{let o="statusCode"in e?e.statusCode:500,t=e.message;if(500===o&&/\[authz-forbidden\]/.test(e.message)?(o=403,t="Token does not have permission on this branch"):403===o&&/\[authz-forbidden\]/.test(e.message)&&(t="Token does not have permission on this branch"),o>=400&&o<500)return this.logger.warn(`Client error ${o} in request ${r.method} ${r.path}: ${t}`),i.headersSent?void 0:i.status(o).json({error:t});this.logger.error(`Server error ${o} in request ${r.method} ${r.path}: ${e.message}`,{stack:e.stack,url:r.url,method:r.method}),i.headersSent||i.status(o).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,r)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:r})}),new Promise((e,r)=>{this.server=this.app.listen(this.config.port,i=>{i?(this.logger.error(`Error starting server: ${i}`),r(i)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(s){let o,t;if(/:/.test(s)){let e=s.substring(s.indexOf(":")+1);if(s=s.substring(0,s.indexOf(":")),/:/.test(e)){o=e.substring(0,e.indexOf(":")),o||(o=void 0),e=e.substring(e.indexOf(":")+1);try{t=JSON.parse(e),"object"!=typeof t?(this.logger.error(`Overrides for plugin ${s} are not valid: ${e}`),t=void 0):this.logger.debug(`Overrides for plugin ${o||s}: ${e}`)}catch(r){this.logger.error(`Failed to parse overrides for plugin ${s}: ${r}, using ${e}`),t=void 0}}else o=e,o||(o=void 0)}else o=void 0;return this.logger.debug(`Loading plugin ${s}`),s.startsWith("core/")&&(s=s.replace("core/",r(i(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((e,r)=>{import(s).then(async i=>{let n;if(i&&i.default&&(i=i.default),t){const e={...this.config,...t};n=new i({...this,config:e})}else n=new i(this);if(!n)return r(new Error(`Unable to load ${s}`));e(await this.registerPlugin(s,n,o)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(e=>r(new Error(`Failed to load plugin ${s}: ${e}`)))})}async registerPlugin(e,r,i){if(r.name||(r.name=e),i&&(r.name=i),this.loadedPlugins[r.name])return this.logger.info(`Plugin ${e} already loaded as ${r.name}`),!1;if(this.logger.debug(`Registering plugin ${e} as ${r.name}`),r.dependencies)for(const e in r.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${r.name} depends on ${e}, loading it first`),await this.loadPlugin(r.dependencies[e]));if(r.api&&(this.logger.debug(`Plugin ${r.name} has API, registering it`),await r.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),r.hooks)for(const e in r.hooks){this.logger.debug(`Plugin ${r.name} has hook ${e}, registering it`);const i=r.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!i||"function"!=typeof i)throw new Error(`Plugin ${r.name}: hook ${e} is invalid`);this.hooks[e].push(i)}return this.loadedPlugins[r.name]=r,!0}}export{DM};
@@ -1 +1 @@
1
- import{fileURLToPath as a}from"url";import{join as _,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(e(a(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(e(a(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",_(e(a(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-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-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""]];export{t as default};
1
+ import{fileURLToPath as a}from"url";import{join as _,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(e(a(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(e(a(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",_(e(a(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-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-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
- let r;const setLogger=t=>{r=t},getLogger=()=>r,tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(t,e,n)=>{r?.warn(`Client error ${t}: ${n}`),e.status(t).json({error:n})},ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(t,e)=>{const n=e instanceof Error&&"statusCode"in e?e.statusCode:500;if(n>=400&&n<500){const s=e instanceof Error?e.message:"Bad request";return r.warn(`Client error ${n}: ${s}`),void t.status(n).json({error:s})}e instanceof Error?r.error(e.message,e):"string"==typeof e?r.error(e):r.error("Server error",e),t.status(n).json({error:"check logs"})},wantJson=(r,t)=>!1!==r.accepts("json")||(badRequest(t),!1),jsonBody=(r,t,...e)=>{try{if(!wantJson(r,t))return!1;const n=r.body;if(e.length>0)for(let r=0;r<e.length;r++)if(null==n[e[r]])return badRequest(t,"Bad content"),!1;return n}catch(r){return badRequest(t,"Bad content"),!1}};export{badRequest,created,getLogger,jsonBody,notFound,ok,serverError,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
1
+ let r;const setLogger=t=>{r=t},getLogger=()=>r,tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(t,e,n)=>{r?.warn(`Client error ${t}: ${n}`),e.status(t).json({error:n})},ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(t,e)=>{const n=e instanceof Error?e.message:String(e);let s=e instanceof Error&&"statusCode"in e?e.statusCode:500;if(500===s&&/\[authz-forbidden\]/.test(n)&&(s=403),s>=400&&s<500){const e=403===s&&/\[authz-forbidden\]/.test(n)?"Token does not have permission on this branch":n;return r.warn(`Client error ${s}: ${e}`),void t.status(s).json({error:e})}e instanceof Error?r.error(e.message,e):"string"==typeof e?r.error(e):r.error("Server error",e),t.status(s).json({error:"check logs"})},wantJson=(r,t)=>!1!==r.accepts("json")||(badRequest(t),!1),jsonBody=(r,t,...e)=>{try{if(!wantJson(r,t))return!1;const n=r.body;if(e.length>0)for(let r=0;r<e.length;r++)if(null==n[e[r]])return badRequest(t,"Bad content"),!1;return n}catch(r){return badRequest(t,"Bad content"),!1}};export{badRequest,created,getLogger,jsonBody,notFound,ok,serverError,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
@@ -0,0 +1 @@
1
+ import{AsyncLocalStorage as t}from"async_hooks";import e from"../../lib/auth/base.js";import{ForbiddenError as i}from"../../lib/errors.js";import{unauthorized as n,ok as r}from"../../lib/expressFormatedResponses.js";import{getParentDn as s,asyncHandler as a}from"../../lib/utils.js";import{verifyLdapPassword as o}from"./authzDynamicHash.js";const h=new t;function asString(t){if(null!=t){if(Array.isArray(t)){const e=t[0];return null==e?void 0:Buffer.isBuffer(e)?e.toString():String(e)}return Buffer.isBuffer(t)?t.toString():String(t)}}function isAtOrUnder(t,e){const i=t.toLowerCase().trim(),n=e.toLowerCase().trim();return!(!i||!n)&&(i===n||i.endsWith(","+n))}class AuthzDynamic extends e{constructor(t){if(super(t),this.name="authzDynamic",this.roles=["auth","authz","configurable"],this.tokens=[],this.lastLoad=0,this.lastFailure=0,this.hooks={ldapsearchrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,t,"read"),[t,e,i]):[t,e,i]},ldapaddrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,s(t),"write"),[t,e,i]):[t,e,i]},ldapmodifyrequest:([t,e,i,n])=>{const r=this.activeToken(n);return r?(this.requireBranchPermission(r,s(t),"write"),[t,e,i,n]):[t,e,i,n]},ldapdeleterequest:t=>{const e=h.getStore();if(!e)return t;const i=Array.isArray(t)?t:[t];for(const t of i)this.requireBranchPermission(e,s(t),"delete");return t},ldaprenamerequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,s(t),"read"),this.requireBranchPermission(n,s(e),"write"),[t,e,i]):[t,e,i]}},this.base=this.config.authz_dynamic_base||"",this.cacheTtlMs=1e3*(this.config.authz_dynamic_cache_ttl||60),this.failureBackoffMs=Math.max(Math.floor(this.cacheTtlMs/4),5e3),this.tokenAttr=this.config.authz_dynamic_token_attribute||"userPassword",this.configAttr=this.config.authz_dynamic_config_attribute||"description",this.tenantAttr=this.config.authz_dynamic_tenant_attribute||"cn",this.reloadEndpoint=Boolean(this.config.authz_dynamic_reload_endpoint),!this.base)throw new Error("authzDynamic requires --authz-dynamic-base (the LDAP branch holding token entries)")}api(t){if(super.api(t),this.reloadEndpoint){const e=this.config.api_prefix||"/api";t.post(`${e}/v1/authz-dynamic/reload`,a(async(t,e)=>{t.authzToken?(await this.reload(),r(e,{success:!0,tokens:this.tokens.length})):n(e)})),this.logger.info(`authzDynamic reload endpoint registered at ${e}/v1/authz-dynamic/reload`)}}async reload(){try{const t=Array.from(new Set(["dn","cn",this.tokenAttr,this.configAttr,this.tenantAttr])),e=await this.server.ldap.search({filter:"(objectClass=*)",scope:"sub",paged:!1,attributes:t},this.base),i=[];for(const t of e.searchEntries||[]){const e="string"==typeof t.dn?t.dn:String(t.dn),n=asString(t.cn)||"",r=asString(t[this.tokenAttr])||"";if(!r)continue;let s,a=[];const o=asString(t[this.configAttr]);if(o)try{const t=JSON.parse(o);t&&"string"==typeof t.tenant&&t.tenant&&(s=t.tenant),t&&Array.isArray(t.bases)&&(a=t.bases.filter(t=>t&&"string"==typeof t.dn&&t.dn.length>0).map(t=>({dn:t.dn,read:Boolean(t.read),write:Boolean(t.write),delete:Boolean(t.delete)})))}catch(t){this.logger.warn(`authzDynamic: invalid JSON in ${this.configAttr} of ${e}: ${String(t)}`)}const h="cn"===this.tenantAttr?n:asString(t[this.tenantAttr])||n,c=s||h;i.push({dn:e,cn:n,tenant:c,hash:r,bases:a})}this.tokens=i,this.lastLoad=Date.now(),this.lastFailure=0,this.logger.info(`authzDynamic: loaded ${i.length} token(s) from ${this.base}`)}catch(t){this.lastFailure=Date.now(),this.logger.error(`authzDynamic: failed to load tokens from ${this.base}: ${String(t)}`)}}async ensureFresh(){const t=Date.now();if(!(this.lastLoad>0&&t-this.lastLoad<this.cacheTtlMs||this.lastFailure>0&&t-this.lastFailure<this.failureBackoffMs))return this.loading||(this.loading=this.reload().finally(()=>{this.loading=void 0})),this.loading}authMethod(t,e,i){t.user?i():this.ensureFresh().then(()=>{const r=t.headers.authorization;if(!r||!/^Bearer\s+/.test(r))return this.logger.warn("authzDynamic: missing or invalid Authorization header"),void n(e);const s=r.slice(r.indexOf(" ")+1).trim();if(!s)return void n(e);let a;for(const t of this.tokens)if(o(s,t.hash)){a=t;break}if(!a){const t=s.length>8?`${s.substring(0,8)}...`:"***";return this.logger.warn(`authzDynamic: unauthorized token ${t}`),void n(e)}t.user=a.tenant,t.authzToken=a,h.run(a,i)}).catch(t=>{this.logger.error(`authzDynamic: authMethod error: ${String(t)}`),n(e)})}activeToken(t){return h.getStore()||t?.authzToken}requireBranchPermission(t,e,n){for(const i of t.bases)if(isAtOrUnder(e,i.dn)&&i[n])return;throw this.logger.debug(`authzDynamic: token "${t.tenant}" denied ${n} on ${e}`),new i(`[authz-forbidden] Token does not have ${n} permission on this branch`)}getConfigApiData(){return{enabled:!0,base:this.base,cacheTtlSeconds:this.cacheTtlMs/1e3,tokenCount:this.tokens.length,reloadEndpoint:this.reloadEndpoint?`${this.config.api_prefix||"/api"}/v1/authz-dynamic/reload`:void 0,tokenAttribute:this.tokenAttr,configAttribute:this.configAttr,tenantAttribute:this.tenantAttr}}_tokens(){return this.tokens}}export{h as authzContext,AuthzDynamic as default};
@@ -0,0 +1 @@
1
+ import e from"crypto";const t={sha1:20,sha256:32,sha512:64,md5:16};function timingSafeStringEqual(t,r){const a=Buffer.from(t,"utf8"),n=Buffer.from(r,"utf8");return a.length===n.length&&e.timingSafeEqual(a,n)}function isCanonicalBase64(e){return 0!==e.length&&e.length%4==0&&(!!/^[A-Za-z0-9+/]+={0,2}$/.test(e)&&Buffer.from(e,"base64").toString("base64")===e)}function verifySalted(r,a,n){if(!isCanonicalBase64(a))return!1;const s=Buffer.from(a,"base64"),f=t[r];if(s.length<=f)return!1;const i=s.subarray(0,f),u=s.subarray(f),o=e.createHash(r).update(Buffer.from(n,"utf8")).update(u).digest();return o.length===i.length&&e.timingSafeEqual(i,o)}function verifyUnsalted(t,r,a){if(!isCanonicalBase64(r))return!1;return timingSafeStringEqual(r,e.createHash(t).update(Buffer.from(a,"utf8")).digest("base64"))}function verifyLdapPassword(e,t){if("string"!=typeof e||0===e.length)return!1;if("string"!=typeof t||0===t.length)return!1;const r=/^\{([A-Za-z0-9-]+)\}(.*)$/.exec(t);if(!r)return timingSafeStringEqual(e,t);const a=r[1].toUpperCase(),n=r[2];switch(a){case"SSHA":return verifySalted("sha1",n,e);case"SHA":return verifyUnsalted("sha1",n,e);case"SSHA256":return verifySalted("sha256",n,e);case"SHA256":return verifyUnsalted("sha256",n,e);case"SSHA512":return verifySalted("sha512",n,e);case"SHA512":return verifyUnsalted("sha512",n,e);case"SMD5":return verifySalted("md5",n,e);case"MD5":return verifyUnsalted("md5",n,e);case"CLEARTEXT":case"PLAIN":return timingSafeStringEqual(e,n);default:return!1}}const r={sha1:"SSHA",sha256:"SSHA256",sha512:"SSHA512"};function sshaHash(t,a,n){const s=n||e.randomBytes(16),f=e.createHash(t).update(Buffer.from(a,"utf8")).update(s).digest();return`{${r[t]}}${Buffer.concat([f,s]).toString("base64")}`}function ssha(e,t){return sshaHash("sha1",e,t)}export{ssha,sshaHash,verifyLdapPassword};
@@ -0,0 +1 @@
1
+ import e from"fs";import{escapeDnValue as s}from"../../lib/utils.js";class BaseResolver{constructor(s){const r=s.ldap_base||"";this.defaultUserBase=s.scim_user_base||r,this.defaultGroupBase=s.scim_group_base||r,this.userTemplate=s.scim_user_base_template||"",this.groupTemplate=s.scim_group_base_template||"";const t=s.scim_base_map||"";if(t)try{const s=e.readFileSync(t,"utf8");this.map=JSON.parse(s)}catch(e){throw new Error(`Failed to load SCIM base map from ${t}: ${e instanceof Error?e.message:String(e)}`)}}applyTemplate(e,r){const t=r?s(r):"";return e.replace(/\{user\}/g,t)}resolve(e,s){const r=s&&"object"==typeof s&&"user"in s?s.user:void 0;if(this.map&&r&&this.map[r]){const s=this.map[r];if("user"===e&&s.userBase)return s.userBase;if("group"===e&&s.groupBase)return s.groupBase}if(this.map&&this.map["*"]){const s=this.map["*"];if("user"===e&&s.userBase)return this.applyTemplate(s.userBase,r);if("group"===e&&s.groupBase)return this.applyTemplate(s.groupBase,r)}const t="user"===e?this.userTemplate:this.groupTemplate;return t?this.applyTemplate(t,r):"user"===e?this.defaultUserBase:this.defaultGroupBase}userBase(e){return this.resolve("user",e)}groupBase(e){return this.resolve("group",e)}}export{BaseResolver};
@@ -0,0 +1 @@
1
+ import{launchHooks as t}from"../../lib/utils.js";import{SCHEMA_BULK_RESPONSE as s}from"./types.js";import{scimInvalidValue as e,ScimError as o}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";const r=/^bulkId:([A-Za-z0-9_-]+)$/;class ScimBulk{constructor(t){this.config=t.config,this.logger=t.logger,this.users=t.users,this.groups=t.groups,this.scimPrefix=t.scimPrefix,this.hooks=t.hooks}async execute(o,r){const i=this.config.scim_bulk_max_operations||100;if(!r.Operations||!Array.isArray(r.Operations))throw e("Missing Operations array");if(r.Operations.length>i)throw e(`Too many operations: ${r.Operations.length} (max ${i})`);const a=new Map,n=[];let c=0;const u=r.failOnErrors??0;for(const t of r.Operations){if(u>0&&c>=u)break;const s=await this.executeOne(o,t,a);n.push(s),parseInt(s.status,10)>=400&&c++}const h={schemas:[s],Operations:n};return t(this.hooks.scimbulkdone,[h]),h}async executeOne(t,s,i){const a={method:s.method,bulkId:s.bulkId,status:"200"};try{if(!s.method||!s.path)throw e("method and path are required");s.data&&"object"==typeof s.data&&this.resolveRefs(s.data,i);const o=s.path.replace(/^\//,"").split("/"),n=o[0],c=o[1];if("Users"!==n&&"Groups"!==n)throw e(`Unsupported bulk path '${s.path}'`);const u="Users"===n;let h=c;if(h&&r.test(h)){const t=r.exec(h),s=i.get(t[1]);if(!s)throw e(`Unknown bulkId reference '${t[1]}'`);h=s.id}switch(s.method){case"POST":{if(h)throw e("POST must not include an id");const o=u?await this.users.create(t,s.data):await this.groups.create(t,s.data);return a.status="201",a.location=this.buildLocation(n,o.id,t),a.version=o.meta?.version||void 0,s.bulkId&&i.set(s.bulkId,{id:o.id,type:u?"User":"Group"}),a}case"PUT":{if(!h)throw e("PUT requires an id");const o=u?await this.users.replace(t,h,s.data):await this.groups.replace(t,h,s.data);return a.status="200",a.location=this.buildLocation(n,o.id,t),a}case"PATCH":{if(!h)throw e("PATCH requires an id");const o=u?await this.users.patch(t,h,s.data):await this.groups.patch(t,h,s.data);return a.status="200",a.location=this.buildLocation(n,o.id,t),a}case"DELETE":if(!h)throw e("DELETE requires an id");return u?await this.users.delete(t,h):await this.groups.delete(t,h),a.status="204",a;default:throw e(`Unsupported method '${String(s.method)}'`)}}catch(t){const s=t instanceof o?t.statusCode:500;return a.status=String(s),a.response={schemas:["urn:ietf:params:scim:api:messages:2.0:Error"],status:String(s),...t instanceof o&&t.scimType?{scimType:t.scimType}:{},detail:t instanceof Error?t.message:String(t)},a}}resolveRefs(t,s){for(const[e,o]of Object.entries(t)){if("string"==typeof o){const i=r.exec(o);if(i){const o=s.get(i[1]);o&&(t[e]=o.id)}continue}if(Array.isArray(o)){const t=o;for(let e=0;e<t.length;e++){const o=t[e];if("string"==typeof o){const i=r.exec(o);if(i){const o=s.get(i[1]);o&&(t[e]=o.id)}}else o&&"object"==typeof o&&this.resolveRefs(o,s)}continue}o&&"object"==typeof o&&this.resolveRefs(o,s)}}buildLocation(t,s,e){const o=this.config.scim_base_url||"";return`${o?o.replace(/\/$/,""):e?.protocol&&e.get?`${e.protocol}://${String(e.get("host")||"")}`:""}${this.scimPrefix}/${t}/${encodeURIComponent(s)}`}}export{ScimBulk};
@@ -0,0 +1 @@
1
+ import e from"fs";import t from"path";import{SCHEMA_SERVICE_PROVIDER_CONFIG as s,SCHEMA_USER as o,SCHEMA_RESOURCE_TYPE as i,SCHEMA_GROUP as r,SCHEMA_SCHEMA as n}from"./types.js";class ScimDiscovery{constructor(e){this.config=e.config,this.schemaDir=e.schemaDir,this.scimPrefix=e.scimPrefix,this.loadedPlugins=e.loadedPlugins}baseUrl(e){const t=this.config.scim_base_url||"";return t?t.replace(/\/$/,""):e?.protocol&&e.get?`${e.protocol}://${String(e.get("host")||"")}`:""}location(e,t){return`${this.baseUrl(e)}${this.scimPrefix}${t}`}authSchemes(){const e=[];return this.loadedPlugins.authToken&&e.push({name:"OAuth Bearer Token",description:'Bearer token configured via --auth-token. Send "Authorization: Bearer <token>".',type:"oauthbearertoken",primary:0===e.length}),this.loadedPlugins.openidconnect&&e.push({name:"OpenID Connect",description:"OIDC-issued access token.",type:"oauth2",specUri:"https://openid.net/specs/openid-connect-core-1_0.html",primary:0===e.length}),this.loadedPlugins.authHmac&&e.push({name:"HMAC",description:"Signed request via HMAC-SHA256.",type:"httpbasic",primary:0===e.length}),0===e.length&&e.push({name:"No authentication",description:"No authentication plugin loaded. NOT RECOMMENDED for production.",type:"httpbasic",primary:!0}),e}serviceProviderConfig(e){return{schemas:[s],patch:{supported:!0},bulk:{supported:!0,maxOperations:this.config.scim_bulk_max_operations||100,maxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},filter:{supported:!0,maxResults:this.config.scim_max_results||200},changePassword:{supported:!1},sort:{supported:!1},etag:{supported:Boolean(this.config.scim_etag)},authenticationSchemes:this.authSchemes(),meta:{resourceType:"ServiceProviderConfig",location:this.location(e,"/ServiceProviderConfig")}}}resourceTypes(e){return[{schemas:[i],id:"User",name:"User",endpoint:"/Users",description:"User Account",schema:o,meta:{resourceType:"ResourceType",location:this.location(e,"/ResourceTypes/User")}},{schemas:[i],id:"Group",name:"Group",endpoint:"/Groups",description:"Group",schema:r,meta:{resourceType:"ResourceType",location:this.location(e,"/ResourceTypes/Group")}}]}resourceType(e,t){return this.resourceTypes(t).find(t=>t.id===e)}schemas(e){const t=[this.loadSchema("User.json",o),this.loadSchema("Group.json",r)].filter(e=>null!=e);for(const s of t)s.meta={resourceType:"Schema",location:this.location(e,`/Schemas/${s.id}`)};return t}schema(e,t){return this.schemas(t).find(t=>t.id===e)}loadSchema(s,o){const i=t.join(this.schemaDir,s);try{const t=e.readFileSync(i,"utf8"),s=JSON.parse(t);return{schemas:[n],id:o,name:s.name||o.split(":").pop()||o,description:s.description,attributes:s.attributes||[]}}catch{return}}}export{ScimDiscovery};
@@ -0,0 +1 @@
1
+ import{HttpError as r}from"../../lib/errors.js";import{SCHEMA_ERROR as e}from"./types.js";const t="application/scim+json";class ScimError extends r{constructor(r,e,t){super(e,r),this.name="ScimError",this.scimType=t}}const scimInvalidFilter=(r="Invalid filter")=>new ScimError(400,r,"invalidFilter"),scimInvalidPath=(r="Invalid path")=>new ScimError(400,r,"invalidPath"),scimInvalidValue=(r="Invalid value")=>new ScimError(400,r,"invalidValue"),scimInvalidSyntax=(r="Invalid syntax")=>new ScimError(400,r,"invalidSyntax"),scimNoTarget=(r="No target")=>new ScimError(400,r,"noTarget"),scimMutability=(r="Immutable attribute")=>new ScimError(400,r,"mutability"),scimUniqueness=(r="Uniqueness violation")=>new ScimError(409,r,"uniqueness"),scimTooMany=(r="Too many results")=>new ScimError(400,r,"tooMany"),scimNotFound=(r="Resource not found")=>new ScimError(404,r);function writeScimError(r,o,i,n){const s={schemas:[e],status:String(o),...n?{scimType:n}:{},detail:i};r.status(o).type(t).json(s)}function extractLdapCode(r){if(null==r)return;if("object"==typeof r&&"code"in r){const e=r.code;if("number"==typeof e)return e}const e=r instanceof Error?r.message:"string"==typeof r?r:"";return/noSuchObject|No such object|code:?\s*(32|0x20)/i.test(e)?32:/entryAlreadyExists|Already[_ ]?Exists|code:?\s*(68|0x44)/i.test(e)?68:/noSuchAttribute|No such attribute|code:?\s*(16|0x10)/i.test(e)?16:void 0}function writeScimErrorFromException(e,t,o=500){const sanitize=r=>/\[authz-forbidden\]/.test(r)?"Token does not have permission on this branch":r;if(t instanceof ScimError)return void writeScimError(e,t.statusCode,sanitize(t.message),t.scimType);if(t instanceof r)return void writeScimError(e,t.statusCode,sanitize(t.message));const i=t instanceof Error?t.message:String(t);if(/\[authz-forbidden\]/.test(i))return void writeScimError(e,403,"Token does not have permission on this branch");const n=extractLdapCode(t);32!==n?68!==n?writeScimError(e,o,sanitize(i)||"Internal error"):writeScimError(e,409,sanitize(i),"uniqueness"):writeScimError(e,404,"Resource not found")}const scimAsyncHandler=r=>(e,t,o)=>{Promise.resolve().then(()=>r(e,t,o)).catch(r=>{t.headersSent?o(r):writeScimErrorFromException(t,r)})};export{t as SCIM_CONTENT_TYPE,ScimError,extractLdapCode,scimAsyncHandler,scimInvalidFilter,scimInvalidPath,scimInvalidSyntax,scimInvalidValue,scimMutability,scimNoTarget,scimNotFound,scimTooMany,scimUniqueness,writeScimError,writeScimErrorFromException};
@@ -0,0 +1 @@
1
+ import{escapeLdapFilter as e}from"../../lib/utils.js";import{scimPathToLdapAttribute as t}from"./mapping.js";import{scimInvalidFilter as s}from"./errors.js";const o=new Set(["and","or","not","pr","eq","ne","co","sw","ew","gt","ge","lt","le","true","false","null"]);class Parser{constructor(e,t){this.tokens=e,this.mapping=t,this.pos=0}peek(e=0){return this.tokens[this.pos+e]}consume(){return this.tokens[this.pos++]}expect(e,t){const o=this.tokens[this.pos];if(o.kind!==e||t&&o.value.toLowerCase()!==t)throw s(`Expected ${t||e} at position ${o.pos}, got '${o.value}'`);return this.pos++,o}parse(){const e=this.parseOr();if("EOF"!==this.peek().kind)throw s(`Unexpected token '${this.peek().value}' at position ${this.peek().pos}`);return e}parseOr(){const e=[this.parseAnd()];for(;"WORD"===this.peek().kind&&"or"===this.peek().value.toLowerCase();)this.consume(),e.push(this.parseAnd());return 1===e.length?e[0]:`(|${e.join("")})`}parseAnd(){const e=[this.parseNot()];for(;"WORD"===this.peek().kind&&"and"===this.peek().value.toLowerCase();)this.consume(),e.push(this.parseNot());return 1===e.length?e[0]:`(&${e.join("")})`}parseNot(){if("WORD"===this.peek().kind&&"not"===this.peek().value.toLowerCase()){this.consume();return`(!${this.parsePrimary()})`}return this.parsePrimary()}parsePrimary(){const e=this.peek();if("LPAREN"===e.kind){this.consume();const e=this.parseOr();return this.expect("RPAREN"),e}if("WORD"===e.kind){const e=this.consume().value;if("LBRACK"===this.peek().kind)throw s(`Complex multi-valued filters ('${e}[...]') are not supported; use 'and' at top level instead`);const t=this.consume();if("WORD"!==t.kind)throw s(`Expected comparison operator at position ${t.pos}`);const n=t.value.toLowerCase();if("pr"===n){return`(${this.resolvePath(e)}=*)`}if(!o.has(n)||["and","or","not"].includes(n))throw s(`Unknown operator '${t.value}' at position ${t.pos}`);const i=this.consume(),r=this.tokenToValue(i);return this.emitComparison(e,n,r)}throw s(`Unexpected token '${e.value}' at position ${e.pos}`)}tokenToValue(e){if("STRING"===e.kind)return e.value;if("NUMBER"===e.kind){const t=Number(e.value);if(Number.isNaN(t))throw s(`Invalid number '${e.value}' at position ${e.pos}`);return t}if("WORD"===e.kind){const t=e.value.toLowerCase();return"true"===t||"false"!==t&&("null"===t?null:e.value)}throw s(`Unexpected value token at position ${e.pos}`)}resolvePath(e){if("id"===e)throw s("filter on 'id' only supports 'eq' with a string value");if("active"===e)return"active";const o=t(e,this.mapping);if(!o)throw s(`Unknown attribute path '${e}'`);return o}emitComparison(t,o,n){const i=this.resolvePath(t);if("active"===i){const e=!0===n||"string"==typeof n&&"true"===n.toLowerCase();if("eq"===o)return e?"(!(pwdAccountLockedTime=*))":"(pwdAccountLockedTime=*)";if("ne"===o)return e?"(pwdAccountLockedTime=*)":"(!(pwdAccountLockedTime=*))";throw s(`Operator '${o}' not supported for 'active'`)}if(null===n){if("eq"===o)return`(!(${i}=*))`;if("ne"===o)return`(${i}=*)`;throw s(`Operator '${o}' not supported with null`)}const r=String(n),a=e(r);switch(o){case"eq":return`(${i}=${a})`;case"ne":return`(!(${i}=${a}))`;case"co":return`(${i}=*${a}*)`;case"sw":return`(${i}=${a}*)`;case"ew":return`(${i}=*${a})`;case"gt":return`(&(${i}>=${a})(!(${i}=${a})))`;case"ge":return`(${i}>=${a})`;case"lt":return`(&(${i}<=${a})(!(${i}=${a})))`;case"le":return`(${i}<=${a})`;default:throw s(`Unknown operator '${o}'`)}}}function scimFilterToLdap(e,t){const o=e.trim();if(!o)return{ldapFilter:"(objectClass=*)",touchesId:!1};const n=/^id\s+eq\s+"([^"\\]*(?:\\.[^"\\]*)*)"\s*$/i.exec(o);if(n)return{ldapFilter:"(objectClass=*)",touchesId:!0,idEquals:n[1].replace(/\\(.)/g,"$1")};const i=function(e){const t=[];let o=0;for(;o<e.length;){const n=e[o];if(" "!==n&&"\t"!==n&&"\n"!==n&&"\r"!==n)if("("!==n)if(")"!==n)if("["!==n){if("]"!==n){if('"'===n){const n=o;o++;let i="";for(;o<e.length&&'"'!==e[o];){if("\\"===e[o]&&o+1<e.length){const t=e[o+1];if('"'===t){i+='"',o+=2;continue}if("\\"===t){i+="\\",o+=2;continue}if("n"===t){i+="\n",o+=2;continue}if("t"===t){i+="\t",o+=2;continue}}i+=e[o],o++}if(o>=e.length)throw s(`Unterminated string literal at position ${n}`);o++,t.push({kind:"STRING",value:i,pos:n});continue}if(n>="0"&&n<="9"){const s=o;for(;o<e.length&&/[0-9.+\-eE]/.test(e[o])&&!/[\s()[\]]/.test(e[o]);)o++;t.push({kind:"NUMBER",value:e.slice(s,o),pos:s});continue}if(/[A-Za-z_$:]/.test(n)){const s=o;for(;o<e.length&&/[A-Za-z0-9_.$:]/.test(e[o]);)o++;t.push({kind:"WORD",value:e.slice(s,o),pos:s});continue}throw s(`Unexpected character '${n}' at position ${o}`)}t.push({kind:"RBRACK",value:"]",pos:o}),o++}else t.push({kind:"LBRACK",value:"[",pos:o}),o++;else t.push({kind:"RPAREN",value:")",pos:o}),o++;else t.push({kind:"LPAREN",value:"(",pos:o}),o++;else o++}return t.push({kind:"EOF",value:"",pos:e.length}),t}(o);return{ldapFilter:new Parser(i,t).parse(),touchesId:/\bid\b/.test(o)}}export{scimFilterToLdap};
@@ -0,0 +1 @@
1
+ import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as r,isChildOf as i,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{mergeMapping as n,loadMappingFile as c,DEFAULT_GROUP_MAPPING as h,requiredLdapAttributes as u,ldapToScimGroup as m,scimGroupToLdap as l}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{patchToModifyRequest as d}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany as y,scimInvalidValue as w,scimUniqueness as $}from"./errors.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=n(h,e?c(e):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const e=this.baseResolver.userBase(t),s=this.baseResolver.groupBase(t),r=this.scimPrefix,i=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",o=this.rdnAttribute,n=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(t.toLowerCase()===n.toLowerCase())return;const c=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!c)return;const h=c[2].replace(/\\(.)/g,"$1"),u=t.toLowerCase();let m;e&&u.endsWith(e.toLowerCase())?m="User":s&&u.endsWith(s.toLowerCase())?m="Group":c[1]===a?m="User":c[1]===o&&(m="Group");const l={value:h};if(m&&(l.type=m,i)){const t="User"===m?"Users":"Groups";l.$ref=`${i.replace(/\/$/,"")}${r}/${t}/${encodeURIComponent(h)}`}return l}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.search({paged:!1,scope:"base",attributes:[...u(this.mapping),"member"]},s)}catch(t){if(32===g(t))throw f(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw f(`Group ${e} not found`);return m(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let a,n=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=p(e.filter,this.mapping);t.idEquals?a=t.idEquals:n=`(&${n}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof b&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const c=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:[...u(this.mapping),"member"],sizeLimit:this.maxResults+1},s)).searchEntries||[],h=c.length;if(h>this.maxResults)throw y(`Filter matched ${h} resources, exceeds max ${this.maxResults}`);const l=c.slice(r-1,r-1+i),d=this.buildMemberResolver(t),g=l.map(e=>m(e,this.mapping,this.ctx(t),d));return{schemas:[o],totalResults:h,startIndex:r,itemsPerPage:g.length,Resources:g}}async create(i,a){if(!a.displayName)throw w("displayName is required");const o=(await e(this.hooks.scimgroupcreate,[a,i]))[0],{rdn:n,attributes:c}=l(o,this.mapping,this.objectClass);if(!n)throw w("displayName is required");s(n,this.rdnAttribute),c[this.rdnAttribute]=n;const h=[];if(o.members&&Array.isArray(o.members))for(const t of o.members){if(!t||!t.value)continue;const e=await this.users.resolveRef(i,t.value);e&&h.push(e)}if(0===h.length){const t=this.config.group_dummy_user||"cn=fakeuser";h.push(t)}c.member=h;const u=this.baseResolver.groupBase(i),m=`${this.rdnAttribute}=${t(n)},${u}`;try{await this.ldap.add(m,c)}catch(t){if(68===g(t))throw $(`Group ${n} already exists`);throw t}const p=await this.get(i,n);return r(this.hooks.scimgroupcreatedone,[p]),p}async replace(t,s,i){await this.get(t,s);const a=(await e(this.hooks.scimgroupupdate,[s,i,t]))[1],{attributes:o}=l(a,this.mapping,this.objectClass),n=[];if(a.members&&Array.isArray(a.members))for(const e of a.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(s,t),h={replace:{}};for(const[t,e]of Object.entries(o))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.modify(c,h);const u=await this.get(t,s);return r(this.hooks.scimgroupupdatedone,[s,u]),u}async patch(t,e,s){await this.get(t,e);const i=await d(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e)});if(!i.add&&!i.replace&&(!i.delete||Array.isArray(i.delete)&&0===i.delete.length))return this.get(t,e);const a=this.config.group_dummy_user||"cn=fakeuser",o=this.dnForId(e,t);if(i.delete&&!Array.isArray(i.delete)&&i.delete.member){const t=await this.ldap.search({paged:!1,scope:"base",attributes:["member"]},o),e=t.searchEntries[0]?.member,s=Array.isArray(e)?e:e?[String(e)]:[],r=Array.isArray(i.delete.member)?i.delete.member:[String(i.delete.member)];0===s.filter(t=>!r.includes(t)).length&&(i.add||(i.add={}),i.add.member=[a])}await this.ldap.modify(o,i);const n=await this.get(t,e);return r(this.hooks.scimgroupupdatedone,[e,n]),n}async delete(t,s){await this.get(t,s);const i=(await e(this.hooks.scimgroupdelete,[s,t]))[0],a=this.dnForId(i,t);await this.ldap.delete(a),r(this.hooks.scimgroupdeletedone,[i])}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e);if(s.includes("=")&&s.includes(","))return i(s,r)?s:void 0;const o=`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}}get groupMapping(){return this.mapping}}export{ScimGroups};
@@ -0,0 +1 @@
1
+ import e from"fs";import{SCHEMA_GROUP as r,SCHEMA_USER as i}from"./types.js";const a=["createTimestamp","modifyTimestamp","entryUUID"],t={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},n={resourceType:"Group",schemas:[r],entries:[{scim:"displayName",ldap:"cn"},{scim:"externalId",ldap:"entryUUID",operational:!0,readOnly:!0}]};function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const i=e.readFileSync(r,"utf8");return JSON.parse(i)}function mergeMapping(e,r){if(!r)return e;const i=new Map;for(const r of e.entries)i.set(r.scim,r);for(const e of r.entries)i.set(e.scim,e);return{...e,entries:Array.from(i.values())}}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const i="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${i}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,a){const t={schemas:[i]},n=resolveScimId(e,a);n&&(t.id=n);for(const i of r.entries){if(i.sub){const r={};for(const[a,t]of Object.entries(i.sub)){const i=asString(e[t]);null!=i&&(r[a]=i)}Object.keys(r).length>0&&(t[i.scim]=r);continue}if(i.ldapPrimary||i.ldapSecondary){const r=[];if(i.ldapPrimary){const a=asString(e[i.ldapPrimary]);null!=a&&r.push({value:a,primary:!0})}if(i.ldapSecondary)for(const a of asArray(e[i.ldapSecondary]))r.push({value:a});r.length>0&&(t[i.scim]=r);continue}if(i.ldap)if("array"===i.multi){const r=asArray(e[i.ldap]);r.length>0&&(t[i.scim]=r)}else{const r=asString(e[i.ldap]);null!=r&&(t[i.scim]=r)}}return t.active=null==e.pwdAccountLockedTime,n&&(t.meta={resourceType:"User",created:asString(e.createTimestamp),lastModified:asString(e.modifyTimestamp),location:buildLocation(n,a)}),t}function scimUserToLdap(e,r,i,a){const t={objectClass:a},n=e.userName||e.id||"";for(const i of r.entries){if(i.readOnly||i.operational)continue;const r=e[i.scim];if(null!=r)if(i.sub&&"object"==typeof r)for(const[e,a]of Object.entries(i.sub)){const i=r[e];"string"==typeof i&&i.length>0&&(t[a]=i)}else{if(i.ldapPrimary||i.ldapSecondary){if(!Array.isArray(r))continue;const e=r,a=e.find(e=>!0===e.primary)||e[0],n=e.filter(e=>e!==a);i.ldapPrimary&&a&&a.value&&(t[i.ldapPrimary]=a.value),i.ldapSecondary&&n.length>0&&(t[i.ldapSecondary]=n.map(e=>e.value).filter(Boolean));continue}i.ldap&&(Array.isArray(r)?t[i.ldap]=r.map(e=>String(e)):"string"!=typeof r&&"number"!=typeof r||(t[i.ldap]=String(r)))}}if(!t.cn&&n&&(t.cn=n),!t.sn){const r=e.name?.familyName||e.displayName||n;r&&(t.sn=r)}return{rdn:n,attributes:t}}function ldapToScimGroup(e,i,a,t){const n={schemas:[r]},s=resolveScimId(e,a);s&&(n.id=s);for(const r of i.entries){if(!r.ldap)continue;const i=e[r.ldap];if(null!=i)if("array"===r.multi){const e=asArray(i);e.length>0&&(n[r.scim]=e)}else{const e=asString(i);null!=e&&(n[r.scim]=e)}}const o=asArray(e.member),l=[];for(const e of o)if(e)if(t){const r=t(e);r&&l.push(r)}else l.push({value:e,type:"User"});return l.length>0&&(n.members=l),s&&(n.meta={resourceType:"Group",created:asString(e.createTimestamp),lastModified:asString(e.modifyTimestamp),location:buildLocation(s,a)}),n}function scimGroupToLdap(e,r,i){const a={objectClass:i},t=e.displayName||e.id||"";for(const i of r.entries){if(i.readOnly||i.operational)continue;if(!i.ldap)continue;const r=e[i.scim];null!=r&&(Array.isArray(r)?a[i.ldap]=r.map(e=>"string"==typeof e?e:JSON.stringify(e)):"string"!=typeof r&&"number"!=typeof r||(a[i.ldap]=String(r)))}return{rdn:t,attributes:a}}function requiredLdapAttributes(e){const r=new Set(["objectClass",...a]);for(const i of e.entries)if(i.ldap&&r.add(i.ldap),i.ldapPrimary&&r.add(i.ldapPrimary),i.ldapSecondary&&r.add(i.ldapSecondary),i.sub)for(const e of Object.values(i.sub))r.add(e);return Array.from(r)}function scimPathToLdapAttribute(e,r){const i=e.split(".")[0],a=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===i){if(e.sub&&a)return e.sub[a];if((e.ldapPrimary||e.ldapSecondary)&&("value"===a||!a))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{n as DEFAULT_GROUP_MAPPING,t as DEFAULT_USER_MAPPING,buildLocation,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,requiredLdapAttributes,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap};
@@ -0,0 +1 @@
1
+ import{scimInvalidValue as e,scimInvalidPath as t,scimNoTarget as r}from"./errors.js";import{scimPathToLdapAttribute as o}from"./mapping.js";function normalizeOp(t){const r=t.toLowerCase();if("add"===r||"remove"===r||"replace"===r)return r;throw e(`Unknown patch op '${t}'`)}const a=new Set(["__proto__","constructor","prototype"]),n=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(a.has(e))throw t(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw t(`Malformed path '${String(e)}'`);let r,o,a;const i=e.indexOf("[");if(i>=0){const n=e.lastIndexOf("]");if(n<=i)throw t(`Malformed path '${e}'`);r=e.slice(0,i),o=e.slice(i+1,n);const l=e.slice(n+1);if(l.length>0){if("."!==l[0])throw t(`Malformed path '${e}'`);a=l.slice(1)}}else{const t=e.indexOf(".");t>=0?(r=e.slice(0,t),a=e.slice(t+1)):r=e}if(!n.test(r))throw t(`Malformed path '${e}'`);if(assertSafeKey(r),null!=a){if(!n.test(a))throw t(`Malformed sub-attribute in '${e}'`);assertSafeKey(a)}return{top:r,filter:o,sub:a}}function mergeAttr(e,t,r){const o=e[t];if(null==o)return void(e[t]=r);const a=[...(n=o,Array.isArray(n)?n.map(e=>"string"==typeof e?e:e.toString()):["string"==typeof n?n:n.toString()]),...Array.isArray(r)?r:[r]];var n;e[t]=a}async function applyOperation(a,n,i){const l=normalizeOp(a.op),s=i.memberAttribute||"member";if(!a.path){if(null==a.value||"object"!=typeof a.value||Array.isArray(a.value))throw r("PATCH without path requires an object value");const e=a.value;for(const[t,r]of Object.entries(e))await applyOperation({op:a.op,path:t,value:r},n,i);return}const{top:f,sub:p,filter:u}=parsePath(a.path);if("members"===f){if(!i.resolveMemberRef)throw r("Member operations require a member resolver");if("add"===l){const e=Array.isArray(a.value)?a.value:[a.value],t=[];for(const r of e){const e="string"==typeof r?r:"object"==typeof r&&null!=r&&"value"in r?String(r.value):"";if(!e)continue;const o=await i.resolveMemberRef(e);o&&t.push(o)}return void(t.length>0&&(n.add||(n.add={}),mergeAttr(n.add,s,t)))}if("remove"===l){const collectFromValue=()=>{if(null==a.value)return[];return(Array.isArray(a.value)?a.value:[a.value]).map(e=>"string"==typeof e?e:"object"==typeof e&&null!=e&&"value"in e?String(e.value):"").filter(Boolean)},e=[...(()=>{if(!u)return[];const e=/value\s+eq\s+"([^"]+)"/i.exec(u);return e?[e[1]]:[]})(),...collectFromValue()],t=[];for(const r of e){const e=await i.resolveMemberRef(r);e&&t.push(e)}return void(t.length>0?(n.delete||(n.delete={}),Array.isArray(n.delete)?n.delete={[s]:t}:mergeAttr(n.delete,s,t)):u||a.value||(n.delete||(n.delete={}),Array.isArray(n.delete)?n.delete.push(s):n.delete[s]=""))}if("replace"===l){const e=Array.isArray(a.value)?a.value:[a.value],t=[];for(const r of e){const e="string"==typeof r?r:"object"==typeof r&&null!=r&&"value"in r?String(r.value):"";if(!e)continue;const o=await i.resolveMemberRef(e);o&&t.push(o)}return void(t.length>0&&(n.replace||(n.replace={}),n.replace[s]=t))}}if(u)throw t(`Complex multi-valued filters are only supported on 'members' (got '${a.path}')`);const c=o(p?`${f}.${p}`:f,i.mapping);if(!c)throw t(`Unknown SCIM attribute path '${a.path}'`);if("remove"===l)return n.delete||(n.delete={}),void(Array.isArray(n.delete)?n.delete.push(c):n.delete[c]="");const y=function(t){if(null!=t)return"string"==typeof t?t:"number"==typeof t||"boolean"==typeof t?String(t):Array.isArray(t)?t.map(t=>{if("string"==typeof t)return t;if("number"==typeof t||"boolean"==typeof t)return String(t);if(t&&"object"==typeof t&&!Array.isArray(t)&&"value"in t&&("string"==typeof t.value||"number"==typeof t.value||"boolean"==typeof t.value))return String(t.value);throw e(`Unsupported array element in PATCH value: ${JSON.stringify(t)}`)}):void 0}(a.value);if(null==y)throw e(`PATCH ${a.op} ${a.path} missing value`);return"add"===l?(n.add||(n.add={}),void mergeAttr(n.add,c,y)):"replace"===l?(n.replace||(n.replace={}),void(n.replace[c]=y)):void 0}async function patchToModifyRequest(t,r){if(!t.Operations||!Array.isArray(t.Operations))throw e("Missing Operations array");const o={};for(const e of t.Operations)await applyOperation(e,o,r);return o}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:a}=parsePath(e.path);if("remove"!==t)if(a){const t=r[o],n=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);n[a]=e.value,r[o]=n}else r[o]=e.value;else if(a){const e=r[o];e&&"object"==typeof e&&delete e[a]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest};
@@ -0,0 +1 @@
1
+ import{fileURLToPath as s}from"url";import{join as i,dirname as e}from"path";import t from"body-parser";import o from"../../abstract/plugin.js";import{BaseResolver as r}from"./baseResolver.js";import{ScimUsers as a}from"./users.js";import{ScimGroups as n}from"./groups.js";import{ScimBulk as c}from"./bulk.js";import{ScimDiscovery as p}from"./discovery.js";import{SCIM_CONTENT_TYPE as m,scimAsyncHandler as d,writeScimError as u}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";class Scim extends o{constructor(t){super(t),this.name="scim",this.roles=["api","configurable"],this.ldap=t.ldap,this.scimPrefix=this.config.scim_prefix||"/scim/v2",this.baseResolver=new r(this.config),this.users=new a({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,hooks:this.registeredHooks}),this.groups=new n({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hooks:this.registeredHooks}),this.bulk=new c({config:this.config,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hooks:this.registeredHooks});const o=this.config.schemas_path||i(e(s(import.meta.url)),"..","..","..","static","schemas");this.discovery=new p({config:this.config,schemaDir:i(o,"scim"),scimPrefix:this.scimPrefix,loadedPlugins:t.loadedPlugins})}scimJson(s,i,e){s.status(i).type(m).json(e)}readScimBody(s){const i=String(s.headers["content-type"]||"");if(i.startsWith("application/scim+json")||i.startsWith("application/json"))return s.body}api(s){const i=this.scimPrefix;s.use(i,t.json({type:["application/json","application/scim+json"],limit:`${this.config.scim_bulk_max_payload_size||1048576}b`})),s.get(`${i}/Users`,d(async(s,i)=>{const e=this.parseListQuery(s),t=await this.users.list(s,e);this.scimJson(i,200,t)})),s.get(`${i}/Users/:id`,d(async(s,i)=>{const e=await this.users.get(s,decodeURIComponent(s.params.id));this.scimJson(i,200,e)})),s.post(`${i}/Users`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.users.create(s,e);this.scimJson(i,201,t)})),s.put(`${i}/Users/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.users.replace(s,decodeURIComponent(s.params.id),e);this.scimJson(i,200,t)})),s.patch(`${i}/Users/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.users.patch(s,decodeURIComponent(s.params.id),e);this.scimJson(i,200,t)})),s.delete(`${i}/Users/:id`,d(async(s,i)=>{await this.users.delete(s,decodeURIComponent(s.params.id)),i.status(204).end()})),s.get(`${i}/Groups`,d(async(s,i)=>{const e=this.parseListQuery(s),t=await this.groups.list(s,e);this.scimJson(i,200,t)})),s.get(`${i}/Groups/:id`,d(async(s,i)=>{const e=await this.groups.get(s,decodeURIComponent(s.params.id));this.scimJson(i,200,e)})),s.post(`${i}/Groups`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.groups.create(s,e);this.scimJson(i,201,t)})),s.put(`${i}/Groups/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.groups.replace(s,decodeURIComponent(s.params.id),e);this.scimJson(i,200,t)})),s.patch(`${i}/Groups/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.groups.patch(s,decodeURIComponent(s.params.id),e);this.scimJson(i,200,t)})),s.delete(`${i}/Groups/:id`,d(async(s,i)=>{await this.groups.delete(s,decodeURIComponent(s.params.id)),i.status(204).end()})),s.post(`${i}/Bulk`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=this.config.scim_bulk_max_payload_size||1048576,o=JSON.stringify(e).length;if(o>t)return u(i,413,`Bulk payload too large: ${o} > ${t}`,"invalidValue");const r=await this.bulk.execute(s,e);this.scimJson(i,200,r)})),s.get(`${i}/ServiceProviderConfig`,d((s,i)=>{this.scimJson(i,200,this.discovery.serviceProviderConfig(s))})),s.get(`${i}/ResourceTypes`,d((s,i)=>{const e=this.discovery.resourceTypes(s);this.scimJson(i,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:e.length,Resources:e,startIndex:1,itemsPerPage:e.length})})),s.get(`${i}/ResourceTypes/:name`,d((s,i)=>{const e=this.discovery.resourceType(s.params.name,s);if(!e)return u(i,404,"ResourceType not found");this.scimJson(i,200,e)})),s.get(`${i}/Schemas`,d((s,i)=>{const e=this.discovery.schemas(s);this.scimJson(i,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:e.length,Resources:e,startIndex:1,itemsPerPage:e.length})})),s.get(`${i}/Schemas/:id`,d((s,i)=>{const e=this.discovery.schema(decodeURIComponent(s.params.id),s);if(!e)return u(i,404,"Schema not found");this.scimJson(i,200,e)}))}parseListQuery(s){const i=s.query,toInt=s=>{if("string"!=typeof s)return;const i=parseInt(s,10);return Number.isFinite(i)?i:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},e="string"==typeof i.sortOrder?i.sortOrder:void 0;return{filter:"string"==typeof i.filter?i.filter:void 0,startIndex:toInt(i.startIndex),count:toInt(i.count),attributes:toCsv(i.attributes),excludedAttributes:toCsv(i.excludedAttributes),sortBy:"string"==typeof i.sortBy?i.sortBy:void 0,sortOrder:"ascending"===e||"descending"===e?e:void 0}}getConfigApiData(){const s=this.scimPrefix;let i;if(this.server.loadedPlugins.static){const s=this.config.static_name||"static";i={user:`/${s}/schemas/scim/User.json`,group:`/${s}/schemas/scim/Group.json`,defaultMapping:`/${s}/schemas/scim/default-mapping.json`}}const e=this.users.userMapping,t=this.groups.groupMapping,o=e.entries.map(s=>s.scim).concat(["id","active"]),r=t.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:i,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:Boolean(this.config.scim_etag),changePassword:!1,maxResults:this.config.scim_max_results||200,bulkMaxOperations:this.config.scim_bulk_max_operations||100,bulkMaxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.config.scim_user_rdn_attribute||"uid",objectClass:this.config.scim_user_object_class,filterableAttributes:o,mapping:e.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.config.scim_group_rdn_attribute||"cn",objectClass:this.config.scim_group_object_class,filterableAttributes:r,mapping:t.entries}],idStrategy:this.config.scim_id_attribute||"rdn",baseResolution:{userBaseTemplate:this.config.scim_user_base_template||void 0,groupBaseTemplate:this.config.scim_group_base_template||void 0,hasBaseMap:Boolean(this.config.scim_base_map)}}}}export{Scim as default};
@@ -0,0 +1 @@
1
+ const s="urn:ietf:params:scim:schemas:core:2.0:User",e="urn:ietf:params:scim:schemas:core:2.0:Group",a="urn:ietf:params:scim:api:messages:2.0:ListResponse",r="urn:ietf:params:scim:api:messages:2.0:Error",m="urn:ietf:params:scim:api:messages:2.0:PatchOp",i="urn:ietf:params:scim:api:messages:2.0:BulkRequest",c="urn:ietf:params:scim:api:messages:2.0:BulkResponse",p="urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig",t="urn:ietf:params:scim:schemas:core:2.0:ResourceType",u="urn:ietf:params:scim:schemas:core:2.0:Schema";export{i as SCHEMA_BULK_REQUEST,c as SCHEMA_BULK_RESPONSE,r as SCHEMA_ERROR,e as SCHEMA_GROUP,a as SCHEMA_LIST_RESPONSE,m as SCHEMA_PATCH_OP,t as SCHEMA_RESOURCE_TYPE,u as SCHEMA_SCHEMA,p as SCHEMA_SERVICE_PROVIDER_CONFIG,s as SCHEMA_USER};
@@ -0,0 +1 @@
1
+ import{escapeDnValue as t,launchHooksChained as s,validateDnValue as e,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as h}from"./types.js";import{mergeMapping as n,loadMappingFile as o,DEFAULT_USER_MAPPING as c,requiredLdapAttributes as l,ldapToScimUser as d,scimUserToLdap as u}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{patchToModifyRequest as m}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany as w,scimInvalidValue as y,scimUniqueness as x}from"./errors.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const s=this.config.scim_user_mapping||"";this.mapping=n(c,s?o(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix}}dnForId(s,e){const i=this.baseResolver.userBase(e);return`${this.rdnAttribute}=${t(s)},${i}`}async get(t,s){const e=this.dnForId(s,t);let i;try{i=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},e)}catch(t){if(32===g(t))throw f(`User ${s} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw f(`User ${s} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,s){const e=this.baseResolver.userBase(t),i=Math.max(1,s.startIndex||1),r=Math.min(this.maxResults,Math.max(0,s.count??this.maxResults));let a,n=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(s.filter){const t=p(s.filter,this.mapping);t.idEquals?a=t.idEquals:n=`(&${n}${t.ldapFilter})`}if(a)try{const s=await this.get(t,a);return{schemas:[h],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[s]}}catch(t){if(t instanceof b&&404===t.statusCode)return{schemas:[h],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const o=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:l(this.mapping),sizeLimit:this.maxResults+1},e)).searchEntries||[],c=o.length;if(c>this.maxResults)throw w(`Filter matched ${c} resources, exceeds max ${this.maxResults}`);const u=o.slice(i-1,i-1+r);s.sortBy,s.sortOrder;const m=u.map(s=>d(s,this.mapping,this.ctx(t)));return{schemas:[h],totalResults:c,startIndex:i,itemsPerPage:m.length,Resources:m}}async create(r,a){if(!a.userName)throw y("userName is required");const h=(await s(this.hooks.scimusercreate,[a,r]))[0],{rdn:n,attributes:o}=u(h,this.mapping,this.ctx(r),this.objectClass);if(!n)throw y("userName is required");e(n,this.rdnAttribute),o[this.rdnAttribute]=n;const c=this.baseResolver.userBase(r),l=`${this.rdnAttribute}=${t(n)},${c}`;try{await this.ldap.add(l,o)}catch(t){if(68===g(t))throw x(`User ${n} already exists`);throw t}const d=await this.get(r,n);return i(this.hooks.scimusercreatedone,[d]),d}async replace(t,e,r){const a=this.dnForId(e,t),h=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},a);if(!h.searchEntries?.length)throw f(`User ${e} not found`);const n=h.searchEntries[0],o=(await s(this.hooks.scimuserupdate,[e,r,t]))[1],{attributes:c}=u(o,this.mapping,this.ctx(t),this.objectClass),d={replace:{},delete:[]},p=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of l(this.mapping))p.has(t)||(null!=c[t]?d.replace[t]=c[t]:hasAttrValue(n[t])&&d.delete.push(t));if(0===Object.keys(d.replace||{}).length&&delete d.replace,0===d.delete.length&&delete d.delete,d.replace||d.delete)try{await this.ldap.modify(a,d)}catch(t){if(16!==g(t))throw t}const m=await this.get(t,e);return i(this.hooks.scimuserupdatedone,[e,m]),m}async patch(t,s,e){await this.get(t,s);const r=await m(e,{mapping:this.mapping});if(!r.add&&!r.replace&&(!r.delete||Array.isArray(r.delete)&&0===r.delete.length))return this.get(t,s);const a=this.dnForId(s,t);await this.ldap.modify(a,r);const h=await this.get(t,s);return i(this.hooks.scimuserupdatedone,[s,h]),h}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimuserdelete,[e,t]))[0],a=this.dnForId(r,t);await this.ldap.delete(a),i(this.hooks.scimuserdeletedone,[r])}async resolveRef(s,e){if(!e)return;const i=this.baseResolver.userBase(s);if(e.includes("=")&&e.includes(","))return r(e,i)?e:void 0;const h=`${this.rdnAttribute}=${t(e)},${i}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},h);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.search({filter:`(${this.rdnAttribute}=${a(e)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}}get userMapping(){return this.mapping}}export{ScimUsers};
@@ -58,6 +58,26 @@ export default abstract class LdapFlat extends DmPlugin {
58
58
  apiDelete(req: Request, res: Response): Promise<void>;
59
59
  apiModify(req: Request, res: Response): Promise<void>;
60
60
  apiMove(req: Request, res: Response): Promise<void>;
61
+ /**
62
+ * Resolve an id (either an RDN value or a full DN) into a DN inside this
63
+ * instance's base branch.
64
+ *
65
+ * A value is treated as a full DN only when it starts with
66
+ * `mainAttribute=`; otherwise it is treated as a raw RDN value and escaped.
67
+ * This matters for cn-based branches where values like `Smith, John` are
68
+ * legal and must not be misclassified as DNs.
69
+ *
70
+ * When the id is a full DN we enforce that its parent equals this.base
71
+ * (LdapFlat entries are, by definition, direct children of the base).
72
+ * The comparison is done on the parsed RDN components so escape-aware
73
+ * payloads like `cn=pwn\,ou=titles,ou=nomenclature,dc=example,dc=com`
74
+ * cannot sneak past a naive textual suffix check: `\,` is a literal comma
75
+ * inside the first RDN's value, so the actual parent of that DN is
76
+ * `ou=nomenclature,dc=example,dc=com` — one level above the expected base.
77
+ *
78
+ * @throws BadRequestError if the provided DN is not a direct child of this.base
79
+ */
80
+ protected resolveDn(id: string): string;
61
81
  addEntry(id: string, additional?: AttributesList, req?: Request): Promise<boolean>;
62
82
  modifyEntry(id: string, changes: ModifyRequest): Promise<boolean>;
63
83
  renameEntry(id: string, newId: string): Promise<boolean>;
@@ -78,6 +78,12 @@ export interface Config {
78
78
  base_url?: string;
79
79
  authz_per_branch_config?: AuthConfig;
80
80
  authz_per_branch_cache_ttl?: number;
81
+ authz_dynamic_base?: string;
82
+ authz_dynamic_cache_ttl?: number;
83
+ authz_dynamic_token_attribute?: string;
84
+ authz_dynamic_config_attribute?: string;
85
+ authz_dynamic_tenant_attribute?: string;
86
+ authz_dynamic_reload_endpoint?: boolean;
81
87
  authz_local_admin_attribute?: string;
82
88
  rate_limit_window_ms?: number;
83
89
  rate_limit_max?: number;
@@ -129,6 +135,24 @@ export interface Config {
129
135
  ppolicy_require_digit?: boolean;
130
136
  ppolicy_require_special?: boolean;
131
137
  ldap_users_base?: string;
138
+ scim_prefix?: string;
139
+ scim_user_base?: string;
140
+ scim_group_base?: string;
141
+ scim_user_base_template?: string;
142
+ scim_group_base_template?: string;
143
+ scim_base_map?: string;
144
+ scim_user_object_class?: string[];
145
+ scim_user_rdn_attribute?: string;
146
+ scim_group_object_class?: string[];
147
+ scim_group_rdn_attribute?: string;
148
+ scim_id_attribute?: string;
149
+ scim_user_mapping?: string;
150
+ scim_group_mapping?: string;
151
+ scim_max_results?: number;
152
+ scim_bulk_max_operations?: number;
153
+ scim_bulk_max_payload_size?: number;
154
+ scim_etag?: boolean;
155
+ scim_base_url?: string;
132
156
  [key: string]: string | string[] | boolean | number | AttributesList | AuthConfig | undefined;
133
157
  }
134
158
  /**
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @module plugins/auth/authzDynamic
3
+ * @author Xavier Guimard <xguimard@linagora.com>
4
+ *
5
+ * LDAP-backed authentication + per-branch authorization plugin.
6
+ *
7
+ * Tokens are stored as entries under a dedicated LDAP branch (one entry per
8
+ * bearer token). The plugin caches them and enforces read / write / delete
9
+ * permissions on subsequent LDAP operations — so each client is scoped to
10
+ * its own subtree without touching the process configuration.
11
+ *
12
+ * Entry shape (conventions, overridable via CLI):
13
+ * dn: cn=<token-name>,<authz-dynamic-base>
14
+ * cn: <token-name>
15
+ * userPassword: {SSHA}… ← secret, hashed
16
+ * description: {"tenant":"acme", ← JSON config
17
+ * "bases":[
18
+ * {"dn":"ou=users,ou=acme,dc=example,dc=com",
19
+ * "read":true,"write":true,"delete":true}
20
+ * ]}
21
+ *
22
+ * The entry is treated as "token for tenant `acme`, permitted to read/write/
23
+ * delete under `ou=users,ou=acme,…`". Multiple bases per token are allowed.
24
+ * Sub-branch matching is used (a permission on `ou=acme` covers
25
+ * `ou=users,ou=acme`).
26
+ */
27
+ import { AsyncLocalStorage } from 'async_hooks';
28
+ import type { Express, Response } from 'express';
29
+ import { type Role } from '../../abstract/plugin';
30
+ import type { DM } from '../../bin';
31
+ import AuthBase, { type DmRequest } from '../../lib/auth/base';
32
+ import type { Hooks } from '../../hooks';
33
+ export interface BranchAcl {
34
+ dn: string;
35
+ read?: boolean;
36
+ write?: boolean;
37
+ delete?: boolean;
38
+ }
39
+ export interface TokenConfig {
40
+ tenant?: string;
41
+ bases?: BranchAcl[];
42
+ }
43
+ export interface TokenEntry {
44
+ dn: string;
45
+ cn: string;
46
+ tenant: string;
47
+ hash: string;
48
+ bases: BranchAcl[];
49
+ }
50
+ /** Extends DmRequest with the resolved token entry for downstream hooks. */
51
+ export interface AuthzDynamicRequest extends DmRequest {
52
+ authzToken?: TokenEntry;
53
+ }
54
+ /**
55
+ * Per-request async context holding the authenticated token.
56
+ *
57
+ * Core plugins (ldapGroups, ldapFlat, etc.) do not always forward the Express
58
+ * `req` object down to `ldapActions.search()` / `.modify()`. Relying solely
59
+ * on `req` would therefore let un-tracked operations bypass authorization.
60
+ * Using AsyncLocalStorage gives us a request-scoped, ambient handle that the
61
+ * authz hooks can read regardless of whether the caller threaded `req`.
62
+ */
63
+ export declare const authzContext: AsyncLocalStorage<TokenEntry>;
64
+ export default class AuthzDynamic extends AuthBase {
65
+ name: string;
66
+ roles: Role[];
67
+ private tokens;
68
+ private lastLoad;
69
+ private lastFailure;
70
+ private readonly cacheTtlMs;
71
+ private readonly failureBackoffMs;
72
+ private readonly base;
73
+ private readonly tokenAttr;
74
+ private readonly configAttr;
75
+ private readonly tenantAttr;
76
+ private readonly reloadEndpoint;
77
+ private loading?;
78
+ constructor(server: DM);
79
+ api(app: Express): void;
80
+ /**
81
+ * Load token entries from LDAP. Swallows errors so a transient LDAP hiccup
82
+ * does not poison the cache — the previous snapshot stays in use.
83
+ */
84
+ reload(): Promise<void>;
85
+ /**
86
+ * Ensure the cache is fresh (blocks if reloading).
87
+ *
88
+ * On a bootstrap or successful reload the cache is valid for
89
+ * `cacheTtlMs`. If the load fails, we still refuse to retry on every
90
+ * request — `failureBackoffMs` (default 1/4 of TTL, min 5 s) keeps the
91
+ * previous snapshot in use and shields LDAP from a thundering-herd
92
+ * during an outage.
93
+ */
94
+ private ensureFresh;
95
+ authMethod(req: DmRequest, res: Response, next: () => void): void;
96
+ /**
97
+ * Authorization hooks. These run after the auth middleware has populated
98
+ * `req.authzToken`, so we can read the allowed bases directly from the
99
+ * request.
100
+ */
101
+ hooks: Hooks;
102
+ /** Read the active token from the async context, or (as a fallback) from `req`. */
103
+ private activeToken;
104
+ /**
105
+ * Throw a ForbiddenError (403) if the token cannot perform `permission` on
106
+ * the branch containing `branchOrDn`. Any ACL entry that is `branchOrDn`
107
+ * itself or a parent of it (sub-branch match) qualifies.
108
+ *
109
+ * The client-facing message omits the target DN to avoid leaking tenant
110
+ * topology through authorization failures; the full context is still
111
+ * logged server-side at debug level for operators.
112
+ */
113
+ private requireBranchPermission;
114
+ /** Expose current cache (primarily for the configApi auto-discovery). */
115
+ getConfigApiData(): Record<string, unknown>;
116
+ _tokens(): TokenEntry[];
117
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Verify a provided bearer token against a stored LDAP-style hash.
3
+ *
4
+ * The stored hash is typically the contents of `userPassword`:
5
+ * either `{SCHEME}<data>` or cleartext.
6
+ */
7
+ export declare function verifyLdapPassword(provided: string, stored: string): boolean;
8
+ /**
9
+ * Convenience helpers for tests / tooling to generate an LDAP-style hash.
10
+ *
11
+ * These are NOT used by the runtime verification path and NOT intended for
12
+ * use as a general password-hashing function: the primary use case is
13
+ * seeding LDAP token fixtures in tests. Production `userPassword` values
14
+ * should be generated by `slappasswd` (or another operator tool) with a
15
+ * scheme of your choice. The RFC 3112 `{SSHA}` scheme uses a single SHA-1
16
+ * round — adequate for LDAP bearer-token interop, not adequate for
17
+ * user-chosen passwords. Prefer `sshaHash('sha512', …)` (`{SSHA512}`) when
18
+ * generating new fixtures.
19
+ */
20
+ export type SshaAlgo = 'sha1' | 'sha256' | 'sha512';
21
+ export declare function sshaHash(algo: SshaAlgo, password: string, salt?: Buffer): string;
22
+ /**
23
+ * Legacy alias: generate an {SSHA} (salted SHA-1) hash.
24
+ * Retained so existing tests keep working. Prefer `sshaHash('sha512', ...)`
25
+ * for new fixtures.
26
+ */
27
+ export declare function ssha(password: string, salt?: Buffer): string;
@@ -0,0 +1,23 @@
1
+ import type { Config } from '../../config/args';
2
+ import type { DmRequest } from '../../lib/auth/base';
3
+ export interface BaseMapEntry {
4
+ userBase?: string;
5
+ groupBase?: string;
6
+ }
7
+ export type BaseMap = Record<string, BaseMapEntry>;
8
+ export declare class BaseResolver {
9
+ private readonly defaultUserBase;
10
+ private readonly defaultGroupBase;
11
+ private readonly userTemplate;
12
+ private readonly groupTemplate;
13
+ private readonly map;
14
+ constructor(config: Config);
15
+ private applyTemplate;
16
+ private resolve;
17
+ userBase(req?: DmRequest | {
18
+ user?: string;
19
+ }): string;
20
+ groupBase(req?: DmRequest | {
21
+ user?: string;
22
+ }): string;
23
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @module plugins/scim/bulk
3
+ * @author Xavier Guimard <xguimard@linagora.com>
4
+ *
5
+ * SCIM 2.0 Bulk operations (RFC 7644 §3.7).
6
+ *
7
+ * Executes operations sequentially. Supports `bulkId` cross-references:
8
+ * a POST with `"bulkId":"abc"` creates a resource; subsequent operations
9
+ * in the same request can reference it as "bulkId:abc" anywhere a SCIM
10
+ * id or member.value is expected (resolved to the concrete id before
11
+ * dispatch).
12
+ */
13
+ import type winston from 'winston';
14
+ import type { Config } from '../../config/args';
15
+ import type { DmRequest } from '../../lib/auth/base';
16
+ import type { ScimUsers } from './users';
17
+ import type { ScimGroups } from './groups';
18
+ import { type BulkRequest, type BulkResponse } from './types';
19
+ import { writeScimErrorFromException } from './errors';
20
+ export interface ScimBulkOptions {
21
+ config: Config;
22
+ logger: winston.Logger;
23
+ users: ScimUsers;
24
+ groups: ScimGroups;
25
+ scimPrefix: string;
26
+ hooks: {
27
+ [K: string]: Function[] | undefined;
28
+ };
29
+ }
30
+ export declare class ScimBulk {
31
+ private readonly config;
32
+ private readonly logger;
33
+ private readonly users;
34
+ private readonly groups;
35
+ private readonly scimPrefix;
36
+ private readonly hooks;
37
+ constructor(opts: ScimBulkOptions);
38
+ execute(req: DmRequest, body: BulkRequest): Promise<BulkResponse>;
39
+ private executeOne;
40
+ private resolveRefs;
41
+ private buildLocation;
42
+ }
43
+ export { writeScimErrorFromException };