ldap-rest 0.3.0 → 0.3.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # LDAP-Rest
4
4
 
5
- Lite LinID directory manager is a lightweight directory manager that provides LDAP integration through a plugin-based architecture.
5
+ **ldap-rest** is a lightweight directory manager that provides LDAP integration through a plugin-based architecture.
6
6
  This system enables directory management operations with configurable authentication, extensible functionality through events/hooks,
7
7
  and extensible REST API.
8
8
 
@@ -1 +1 @@
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,validateDnValue as h,escapeDnValue as c,launchHooksChained as d,escapeLdapFilter as g}from"../../lib/utils.js";import{BadRequestError as l,NotFoundError as p,ConflictError as f}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 l("An organization link cannot be deleted");if(a)throw new l("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 l('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;h(a.ou,"ou");const r=a.parentDn||this.config.ldap_top_organization,s=`ou=${c(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 l("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 l("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 l("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new l("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 p(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new l(`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 l(`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 l(`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 l(`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 l(`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 f(`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 l("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 p("Top organization not found");const[,a]=await d(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 p(`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 l(`Invalid value for field ${t}`);for(const[t,a]of Object.entries(this.schema.attributes))if(a.required&&null==i[t])throw new l(`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 l(`Invalid value for field ${t}`);if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!this._validateOneChange(t,a))throw new l(`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=[],e=g(i);try{const n=await this.server.ldap.search({paged:!1,scope:"one",filter:`(&(objectClass=organizationalUnit)(|(ou=*${e}*)(description=*${e}*)))`},t);this.server.logger.debug(`Found ${n.searchEntries.length} sub-OUs matching "${i}" for ${t}`),a.push(...n.searchEntries)}catch(a){this.server.logger.debug(`No sub-OUs found matching "${i}" for ${t}: ${a}`)}const n=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,""),r=g(t),s=`(&(${this.config.ldap_organization_link_attribute}=${r})(|(uid=*${e}*)(cn=*${e}*)(mail=*${e}*)(sn=*${e}*)(givenName=*${e}*)))`;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);let h=0;for await(const t of o)h+=t.searchEntries.length,a.push(...t.searchEntries);return this.server.logger.debug(`Found ${h} 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 a from"./organizations.js";console.warn("[ldap-rest] Plugin `core/ldap/organization` is deprecated; use `core/ldap/organizations` instead.");export{a as default};
@@ -0,0 +1 @@
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,validateDnValue as h,escapeDnValue as c,escapeLdapFilter as d,launchHooksChained as g,getParentDn as l,getRdn as p,isChildOf as f}from"../../lib/utils.js";import{BadRequestError as u,NotFoundError as w,ConflictError as $}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 u("An organization link cannot be deleted");if(a)throw new u("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 u('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;h(a.ou,"ou");const r=a.parentDn||this.config.ldap_top_organization,s=`ou=${c(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 u("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 u("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 u("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new u("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 w(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new u(`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 u(`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=${d(r)})`,scope:"sub"},this.config.ldap_top_organization);if(0===o.searchEntries.length)throw new u(`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 u(`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 u(`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 $(`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 u("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 w("Top organization not found");const[,a]=await g(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 w(`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,s=l(r);let o=`(${this.config.ldap_organization_link_attribute}=${d(t)})`;n&&(o=`(&${o}(objectClass=${d(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${s}`);const h=await this.server.ldap.search({paged:!0,filter:o},s,i);let c=0,g=0;for await(const t of h){g+=t.searchEntries.length;const i=e-c;if(i>0){const e=t.searchEntries.slice(0,i);a.push(...e),c+=e.length}}return this.server.logger.debug(`Found ${g} linked entities for ${t}, returning ${c}`),g>e&&a.push({dn:`more-${t}`,cn:[`... ${g-e} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:g.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 u(`Invalid value for field ${t}`);for(const[t,a]of Object.entries(this.schema.attributes))if(a.required&&null==i[t])throw new u(`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 u(`Invalid value for field ${t}`);if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!this._validateOneChange(t,a))throw new u(`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){let e;try{e=await this.server.ldap.search({paged:!1,scope:"base"},i)}catch(t){throw new w(`Target organization ${i} not found: ${t}`)}if(!e.searchEntries||0===e.searchEntries.length)throw new w(`Target organization ${i} not found`);if(!this.isOu(e.searchEntries[0]))throw new u(`Target ${i} is not an organizational unit`);const n=`${p(t)},${i}`;let r;try{r=await this.server.ldap.search({paged:!1,scope:"base"},t)}catch(t){throw new w(`Source organization not found: ${t}`)}if(!r.searchEntries||0===r.searchEntries.length)throw new w(`Source organization ${t} not found`);if(n===t)throw new u("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||f(i,t))throw new u("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,n,a),this.logger.info(`Moved organization from ${t} to ${n}`)}catch(t){throw new Error(`Failed to move organization: ${t}`)}return{newDn:n}}async deleteOrganization(t){return await this.server.ldap.delete(t)}async searchOrganisationSubnodes(t,i){const a=[],e=d(i);try{const n=await this.server.ldap.search({paged:!1,scope:"one",filter:`(&(objectClass=organizationalUnit)(|(ou=*${e}*)(description=*${e}*)))`},t);this.server.logger.debug(`Found ${n.searchEntries.length} sub-OUs matching "${i}" for ${t}`),a.push(...n.searchEntries)}catch(a){this.server.logger.debug(`No sub-OUs found matching "${i}" for ${t}: ${a}`)}const n=this.config.ldap_top_organization,r=l(n),s=d(t),o=`(&(${this.config.ldap_organization_link_attribute}=${s})(|(uid=*${e}*)(cn=*${e}*)(mail=*${e}*)(sn=*${e}*)(givenName=*${e}*)))`;this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`);const h=await this.server.ldap.search({paged:!0,filter:o},r);let c=0;for await(const t of h)c+=t.searchEntries.length,a.push(...t.searchEntries);return this.server.logger.debug(`Found ${c} 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/top`,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};
@@ -38,6 +38,113 @@ export interface LdapFlatConfig {
38
38
  */
39
39
  hookPrefix: string;
40
40
  }
41
+ /**
42
+ * Generic OpenAPI schemas for the flat LDAP-entry CRUD surface.
43
+ * Every concrete plugin that extends LdapFlat (users, mailgroups, …)
44
+ * exposes these shapes, with `mainAttribute` and concrete attribute
45
+ * names varying per instance.
46
+ *
47
+ * @openapi-component
48
+ * FlatEntry:
49
+ * type: object
50
+ * description: |
51
+ * A single LDAP entry from a flat branch. The `dn` field is always
52
+ * present; all other fields depend on the concrete plugin's schema
53
+ * (e.g. `uid`, `cn`, `mail`, `sn` for users).
54
+ * required: [dn]
55
+ * properties:
56
+ * dn:
57
+ * type: string
58
+ * description: Fully-qualified distinguished name of the entry.
59
+ * example: uid=alice,ou=users,dc=example,dc=com
60
+ * additionalProperties:
61
+ * oneOf:
62
+ * - type: string
63
+ * - type: array
64
+ * items: { type: string }
65
+ * example:
66
+ * dn: uid=alice,ou=users,dc=example,dc=com
67
+ * uid: alice
68
+ * cn: Alice Smith
69
+ * sn: Smith
70
+ * mail: alice@example.com
71
+ * FlatList:
72
+ * type: object
73
+ * description: |
74
+ * A map of entries returned by the LIST endpoint, keyed by the
75
+ * entry's `mainAttribute` value (e.g. the `uid` for users). This
76
+ * is the `LdapList` shape (`Record<string, FlatEntry>`).
77
+ * additionalProperties:
78
+ * $ref: '#/components/schemas/FlatEntry'
79
+ * example:
80
+ * alice:
81
+ * dn: uid=alice,ou=users,dc=example,dc=com
82
+ * uid: alice
83
+ * cn: Alice Smith
84
+ * mail: alice@example.com
85
+ * bob:
86
+ * dn: uid=bob,ou=users,dc=example,dc=com
87
+ * uid: bob
88
+ * cn: Bob Jones
89
+ * mail: bob@example.com
90
+ * FlatCreate:
91
+ * type: object
92
+ * description: |
93
+ * Body for creating a new entry. The plugin's `mainAttribute` field
94
+ * (e.g. `uid`) is required and used to build the DN. All other
95
+ * attributes are plugin-specific; unknown attributes are accepted
96
+ * unless the plugin's schema is in strict mode.
97
+ * additionalProperties: true
98
+ * example:
99
+ * uid: carol
100
+ * cn: Carol White
101
+ * sn: White
102
+ * mail: carol@example.com
103
+ * FlatModify:
104
+ * type: object
105
+ * description: |
106
+ * Partial-update body. Supports `add`, `replace`, and `delete`
107
+ * maps matching the LDAP modify operation. The `mainAttribute`
108
+ * (e.g. `uid`) cannot be changed through this endpoint — use the
109
+ * rename API instead.
110
+ * properties:
111
+ * add:
112
+ * type: object
113
+ * description: Attributes whose values are appended.
114
+ * additionalProperties: true
115
+ * replace:
116
+ * type: object
117
+ * description: Attributes whose values are replaced wholesale.
118
+ * additionalProperties: true
119
+ * delete:
120
+ * description: |
121
+ * Attributes (or specific values) to remove. Either an array
122
+ * of attribute names or a map of attribute → value(s).
123
+ * oneOf:
124
+ * - type: array
125
+ * items: { type: string }
126
+ * - type: object
127
+ * additionalProperties: true
128
+ * example:
129
+ * replace:
130
+ * mail: carol.white@example.com
131
+ * cn: Carol White-Smith
132
+ * FlatMoveRequest:
133
+ * type: object
134
+ * description: |
135
+ * Body for moving an entry to a different organisational unit.
136
+ * Requires `ldap_organization_link_attribute` and
137
+ * `ldap_organization_path_attribute` to be defined in the plugin's
138
+ * schema.
139
+ * required: [targetOrgDn]
140
+ * properties:
141
+ * targetOrgDn:
142
+ * type: string
143
+ * description: DN of the destination organisational unit.
144
+ * example: ou=engineering,ou=departments,dc=example,dc=com
145
+ * example:
146
+ * targetOrgDn: ou=engineering,ou=departments,dc=example,dc=com
147
+ */
41
148
  export default abstract class LdapFlat extends DmPlugin {
42
149
  base: string;
43
150
  ldap: ldapActions;
@@ -10,6 +10,57 @@
10
10
  */
11
11
  import type { Express } from 'express';
12
12
  import DmPlugin, { type Role } from '../abstract/plugin';
13
+ /**
14
+ * @openapi-component
15
+ * Config:
16
+ * type: object
17
+ * description: |
18
+ * Aggregated server configuration. The top-level fields are always
19
+ * present; `features` is a dynamic map whose keys are the names of
20
+ * loaded plugins that declare the `configurable` role. Each plugin
21
+ * contributes an arbitrary sub-object — consult the individual plugin
22
+ * documentation for its exact shape.
23
+ * additionalProperties: true
24
+ * properties:
25
+ * apiPrefix:
26
+ * type: string
27
+ * description: The API base path configured on this server.
28
+ * example: /api
29
+ * ldapBase:
30
+ * type: string
31
+ * description: The root LDAP suffix used by this server.
32
+ * example: dc=example,dc=com
33
+ * features:
34
+ * type: object
35
+ * description: |
36
+ * Per-plugin configuration blocks. Keys match the plugin's
37
+ * `name` field; values are plugin-specific objects.
38
+ * additionalProperties: true
39
+ * example:
40
+ * ldapGroups:
41
+ * enabled: true
42
+ * base: ou=groups,dc=example,dc=com
43
+ * mainAttribute: cn
44
+ * objectClass: [top, groupOfNames]
45
+ * static:
46
+ * enabled: true
47
+ * staticPath: /static
48
+ * appAccountsApi:
49
+ * enabled: true
50
+ * base: ou=app-accounts,dc=example,dc=com
51
+ * maxAccounts: 5
52
+ * example:
53
+ * apiPrefix: /api
54
+ * ldapBase: dc=example,dc=com
55
+ * features:
56
+ * ldapGroups:
57
+ * enabled: true
58
+ * base: ou=groups,dc=example,dc=com
59
+ * mainAttribute: cn
60
+ * static:
61
+ * enabled: true
62
+ * staticPath: /static
63
+ */
13
64
  export default class ConfigApi extends DmPlugin {
14
65
  name: string;
15
66
  roles: Role[];
@@ -5,6 +5,27 @@
5
5
  */
6
6
  import type { Express } from 'express';
7
7
  import DmPlugin, { type Role } from '../../abstract/plugin';
8
+ /**
9
+ * @openapi-component
10
+ * Hello:
11
+ * type: object
12
+ * description: Response returned by the hello-world demo endpoint.
13
+ * properties:
14
+ * message:
15
+ * type: string
16
+ * description: Static greeting string.
17
+ * example: Hello
18
+ * hookResults:
19
+ * type: array
20
+ * description: |
21
+ * Results collected from every function registered on the `hello`
22
+ * hook. Empty when no hook listeners are installed.
23
+ * items: {}
24
+ * example: []
25
+ * example:
26
+ * message: Hello
27
+ * hookResults: []
28
+ */
8
29
  export default class HelloWorld extends DmPlugin {
9
30
  name: string;
10
31
  roles: Role[];
@@ -6,6 +6,71 @@
6
6
  import type { Express } from 'express';
7
7
  import DmPlugin, { type Role } from '../../abstract/plugin';
8
8
  import type { DM } from '../../bin';
9
+ /**
10
+ * Shared OpenAPI schemas surfaced by this plugin. Picked up by
11
+ * scripts/generate-openapi.ts and merged into `components.schemas`.
12
+ *
13
+ * @openapi-component
14
+ * BulkImportResult:
15
+ * type: object
16
+ * description: Summary returned after a bulk import operation.
17
+ * required: [success, total, created, updated, skipped, failed, errors, details]
18
+ * properties:
19
+ * success:
20
+ * type: boolean
21
+ * description: True when no fatal error aborted the import early.
22
+ * total:
23
+ * type: integer
24
+ * description: Total number of CSV data rows (excluding header).
25
+ * example: 42
26
+ * created:
27
+ * type: integer
28
+ * description: Entries successfully created (or counted in dry-run).
29
+ * example: 38
30
+ * updated:
31
+ * type: integer
32
+ * description: Existing entries updated.
33
+ * example: 2
34
+ * skipped:
35
+ * type: integer
36
+ * description: Existing entries skipped because `updateExisting` was false.
37
+ * example: 1
38
+ * failed:
39
+ * type: integer
40
+ * description: Rows that caused an error.
41
+ * example: 1
42
+ * errors:
43
+ * type: array
44
+ * items:
45
+ * type: object
46
+ * required: [line, error]
47
+ * properties:
48
+ * line:
49
+ * type: integer
50
+ * description: 1-based CSV line number (2 = first data row).
51
+ * identifier:
52
+ * type: string
53
+ * description: Value of the main attribute for the failing row (if known).
54
+ * error:
55
+ * type: string
56
+ * description: Error message.
57
+ * example:
58
+ * - line: 5
59
+ * identifier: bob
60
+ * error: 'Missing required attribute: mail'
61
+ * details:
62
+ * type: object
63
+ * required: [duration, linesProcessed]
64
+ * properties:
65
+ * duration:
66
+ * type: string
67
+ * description: Wall-clock time for the import (e.g. `1.2s`).
68
+ * example: 1.2s
69
+ * linesProcessed:
70
+ * type: integer
71
+ * description: Total data rows attempted.
72
+ * example: 42
73
+ */
9
74
  export default class LdapBulkImport extends DmPlugin {
10
75
  name: string;
11
76
  roles: Role[];
@@ -10,6 +10,74 @@ export interface postAdd {
10
10
  [key: string]: AttributeValue | undefined;
11
11
  }
12
12
  export type postModify = ModifyRequest;
13
+ /**
14
+ * Shared OpenAPI schemas surfaced by this plugin. Picked up by
15
+ * scripts/generate-openapi.ts and merged into `components.schemas`.
16
+ *
17
+ * @openapi-component
18
+ * Group:
19
+ * type: object
20
+ * description: An LDAP group entry (groupOfNames / groupOfUniqueNames).
21
+ * required: [dn, cn]
22
+ * properties:
23
+ * dn:
24
+ * type: string
25
+ * description: Fully-qualified distinguished name of the entry.
26
+ * example: cn=admins,ou=groups,dc=example,dc=com
27
+ * cn:
28
+ * type: string
29
+ * description: Common name (RDN attribute) of the group.
30
+ * example: admins
31
+ * description:
32
+ * type: string
33
+ * example: Server administrators
34
+ * member:
35
+ * type: array
36
+ * items: { type: string }
37
+ * description: DNs of the group's members.
38
+ * example:
39
+ * - uid=alice,ou=users,dc=example,dc=com
40
+ * - uid=bob,ou=users,dc=example,dc=com
41
+ * GroupCreate:
42
+ * type: object
43
+ * required: [cn]
44
+ * properties:
45
+ * cn:
46
+ * type: string
47
+ * description: Common name of the new group.
48
+ * description:
49
+ * type: string
50
+ * member:
51
+ * type: array
52
+ * items: { type: string }
53
+ * description: Initial members (DNs).
54
+ * example:
55
+ * cn: admins
56
+ * description: Server administrators
57
+ * member:
58
+ * - uid=alice,ou=users,dc=example,dc=com
59
+ * GroupModify:
60
+ * type: object
61
+ * description: |
62
+ * Partial update. Each provided attribute is replaced wholesale. Use
63
+ * the dedicated `/members` endpoints to add/remove members one at a
64
+ * time without supplying the full list.
65
+ * properties:
66
+ * description: { type: string }
67
+ * member:
68
+ * type: array
69
+ * items: { type: string }
70
+ * example:
71
+ * description: Updated description
72
+ * Error:
73
+ * type: object
74
+ * properties:
75
+ * error: { type: string }
76
+ * code: { type: integer }
77
+ * example:
78
+ * error: Group not found
79
+ * code: 404
80
+ */
13
81
  export default class LdapGroups extends DmPlugin {
14
82
  name: string;
15
83
  roles: Role[];
@@ -1,65 +1,8 @@
1
- import type { Express, Request, Response } from 'express';
2
- import DmPlugin, { type Role } from '../../abstract/plugin';
3
- import { DM } from '../../bin';
4
- import { Hooks } from '../../hooks';
5
- import { AttributesList, AttributeValue, ModifyRequest } from '../../lib/ldapActions';
6
- import type { Schema } from '../../config/schema';
7
- export default class LdapOrganizations extends DmPlugin {
8
- name: string;
9
- roles: Role[];
10
- pathAttr: string;
11
- linkAttr: string;
12
- schema?: Schema;
13
- constructor(dm: DM);
14
- /**
15
- * API routes for LDAP organizations
16
- * @param app Express application
17
- */
18
- api(app: Express): void;
19
- apiAdd(req: Request, res: Response): Promise<void>;
20
- apiModify(req: Request, res: Response): Promise<void>;
21
- apiDelete(req: Request, res: Response): Promise<void>;
22
- apiMove(req: Request, res: Response): Promise<void>;
23
- /**
24
- * Consistency checks on any entry
25
- */
26
- hooks: Hooks;
27
- /**
28
- * Check if the department link is valid
29
- * @param entry LDAP entry to check
30
- */
31
- checkDeptLink(entry: AttributesList): Promise<void>;
32
- /**
33
- * Check if the department path is valid
34
- * @param entry LDAP entry to check
35
- */
36
- checkDeptPath(entry: AttributesList): Promise<void>;
37
- isEmptyOrganization(dn: string): Promise<void>;
38
- /**
39
- * Check if entry is an organisation
40
- * @param entry LDAP entry to check
41
- * @returns True if entry is an organisation, false otherwise
42
- */
43
- isOu(entry: AttributesList): boolean;
44
- getOrganisationTop(req?: Request): Promise<AttributesList | AttributesList[]>;
45
- getOrganisationByDn(dn: string, req?: Request): Promise<AttributesList>;
46
- getOrganisationSubnodes(dn: string, req?: Request): Promise<AttributesList[]>;
47
- addOrganization(dn: string, entry: AttributesList, req?: Request): Promise<boolean>;
48
- modifyOrganization(dn: string, changes: ModifyRequest): Promise<boolean>;
49
- validateNewOrganization(dn: string, entry: AttributesList): boolean;
50
- validateChanges(dn: string, changes: ModifyRequest): boolean;
51
- _validateOneChange(field: string, value: AttributeValue | null): boolean;
52
- /**
53
- * Move an organization to a different parent organization
54
- * Uses LDAP modifyDN to change the DN hierarchy
55
- */
56
- moveOrganization(dn: string, targetOrgDn: string, req?: Request): Promise<{
57
- newDn: string;
58
- }>;
59
- deleteOrganization(dn: string): Promise<boolean>;
60
- searchOrganisationSubnodes(dn: string, query: string): Promise<AttributesList[]>;
61
- /**
62
- * Provide configuration for config API
63
- */
64
- getConfigApiData(): Record<string, unknown>;
65
- }
1
+ /**
2
+ * @deprecated Use `core/ldap/organizations` instead. This file is a
3
+ * backwards-compatibility shim for setups that still load the plugin under
4
+ * the historical (singular) filename. It will be removed in a future major
5
+ * release.
6
+ */
7
+ import LdapOrganizations from './organizations';
8
+ export default LdapOrganizations;
@@ -0,0 +1,155 @@
1
+ import type { Express, Request, Response } from 'express';
2
+ import DmPlugin, { type Role } from '../../abstract/plugin';
3
+ import { DM } from '../../bin';
4
+ import { Hooks } from '../../hooks';
5
+ import { AttributesList, AttributeValue, ModifyRequest } from '../../lib/ldapActions';
6
+ import type { Schema } from '../../config/schema';
7
+ /**
8
+ * Shared OpenAPI schemas surfaced by this plugin. Picked up by
9
+ * scripts/generate-openapi.ts and merged into `components.schemas`.
10
+ *
11
+ * @openapi-component
12
+ * Organization:
13
+ * type: object
14
+ * description: An LDAP organizational unit entry.
15
+ * required: [dn, ou]
16
+ * properties:
17
+ * dn:
18
+ * type: string
19
+ * description: Fully-qualified distinguished name of the entry.
20
+ * example: ou=Engineering,ou=organizations,dc=example,dc=com
21
+ * ou:
22
+ * type: string
23
+ * description: Organizational unit name (RDN attribute).
24
+ * example: Engineering
25
+ * o:
26
+ * type: string
27
+ * description: Organization display name.
28
+ * example: Engineering Department
29
+ * l:
30
+ * type: string
31
+ * description: Locality / city.
32
+ * example: Paris
33
+ * description:
34
+ * type: string
35
+ * example: Software engineering division
36
+ * organizationLink:
37
+ * type: string
38
+ * description: DN of the parent organization (link attribute).
39
+ * example: ou=organizations,dc=example,dc=com
40
+ * organizationPath:
41
+ * type: string
42
+ * description: |
43
+ * Human-readable slash-separated path to this node, used by the
44
+ * UI tree. Set automatically on creation and move.
45
+ * example: Engineering / Backend
46
+ * member:
47
+ * type: array
48
+ * items: { type: string }
49
+ * description: DNs of entries linked to this organization.
50
+ * example:
51
+ * - uid=alice,ou=users,dc=example,dc=com
52
+ * OrgSummary:
53
+ * type: object
54
+ * description: Lightweight organization reference (used in node lists).
55
+ * required: [dn, ou]
56
+ * properties:
57
+ * dn: { type: string, example: ou=HR,ou=organizations,dc=example,dc=com }
58
+ * ou: { type: string, example: HR }
59
+ * description: { type: string }
60
+ * objectClass:
61
+ * type: array
62
+ * items: { type: string }
63
+ * example: [top, organizationalUnit]
64
+ * OrganizationCreate:
65
+ * type: object
66
+ * required: [ou]
67
+ * properties:
68
+ * ou:
69
+ * type: string
70
+ * description: Name of the new organizational unit.
71
+ * example: Marketing
72
+ * parentDn:
73
+ * type: string
74
+ * description: |
75
+ * DN of the parent organizational unit. Defaults to the configured
76
+ * top organization when omitted.
77
+ * example: ou=organizations,dc=example,dc=com
78
+ * description: { type: string }
79
+ * l: { type: string }
80
+ * example:
81
+ * ou: Marketing
82
+ * parentDn: ou=organizations,dc=example,dc=com
83
+ * description: Marketing division
84
+ * OrganizationModify:
85
+ * type: object
86
+ * description: |
87
+ * Partial update. Any provided attribute is replaced wholesale.
88
+ * `ou` cannot be changed via this endpoint — use the move endpoint
89
+ * to relocate the node within the tree.
90
+ * properties:
91
+ * description: { type: string }
92
+ * l: { type: string }
93
+ * o: { type: string }
94
+ * example:
95
+ * description: Updated description for Marketing
96
+ */
97
+ export default class LdapOrganizations extends DmPlugin {
98
+ name: string;
99
+ roles: Role[];
100
+ pathAttr: string;
101
+ linkAttr: string;
102
+ schema?: Schema;
103
+ constructor(dm: DM);
104
+ /**
105
+ * API routes for LDAP organizations
106
+ * @param app Express application
107
+ */
108
+ api(app: Express): void;
109
+ apiAdd(req: Request, res: Response): Promise<void>;
110
+ apiModify(req: Request, res: Response): Promise<void>;
111
+ apiDelete(req: Request, res: Response): Promise<void>;
112
+ apiMove(req: Request, res: Response): Promise<void>;
113
+ /**
114
+ * Consistency checks on any entry
115
+ */
116
+ hooks: Hooks;
117
+ /**
118
+ * Check if the department link is valid
119
+ * @param entry LDAP entry to check
120
+ */
121
+ checkDeptLink(entry: AttributesList): Promise<void>;
122
+ /**
123
+ * Check if the department path is valid
124
+ * @param entry LDAP entry to check
125
+ */
126
+ checkDeptPath(entry: AttributesList): Promise<void>;
127
+ isEmptyOrganization(dn: string): Promise<void>;
128
+ /**
129
+ * Check if entry is an organisation
130
+ * @param entry LDAP entry to check
131
+ * @returns True if entry is an organisation, false otherwise
132
+ */
133
+ isOu(entry: AttributesList): boolean;
134
+ getOrganisationTop(req?: Request): Promise<AttributesList | AttributesList[]>;
135
+ getOrganisationByDn(dn: string, req?: Request): Promise<AttributesList>;
136
+ getOrganisationSubnodes(dn: string, req?: Request): Promise<AttributesList[]>;
137
+ addOrganization(dn: string, entry: AttributesList, req?: Request): Promise<boolean>;
138
+ modifyOrganization(dn: string, changes: ModifyRequest): Promise<boolean>;
139
+ validateNewOrganization(dn: string, entry: AttributesList): boolean;
140
+ validateChanges(dn: string, changes: ModifyRequest): boolean;
141
+ _validateOneChange(field: string, value: AttributeValue | null): boolean;
142
+ /**
143
+ * Move an organization to a different parent organization
144
+ * Uses LDAP modifyDN to change the DN hierarchy
145
+ */
146
+ moveOrganization(dn: string, targetOrgDn: string, req?: Request): Promise<{
147
+ newDn: string;
148
+ }>;
149
+ deleteOrganization(dn: string): Promise<boolean>;
150
+ searchOrganisationSubnodes(dn: string, query: string): Promise<AttributesList[]>;
151
+ /**
152
+ * Provide configuration for config API
153
+ */
154
+ getConfigApiData(): Record<string, unknown>;
155
+ }