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 +1 -1
- package/dist/plugins/ldap/organization.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -0
- package/dist/src/abstract/ldapFlat.d.ts +107 -0
- package/dist/src/plugins/configApi.d.ts +51 -0
- package/dist/src/plugins/demo/helloworld.d.ts +21 -0
- package/dist/src/plugins/ldap/bulkImport.d.ts +65 -0
- package/dist/src/plugins/ldap/groups.d.ts +68 -0
- package/dist/src/plugins/ldap/organization.d.ts +8 -65
- package/dist/src/plugins/ldap/organizations.d.ts +155 -0
- package/dist/src/plugins/ldap/passwordPolicy.d.ts +178 -0
- package/dist/src/plugins/scim/scim.d.ts +389 -0
- package/dist/src/plugins/static.d.ts +33 -0
- package/dist/src/plugins/twake/appAccountsApi.d.ts +63 -0
- package/dist/src/plugins/twake/james.d.ts +47 -0
- package/docs/plugin-development/README.md +2 -1
- package/docs/plugin-development/openapi.md +144 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# LDAP-Rest
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
}
|