ldap-rest 0.1.1 → 0.1.3
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.
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/plugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/errors.js +1 -0
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/auth/trustedProxy.js +1 -0
- package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organization.js +1 -1
- package/dist/plugins/priority.json.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/plugins/weblogs.js +1 -1
- package/dist/src/abstract/plugin.d.ts +1 -0
- package/dist/src/bin/index.d.ts +5 -0
- package/dist/src/config/args.d.ts +2 -0
- package/dist/src/lib/errors.d.ts +47 -0
- package/dist/src/lib/utils.d.ts +17 -0
- package/dist/src/plugins/auth/trustedProxy.d.ts +67 -0
- package/dist/src/plugins/twake/james.d.ts +9 -0
- package/dist/src/plugins/weblogs.d.ts +1 -0
- package/docs/authentication.md +133 -0
- package/docs/twakeJames.md +61 -0
- package/package.json +8 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"fs";import{wantJson as e,serverError as i,notFound as r,jsonBody as s,created as a,tryMethod as n}from"../lib/expressFormatedResponses.js";import{transformSchemas as o,launchHooksChained as h,launchHooks as d}from"../lib/utils.js";import c from"./plugin.js";class LdapFlat extends c{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(o(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}`,async(t,r)=>{if(e(t,r))try{const e={};t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(e.filter=`(${t.query.attribute}=*${t.query.match}*)`),t.query.attributes&&"string"==typeof t.query.attributes&&(e.attributes=t.query.attributes.split(","));const i=await this.listEntries(e);r.json(i)}catch(t){return i(r,t)}}),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiGet(t,e)),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,async(t,e)=>this.apiAdd(t,e)),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiDelete(t,e)),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiModify(t,e)),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,async(t,e)=>this.apiMove(t,e))}async apiGet(t,s){if(!e(t,s))return;const a=decodeURIComponent(t.params.id);try{const t=/,/.test(a)?a:`${this.mainAttribute}=${a},${this.base}`,e=await this.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)return r(s,`${this.singularName} not found`);s.json(e.searchEntries[0])}catch(t){return t.code&&32===t.code?r(s,`${this.singularName} not found`):i(s,t)}}async apiAdd(t,e){const r=s(t,e,this.mainAttribute);if(!r)return;const n=r[this.mainAttribute],o={...r};delete o[this.mainAttribute],delete o.dn;try{await this.addEntry(n,o,t);const i=await this.searchEntriesByName(n,!1);return a(e,i[n])}catch(t){return i(e,t)}}async apiDelete(t,i){if(!e(t,i))return;const r=decodeURIComponent(t.params.id);await n(i,this.deleteEntry.bind(this),r)}async apiModify(t,e){const i=s(t,e);if(!i)return;const r=decodeURIComponent(t.params.id);await n(e,this.modifyEntry.bind(this),r,i)}async apiMove(t,r){if(!e(t,r))return;const a=s(t,r,"targetOrgDn");if(!a)return;const n=decodeURIComponent(t.params.id),{targetOrgDn:o}=a;if(o&&"string"==typeof o)try{const e=await this.moveEntry(n,o,t);r.json({success:!0,...e})}catch(t){return i(r,t)}else r.status(400).json({error:"Missing or invalid targetOrgDn in request body"})}async addEntry(t,e={},i){let r;if(new RegExp(`^${this.mainAttribute}=`).test(t)){r=t;const e=`,${this.base}`;if(!r.endsWith(e))throw new Error(`DN must be in the flat branch "${this.base}". Provided DN "${r}" does not end with "${e}"`);t=t.replace(new RegExp(`^${this.mainAttribute}=([^,]+).*`),"$1")}else r=`${this.mainAttribute}=${t},${this.base}`;await this.validateNewEntry(r,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let s,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"),[r,a]=await h(this.registeredHooks[`${this.hookPrefix}add`],[r,a]),this.logger.debug("Adding LDAP entry:",{dn:r,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{s=await this.ldap.add(r,a,i)}catch(t){throw this.logger.error("LDAP add failed:",{dn:r,entry:JSON.stringify(a,null,2),error:t}),new Error(`Failed to add ${this.singularName} ${r}: ${t}`)}return d(this.registeredHooks[`${this.hookPrefix}adddone`],[r,a]),s}async modifyEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`;const r=this.opNumber();if([i,e]=await h(this.registeredHooks[`${this.hookPrefix}modify`],[i,e,r]),e.add&&e.add[this.mainAttribute])throw new Error(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new Error(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new Error(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new Error(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(i,e);const s=await this.ldap.modify(i,e);return d(this.registeredHooks[`${this.hookPrefix}modifydone`],[i,e,r]),s}async renameEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`,r=/,/.test(e)?e:`${this.mainAttribute}=${e},${this.base}`;[i,r]=await h(this.registeredHooks[`${this.hookPrefix}rename`],[i,r]);const s=await this.ldap.rename(i,r);return d(this.registeredHooks[`${this.hookPrefix}renamedone`],[i,r]),s}async deleteEntry(t){let e=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`;e=await h(this.registeredHooks[`${this.hookPrefix}delete`],e);const i=await this.ldap.delete(e);return d(this.registeredHooks[`${this.hookPrefix}deletedone`],e),i}async moveEntry(t,e,i){const r=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`,s=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[s]||!this.schema?.attributes[a])throw new Error(`Schema for ${this.singularName} does not support move operation (missing ${s} or ${a})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[s]},r,i);if(!n.searchEntries||0===n.searchEntries.length)throw new Error(`Entry ${r} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await h(this.registeredHooks[`${this.hookPrefix}move`],[r,e,i]);const d={replace:{[s]:e,[a]:o}};return await this.modifyEntry(t,d),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const r=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!r.searchEntries||0===r.searchEntries.length)throw new Error(`Organization ${t} not found`);const s=r.searchEntries[0];if(s[i]){const t=s[i];return Array.isArray(t)?String(t[0]):String(t)}const a=s.ou||s.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 r=await this.ldap.search(i,this.base),s={};for await(const t of r)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),s[i]=t}});return s}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const r=e?`(${this.mainAttribute}=*${t}*)`:`(${this.mainAttribute}=${t})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new Error(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const r=JSON.stringify(e.default);if(r!==JSON.stringify(i))throw new Error(`Attribute "${t}" is fixed and cannot be modified. Expected: ${r}`)}if(!await this._validateOneChange(t,i))throw new Error(`Invalid value for attribute "${t}"`);if(e.required&&!i)throw new Error(`Attribute "${t}" is required`)}for(const[t,i]of Object.entries(this.schema.attributes))if(i.required&&!e[t])throw new Error(`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 Error(`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 Error(`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 Error(`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 Error(`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 Error(`Field ${t} must be a string (DN pointer)`);const r=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>new RegExp(`,?${t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}$`,"i").test(r)))throw new Error(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.search({paged:!1,scope:"base"},r);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new Error(`Field ${t} points to non-existent DN: ${r}`)}catch(e){throw new Error(`Field ${t} points to invalid or non-existent DN: ${r}`)}}if(i.test){const t=new RegExp(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 a}from"../lib/expressFormatedResponses.js";import{transformSchemas as r,asyncHandler as n,launchHooksChained as o,launchHooks as h}from"../lib/utils.js";import{NotFoundError as d,BadRequestError as c,ConflictError as u}from"../lib/errors.js";import l from"./plugin.js";class LdapFlat extends l{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={};t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(s.filter=`(${t.query.attribute}=*${t.query.match}*)`),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}=${s},${this.base}`,e=await this.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new d(`${this.singularName} not found`);i.json(e.searchEntries[0])}catch(t){if(t.code&&32===t.code)throw new d(`${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 c("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 c(`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 s=`${this.mainAttribute}=${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 o(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 h(this.registeredHooks[`${this.hookPrefix}adddone`],[s,r]),a}async modifyEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`;const s=this.opNumber();if([i,e]=await o(this.registeredHooks[`${this.hookPrefix}modify`],[i,e,s]),e.add&&e.add[this.mainAttribute])throw new u(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new c(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new c(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new c(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(i,e);const a=await this.ldap.modify(i,e);return h(this.registeredHooks[`${this.hookPrefix}modifydone`],[i,e,s]),a}async renameEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`,s=/,/.test(e)?e:`${this.mainAttribute}=${e},${this.base}`;[i,s]=await o(this.registeredHooks[`${this.hookPrefix}rename`],[i,s]);const a=await this.ldap.rename(i,s);return h(this.registeredHooks[`${this.hookPrefix}renamedone`],[i,s]),a}async deleteEntry(t){let e=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`;e=await o(this.registeredHooks[`${this.hookPrefix}delete`],e);const i=await this.ldap.delete(e);return h(this.registeredHooks[`${this.hookPrefix}deletedone`],e),i}async moveEntry(t,e,i){const s=/,/.test(t)?t:`${this.mainAttribute}=${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 c(`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 d(`Entry ${s} not found`);const h=await this.getDepartmentPath(e,i);[,e]=await o(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const u={replace:{[a]:e,[r]:h}};return await this.modifyEntry(t,u),{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 d(`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 c(`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 c(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new c(`Invalid value for attribute "${t}"`);if(e.required&&!i)throw new c(`Attribute "${t}" is required`)}for(const[t,i]of Object.entries(this.schema.attributes))if(i.required&&!e[t])throw new c(`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 c(`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 c(`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 c(`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 c(`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 c(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>new RegExp(`,?${t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}$`,"i").test(s)))throw new c(`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 c(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new c(`Field ${t} points to invalid or non-existent DN: ${s}`)}}if(i.test){const t=new RegExp(i.test);return Array.isArray(e)?e.every(e=>t.test(e)):t.test(e)}return!0}}export{LdapFlat as default};
|
package/dist/abstract/plugin.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{asyncHandler}from"../lib/utils.js";class DmPlugin{constructor(
|
|
1
|
+
export{asyncHandler}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DmPlugin{constructor(r){this.registeredHooks={},this.server=r,this.config=r.config,this.logger=r.logger,this.registeredHooks=r.hooks}opNumber(){return this.server.operationSequence++}}export{DmPlugin as default};
|
package/dist/bin/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as e}from"url";import{join as
|
|
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,escapeLdapFilter,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas}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};
|
package/dist/config/args.js
CHANGED
|
@@ -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"],["--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"],["--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"]];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"],["--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"],["--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"]];export{t as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class HttpError extends Error{constructor(r,o=500){super(r),this.statusCode=o,this.name="HttpError"}}class BadRequestError extends HttpError{constructor(r="Bad request"){super(r,400),this.name="BadRequestError"}}class UnauthorizedError extends HttpError{constructor(r="Unauthorized"){super(r,401),this.name="UnauthorizedError"}}class ForbiddenError extends HttpError{constructor(r="Forbidden"){super(r,403),this.name="ForbiddenError"}}class NotFoundError extends HttpError{constructor(r="Not found"){super(r,404),this.name="NotFoundError"}}class ConflictError extends HttpError{constructor(r="Conflict"){super(r,409),this.name="ConflictError"}}class UriTooLongError extends HttpError{constructor(r="URI Too Long"){super(r,414),this.name="UriTooLongError"}}class TooManyRequestsError extends HttpError{constructor(r="Too Many Requests"){super(r,429),this.name="TooManyRequestsError"}}class BadGatewayError extends HttpError{constructor(r="Bad Gateway"){super(r,502),this.name="BadGatewayError"}}class ServiceUnavailableError extends HttpError{constructor(r="Service Unavailable"){super(r,503),this.name="ServiceUnavailableError"}}class GatewayTimeoutError extends HttpError{constructor(r="Gateway Timeout"){super(r,504),this.name="GatewayTimeoutError"}}export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError};
|
|
@@ -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
|
|
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 s=await t(...e);return ok(r,s)}catch(t){return serverError(r,t)}},_rejectResponse=(r,t,e)=>{t.status(r).json({error:e})},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 s=e instanceof Error&&"statusCode"in e?e.statusCode:500;if(s>=400&&s<500){const n=e instanceof Error?e.message:"Bad request";return r.warn(`Client error ${s}: ${n}`),void t.status(s).json({error:n})}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 s=r.body;if(e.length>0)for(let r=0;r<e.length;r++)if(null==s[e[r]])return badRequest(t,"Bad content"),!1;return s}catch(r){return badRequest(t,"Bad content"),!1}};export{badRequest,created,getLogger,jsonBody,notFound,ok,serverError,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
|
package/dist/lib/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getLogger as
|
|
1
|
+
import{getLogger as e}from"./expressFormatedResponses.js";const t=e(),launchHooks=async(e,...r)=>{if(e)for(const n of e)if(n)try{await n(...r)}catch(e){t.error("Hook error",e)}},launchHooksChained=async(e,t)=>{if(e)for(const r of e)r&&(t=await r(t));return t},transformSchemas=(e,t)=>e.toString().replace(/__(\S+)__/g,(e,r)=>{if(!r||"string"!=typeof r)return e;const n=r.trim().toLowerCase();return t[n]?"object"!=typeof t[n]?t[n]:JSON.stringify(t[n]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function parseDn(e){const t=[];let r="",n=!1;for(let o=0;o<e.length;o++){const s=e[o];n?(r+=s,n=!1):"\\"===s?(r+=s,n=!0):","===s?(t.push(r.trim()),r=""):r+=s}return r&&t.push(r.trim()),t}function getParentDn(e){const t=parseDn(e);return t.length<=1?e:t.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function isChildOf(e,t){const r=e.toLowerCase(),n=t.toLowerCase();if(!r.endsWith(n))return!1;if(r.length===n.length)return!1;return r.substring(0,r.length-n.length).endsWith(",")}const asyncHandler=e=>(t,r,n)=>{Promise.resolve(e(t,r,n)).catch(n)};export{asyncHandler,escapeLdapFilter,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import r from"../../abstract/plugin.js";class TrustedProxy extends r{constructor(...r){super(...r),this.name="trustedProxy",this.roles=["protect"],this.trustedNetworks=[];const e=this.config.trusted_proxy;if(!e||!Array.isArray(e)||0===e.length)throw new Error("TrustedProxy plugin requires trusted_proxy configuration (array of IPs or CIDR ranges)");for(const r of e)try{this.trustedNetworks.push(this.parseCIDR(r))}catch(e){throw new Error(`Invalid trusted proxy address "${r}": ${e instanceof Error?e.message:String(e)}`)}this.authHeader=(this.config.trusted_proxy_auth_header||"Auth-User").toLowerCase(),this.logger.info(`Trusted proxy validation enabled for ${e.length} address(es): ${e.join(", ")} (auth header: ${this.authHeader})`)}api(r){r.use((r,e,s)=>{const t=r.socket.remoteAddress;if(!t)return this.logger.debug("No remote address, removing X-Forwarded-For header"),delete r.headers["x-forwarded-for"],s();const o=r.headers["x-forwarded-for"];if(this.isTrustedProxy(t)){r.trustedProxy=!0;const e=r.headers[this.authHeader];e?(r.proxyAuthUser=Array.isArray(e)?e[0]:e,this.logger.debug(`Request from trusted proxy ${t}, Auth-User: ${r.proxyAuthUser}, X-Forwarded-For: ${o?String(o):"(none)"}`)):this.logger.debug(`Request from trusted proxy ${t}, X-Forwarded-For: ${o?String(o):"(none)"}`)}else r.trustedProxy=!1,o&&(this.logger.warn(`Removing X-Forwarded-For header from untrusted source ${t} (was: ${String(o)})`),delete r.headers["x-forwarded-for"]);s()})}isTrustedProxy(r){try{const e=this.normalizeIP(r),s=this.parseIP(e);for(const r of this.trustedNetworks)if(s.isIPv6===r.isIPv6&&(s.ip&r.mask)===(r.ip&r.mask))return!0;return!1}catch(e){return this.logger.error(`Failed to parse IP address "${r}": ${e instanceof Error?e.message:String(e)}`),!1}}normalizeIP(r){const e=r.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);return e?e[1]:r}parseIP(r){return r.includes(":")?{ip:this.parseIPv6(r),isIPv6:!0}:{ip:this.parseIPv4(r),isIPv6:!1}}parseCIDR(r){const[e,s]=r.split("/"),t=e.includes(":");let o,n;const i=t?128:32;if(o=t?this.parseIPv6(e):this.parseIPv4(e),void 0!==s){const r=parseInt(s,10);if(isNaN(r)||r<0||r>i)throw new Error(`Invalid prefix length: ${s}`);if(0===r)n=0n;else{const e=BigInt(i);n=(1n<<e)-1n^(1n<<e-BigInt(r))-1n}}else n=t?(1n<<128n)-1n:(1n<<32n)-1n;return{ip:o,mask:n,isIPv6:t}}parseIPv4(r){const e=r.split(".");if(4!==e.length)throw new Error(`Invalid IPv4 address: ${r}`);let s=0n;for(const t of e){const e=parseInt(t,10);if(isNaN(e)||e<0||e>255)throw new Error(`Invalid IPv4 address: ${r}`);s=s<<8n|BigInt(e)}return s}parseIPv6(r){let e=r;if(r.includes("::")){const s=r.split("::");if(s.length>2)throw new Error(`Invalid IPv6 address: ${r}`);const t=s[0]?s[0].split(":"):[],o=s[1]?s[1].split(":"):[],n=8-t.length-o.length;if(n<0)throw new Error(`Invalid IPv6 address: ${r}`);const i=Array(n).fill("0");e=[...t,...i,...o].join(":")}const s=e.split(":");if(8!==s.length)throw new Error(`Invalid IPv6 address: ${r}`);let t=0n;for(const e of s){const s=parseInt(e||"0",16);if(isNaN(s)||s<0||s>65535)throw new Error(`Invalid IPv6 address: ${r}`);t=t<<16n|BigInt(s)}return t}}export{TrustedProxy as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"../../abstract/plugin.js";import{launchHooksChained as r,launchHooks as s}from"../../lib/utils.js";class ExternalUsersInGroups extends e{constructor(e){super(e),this.name="externalUsersInGroups",this.roles=["consistency"],this.dependencies={ldapGroups:"core/ldap/
|
|
1
|
+
import e from"../../abstract/plugin.js";import{launchHooksChained as r,launchHooks as s}from"../../lib/utils.js";class ExternalUsersInGroups extends e{constructor(e){super(e),this.name="externalUsersInGroups",this.roles=["consistency"],this.dependencies={ldapGroups:"core/ldap/groups"},this.hooks={ldapgroupvalidatemembers:async([e,a])=>(await Promise.all(a.map(e=>e.replace(/\s/g,"")).map(e=>this.server.ldap.queryLimit(async()=>{if(!new RegExp(`mail=([^,]+),${this.config.external_members_branch}$`).test(e))return!1;try{return await this.ldap.search({paged:!1,scope:"base"},e),!0}catch{const a=e.replace(/^mail=([^,]+),.*$/,"$1");if(!a)throw new Error(`Malformed member ${e}`);if(this.config.mail_domain&&Array.isArray(this.config.mail_domain)){const e=a.split("@")[1];if(e&&this.config.mail_domain.includes(e))throw new Error(`Cannot create external user with managed domain: ${a}`)}let t={objectClass:this.config.external_branch_class||this.config.user_class,mail:a,[this.config.ldap_groups_main_attribute]:a,sn:a};[e,t]=await r(this.registeredHooks.externaluserentry,[e,t]);try{return await this.ldap.add(e,t),s(this.registeredHooks.externaluseradded,e,t),!0}catch(r){throw new Error(`Unable to insert ${e}: ${r instanceof Error?r.message:String(r)}`)}}}))),[e,a])},this.ldap=e.ldap}}export{ExternalUsersInGroups as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,badRequest as i,ok as s,serverError as a,jsonBody as o,tryMethod as n,notFound as d}from"../../lib/expressFormatedResponses.js";import{transformSchemas as c,launchHooksChained as h,launchHooks as p}from"../../lib/utils.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async e=>{let t=e;return Array.isArray(t)||(t=[t]),this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)}),e}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(c(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,async(e,t)=>{if(r(e,t))try{const r={};if(e.query.match&&("string"!=typeof e.query.match||!/^[\w*=()&|]+$/.test(e.query.match)))return i(t,"Invalid match query");e.query.match&&(r.filter=/=/.test(e.query.match)?`${e.query.match}`:`(${this.cn}=${e.query.match})`),e.query.attributes&&"string"==typeof e.query.attributes&&(r.attributes=e.query.attributes.split(","));const a=await this.listGroups(r);return s(t,a)}catch(e){return a(t,e)}}),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiGet(e,t)),e.post(`${this.config.api_prefix}/v1/ldap/groups`,async(e,t)=>this.apiAdd(e,t,this.cn)),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiDelete(e,t)),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiModify(e,t)),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)return i(t,"cn is required");const s=o(e,t,"member");s&&await n(t,this.addMember.bind(this),r,s.member)}),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!s||!a)return i(t,"cn and member are required");await n(t,this.deleteMember.bind(this),s,a)}),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");const a=o(e,t,"targetOrgDn");a&&await n(t,this.moveGroup.bind(this),s,a.targetOrgDn,e)}),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");const a=o(e,t,"newCn");a&&await n(t,this.renameGroup.bind(this),s,a.newCn)})}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${i},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)return d(t,"Group not found");t.json(r.searchEntries[0])}catch(e){return e.code&&32===e.code?d(t,"Group not found"):a(t,e)}}async apiAdd(e,t,...r){const i=o(e,t,...r);if(!i)return;const s=i[this.cn],a=i.member?i.member:[],d=Object.fromEntries(Object.entries(i).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await n(t,this.addGroup.bind(this),s,a,d)}async apiDelete(e,t){if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");await n(t,this.deleteGroup.bind(this),s)}async apiModify(e,t){const r=o(e,t);if(!r)return;const s=this.fixDn(decodeURIComponent(e.params.cn));if(!s)return i(t);const a=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await n(t,this.modifyGroup.bind(this),s,a)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=([^,]+).*`),"$1")):i=`${this.cn}=${e},${this.base}`,await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});const s=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||s.includes("twakeGroup")||s.includes("twakeStaticGroup")||s.push("twakeGroup");let a,o={objectClass:s,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&o.member.push(...t),[i,o]=await h(this.registeredHooks.ldapgroupadd,[i,o]);try{a=await this.ldap.add(i,o)}catch(e){throw new Error(`Failed to add group ${i}: ${e}`)}return p(this.registeredHooks.ldapgroupadddone,[i,o]),a}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;const i=this.opNumber();if([r,t]=await h(this.registeredHooks.ldapgroupmodify,[r,t,i]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(r,t);const s=await this.ldap.modify(r,t);return p(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${t},${this.base}`;[r,i]=await h(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return p(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;t=await h(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return p(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await h(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;if([e,t]=await h(this.registeredHooks.ldapgroupdeletemember,[e,t]),t===this.config.group_dummy_user)throw new Error("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${e}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r))))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${e},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let d;try{d=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===d.searchEntries.length)throw new Error(`Target organization ${t} not found`);const c=d.searchEntries[0][s];if(!c)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:c}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)e.searchEntries.map(e=>{const t=e[this.cn];t&&(i[t]=e),Array.isArray(i[t].member)||(i[t].member=[i[t].member]),i[t].member=i[t].member.filter(e=>e!==this.config.group_dummy_user)});return i=await h(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=t?`(${this.cn}=*${e}*)`:`(${this.cn}=${e})`;return await this.listGroups({filter:i,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${e},${this.base}`)}async validateMembers(e,t){if([e,t]=await h(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await h(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new Error(`Invalid value for field ${e}`);for(const[e,r]of Object.entries(this.schema.attributes))if(r.required&&null==t[e])throw new Error(`Missing required field ${e}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await h(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new Error(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new Error(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new Error(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test){"string"==typeof r.items.test&&(r.items.test=new RegExp(r.items.test));for(let i of t){if(typeof i!==r.items.type)throw new Error(`Field ${e} must be of type ${r.items.type}`);if("string"!=typeof i&&(i=i.toString()),r.items.test&&!r.items.test.test(i))throw new Error(`Field ${e} has invalid value ${i}`)}}}else if("pointer"===r.type){if("string"!=typeof t)throw new Error(`Field ${e} must be a string (DN pointer)`);const i=t;if(r.branch&&r.branch.length>0){if(!r.branch.some(e=>new RegExp(`,?${e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}$`,"i").test(i)))throw new Error(`Field ${e} must point to a DN within allowed branches: ${r.branch.join(", ")}`)}try{const t=await this.ldap.search({paged:!1,scope:"base"},i);if(!t||!t.searchEntries||0===t.searchEntries.length)throw new Error(`Field ${e} points to non-existent DN: ${i}`)}catch(t){throw new Error(`Field ${e} points to invalid or non-existent DN: ${i}: ${t}`)}if(r.test&&("string"==typeof r.test&&(r.test=new RegExp(r.test)),r.test&&!r.test.test(i)))throw new Error(`Field ${e} has invalid value ${i}`)}else{if(typeof t!==r.type)return!1;if("string"!=typeof t&&(t=t.toString()),r.test&&("string"==typeof r.test&&(r.test=new RegExp(r.test)),r.test&&!r.test.test(t)))throw new Error(`Field ${e} has invalid value ${t}`)}return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
|
|
1
|
+
import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{transformSchemas as o,asyncHandler as n,launchHooksChained as d,launchHooks as h}from"../../lib/utils.js";import{NotFoundError as c,BadRequestError as p}from"../../lib/errors.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async e=>{let t=e;return Array.isArray(t)||(t=[t]),this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)}),e}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(o(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match&&("string"!=typeof e.query.match||!/^[\w*=()&|]+$/.test(e.query.match)))throw new p("Invalid match query");e.query.match&&(i.filter=/=/.test(e.query.match)?`${e.query.match}`:`(${this.cn}=${e.query.match})`),e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups(i);return a(t,s)})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,n(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new p("cn is required");const a=i(e,t,"member");a&&await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,n(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new p("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new p("cn is required");const o=i(e,t,"targetOrgDn");o&&await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new p("cn is required");const o=i(e,t,"newCn");o&&await s(t,this.renameGroup.bind(this),a,o.newCn)}))}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${i},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)throw new c("Group not found");t.json(r.searchEntries[0])}catch(e){if(e.code&&32===e.code)throw new c("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;const o=a[this.cn],n=a.member?a.member:[],d=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,d)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new p("cn is required");await s(t,this.deleteGroup.bind(this),i)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new p("cn is required");const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=([^,]+).*`),"$1")):i=`${this.cn}=${e},${this.base}`,await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});const s=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||s.includes("twakeGroup")||s.includes("twakeStaticGroup")||s.push("twakeGroup");let a,o={objectClass:s,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&o.member.push(...t),[i,o]=await d(this.registeredHooks.ldapgroupadd,[i,o]);try{a=await this.ldap.add(i,o)}catch(e){throw new Error(`Failed to add group ${i}: ${e}`)}return h(this.registeredHooks.ldapgroupadddone,[i,o]),a}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;const i=this.opNumber();if([r,t]=await d(this.registeredHooks.ldapgroupmodify,[r,t,i]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(r,t);const s=await this.ldap.modify(r,t);return h(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${t},${this.base}`;[r,i]=await d(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return h(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;t=await d(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return h(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await d(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;if([e,t]=await d(this.registeredHooks.ldapgroupdeletemember,[e,t]),t===this.config.group_dummy_user)throw new Error("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${e}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r))))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${e},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let d;try{d=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===d.searchEntries.length)throw new Error(`Target organization ${t} not found`);const h=d.searchEntries[0][s];if(!h)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:h}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)e.searchEntries.map(e=>{const t=e[this.cn];t&&(i[t]=e),Array.isArray(i[t].member)||(i[t].member=[i[t].member]),i[t].member=i[t].member.filter(e=>e!==this.config.group_dummy_user)});return i=await d(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=t?`(${this.cn}=*${e}*)`:`(${this.cn}=${e})`;return await this.listGroups({filter:i,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${e},${this.base}`)}async validateMembers(e,t){if([e,t]=await d(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await d(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new Error(`Invalid value for field ${e}`);for(const[e,r]of Object.entries(this.schema.attributes))if(r.required&&null==t[e])throw new Error(`Missing required field ${e}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await d(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new Error(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new Error(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new Error(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test){"string"==typeof r.items.test&&(r.items.test=new RegExp(r.items.test));for(let i of t){if(typeof i!==r.items.type)throw new Error(`Field ${e} must be of type ${r.items.type}`);if("string"!=typeof i&&(i=i.toString()),r.items.test&&!r.items.test.test(i))throw new Error(`Field ${e} has invalid value ${i}`)}}}else if("pointer"===r.type){if("string"!=typeof t)throw new Error(`Field ${e} must be a string (DN pointer)`);const i=t;if(r.branch&&r.branch.length>0){if(!r.branch.some(e=>new RegExp(`,?${e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}$`,"i").test(i)))throw new Error(`Field ${e} must point to a DN within allowed branches: ${r.branch.join(", ")}`)}try{const t=await this.ldap.search({paged:!1,scope:"base"},i);if(!t||!t.searchEntries||0===t.searchEntries.length)throw new Error(`Field ${e} points to non-existent DN: ${i}`)}catch(t){throw new Error(`Field ${e} points to invalid or non-existent DN: ${i}: ${t}`)}if(r.test&&("string"==typeof r.test&&(r.test=new RegExp(r.test)),r.test&&!r.test.test(i)))throw new Error(`Field ${e} has invalid value ${i}`)}else{if(typeof t!==r.type)return!1;if("string"!=typeof t&&(t=t.toString()),r.test&&("string"==typeof r.test&&(r.test=new RegExp(r.test)),r.test&&!r.test.test(t)))throw new Error(`Field ${e} has invalid value ${t}`)}return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"fs";import i from"../../abstract/plugin.js";import{tryMethodData as a,badRequest as e,jsonBody as r,tryMethod as n,wantJson as s}from"../../lib/expressFormatedResponses.js";import{transformSchemas as o,launchHooksChained as h}from"../../lib/utils.js";class LdapOrganizations extends i{constructor(i){if(super(i),this.name="ldapOrganizations",this.roles=["api","consistency","configurable"],this.hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a])=>{let e,r={},n={};const checkIsOu=async()=>{if(void 0!==e)return e;if(i.replace?.objectClass)e=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)e=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);e=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return e};if(i.delete){const t=Array.isArray(i.delete)?i.delete.includes(this.linkAttr):i.delete[this.linkAttr],a=Array.isArray(i.delete)?i.delete.includes(this.pathAttr):i.delete[this.pathAttr];if(t||a){if(!await checkIsOu()&&t)throw new Error("An organization link cannot be deleted");if(a)throw new Error("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(r={...i.replace}),i.replace[this.pathAttr]&&(n={...i.replace})),i.add&&(i.add[this.linkAttr]&&(r={...r,...i.add}),i.add[this.pathAttr]&&(n={...n,...i.add})),Object.keys(r).length>0){await checkIsOu()||await this.checkDeptLink(r)}if(Object.keys(n).length>0){await checkIsOu()&&await this.checkDeptPath(n)}return[t,i,a]},ldapdeleterequest:async([t])=>(/^ou=/.test(t)&&await this.isEmptyOrganization(t),[t]),ldaprenamerequest:([t,i])=>[t,i]},!this.config.ldap_top_organization)throw new Error("Missing --ldap-top-organization");this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,this.config.organization_schema&&t.readFile(this.config.organization_schema,(t,i)=>{if(t)this.logger.error(`Failed to load organization schema from ${this.config.organization_schema}: ${t}`);else try{this.schema=JSON.parse(o(i.toString(),this.config)),this.logger.debug("Organization schema loaded")}catch(t){this.logger.error(`Failed to parse organization schema: ${t}`)}})}api(t){t.get(`${this.config.api_prefix}/v1/ldap/organizations/top`,async(t,i)=>{await a(i,this.getOrganisationTop.bind(this),t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,this.getOrganisationByDn.bind(this),e,t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,this.getOrganisationSubnodes.bind(this),e,t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,async(t,i)=>{const r=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)return e(i,'query parameter "q" is required');await a(i,this.searchOrganisationSubnodes.bind(this),r,n)}),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,async(t,i)=>this.apiAdd(t,i)),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>this.apiModify(t,i)),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>this.apiDelete(t,i)),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,async(t,i)=>this.apiMove(t,i))}async apiAdd(t,i){const a=r(t,i,"ou");if(!a)return;const e=a.parentDn||this.config.ldap_top_organization,s=`ou=${a.ou},${e}`,o={objectClass:this.config.ldap_organization_class,ou:a.ou,...Object.fromEntries(Object.entries(a).filter(([t])=>"ou"!==t&&"parentDn"!==t))};await n(i,this.addOrganization.bind(this),s,o,t)}async apiModify(t,i){const a=r(t,i);if(!a)return;const s=decodeURIComponent(t.params.dn);if(!s)return e(i,"dn is required");await n(i,this.modifyOrganization.bind(this),s,a)}async apiDelete(t,i){if(!s(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)return e(i,"dn is required");await n(i,this.deleteOrganization.bind(this),a)}async apiMove(t,i){if(!s(t,i))return;const n=r(t,i,"targetOrgDn");if(!n)return;const o=decodeURIComponent(t.params.dn);if(!o)return e(i,"dn is required");const{targetOrgDn:h}=n;if(!h||"string"!=typeof h)return e(i,"Missing or invalid targetOrgDn in request body");await a(i,this.moveOrganization.bind(this),o,h,t)}async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new Error(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new Error(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t){if(t[this.pathAttr]){const i=t[this.pathAttr],a=Array.isArray(i)?i[0]:i,e=this.config.ldap_organization_path_separator||" / ";let r=a;if(this.isOu(t)){const i=t.ou,n=Array.isArray(i)?i[0]:i;if(!a.startsWith(n+e))throw new Error(`Organization path must start with its own name followed by separator "${e}"`);r=a.slice(n.length+e.length)}const[n,s]=r.split(e,2),o=await this.server.ldap.search({paged:!1,filter:`(ou=${n})`,scope:"sub"},this.config.ldap_top_organization);if(0===o.searchEntries.length)throw new Error(`Invalid organization path ${a}`);if(s){let t=!1;for(const i of o.searchEntries){const a=i[this.pathAttr],e=Array.isArray(a)?a[0]:a;if(e&&e===s){t=!0;break}}if(!t)throw new Error(`Invalid organization path ${a}: no matching entry for ${n} with path ${s}`)}else{let t=!1;for(const i of o.searchEntries){const a=i.dn,r=this.config.ldap_top_organization;if(a.toLowerCase()===r.toLowerCase()||a.toLowerCase().endsWith(`,${r.toLowerCase()}`)){const a=i[this.pathAttr],r=Array.isArray(a)?a[0]:a;if(!r||r===n||!r.includes(e)){t=!0;break}}}if(!t)throw new Error(`Invalid organization path ${a}: no matching top-level entry for ${n}`)}}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${t})`})).searchEntries.length>0)throw new Error(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=t.objectClass.map(t=>t.toLowerCase());return this.config.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.config.ldap_top_organization)throw new Error("No top organization configured");const i=await this.server.ldap.search({paged:!1,scope:"base"},this.config.ldap_top_organization,t);if(1!==i.searchEntries.length)throw new Error("Top organization not found");const[,a]=await h(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){const a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i);if(1!==a.searchEntries.length)throw new Error(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.config.ldap_organization_max_subnodes||50,r=i?.query?.objectClass;if(!r||"organizationalUnit"===r)try{const e=await this.server.ldap.search({paged:!1,scope:"one",filter:"(objectClass=organizationalUnit)"},t,i);this.server.logger.debug(`Found ${e.searchEntries.length} sub-OUs for ${t}`),a.push(...e.searchEntries)}catch(i){this.server.logger.debug(`No sub-OUs found for ${t}: ${i}`)}const n=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,"");let s=`(${this.config.ldap_organization_link_attribute}=${t})`;r&&(s=`(&${s}(objectClass=${r}))`),this.server.logger.debug(`Searching for linked entities with filter: ${s} in ${n}`);const o=await this.server.ldap.search({paged:!0,filter:s},n,i);let h=0,c=0;for await(const t of o){c+=t.searchEntries.length;const i=e-h;if(i>0){const e=t.searchEntries.slice(0,i);a.push(...e),h+=e.length}}return this.server.logger.debug(`Found ${c} linked entities for ${t}, returning ${h}`),c>e&&a.push({dn:`more-${t}`,cn:[`... ${c-e} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:c.toString(),_displayedCount:e.toString()}),a}async addOrganization(t,i,a){return this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i){return this.validateChanges(t,i),await this.server.ldap.modify(t,i)}validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!this._validateOneChange(t,a))throw new Error(`Invalid value for field ${t}`);for(const[t,a]of Object.entries(this.schema.attributes))if(a.required&&null==i[t])throw new Error(`Missing required field ${t}`);return!0}validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!this._validateOneChange(t,a))throw new Error(`Invalid value for field ${t}`);if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!this._validateOneChange(t,a))throw new Error(`Invalid value for field ${t}`);return!0}_validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new Error(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new Error(`Field ${t} is required`);return!0}if(a.type)switch(a.type){case"string":if(Array.isArray(i))throw new Error(`Field ${t} must be a single value`);break;case"array":if(!Array.isArray(i))throw new Error(`Field ${t} must be an array`)}if(a.test){const e=Array.isArray(i)?i[0]:i;if(!("string"==typeof a.test?new RegExp(a.test):a.test).test(String(e)))throw new Error(`Field ${t} does not match required pattern`)}return!0}async moveOrganization(t,i,a){try{const t=await this.server.ldap.search({paged:!1,scope:"base"},i);if(!t.searchEntries||0===t.searchEntries.length)throw new Error(`Target organization ${i} not found`);if(!this.isOu(t.searchEntries[0]))throw new Error(`Target ${i} is not an organizational unit`)}catch(t){throw new Error(`Invalid target organization: ${t}`)}const e=`${t.split(",")[0]},${i}`;try{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);if(!i.searchEntries||0===i.searchEntries.length)throw new Error(`Source organization ${t} not found`)}catch(t){throw new Error(`Source organization not found: ${t}`)}if(e===t)throw new Error("Cannot move organization to its current location");if(i===t||i.endsWith(`,${t}`))throw new Error("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,e,a),this.logger.info(`Moved organization from ${t} to ${e}`)}catch(t){throw new Error(`Failed to move organization: ${t}`)}return{newDn:e}}async deleteOrganization(t){return await this.server.ldap.delete(t)}async searchOrganisationSubnodes(t,i){const a=[];try{const e=await this.server.ldap.search({paged:!1,scope:"one",filter:`(&(objectClass=organizationalUnit)(|(ou=*${i}*)(description=*${i}*)))`},t);this.server.logger.debug(`Found ${e.searchEntries.length} sub-OUs matching "${i}" for ${t}`),a.push(...e.searchEntries)}catch(a){this.server.logger.debug(`No sub-OUs found matching "${i}" for ${t}: ${a}`)}const e=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,""),r=`(&(${this.config.ldap_organization_link_attribute}=${t})(|(uid=*${i}*)(cn=*${i}*)(mail=*${i}*)(sn=*${i}*)(givenName=*${i}*)))`;this.server.logger.debug(`Searching for linked entities with filter: ${r} in ${e}`);const n=await this.server.ldap.search({paged:!0,filter:r},e);let s=0;for await(const t of n)s+=t.searchEntries.length,a.push(...t.searchEntries);return this.server.logger.debug(`Found ${s} linked entities matching "${i}" for ${t}`),a}getConfigApiData(){const t=this.config.api_prefix||"/api";let i;if(this.server.loadedPlugins.static&&this.config.organization_schema){const t=this.config.static_name||"static",a=this.config.organization_schema.indexOf("/schemas/");if(-1!==a){i=`/${t}${this.config.organization_schema.substring(a)}`}}return{enabled:!0,topOrganization:this.config.ldap_top_organization||"",organizationClass:this.config.ldap_organization_class||["top","organizationalUnit"],linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.config.ldap_organization_path_separator||" / ",maxSubnodes:this.config.ldap_organization_max_subnodes||50,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}export{LdapOrganizations as default};
|
|
1
|
+
import t from"fs";import i from"../../abstract/plugin.js";import{tryMethodData as a,jsonBody as e,tryMethod as n,wantJson as r}from"../../lib/expressFormatedResponses.js";import{transformSchemas as s,asyncHandler as o,launchHooksChained as h}from"../../lib/utils.js";import{BadRequestError as c,NotFoundError as d,ConflictError as g}from"../../lib/errors.js";class LdapOrganizations extends i{constructor(i){if(super(i),this.name="ldapOrganizations",this.roles=["api","consistency","configurable"],this.hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a])=>{let e,n={},r={};const checkIsOu=async()=>{if(void 0!==e)return e;if(i.replace?.objectClass)e=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)e=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);e=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return e};if(i.delete){const t=Array.isArray(i.delete)?i.delete.includes(this.linkAttr):i.delete[this.linkAttr],a=Array.isArray(i.delete)?i.delete.includes(this.pathAttr):i.delete[this.pathAttr];if(t||a){if(!await checkIsOu()&&t)throw new c("An organization link cannot be deleted");if(a)throw new c("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(n={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(n={...n,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(n).length>0){await checkIsOu()||await this.checkDeptLink(n)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r)}return[t,i,a]},ldapdeleterequest:async([t])=>(/^ou=/.test(t)&&await this.isEmptyOrganization(t),[t]),ldaprenamerequest:([t,i])=>[t,i]},!this.config.ldap_top_organization)throw new Error("Missing --ldap-top-organization");this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,this.config.organization_schema&&t.readFile(this.config.organization_schema,(t,i)=>{if(t)this.logger.error(`Failed to load organization schema from ${this.config.organization_schema}: ${t}`);else try{this.schema=JSON.parse(s(i.toString(),this.config)),this.logger.debug("Organization schema loaded")}catch(t){this.logger.error(`Failed to parse organization schema: ${t}`)}})}api(t){t.get(`${this.config.api_prefix}/v1/ldap/organizations/top`,async(t,i)=>{await a(i,this.getOrganisationTop.bind(this),t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,this.getOrganisationByDn.bind(this),e,t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,this.getOrganisationSubnodes.bind(this),e,t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,o(async(t,i)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new c('query parameter "q" is required');await a(i,this.searchOrganisationSubnodes.bind(this),e,n)})),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,o(async(t,i)=>this.apiAdd(t,i))),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiModify(t,i))),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiDelete(t,i))),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,o(async(t,i)=>this.apiMove(t,i)))}async apiAdd(t,i){const a=e(t,i,"ou");if(!a)return;const r=a.parentDn||this.config.ldap_top_organization,s=`ou=${a.ou},${r}`,o={objectClass:this.config.ldap_organization_class,ou:a.ou,...Object.fromEntries(Object.entries(a).filter(([t])=>"ou"!==t&&"parentDn"!==t))};await n(i,this.addOrganization.bind(this),s,o,t)}async apiModify(t,i){const a=e(t,i);if(!a)return;const r=decodeURIComponent(t.params.dn);if(!r)throw new c("dn is required");await n(i,this.modifyOrganization.bind(this),r,a)}async apiDelete(t,i){if(!r(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new c("dn is required");await n(i,this.deleteOrganization.bind(this),a)}async apiMove(t,i){if(!r(t,i))return;const n=e(t,i,"targetOrgDn");if(!n)return;const s=decodeURIComponent(t.params.dn);if(!s)throw new c("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new c("Missing or invalid targetOrgDn in request body");await a(i,this.moveOrganization.bind(this),s,o,t)}async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new d(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new c(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t){if(t[this.pathAttr]){const i=t[this.pathAttr],a=Array.isArray(i)?i[0]:i,e=this.config.ldap_organization_path_separator||" / ";let n=a;if(this.isOu(t)){const i=t.ou,r=Array.isArray(i)?i[0]:i;if(!a.startsWith(r+e))throw new c(`Organization path must start with its own name followed by separator "${e}"`);n=a.slice(r.length+e.length)}const[r,s]=n.split(e,2),o=await this.server.ldap.search({paged:!1,filter:`(ou=${r})`,scope:"sub"},this.config.ldap_top_organization);if(0===o.searchEntries.length)throw new c(`Invalid organization path ${a}`);if(s){let t=!1;for(const i of o.searchEntries){const a=i[this.pathAttr],e=Array.isArray(a)?a[0]:a;if(e&&e===s){t=!0;break}}if(!t)throw new c(`Invalid organization path ${a}: no matching entry for ${r} with path ${s}`)}else{let t=!1;for(const i of o.searchEntries){const a=i.dn,n=this.config.ldap_top_organization;if(a.toLowerCase()===n.toLowerCase()||a.toLowerCase().endsWith(`,${n.toLowerCase()}`)){const a=i[this.pathAttr],n=Array.isArray(a)?a[0]:a;if(!n||n===r||!n.includes(e)){t=!0;break}}}if(!t)throw new c(`Invalid organization path ${a}: no matching top-level entry for ${r}`)}}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${t})`})).searchEntries.length>0)throw new g(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=t.objectClass.map(t=>t.toLowerCase());return this.config.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.config.ldap_top_organization)throw new c("No top organization configured");const i=await this.server.ldap.search({paged:!1,scope:"base"},this.config.ldap_top_organization,t);if(1!==i.searchEntries.length)throw new d("Top organization not found");const[,a]=await h(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){const a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i);if(1!==a.searchEntries.length)throw new d(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.config.ldap_organization_max_subnodes||50,n=i?.query?.objectClass;if(!n||"organizationalUnit"===n)try{const e=await this.server.ldap.search({paged:!1,scope:"one",filter:"(objectClass=organizationalUnit)"},t,i);this.server.logger.debug(`Found ${e.searchEntries.length} sub-OUs for ${t}`),a.push(...e.searchEntries)}catch(i){this.server.logger.debug(`No sub-OUs found for ${t}: ${i}`)}const r=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,"");let s=`(${this.config.ldap_organization_link_attribute}=${t})`;n&&(s=`(&${s}(objectClass=${n}))`),this.server.logger.debug(`Searching for linked entities with filter: ${s} in ${r}`);const o=await this.server.ldap.search({paged:!0,filter:s},r,i);let h=0,c=0;for await(const t of o){c+=t.searchEntries.length;const i=e-h;if(i>0){const e=t.searchEntries.slice(0,i);a.push(...e),h+=e.length}}return this.server.logger.debug(`Found ${c} linked entities for ${t}, returning ${h}`),c>e&&a.push({dn:`more-${t}`,cn:[`... ${c-e} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:c.toString(),_displayedCount:e.toString()}),a}async addOrganization(t,i,a){return this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i){return this.validateChanges(t,i),await this.server.ldap.modify(t,i)}validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!this._validateOneChange(t,a))throw new c(`Invalid value for field ${t}`);for(const[t,a]of Object.entries(this.schema.attributes))if(a.required&&null==i[t])throw new c(`Missing required field ${t}`);return!0}validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!this._validateOneChange(t,a))throw new c(`Invalid value for field ${t}`);if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!this._validateOneChange(t,a))throw new c(`Invalid value for field ${t}`);return!0}_validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new Error(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new Error(`Field ${t} is required`);return!0}if(a.type)switch(a.type){case"string":if(Array.isArray(i))throw new Error(`Field ${t} must be a single value`);break;case"array":if(!Array.isArray(i))throw new Error(`Field ${t} must be an array`)}if(a.test){const e=Array.isArray(i)?i[0]:i;if(!("string"==typeof a.test?new RegExp(a.test):a.test).test(String(e)))throw new Error(`Field ${t} does not match required pattern`)}return!0}async moveOrganization(t,i,a){try{const t=await this.server.ldap.search({paged:!1,scope:"base"},i);if(!t.searchEntries||0===t.searchEntries.length)throw new Error(`Target organization ${i} not found`);if(!this.isOu(t.searchEntries[0]))throw new Error(`Target ${i} is not an organizational unit`)}catch(t){throw new Error(`Invalid target organization: ${t}`)}const e=`${t.split(",")[0]},${i}`;try{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);if(!i.searchEntries||0===i.searchEntries.length)throw new Error(`Source organization ${t} not found`)}catch(t){throw new Error(`Source organization not found: ${t}`)}if(e===t)throw new Error("Cannot move organization to its current location");if(i===t||i.endsWith(`,${t}`))throw new Error("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,e,a),this.logger.info(`Moved organization from ${t} to ${e}`)}catch(t){throw new Error(`Failed to move organization: ${t}`)}return{newDn:e}}async deleteOrganization(t){return await this.server.ldap.delete(t)}async searchOrganisationSubnodes(t,i){const a=[];try{const e=await this.server.ldap.search({paged:!1,scope:"one",filter:`(&(objectClass=organizationalUnit)(|(ou=*${i}*)(description=*${i}*)))`},t);this.server.logger.debug(`Found ${e.searchEntries.length} sub-OUs matching "${i}" for ${t}`),a.push(...e.searchEntries)}catch(a){this.server.logger.debug(`No sub-OUs found matching "${i}" for ${t}: ${a}`)}const e=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,""),n=`(&(${this.config.ldap_organization_link_attribute}=${t})(|(uid=*${i}*)(cn=*${i}*)(mail=*${i}*)(sn=*${i}*)(givenName=*${i}*)))`;this.server.logger.debug(`Searching for linked entities with filter: ${n} in ${e}`);const r=await this.server.ldap.search({paged:!0,filter:n},e);let s=0;for await(const t of r)s+=t.searchEntries.length,a.push(...t.searchEntries);return this.server.logger.debug(`Found ${s} linked entities matching "${i}" for ${t}`),a}getConfigApiData(){const t=this.config.api_prefix||"/api";let i;if(this.server.loadedPlugins.static&&this.config.organization_schema){const t=this.config.static_name||"static",a=this.config.organization_schema.indexOf("/schemas/");if(-1!==a){i=`/${t}${this.config.organization_schema.substring(a)}`}}return{enabled:!0,topOrganization:this.config.ldap_top_organization||"",organizationClass:this.config.ldap_organization_class||["top","organizationalUnit"],linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.config.ldap_organization_path_separator||" / ",maxSubnodes:this.config.ldap_organization_max_subnodes||50,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}export{LdapOrganizations as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=["core/weblogs","core/auth/crowdsec","core/auth/rateLimit","core/auth/token","core/auth/llng","core/auth/openidconnect"];export{e as default};
|
|
1
|
+
var e=["core/auth/trustedProxy","core/weblogs","core/auth/crowdsec","core/auth/rateLimit","core/auth/token","core/auth/llng","core/auth/openidconnect"];export{e as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import i from"node-fetch";import{TwakePlugin as t}from"../../abstract/twakePlugin.js";class James extends t{constructor(i){super(i,"james_webadmin_url","james_webadmin_token","james_concurrency"),this.name="james",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange",ldapGroups:"core/ldap/groups"},this.hooks={ldapadddone:async i=>{const[t,e]=i,a=e[this.mailAttr],s=e[this.quotaAttr],r=e[this.aliasAttr];if(!a)return;const n=Array.isArray(a)?String(a[0]):String(a);if(this.initDelay>0&&await new Promise(i=>setTimeout(i,this.initDelay)),s){const i=Array.isArray(s)?Number(s[0]):Number(s);!isNaN(i)&&i>0&&await this.callWebAdminApi("ldapadddone:quota",`${this.webadminUrl}/quota/users/${n}/size`,"PUT",t,i.toString(),{mail:n,quota:i})}if(r){const i=this.getAliases(r);await Promise.all(i.map(i=>this.callWebAdminApi("ldapadddone:alias",`${this.webadminUrl}/address/aliases/${n}/sources/${i}`,"PUT",t,null,{mail:n,alias:i})))}const l=this.getDisplayNameFromAttributes(e);l&&await this.updateJamesIdentity(t,n,l)},onLdapMailChange:async(i,t,e)=>{const a=t?Array.isArray(t)?String(t[0]):String(t):null,s=e?Array.isArray(e)?String(e[0]):String(e):null;if(a){await this.callWebAdminApi("onLdapMailChange",`${this.webadminUrl}/users/${a}/rename/${s}?action=rename`,"POST",i,null,{oldmail:a,newmail:s});try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.aliasAttr]},i);if(t.searchEntries&&t.searchEntries.length>0){const e=this.getAliases(t.searchEntries[0][this.aliasAttr]);e.length>0&&await Promise.all([...e.map(t=>this.callWebAdminApi("onLdapMailChange-delete",`${this.webadminUrl}/address/aliases/${a}/sources/${t}`,"DELETE",i,null,{oldmail:a,alias:t})),...e.map(t=>this.callWebAdminApi("onLdapMailChange-create",`${this.webadminUrl}/address/aliases/${s}/sources/${t}`,"PUT",i,null,{newmail:s,alias:t}))])}}catch(i){this.logger.debug("Could not fetch aliases for mail change:",i)}}else this.logger.debug(`Skipping mail rename for ${i}: oldmail is empty (mail attribute was added, not changed)`)},onLdapAliasChange:async(i,t,e,a)=>{const s=e.map(i=>this.normalizeAlias(i)),r=a.map(i=>this.normalizeAlias(i)),n=s.filter(i=>!r.includes(i)),l=r.filter(i=>!s.includes(i));await Promise.all([...n.map(e=>this.callWebAdminApi("onLdapAliasChange-delete",`${this.webadminUrl}/address/aliases/${t}/sources/${e}`,"DELETE",i,null,{mail:t,alias:e,action:"delete"})),...l.map(e=>this.callWebAdminApi("onLdapAliasChange-add",`${this.webadminUrl}/address/aliases/${t}/sources/${e}`,"PUT",i,null,{mail:t,alias:e,action:"add"}))])},onLdapQuotaChange:(i,t,e,a)=>this.callWebAdminApi("onLdapQuotaChange",`${this.webadminUrl}/quota/users/${t}/size`,"PUT",i,a.toString(),{oldQuota:e,newQuota:a}),onLdapForwardChange:async(i,t,e,a)=>{const s=this.extractMailDomain(t);if(!s)return void this.logger.error(`Cannot extract domain from mail ${t} for forward management`);const r=e.filter(i=>!a.includes(i)),n=a.filter(i=>!e.includes(i));await Promise.all([...r.map(e=>this.callWebAdminApi("onLdapForwardChange-delete",`${this.webadminUrl}/domains/${s}/forwards/${t}/${e}`,"DELETE",i,null,{mail:t,forward:e,domain:s,action:"delete"})),...n.map(e=>this.callWebAdminApi("onLdapForwardChange-add",`${this.webadminUrl}/domains/${s}/forwards/${t}/${e}`,"PUT",i,null,{mail:t,forward:e,domain:s,action:"add"}))])},onLdapChange:async(i,t)=>{this.config.delegation_attribute&&t[this.config.delegation_attribute]&&await this._handleDelegationChange(i,t)},onLdapDisplayNameChange:async i=>{const t=[this.mailAttr,this.displayNameAttr,"cn","givenName","sn"],e=await this.ldapGetAttributes(i,t);if(!e)return void this.logger.warn(`Cannot update James identity: entry not found for ${i}`);const a=this.attributeToString(e[this.mailAttr]);if(!a)return void this.logger.warn(`Cannot update James identity: no mail found for ${i}`);const s=this.getDisplayNameFromAttributes(e);if(s)return this.updateJamesIdentity(i,a,s);this.logger.warn(`Cannot update James identity: no display name found for ${i}`)},ldapgroupadddone:async i=>{const[t,e]=i,a=e.mail;if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);const s=this.getMailboxType(e),r=Array.isArray(a)?String(a[0]):String(a);if("teamMailbox"===s){const i=this.config.james_mailing_list_branch||[];if(i.length>0&&this.isInAllowedBranches(t,i))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:t,mailboxType:"teamMailbox",error:`Team mailbox cannot be in mailing list branches: ${i.join(", ")}`});await this.createTeamMailbox(t,e,r)}else if("mailingList"===s||null===s){const i=this.config.james_mailing_list_branch||[];if(i.length>0&&!this.isInAllowedBranches(t,i))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:t,mailboxType:s||"mailingList (default)",error:`Mailing list must be in allowed branches: ${i.join(", ")}`});await this.createMailingList(t,e,r)}},ldapgroupmodifydone:async i=>{const[t,e]=i;if(e.replace?.[this.mailboxTypeAttr]||e.add?.[this.mailboxTypeAttr])return void await this.handleMailboxTypeTransition(t,e);const a=await this.getGroupMail(t);if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);const s="teamMailbox"===await this.getGroupMailboxType(t),r=s?this.extractMailDomain(a):null,n=[];if(e.add?.member){const i=e.add.member,l=Array.isArray(i)?i:[i],o=await this.getMemberEmails(l);n.push(...o.map(i=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${i}`,"PUT",t,null,{domain:r,groupMail:a,memberMail:i,action:"add"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${i}`,"PUT",t,null,{groupMail:a,memberMail:i,action:"add"})))}if(this.isAttributesList(e.delete)){const i=e.delete.member;if(i){const e=Array.isArray(i)?i:[i],l=await this.getMemberEmails(e);n.push(...l.map(i=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${i}`,"DELETE",t,null,{domain:r,groupMail:a,memberMail:i,action:"delete"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${i}`,"DELETE",t,null,{groupMail:a,memberMail:i,action:"delete"})))}}await Promise.all(n)},ldapgroupdeletedone:async i=>{const t=await this.getGroupMail(i);if(!t)return void this.logger.debug(`Group ${i} has no mail attribute, skipping James sync`);"teamMailbox"===await this.getGroupMailboxType(i)?await this.removeAllTeamMailboxMembers(i,t):await this.callWebAdminApi("ldapgroupdeletedone:mailingList",`${this.webadminUrl}/address/groups/${t}`,"DELETE",i,null,{groupMail:t})}},this.quotaAttr=this.config.quota_attribute||"mailQuotaSize",this.aliasAttr=this.config.alias_attribute||"mailAlternateAddress",this.mailboxTypeAttr=this.config.james_mailbox_type_attribute||"twakeMailboxType",this.initDelay="number"==typeof this.config.james_init_delay?this.config.james_init_delay:1e3}async handleMailboxTypeTransition(i,t){const e=await this.ldapGetAttributes(i,["mail","member",this.mailboxTypeAttr]);if(!e)return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,error:"Could not fetch group attributes"});const a=e.mail;if(!a)return void this.logger.debug(`Group ${i} has no mail attribute, skipping mailbox type transition`);const s=Array.isArray(a)?String(a[0]):String(a),r=t.replace?.[this.mailboxTypeAttr]||t.add?.[this.mailboxTypeAttr],n=Array.isArray(r)?String(r[0]):String(r),l=this.getMailboxType({[this.mailboxTypeAttr]:n}),o=this.getMailboxType(e);if(this.logger.info({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,mail:s,oldType:o||"none",newType:l||"none"}),o!==l)if("mailingList"===o?await this.deleteMailingList(i,s):"teamMailbox"===o&&await this.removeAllTeamMailboxMembers(i,s),"mailingList"===l){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&!this.isInAllowedBranches(i,t))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,newType:l,error:`Mailing list must be in allowed branches: ${t.join(", ")}`});await this.createMailingList(i,e,s)}else if("teamMailbox"===l){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&this.isInAllowedBranches(i,t))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,newType:l,error:`Team mailbox cannot be in mailing list branches: ${t.join(", ")}`});await this.createTeamMailbox(i,e,s)}}getDisplayNameFromAttributes(i){const t=this.attributeToString(i[this.displayNameAttr]);if(t)return t;const e=this.attributeToString(i.cn);if(e)return e;const a=this.attributeToString(i.givenName),s=this.attributeToString(i.sn);if(a||s){const i=[];return a&&i.push(a),s&&i.push(s),i.join(" ")}const r=this.attributeToString(i[this.mailAttr]);return r||null}async getMailFromDN(i){const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):null}async getDisplayNameFromDN(i){const t=[this.displayNameAttr,"cn","givenName","sn",this.mailAttr],e=await this.ldapGetAttributes(i,t);return e?this.getDisplayNameFromAttributes(e):null}async generateSignature(i){const t=this.config.james_signature_template;if(!t)return null;const e=await this.ldapGetAttributes(i);if(!e)return null;let a=t;return a=a.replace(/\{(\w+)\}/g,(i,t)=>"string"==typeof t&&e&&"object"==typeof e&&Object.prototype.hasOwnProperty.call(e,t)&&this.attributeToString(e[t])||""),a}async updateJamesIdentity(t,e,a){const s={plugin:this.name,event:"onLdapDisplayNameChange",result:"error",dn:t,mail:e,displayName:a};try{const r=`${this.webadminUrl}/jmap/identities/${e}`,n=await this.requestLimit(()=>i(r,{method:"GET",headers:this.createHeaders()}));if(!n.ok)return void this.logger.error({...s,step:"get_identities",http_status:n.status,http_status_text:n.statusText});const l=await n.json(),o=l.find(i=>i.email===e)||l[0];if(!o)return void this.logger.warn({...s,step:"find_identity",message:"No identity found for user"});const m=await this.generateSignature(t),d=`${this.webadminUrl}/jmap/identities/${e}/${o.id}`,h={id:o.id,email:o.email,name:a};m&&(h.htmlSignature=m);const g=await this.requestLimit(()=>i(d,{method:"PUT",headers:this.createHeaders("application/json"),body:JSON.stringify(h)}));g.ok?this.logger.info({...s,result:"success",http_status:g.status}):this.logger.error({...s,step:"update_identity",http_status:g.status,http_status_text:g.statusText})}catch(i){this.logger.error({...s,error:`${i}`})}}isAttributesList(i){return void 0!==i&&"object"==typeof i&&!Array.isArray(i)}getMailboxType(i){const t=i[this.mailboxTypeAttr];if(!t)return null;const e=(Array.isArray(t)?String(t[0]):String(t)).match(/^cn=([^,]+)/);if(!e)return null;const a=e[1];return"group"===a||"mailingList"===a||"teamMailbox"===a?a:null}isInAllowedBranches(i,t){return!t||0===t.length||t.some(t=>i===t||i.endsWith(","+t))}async createMailingList(i,t,e){const a=t.member||[],s=Array.isArray(a)?a:[a];this.logger.debug(`Creating mailing list ${e} in James with ${s.length} members`);const r=await this.getMemberEmails(s);await Promise.all(r.map(t=>this.callWebAdminApi("ldapgroupadddone:mailingList",`${this.webadminUrl}/address/groups/${e}/${t}`,"PUT",i,null,{groupMail:e,memberMail:t})))}async createTeamMailbox(i,t,e){const a=this.extractMailDomain(e);if(!a)return void this.logger.error({plugin:this.name,event:"createTeamMailbox",dn:i,mail:e,error:"Cannot extract domain from mail address"});const s=t.member||[],r=Array.isArray(s)?s:[s];this.logger.debug(`Creating team mailbox ${e} in James with ${r.length} members`);const n=await this.getMemberEmails(r),l=[];l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:create",`${this.webadminUrl}/domains/${a}/team-mailboxes/${e}`,"PUT",i,null,{domain:a,teamMailbox:e}));for(const t of n)l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:addMember",`${this.webadminUrl}/domains/${a}/team-mailboxes/${e}/members/${t}`,"PUT",i,null,{domain:a,teamMailbox:e,memberMail:t}));await Promise.all(l)}async deleteTeamMailbox(i,t){const e=this.extractMailDomain(t);e?await this.callWebAdminApi("ldapgroupdeletedone:teamMailbox",`${this.webadminUrl}/domains/${e}/team-mailboxes/${t}`,"DELETE",i,null,{domain:e,teamMailbox:t}):this.logger.error({plugin:this.name,event:"deleteTeamMailbox",dn:i,mail:t,error:"Cannot extract domain from mail address"})}async removeAllTeamMailboxMembers(i,t){const e=this.extractMailDomain(t);if(e)try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:["member"]},i);if(a.searchEntries&&a.searchEntries.length>0){const s=a.searchEntries[0].member||[],r=Array.isArray(s)?s:[s],n=await this.getMemberEmails(r);await Promise.all(n.map(a=>this.callWebAdminApi("removeAllTeamMailboxMembers",`${this.webadminUrl}/domains/${e}/team-mailboxes/${t}/members/${a}`,"DELETE",i,null,{domain:e,teamMailbox:t,memberMail:a}))),this.logger.info({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:i,teamMailbox:t,membersRemoved:n.length,message:"All members removed from team mailbox (mailbox preserved)"})}}catch(i){const e=i instanceof Error?i.message:String(i);this.logger.error(`Failed to remove members from team mailbox ${t}: ${e}`)}else this.logger.error({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:i,mail:t,error:"Cannot extract domain from mail address"})}async deleteMailingList(i,t){await this.callWebAdminApi("deleteMailingList",`${this.webadminUrl}/address/groups/${t}`,"DELETE",i,null,{groupMail:t})}async getMemberEmails(i){const t=i.filter(i=>i!==this.config.group_dummy_user).map(i=>this.server.ldap.queryLimit(async()=>{const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):null}));return(await Promise.all(t)).filter(i=>null!==i)}async getGroupMail(i){const t=await this.ldapGetAttributes(i,["mail"]);return t?this.attributeToString(t.mail):null}async getGroupMailboxType(i){try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.mailboxTypeAttr]},i);if(t.searchEntries&&t.searchEntries.length>0)return this.getMailboxType(t.searchEntries[0])}catch(t){const e=t instanceof Error?t.message:String(t);this.logger.debug(`Could not get mailbox type for group ${i}: ${e}`)}return null}async _handleDelegationChange(i,t){const e=await this.ldapGetAttributes(i,[this.mailAttr]);if(!e)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:i,message:"Could not find user entry to get mail attribute"});const a=e[this.mailAttr];if(!a||"string"!=typeof a)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:i,message:"User has no mail attribute, cannot manage delegation"});const s=this.config.delegation_attribute;if(!s)return;const[r,n]=t[s]||[],l=this._normalizeToArray(r),o=this._normalizeToArray(n),m=o.filter(i=>!l.includes(i)),d=l.filter(i=>!o.includes(i)),h=m.map(i=>this.server.ldap.queryLimit(async()=>{const t=await this._getDelegateEmail(i);return{dn:i,email:t}})),g=d.map(i=>this.server.ldap.queryLimit(async()=>{const t=await this._getDelegateEmail(i);return{dn:i,email:t}})),[u,p]=await Promise.all([Promise.all(h),Promise.all(g)]),c=[];for(const{dn:t,email:e}of u)e&&c.push(this.callWebAdminApi("onLdapChange:addDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${e}`,"PUT",i,null,{userMail:a,delegateEmail:e,delegateDN:t,action:"add"}));for(const{dn:t,email:e}of p)e&&c.push(this.callWebAdminApi("onLdapChange:removeDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${e}`,"DELETE",i,null,{userMail:a,delegateEmail:e,delegateDN:t,action:"remove"}));await Promise.all(c)}async _getDelegateEmail(i){const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):(this.logger.warn({plugin:this.name,event:"getDelegateEmail",dn:i,message:"Could not resolve delegate DN to email"}),null)}_normalizeToArray(i){return i?Array.isArray(i)?i:[i]:[]}}export{James as default};
|
|
1
|
+
import t from"node-fetch";import{TwakePlugin as e}from"../../abstract/twakePlugin.js";import{asyncHandler as i,escapeLdapFilter as a}from"../../lib/utils.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";class James extends e{constructor(t){super(t,"james_webadmin_url","james_webadmin_token","james_concurrency"),this.name="james",this.roles=["consistency","api"],this.dependencies={onLdapChange:"core/ldap/onChange",ldapGroups:"core/ldap/groups"},this.hooks={ldapadddone:async t=>{const[e,i]=t,a=i[this.mailAttr],s=i[this.quotaAttr],r=i[this.aliasAttr];if(!a)return;const n=Array.isArray(a)?String(a[0]):String(a);if(this.initDelay>0&&await new Promise(t=>setTimeout(t,this.initDelay)),s){const t=Array.isArray(s)?Number(s[0]):Number(s);!isNaN(t)&&t>0&&await this.callWebAdminApi("ldapadddone:quota",`${this.webadminUrl}/quota/users/${n}/size`,"PUT",e,t.toString(),{mail:n,quota:t})}if(r){const t=this.getAliases(r);await Promise.all(t.map(t=>this.callWebAdminApi("ldapadddone:alias",`${this.webadminUrl}/address/aliases/${n}/sources/${t}`,"PUT",e,null,{mail:n,alias:t})))}const l=this.getDisplayNameFromAttributes(i);l&&await this.updateJamesIdentity(e,n,l)},onLdapMailChange:async(t,e,i)=>{const a=e?Array.isArray(e)?String(e[0]):String(e):null,s=i?Array.isArray(i)?String(i[0]):String(i):null;if(a){await this.callWebAdminApi("onLdapMailChange",`${this.webadminUrl}/users/${a}/rename/${s}?action=rename`,"POST",t,null,{oldmail:a,newmail:s});try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.aliasAttr]},t);if(e.searchEntries&&e.searchEntries.length>0){const i=this.getAliases(e.searchEntries[0][this.aliasAttr]);i.length>0&&await Promise.all([...i.map(e=>this.callWebAdminApi("onLdapMailChange-delete",`${this.webadminUrl}/address/aliases/${a}/sources/${e}`,"DELETE",t,null,{oldmail:a,alias:e})),...i.map(e=>this.callWebAdminApi("onLdapMailChange-create",`${this.webadminUrl}/address/aliases/${s}/sources/${e}`,"PUT",t,null,{newmail:s,alias:e}))])}}catch(t){this.logger.debug("Could not fetch aliases for mail change:",t)}}else this.logger.debug(`Skipping mail rename for ${t}: oldmail is empty (mail attribute was added, not changed)`)},onLdapAliasChange:async(t,e,i,a)=>{const s=i.map(t=>this.normalizeAlias(t)),r=a.map(t=>this.normalizeAlias(t)),n=s.filter(t=>!r.includes(t)),l=r.filter(t=>!s.includes(t));await Promise.all([...n.map(i=>this.callWebAdminApi("onLdapAliasChange-delete",`${this.webadminUrl}/address/aliases/${e}/sources/${i}`,"DELETE",t,null,{mail:e,alias:i,action:"delete"})),...l.map(i=>this.callWebAdminApi("onLdapAliasChange-add",`${this.webadminUrl}/address/aliases/${e}/sources/${i}`,"PUT",t,null,{mail:e,alias:i,action:"add"}))])},onLdapQuotaChange:(t,e,i,a)=>this.callWebAdminApi("onLdapQuotaChange",`${this.webadminUrl}/quota/users/${e}/size`,"PUT",t,a.toString(),{oldQuota:i,newQuota:a}),onLdapForwardChange:async(t,e,i,a)=>{const s=this.extractMailDomain(e);if(!s)return void this.logger.error(`Cannot extract domain from mail ${e} for forward management`);const r=i.filter(t=>!a.includes(t)),n=a.filter(t=>!i.includes(t));await Promise.all([...r.map(i=>this.callWebAdminApi("onLdapForwardChange-delete",`${this.webadminUrl}/domains/${s}/forwards/${e}/${i}`,"DELETE",t,null,{mail:e,forward:i,domain:s,action:"delete"})),...n.map(i=>this.callWebAdminApi("onLdapForwardChange-add",`${this.webadminUrl}/domains/${s}/forwards/${e}/${i}`,"PUT",t,null,{mail:e,forward:i,domain:s,action:"add"}))])},onLdapChange:async(t,e)=>{this.config.delegation_attribute&&e[this.config.delegation_attribute]&&await this._handleDelegationChange(t,e)},onLdapDisplayNameChange:async t=>{const e=[this.mailAttr,this.displayNameAttr,"cn","givenName","sn"],i=await this.ldapGetAttributes(t,e);if(!i)return void this.logger.warn(`Cannot update James identity: entry not found for ${t}`);const a=this.attributeToString(i[this.mailAttr]);if(!a)return void this.logger.warn(`Cannot update James identity: no mail found for ${t}`);const s=this.getDisplayNameFromAttributes(i);if(s)return this.updateJamesIdentity(t,a,s);this.logger.warn(`Cannot update James identity: no display name found for ${t}`)},ldapgroupadddone:async t=>{const[e,i]=t,a=i.mail;if(!a)return void this.logger.debug(`Group ${e} has no mail attribute, skipping James sync`);const s=this.getMailboxType(i),r=Array.isArray(a)?String(a[0]):String(a);if("teamMailbox"===s){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&this.isInAllowedBranches(e,t))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:e,mailboxType:"teamMailbox",error:`Team mailbox cannot be in mailing list branches: ${t.join(", ")}`});await this.createTeamMailbox(e,i,r)}else if("mailingList"===s||null===s){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&!this.isInAllowedBranches(e,t))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:e,mailboxType:s||"mailingList (default)",error:`Mailing list must be in allowed branches: ${t.join(", ")}`});await this.createMailingList(e,i,r)}},ldapgroupmodifydone:async t=>{const[e,i]=t;if(i.replace?.[this.mailboxTypeAttr]||i.add?.[this.mailboxTypeAttr])return void await this.handleMailboxTypeTransition(e,i);const a=await this.getGroupMail(e);if(!a)return void this.logger.debug(`Group ${e} has no mail attribute, skipping James sync`);const s="teamMailbox"===await this.getGroupMailboxType(e),r=s?this.extractMailDomain(a):null,n=[];if(i.add?.member){const t=i.add.member,l=Array.isArray(t)?t:[t],o=await this.getMemberEmails(l);n.push(...o.map(t=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${t}`,"PUT",e,null,{domain:r,groupMail:a,memberMail:t,action:"add"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${t}`,"PUT",e,null,{groupMail:a,memberMail:t,action:"add"})))}if(this.isAttributesList(i.delete)){const t=i.delete.member;if(t){const i=Array.isArray(t)?t:[t],l=await this.getMemberEmails(i);n.push(...l.map(t=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${t}`,"DELETE",e,null,{domain:r,groupMail:a,memberMail:t,action:"delete"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${t}`,"DELETE",e,null,{groupMail:a,memberMail:t,action:"delete"})))}}await Promise.all(n)},ldapgroupdeletedone:async t=>{const e=await this.getGroupMail(t);if(!e)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);"teamMailbox"===await this.getGroupMailboxType(t)?await this.removeAllTeamMailboxMembers(t,e):await this.callWebAdminApi("ldapgroupdeletedone:mailingList",`${this.webadminUrl}/address/groups/${e}`,"DELETE",t,null,{groupMail:e})}},this.quotaAttr=this.config.quota_attribute||"mailQuotaSize",this.aliasAttr=this.config.alias_attribute||"mailAlternateAddress",this.mailboxTypeAttr=this.config.james_mailbox_type_attribute||"twakeMailboxType",this.initDelay="number"==typeof this.config.james_init_delay?this.config.james_init_delay:1e3}api(t){const e=this.config.api_prefix||"/api";t.get(`${e}/v1/users/:user/quota-usage`,i((t,e)=>this.getUserQuota(t,e))),this.logger.info(`James API registered at ${e}/v1/users/:user/quota-usage`)}async getUserQuota(e,i){if(!s(e,i))return;const r=e.params.user;try{const e=a(r),s=await this.server.ldap.search({scope:"sub",filter:`(uid=${e})`,paged:!1,attributes:[this.mailAttr]},this.config.ldap_base||""),n=s.searchEntries?.[0];if(!n)return void i.status(404).json({error:`User ${r} not found`});const l=n[this.mailAttr];if(!l)return void i.status(400).json({error:`User ${r} has no mail attribute`});const o=Array.isArray(l)?String(l[0]):String(l),m=`${this.webadminUrl}/quota/users/${o}`,d=await this.requestLimit(()=>t(m,{method:"GET",headers:this.createHeaders()}));if(!d.ok)return void(404===d.status?i.status(404).json({error:`Quota not found for user ${r}`}):i.status(502).json({error:`Failed to retrieve quota from James: ${d.statusText}`}));const h=await d.json();i.json(h)}catch(t){this.logger.error({plugin:this.name,event:"getUserQuota",username:r,error:t instanceof Error?t.message:String(t)}),i.status(500).json({error:"Internal server error while retrieving quota"})}}async handleMailboxTypeTransition(t,e){const i=await this.ldapGetAttributes(t,["mail","member",this.mailboxTypeAttr]);if(!i)return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,error:"Could not fetch group attributes"});const a=i.mail;if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping mailbox type transition`);const s=Array.isArray(a)?String(a[0]):String(a),r=e.replace?.[this.mailboxTypeAttr]||e.add?.[this.mailboxTypeAttr],n=Array.isArray(r)?String(r[0]):String(r),l=this.getMailboxType({[this.mailboxTypeAttr]:n}),o=this.getMailboxType(i);if(this.logger.info({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,mail:s,oldType:o||"none",newType:l||"none"}),o!==l)if("mailingList"===o?await this.deleteMailingList(t,s):"teamMailbox"===o&&await this.removeAllTeamMailboxMembers(t,s),"mailingList"===l){const e=this.config.james_mailing_list_branch||[];if(e.length>0&&!this.isInAllowedBranches(t,e))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,newType:l,error:`Mailing list must be in allowed branches: ${e.join(", ")}`});await this.createMailingList(t,i,s)}else if("teamMailbox"===l){const e=this.config.james_mailing_list_branch||[];if(e.length>0&&this.isInAllowedBranches(t,e))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,newType:l,error:`Team mailbox cannot be in mailing list branches: ${e.join(", ")}`});await this.createTeamMailbox(t,i,s)}}getDisplayNameFromAttributes(t){const e=this.attributeToString(t[this.displayNameAttr]);if(e)return e;const i=this.attributeToString(t.cn);if(i)return i;const a=this.attributeToString(t.givenName),s=this.attributeToString(t.sn);if(a||s){const t=[];return a&&t.push(a),s&&t.push(s),t.join(" ")}const r=this.attributeToString(t[this.mailAttr]);return r||null}async getMailFromDN(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}async getDisplayNameFromDN(t){const e=[this.displayNameAttr,"cn","givenName","sn",this.mailAttr],i=await this.ldapGetAttributes(t,e);return i?this.getDisplayNameFromAttributes(i):null}async generateSignature(t){const e=this.config.james_signature_template;if(!e)return null;const i=await this.ldapGetAttributes(t);if(!i)return null;let a=e;return a=a.replace(/\{(\w+)\}/g,(t,e)=>"string"==typeof e&&i&&"object"==typeof i&&Object.prototype.hasOwnProperty.call(i,e)&&this.attributeToString(i[e])||""),a}async updateJamesIdentity(e,i,a){const s={plugin:this.name,event:"onLdapDisplayNameChange",result:"error",dn:e,mail:i,displayName:a};try{const r=`${this.webadminUrl}/jmap/identities/${i}`,n=await this.requestLimit(()=>t(r,{method:"GET",headers:this.createHeaders()}));if(!n.ok)return void this.logger.error({...s,step:"get_identities",http_status:n.status,http_status_text:n.statusText});const l=await n.json(),o=l.find(t=>t.email===i)||l[0];if(!o)return void this.logger.warn({...s,step:"find_identity",message:"No identity found for user"});const m=await this.generateSignature(e),d=`${this.webadminUrl}/jmap/identities/${i}/${o.id}`,h={id:o.id,email:o.email,name:a};m&&(h.htmlSignature=m);const g=await this.requestLimit(()=>t(d,{method:"PUT",headers:this.createHeaders("application/json"),body:JSON.stringify(h)}));g.ok?this.logger.info({...s,result:"success",http_status:g.status}):this.logger.error({...s,step:"update_identity",http_status:g.status,http_status_text:g.statusText})}catch(t){this.logger.error({...s,error:`${t}`})}}isAttributesList(t){return void 0!==t&&"object"==typeof t&&!Array.isArray(t)}getMailboxType(t){const e=t[this.mailboxTypeAttr];if(!e)return null;const i=(Array.isArray(e)?String(e[0]):String(e)).match(/^cn=([^,]+)/);if(!i)return null;const a=i[1];return"group"===a||"mailingList"===a||"teamMailbox"===a?a:null}isInAllowedBranches(t,e){return!e||0===e.length||e.some(e=>t===e||t.endsWith(","+e))}async createMailingList(t,e,i){const a=e.member||[],s=Array.isArray(a)?a:[a];this.logger.debug(`Creating mailing list ${i} in James with ${s.length} members`);const r=await this.getMemberEmails(s);await Promise.all(r.map(e=>this.callWebAdminApi("ldapgroupadddone:mailingList",`${this.webadminUrl}/address/groups/${i}/${e}`,"PUT",t,null,{groupMail:i,memberMail:e})))}async createTeamMailbox(t,e,i){const a=this.extractMailDomain(i);if(!a)return void this.logger.error({plugin:this.name,event:"createTeamMailbox",dn:t,mail:i,error:"Cannot extract domain from mail address"});const s=e.member||[],r=Array.isArray(s)?s:[s];this.logger.debug(`Creating team mailbox ${i} in James with ${r.length} members`);const n=await this.getMemberEmails(r),l=[];l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:create",`${this.webadminUrl}/domains/${a}/team-mailboxes/${i}`,"PUT",t,null,{domain:a,teamMailbox:i}));for(const e of n)l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:addMember",`${this.webadminUrl}/domains/${a}/team-mailboxes/${i}/members/${e}`,"PUT",t,null,{domain:a,teamMailbox:i,memberMail:e}));await Promise.all(l)}async deleteTeamMailbox(t,e){const i=this.extractMailDomain(e);i?await this.callWebAdminApi("ldapgroupdeletedone:teamMailbox",`${this.webadminUrl}/domains/${i}/team-mailboxes/${e}`,"DELETE",t,null,{domain:i,teamMailbox:e}):this.logger.error({plugin:this.name,event:"deleteTeamMailbox",dn:t,mail:e,error:"Cannot extract domain from mail address"})}async removeAllTeamMailboxMembers(t,e){const i=this.extractMailDomain(e);if(i)try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:["member"]},t);if(a.searchEntries&&a.searchEntries.length>0){const s=a.searchEntries[0].member||[],r=Array.isArray(s)?s:[s],n=await this.getMemberEmails(r);await Promise.all(n.map(a=>this.callWebAdminApi("removeAllTeamMailboxMembers",`${this.webadminUrl}/domains/${i}/team-mailboxes/${e}/members/${a}`,"DELETE",t,null,{domain:i,teamMailbox:e,memberMail:a}))),this.logger.info({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:t,teamMailbox:e,membersRemoved:n.length,message:"All members removed from team mailbox (mailbox preserved)"})}}catch(t){const i=t instanceof Error?t.message:String(t);this.logger.error(`Failed to remove members from team mailbox ${e}: ${i}`)}else this.logger.error({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:t,mail:e,error:"Cannot extract domain from mail address"})}async deleteMailingList(t,e){await this.callWebAdminApi("deleteMailingList",`${this.webadminUrl}/address/groups/${e}`,"DELETE",t,null,{groupMail:e})}async getMemberEmails(t){const e=t.filter(t=>t!==this.config.group_dummy_user).map(t=>this.server.ldap.queryLimit(async()=>{const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}));return(await Promise.all(e)).filter(t=>null!==t)}async getGroupMail(t){const e=await this.ldapGetAttributes(t,["mail"]);return e?this.attributeToString(e.mail):null}async getGroupMailboxType(t){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.mailboxTypeAttr]},t);if(e.searchEntries&&e.searchEntries.length>0)return this.getMailboxType(e.searchEntries[0])}catch(e){const i=e instanceof Error?e.message:String(e);this.logger.debug(`Could not get mailbox type for group ${t}: ${i}`)}return null}async _handleDelegationChange(t,e){const i=await this.ldapGetAttributes(t,[this.mailAttr]);if(!i)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"Could not find user entry to get mail attribute"});const a=i[this.mailAttr];if(!a||"string"!=typeof a)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"User has no mail attribute, cannot manage delegation"});const s=this.config.delegation_attribute;if(!s)return;const[r,n]=e[s]||[],l=this._normalizeToArray(r),o=this._normalizeToArray(n),m=o.filter(t=>!l.includes(t)),d=l.filter(t=>!o.includes(t)),h=m.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),g=d.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),[u,p]=await Promise.all([Promise.all(h),Promise.all(g)]),c=[];for(const{dn:e,email:i}of u)i&&c.push(this.callWebAdminApi("onLdapChange:addDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${i}`,"PUT",t,null,{userMail:a,delegateEmail:i,delegateDN:e,action:"add"}));for(const{dn:e,email:i}of p)i&&c.push(this.callWebAdminApi("onLdapChange:removeDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${i}`,"DELETE",t,null,{userMail:a,delegateEmail:i,delegateDN:e,action:"remove"}));await Promise.all(c)}async _getDelegateEmail(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):(this.logger.warn({plugin:this.name,event:"getDelegateEmail",dn:t,message:"Could not resolve delegate DN to email"}),null)}_normalizeToArray(t){return t?Array.isArray(t)?t:[t]:[]}}export{James as default};
|
package/dist/plugins/weblogs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import e from"../abstract/plugin.js";class WebLogs extends e{constructor(){super(...arguments),this.name="weblogs",this.roles=["logging"]}api(e){e.use((e,r,o)=>{let s=!0;this.logger.debug(`Incoming request: ${e.method} ${e.originalUrl}`);const t=Date.now();r.on("finish",()=>{const o={};e.proxyAuthUser?o.user=e.proxyAuthUser:e.user&&(o.user=e.user),s&&this.log(e,r,t,o),s=!1}),r.on("error",o=>{s&&this.log(e,r,t,{error:o.message}),s=!1}),r.on("close",()=>{r.writableEnded||(s&&this.log(e,r,t,{error:`Connection closed before response was sent for ${e.method} ${e.originalUrl}`}),s=!1)}),o()})}log(e,r,o,s){const t=Date.now()-o,n={method:e.method,url:e.originalUrl,status:r.statusCode,duration:t,ip:this.getClientIp(e),...s};this.logger.notice(n)}getClientIp(e){const r=e.headers["x-forwarded-for"];if(r){return(Array.isArray(r)?r[0]:r).split(",")[0].trim()}return e.socket.remoteAddress||"unknown"}}export{WebLogs as default};
|
|
@@ -7,6 +7,7 @@ import type winston from 'winston';
|
|
|
7
7
|
import type { Config, DM } from '../bin';
|
|
8
8
|
import type { Hooks, MaybePromise } from '../hooks';
|
|
9
9
|
export { asyncHandler } from '../lib/utils';
|
|
10
|
+
export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, UriTooLongError, TooManyRequestsError, BadGatewayError, ServiceUnavailableError, GatewayTimeoutError, } from '../lib/errors';
|
|
10
11
|
export type Role = 'auth' | 'authz' | 'protect' | 'api' | 'logging' | 'demo' | 'consistency' | 'configurable';
|
|
11
12
|
export default abstract class DmPlugin {
|
|
12
13
|
/**
|
package/dist/src/bin/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ import ldapActions from '../lib/ldapActions';
|
|
|
19
19
|
import type DmPlugin from '../abstract/plugin';
|
|
20
20
|
export type { Config };
|
|
21
21
|
export * from '../lib/utils';
|
|
22
|
+
export { asyncHandler } from '../lib/utils';
|
|
23
|
+
export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, UriTooLongError, TooManyRequestsError, BadGatewayError, ServiceUnavailableError, GatewayTimeoutError, } from '../lib/errors';
|
|
22
24
|
/**
|
|
23
25
|
* @class DM
|
|
24
26
|
*/
|
|
@@ -36,7 +38,10 @@ export declare class DM {
|
|
|
36
38
|
ldap: ldapActions;
|
|
37
39
|
operationSequence: number;
|
|
38
40
|
logger: winston.Logger;
|
|
41
|
+
private _errorMiddlewareSetup;
|
|
42
|
+
private _errorMiddleware?;
|
|
39
43
|
constructor();
|
|
44
|
+
setupErrorMiddleware(): void;
|
|
40
45
|
run(): Promise<void>;
|
|
41
46
|
stop(): void;
|
|
42
47
|
loadPlugin(pluginName: string): Promise<boolean>;
|
|
@@ -84,6 +84,8 @@ export interface Config {
|
|
|
84
84
|
crowdsec_url?: string;
|
|
85
85
|
crowdsec_api_key?: string;
|
|
86
86
|
crowdsec_cache_ttl?: number;
|
|
87
|
+
trusted_proxy?: string[];
|
|
88
|
+
trusted_proxy_auth_header?: string;
|
|
87
89
|
mail_attribute?: string;
|
|
88
90
|
quota_attribute?: string;
|
|
89
91
|
delegation_attribute?: string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom HTTP Error classes with status codes
|
|
3
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Base HTTP Error class with status code
|
|
7
|
+
*/
|
|
8
|
+
export declare class HttpError extends Error {
|
|
9
|
+
statusCode: number;
|
|
10
|
+
constructor(message: string, statusCode?: number);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Client errors (4xx)
|
|
14
|
+
*/
|
|
15
|
+
export declare class BadRequestError extends HttpError {
|
|
16
|
+
constructor(message?: string);
|
|
17
|
+
}
|
|
18
|
+
export declare class UnauthorizedError extends HttpError {
|
|
19
|
+
constructor(message?: string);
|
|
20
|
+
}
|
|
21
|
+
export declare class ForbiddenError extends HttpError {
|
|
22
|
+
constructor(message?: string);
|
|
23
|
+
}
|
|
24
|
+
export declare class NotFoundError extends HttpError {
|
|
25
|
+
constructor(message?: string);
|
|
26
|
+
}
|
|
27
|
+
export declare class ConflictError extends HttpError {
|
|
28
|
+
constructor(message?: string);
|
|
29
|
+
}
|
|
30
|
+
export declare class UriTooLongError extends HttpError {
|
|
31
|
+
constructor(message?: string);
|
|
32
|
+
}
|
|
33
|
+
export declare class TooManyRequestsError extends HttpError {
|
|
34
|
+
constructor(message?: string);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Server errors (5xx)
|
|
38
|
+
*/
|
|
39
|
+
export declare class BadGatewayError extends HttpError {
|
|
40
|
+
constructor(message?: string);
|
|
41
|
+
}
|
|
42
|
+
export declare class ServiceUnavailableError extends HttpError {
|
|
43
|
+
constructor(message?: string);
|
|
44
|
+
}
|
|
45
|
+
export declare class GatewayTimeoutError extends HttpError {
|
|
46
|
+
constructor(message?: string);
|
|
47
|
+
}
|
package/dist/src/lib/utils.d.ts
CHANGED
|
@@ -7,6 +7,23 @@ import type { Config } from '../bin';
|
|
|
7
7
|
export declare const launchHooks: (hooks: Function[] | undefined, ...args: unknown[]) => Promise<void>;
|
|
8
8
|
export declare const launchHooksChained: <T>(hooks: Function[] | undefined, args: T) => Promise<T>;
|
|
9
9
|
export declare const transformSchemas: (schemas: string | Buffer, config: Config) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Escape special characters in LDAP filter values according to RFC 4515
|
|
12
|
+
* Prevents LDAP injection attacks by escaping characters that have special meaning in LDAP filters
|
|
13
|
+
*
|
|
14
|
+
* @param value - The value to escape
|
|
15
|
+
* @returns The escaped value safe for use in LDAP filters
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* escapeLdapFilter('user*')
|
|
20
|
+
* // => 'user\\2a'
|
|
21
|
+
*
|
|
22
|
+
* escapeLdapFilter('Smith, John (admin)')
|
|
23
|
+
* // => 'Smith, John \\28admin\\29'
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function escapeLdapFilter(value: string): string;
|
|
10
27
|
/**
|
|
11
28
|
* Parse a Distinguished Name (DN) into its component parts (RDNs)
|
|
12
29
|
* Handles escaped commas and other special characters
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/auth/trustedProxy
|
|
3
|
+
* Trusted proxy plugin to validate X-Forwarded-For headers
|
|
4
|
+
*
|
|
5
|
+
* This plugin validates that X-Forwarded-For headers only come from trusted
|
|
6
|
+
* upstream reverse proxies. If the request comes from an untrusted IP,
|
|
7
|
+
* the X-Forwarded-For header is removed to prevent IP spoofing attacks.
|
|
8
|
+
*
|
|
9
|
+
* This plugin MUST be loaded BEFORE any plugin that uses X-Forwarded-For
|
|
10
|
+
* (like crowdsec and rateLimit) to ensure they receive sanitized headers.
|
|
11
|
+
*
|
|
12
|
+
* Configuration:
|
|
13
|
+
* - trusted_proxy: Array of trusted proxy IPs or CIDR ranges (required)
|
|
14
|
+
* Example: ["127.0.0.1", "10.0.0.0/8", "192.168.1.0/24", "::1"]
|
|
15
|
+
* - trusted_proxy_auth_header: Header name containing the authenticated user
|
|
16
|
+
* from trusted proxy (optional, default: "Auth-User")
|
|
17
|
+
*
|
|
18
|
+
* When a request comes from a trusted proxy:
|
|
19
|
+
* - X-Forwarded-For header is preserved
|
|
20
|
+
* - The request is marked as trusted (req.trustedProxy = true)
|
|
21
|
+
* - If trusted_proxy_auth_header is set and the header is present,
|
|
22
|
+
* req.proxyAuthUser is set to its value for use by other plugins (e.g., weblogs)
|
|
23
|
+
*
|
|
24
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
25
|
+
*/
|
|
26
|
+
import type { Express } from 'express';
|
|
27
|
+
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
28
|
+
declare global {
|
|
29
|
+
namespace Express {
|
|
30
|
+
interface Request {
|
|
31
|
+
trustedProxy?: boolean;
|
|
32
|
+
proxyAuthUser?: string;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export default class TrustedProxy extends DmPlugin {
|
|
37
|
+
name: string;
|
|
38
|
+
roles: Role[];
|
|
39
|
+
private trustedNetworks;
|
|
40
|
+
private authHeader;
|
|
41
|
+
constructor(...args: ConstructorParameters<typeof DmPlugin>);
|
|
42
|
+
api(app: Express): void;
|
|
43
|
+
/**
|
|
44
|
+
* Check if an IP address belongs to a trusted proxy network
|
|
45
|
+
*/
|
|
46
|
+
private isTrustedProxy;
|
|
47
|
+
/**
|
|
48
|
+
* Normalize IP address (handle IPv4-mapped IPv6 addresses)
|
|
49
|
+
*/
|
|
50
|
+
private normalizeIP;
|
|
51
|
+
/**
|
|
52
|
+
* Parse an IP address into a bigint for comparison
|
|
53
|
+
*/
|
|
54
|
+
private parseIP;
|
|
55
|
+
/**
|
|
56
|
+
* Parse a CIDR notation string into network address and mask
|
|
57
|
+
*/
|
|
58
|
+
private parseCIDR;
|
|
59
|
+
/**
|
|
60
|
+
* Parse an IPv4 address into a bigint
|
|
61
|
+
*/
|
|
62
|
+
private parseIPv4;
|
|
63
|
+
/**
|
|
64
|
+
* Parse an IPv6 address into a bigint
|
|
65
|
+
*/
|
|
66
|
+
private parseIPv6;
|
|
67
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
1
2
|
import TwakePlugin from '../../abstract/twakePlugin';
|
|
2
3
|
import { type Role } from '../../abstract/plugin';
|
|
3
4
|
import type { DM } from '../../bin';
|
|
@@ -16,6 +17,14 @@ export default class James extends TwakePlugin {
|
|
|
16
17
|
private mailboxTypeAttr;
|
|
17
18
|
private initDelay;
|
|
18
19
|
constructor(server: DM);
|
|
20
|
+
/**
|
|
21
|
+
* Register API routes
|
|
22
|
+
*/
|
|
23
|
+
api(app: Express): void;
|
|
24
|
+
/**
|
|
25
|
+
* Get quota information for a user
|
|
26
|
+
*/
|
|
27
|
+
private getUserQuota;
|
|
19
28
|
hooks: Hooks;
|
|
20
29
|
/**
|
|
21
30
|
* Handle mailbox type transitions when twakeMailboxType changes
|
package/docs/authentication.md
CHANGED
|
@@ -11,6 +11,139 @@ LDAP-Rest provides multiple authentication plugins to secure API access. These p
|
|
|
11
11
|
5. **OpenID Connect** (`core/auth/openidconnect`) - OAuth 2.0 / OpenID Connect authentication
|
|
12
12
|
6. **Authorization Per Branch** (`core/auth/authzPerBranch`) - Branch-level access control (see [authzPerBranch.md](authzPerBranch.md))
|
|
13
13
|
|
|
14
|
+
## Security Plugins
|
|
15
|
+
|
|
16
|
+
1. **Trusted Proxy** (`core/auth/trustedProxy`) - Validate X-Forwarded-For headers from reverse proxies
|
|
17
|
+
2. **Rate Limiting** (`core/auth/rateLimit`) - Prevent brute force attacks
|
|
18
|
+
3. **CrowdSec** (`core/auth/crowdsec`) - IP reputation and blocking
|
|
19
|
+
|
|
20
|
+
## Trusted Proxy
|
|
21
|
+
|
|
22
|
+
When running behind a reverse proxy (nginx, Apache, HAProxy, etc.), client IP addresses are typically passed via `X-Forwarded-For` headers. The `trustedProxy` plugin validates these headers to prevent IP spoofing attacks.
|
|
23
|
+
|
|
24
|
+
### Configuration
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
--plugin core/auth/trustedProxy \
|
|
28
|
+
--trusted-proxy "127.0.0.1" \
|
|
29
|
+
--trusted-proxy "10.0.0.0/8" \
|
|
30
|
+
--trusted-proxy "192.168.0.0/16"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Environment Variables:**
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Comma-separated list of trusted proxy IPs or CIDR ranges
|
|
37
|
+
DM_TRUSTED_PROXIES="127.0.0.1,10.0.0.0/8,192.168.0.0/16,::1"
|
|
38
|
+
|
|
39
|
+
# Optional: Header name for authenticated user from proxy (default: Auth-User)
|
|
40
|
+
DM_TRUSTED_PROXY_AUTH_HEADER="Auth-User"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### How It Works
|
|
44
|
+
|
|
45
|
+
1. **Request arrives** from reverse proxy
|
|
46
|
+
2. **Plugin checks** if `req.socket.remoteAddress` matches a trusted proxy
|
|
47
|
+
3. **If trusted:**
|
|
48
|
+
- `X-Forwarded-For` header is preserved
|
|
49
|
+
- `Auth-User` header (or custom header) is extracted for logging
|
|
50
|
+
- Request is marked as `req.trustedProxy = true`
|
|
51
|
+
4. **If untrusted:**
|
|
52
|
+
- `X-Forwarded-For` header is **removed** to prevent IP spoofing
|
|
53
|
+
- Request is marked as `req.trustedProxy = false`
|
|
54
|
+
5. **Other plugins** (rate limiting, CrowdSec, weblogs) see sanitized headers
|
|
55
|
+
|
|
56
|
+
### Supported Formats
|
|
57
|
+
|
|
58
|
+
- **IPv4 addresses:** `192.168.1.1`
|
|
59
|
+
- **IPv6 addresses:** `::1`, `fe80::1`
|
|
60
|
+
- **CIDR ranges:** `10.0.0.0/8`, `192.168.0.0/16`, `2001:db8::/32`
|
|
61
|
+
- **IPv4-mapped IPv6:** Automatically handled (e.g., `::ffff:127.0.0.1` matches `127.0.0.1`)
|
|
62
|
+
|
|
63
|
+
### Auth-User Header
|
|
64
|
+
|
|
65
|
+
When requests come from a trusted proxy, the plugin can extract an authenticated username from a header set by the proxy. This is useful for:
|
|
66
|
+
|
|
67
|
+
- Logging who made the request (even if LDAP-Rest doesn't handle authentication)
|
|
68
|
+
- Passing identity from an upstream authentication system
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Custom header name (default: Auth-User)
|
|
72
|
+
DM_TRUSTED_PROXY_AUTH_HEADER="X-Remote-User"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The extracted username is:
|
|
76
|
+
- Available as `req.proxyAuthUser` for other plugins
|
|
77
|
+
- Used by the `weblogs` plugin to log the user field
|
|
78
|
+
|
|
79
|
+
### Example: nginx Configuration
|
|
80
|
+
|
|
81
|
+
```nginx
|
|
82
|
+
upstream ldap_rest {
|
|
83
|
+
server 127.0.0.1:8081;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
server {
|
|
87
|
+
listen 443 ssl;
|
|
88
|
+
server_name api.example.com;
|
|
89
|
+
|
|
90
|
+
location / {
|
|
91
|
+
proxy_pass http://ldap_rest;
|
|
92
|
+
proxy_set_header Host $host;
|
|
93
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
94
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
95
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
96
|
+
|
|
97
|
+
# Optional: Pass authenticated user from nginx auth
|
|
98
|
+
proxy_set_header Auth-User $remote_user;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
LDAP-Rest configuration:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
DM_TRUSTED_PROXIES="127.0.0.1"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Example: HAProxy Configuration
|
|
110
|
+
|
|
111
|
+
```haproxy
|
|
112
|
+
frontend https
|
|
113
|
+
bind *:443 ssl crt /etc/ssl/cert.pem
|
|
114
|
+
default_backend ldap_rest
|
|
115
|
+
|
|
116
|
+
backend ldap_rest
|
|
117
|
+
option forwardfor
|
|
118
|
+
http-request set-header X-Forwarded-Proto https
|
|
119
|
+
server ldap_rest 127.0.0.1:8081
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Use Cases
|
|
123
|
+
|
|
124
|
+
- **Reverse Proxy Deployments:** Ensure correct client IP for rate limiting and logging
|
|
125
|
+
- **Load Balancers:** Trust headers from known load balancer IPs
|
|
126
|
+
- **CDN Integration:** Trust headers from CDN edge servers
|
|
127
|
+
- **Kubernetes:** Trust headers from ingress controllers
|
|
128
|
+
- **Security:** Prevent attackers from spoofing `X-Forwarded-For` to bypass rate limits
|
|
129
|
+
|
|
130
|
+
### Security Considerations
|
|
131
|
+
|
|
132
|
+
- **Only trust known proxies:** Never use `0.0.0.0/0` or trust all IPs
|
|
133
|
+
- **Use specific IPs/ranges:** Limit to your actual proxy infrastructure
|
|
134
|
+
- **HTTPS between proxy and LDAP-Rest:** Prevent header injection attacks
|
|
135
|
+
- **Monitor logs:** Watch for warnings about removed X-Forwarded-For headers
|
|
136
|
+
|
|
137
|
+
### Plugin Load Order
|
|
138
|
+
|
|
139
|
+
The `trustedProxy` plugin is automatically loaded **first** (via `priority.json`) to ensure all other plugins see sanitized headers:
|
|
140
|
+
|
|
141
|
+
1. `core/auth/trustedProxy` - Sanitizes X-Forwarded-For headers
|
|
142
|
+
2. `core/weblogs` - Logs requests with correct client IP
|
|
143
|
+
3. `core/auth/crowdsec` - Checks IP reputation
|
|
144
|
+
4. `core/auth/rateLimit` - Rate limits by IP
|
|
145
|
+
5. Authentication plugins...
|
|
146
|
+
|
|
14
147
|
## Token Authentication
|
|
15
148
|
|
|
16
149
|
Simple stateless authentication using bearer tokens.
|
package/docs/twakeJames.md
CHANGED
|
@@ -243,6 +243,66 @@ proxyAddresses: SMTP:john.doe@company.com
|
|
|
243
243
|
|
|
244
244
|
The `smtp:` prefix is automatically stripped. The capitalization (SMTP vs smtp) is ignored - only the email address after the colon is used.
|
|
245
245
|
|
|
246
|
+
## REST API
|
|
247
|
+
|
|
248
|
+
The James plugin exposes a REST API to retrieve quota usage and limits for users.
|
|
249
|
+
|
|
250
|
+
### GET /api/v1/users/:user/quota-usage
|
|
251
|
+
|
|
252
|
+
Retrieve quota usage and limits for a user from James. This endpoint returns both the quota limits configured for the user and their current mailbox usage.
|
|
253
|
+
|
|
254
|
+
**Request:**
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
GET /api/v1/users/jdoe/quota-usage
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Response (200 OK):**
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"global": { "count": 1000, "size": 1000000000 },
|
|
265
|
+
"domain": { "count": 800, "size": 800000000 },
|
|
266
|
+
"user": { "count": 500, "size": 75000000 },
|
|
267
|
+
"computed": { "count": 500, "size": 75000000 },
|
|
268
|
+
"occupation": {
|
|
269
|
+
"size": 50000000,
|
|
270
|
+
"count": 250,
|
|
271
|
+
"ratio": {
|
|
272
|
+
"size": 0.67,
|
|
273
|
+
"count": 0.5,
|
|
274
|
+
"max": 0.67
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Response Fields:**
|
|
281
|
+
|
|
282
|
+
- `global`: Global quota limit (server-wide)
|
|
283
|
+
- `domain`: Domain-specific quota limit
|
|
284
|
+
- `user`: User-specific quota limit from LDAP
|
|
285
|
+
- `computed`: Effective quota limit applied (resolved from upper values)
|
|
286
|
+
- `occupation`: **Current mailbox usage statistics**
|
|
287
|
+
- `size`: Storage currently used (bytes)
|
|
288
|
+
- `count`: Number of messages in mailbox
|
|
289
|
+
- `ratio`: Usage ratios (used / limit)
|
|
290
|
+
|
|
291
|
+
**Error Responses:**
|
|
292
|
+
|
|
293
|
+
- `404`: User not found in LDAP or quota not found in James
|
|
294
|
+
- `400`: User has no mail attribute
|
|
295
|
+
- `502`: Failed to retrieve quota from James
|
|
296
|
+
- `500`: Internal server error
|
|
297
|
+
|
|
298
|
+
**Example:**
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Get quota usage for user 'jdoe'
|
|
302
|
+
curl http://localhost:8081/api/v1/users/jdoe/quota-usage \
|
|
303
|
+
-H "Authorization: Bearer admin-token"
|
|
304
|
+
```
|
|
305
|
+
|
|
246
306
|
## James WebAdmin API
|
|
247
307
|
|
|
248
308
|
### Endpoints Used
|
|
@@ -251,6 +311,7 @@ The `smtp:` prefix is automatically stripped. The capitalization (SMTP vs smtp)
|
|
|
251
311
|
| ------------------- | ------------------------------------------ | ------ |
|
|
252
312
|
| Rename account | `/users/{old}/rename/{new}?action=rename` | POST |
|
|
253
313
|
| Update quota | `/quota/users/{mail}/size` | PUT |
|
|
314
|
+
| Get quota | `/quota/users/{mail}` | GET |
|
|
254
315
|
| Add delegation | `/users/{mail}/authorizedUsers/{delegate}` | PUT |
|
|
255
316
|
| Remove delegation | `/users/{mail}/authorizedUsers/{delegate}` | DELETE |
|
|
256
317
|
| Add alias | `/address/aliases/{mail}/sources/{alias}` | PUT |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ldap-rest",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Light directory manager",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Identity-Manager"
|
|
@@ -64,6 +64,10 @@
|
|
|
64
64
|
"import": "./dist/plugins/auth/totp.js",
|
|
65
65
|
"types": "./dist/src/plugins/auth/totp.d.ts"
|
|
66
66
|
},
|
|
67
|
+
"./plugin-auth-trustedproxy": {
|
|
68
|
+
"import": "./dist/plugins/auth/trustedProxy.js",
|
|
69
|
+
"types": "./dist/src/plugins/auth/trustedProxy.d.ts"
|
|
70
|
+
},
|
|
67
71
|
"./plugin-configapi": {
|
|
68
72
|
"import": "./dist/plugins/configApi.js",
|
|
69
73
|
"types": "./dist/src/plugins/configApi.d.ts"
|
|
@@ -450,6 +454,9 @@
|
|
|
450
454
|
"test:dev": "./.dev.mk test",
|
|
451
455
|
"test:one": "mocha --config .mocharc-one.json"
|
|
452
456
|
},
|
|
457
|
+
"overrides": {
|
|
458
|
+
"glob": ">=11.0.4"
|
|
459
|
+
},
|
|
453
460
|
"dependencies": {
|
|
454
461
|
"body-parser": "^2.2.0",
|
|
455
462
|
"csv-parse": "^6.1.0",
|