ldap-rest 0.21.1 → 0.21.2
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
import{assertModifyRequest as t}from"../lib/ldapActions.js";import{wantJson as e,jsonBody as i,created as s,tryMethod as r,multiStatus as a}from"../lib/expressFormatedResponses.js";import{getCompiledRegex as n,escapeDnValue as o,asyncHandler as h,escapeRegex as c,validateDnValue as d,isDummyMemberDn as u,normalizeDn as l,getParentDn as f,launchHooksChained as m,launchHooks as b,escapeLdapFilter as p,isDnInBranch as w,substringSearchFilter as g}from"../lib/utils.js";import{neverReturnAttributes as y,schemaAttribute as $,invalidValueMessage as A,modifiedAttributeNames as v,toDirectoryEntry as E,missingRequiredAttribute as x,toDirectoryChanges as N,checkDnValues as j}from"../config/schema.js";import{BadRequestError as C,ConflictError as k,NotFoundError as D,HttpError as O,UnprocessableEntityError as P}from"../lib/errors.js";import{extractLdapCode as q}from"../lib/ldapCodes.js";import{loadSchemaFile as _}from"../lib/schemaFile.js";import L from"./plugin.js";class LdapFlat extends L{constructor(t,e){if(super(t),this.ldap=t.ldap,this.base=e.base,this.mainAttribute=e.mainAttribute,this.objectClass=e.objectClass,this.defaultAttributes=e.defaultAttributes||{},this.singularName=e.singularName,this.pluralName=e.pluralName,this.hookPrefix=e.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(this.ldap.declareObjectClasses(this.base,this.objectClass),e.schemaPath)try{this.schema=_(e.schemaPath,{config:this.config}),this.logger.info(`${this.singularName} schema loaded from ${e.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${e.schemaPath}: ${t}`)}}clientForbiddenAttributes(){const t=new Set;if(!this.schema)return t;for(const[e,i]of Object.entries(this.schema.attributes))(e!==this.mainAttribute||i.generatedFrom)&&(i.generated||i.readOnly)&&t.add(e.toLowerCase());return t}hiddenAttributes(){return y([this.schema])}entryAttributes(){return["*",...Object.keys(this.schema?.attributes??{})]}project(t){const e=this.hiddenAttributes();if(0===e.size)return t;const i={};for(const[s,r]of Object.entries(t))e.has(s.split(";")[0].toLowerCase())||(i[s]=r);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,s]of Object.entries(t))e[i]=this.project(s);return e}rejectForbiddenInput(t){const e=this.clientForbiddenAttributes();if(0!==e.size)for(const i of t){if("dn"===i)continue;if(!e.has(i.split(";")[0].toLowerCase()))continue;const t=$(this.schema,i)?.[1];throw new C(t?.readOnly?`Attribute "${i}" is read-only and cannot be set`:`Attribute "${i}" is computed by the server and cannot be set`)}}async generateMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const s=t[i.attribute],r=Array.isArray(s)?s[0]:s;if(null==r||""===r)throw new C(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let a=String(r);if(i.extract){const t=n(i.extract).exec(a);t&&(a=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(a=a.toLowerCase()),i.strip&&(a=a.replace(n(i.strip,"g"),"")),!a)throw new C(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);if(!await this.rdnExists(a))return a;if("suffix"!==i.onCollision)throw new k(`${this.singularName} "${a}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const e=`${a}-${t}`;if(!await this.rdnExists(e))return e}throw new k(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${o(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}invalidValueMessage(t){return A(t,this.schema?.attributes[t])}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,h(async(t,i)=>{if(!e(t,i))return;const s={};let r,a;if(t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(s.filter=g(t.query.match,t.query.attribute)),t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(",")),void 0!==t.query.limit){const e=t.query.limit;if(r="string"==typeof e&&/^\d+$/.test(e)?+e:NaN,!Number.isSafeInteger(r)||r<1)throw new C('Query parameter "limit" must be a positive integer')}try{a=await this.listEntriesLimited({...s,limit:r,req:t})}catch(t){if(void 0!==r||4!==q(t))throw t;throw new P("Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`.")}a.truncated&&i.set("X-Result-Truncated","true"),i.json(this.projectList(a.entries))})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,h(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,h(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,h(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,h(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,h(async(t,e)=>this.apiMove(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,h(async(t,e)=>this.apiRename(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const e=this.resolveDn(s),r=await this.ldap.search({paged:!1,scope:"base",attributes:this.entryAttributes()},e,t);if(0===r.searchEntries.length)throw new D(`${this.singularName} not found`);i.json(this.project(r.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new D(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const r=this.schema?.attributes[this.mainAttribute],a=Boolean(r?.generated&&r.generatedFrom),n=a?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o=a?await this.generateMainAttribute(n):n[this.mainAttribute],h={...n};delete h[this.mainAttribute],delete h.dn,await this.addEntry(o,h,t);const c=await this.searchEntriesByName(o,!1);return s(e,this.project(c[o]))}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await r(i,this.deleteEntry.bind(this),s,t)}async apiModify(e,s){const a=i(e,s);if(!a)return;t(a),this.rejectForbiddenInput(v(a));const n=decodeURIComponent(e.params.id);await r(s,this.modifyEntry.bind(this),n,a,e)}async apiMove(t,s){if(!e(t,s))return;const r=i(t,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(t.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new C("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,t);s.json({success:!0,...o})}async apiRename(t,s){if(!e(t,s))return;const r=i(t,s,"newId");if(!r)return;const n=r.newId;if("string"!=typeof n)throw new C("Missing or invalid newId in request body");if(this.rejectForbiddenInput(Object.keys(r).filter(t=>"newId"!==t)),new RegExp(`^${c(this.mainAttribute)}=`,"i").test(n))throw new C(`newId must be a ${this.mainAttribute} value, not a DN`);try{d(n,this.mainAttribute)}catch(t){throw new C(t instanceof Error?t.message:"Invalid newId")}if(!await this._validateOneChange(this.mainAttribute,n))throw new C(this.invalidValueMessage(this.mainAttribute));const o=decodeURIComponent(t.params.id),h=this.resolveDn(o),f=this.resolveDn(n),m=this.config.group_dummy_user;if(m&&(u(h,m)||u(f,m)))throw new k(`${m} is the configured placeholder member and cannot be renamed`);const b=l(h)===l(f),p=await this.entryExists(h),w=b?p:await this.entryExists(f),g=this.requirePlugin("ldapEnterpriseRules");let y=[];if(b){if(!p)throw new D(`${this.singularName} not found`)}else if(p){if(w)throw new k(`${this.singularName} "${n}" already exists`);y=await(g?.findReferences(h))||[],await this.renameEntry(h,f,t)}else{if(!w)throw new D(`${this.singularName} not found`);y=await(g?.findReferences(h))||[]}const $=g&&y.length>0?await g.rewriteReferences(y,h,f,t):{updated:0,failed:[]},A={success:0===$.failed.length,dn:f,referencesUpdated:$.updated};0!==$.failed.length?a(s,{...A,referencesFailed:$.failed}):s.json(A)}async entryExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(new RegExp(`^${c(this.mainAttribute)}=`,"i").test(t)){const e=f(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new C(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${o(t)},${this.base}`}async addEntry(t,e={},i){let s;new RegExp(`^${this.mainAttribute}=`,"i").test(t)?(s=this.resolveDn(t),t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(d(t,this.mainAttribute),s=this.resolveDn(t)),await this.validateNewEntry(s,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let r,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};E(this.schema,a),this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,a]=await m(this.registeredHooks[`${this.hookPrefix}add`],[s,a]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{r=await this.ldap.add(s,a,i)}catch(t){const e=t instanceof O&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),t instanceof O)throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return b(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e,i){let s=this.resolveDn(t);const r=this.opNumber();if([s,e]=await m(this.registeredHooks[`${this.hookPrefix}modify`],[s,e,r]),e.add&&e.add[this.mainAttribute])throw new k(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new C(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new C(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new C(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(s,e);const a=await this.ldap.modify(s,e,i);return b(this.registeredHooks[`${this.hookPrefix}modifydone`],[s,e,r]),a}async renameEntry(t,e,i){/,/.test(t)||d(t,this.mainAttribute),/,/.test(e)||d(e,this.mainAttribute);let s=this.resolveDn(t),r=this.resolveDn(e);[s,r]=await m(this.registeredHooks[`${this.hookPrefix}rename`],[s,r]);const a=await(i?this.ldap.forRequest(i).rename(s,r):this.ldap.rename(s,r));return b(this.registeredHooks[`${this.hookPrefix}renamedone`],[s,r]),a}async deleteEntry(t,e){let i=this.resolveDn(t);i=await m(this.registeredHooks[`${this.hookPrefix}delete`],i);const s=await this.ldap.delete(i,e);return b(this.registeredHooks[`${this.hookPrefix}deletedone`],i),s}async moveEntry(t,e,i){const s=this.resolveDn(t),r=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[r]||!this.schema?.attributes[a])throw new C(`Schema for ${this.singularName} does not support move operation (missing ${r} or ${a})`);let n;try{n=await this.ldap.search({paged:!1,scope:"base",attributes:[r]},s,i)}catch(t){if(32===q(t))throw new D(`Entry ${s} not found`);throw t}if(!n.searchEntries||0===n.searchEntries.length)throw new D(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await m(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h,i),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const s=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!s.searchEntries||0===s.searchEntries.length)throw new D(`Organization ${t} not found`);const r=s.searchEntries[0];if(r[i]){const t=r[i];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){if(e instanceof D)throw e;const i=q(e);if(32===i)throw new D(`Organization ${t} not found`);if(34===i)throw new C(`${t} is not a valid DN`);throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries(t){return(await this.listEntriesLimited(t)).entries}async listEntriesLimited({filter:t,attributes:e,req:i,limit:s}){const r={paged:void 0===s||{pageSize:Math.min(s+2,100)},filter:t||"(objectClass=*)"};if(e&&e.length>0){const t=this.mainAttribute.toLowerCase();r.attributes=e.some(e=>e.toLowerCase()===t)?e:[...e,this.mainAttribute]}const a=function(t,e){const i={},s=new Set;return{entries:i,add(r){for(const a of r){const r=a[t],n=Array.isArray(r)?r[0]:r;if(null==n||""===n)continue;const o=String(n);if(!s.has(o)){if(void 0!==e&&s.size>=e)return!1;s.add(o)}i[o]=a}return!0}}}(this.mainAttribute,s);try{const t=await this.ldap.search(r,this.base,i);for await(const e of t)if(!a.add(e.searchEntries))return{entries:a.entries,truncated:!0};return{entries:a.entries,truncated:!1}}catch(t){if(void 0===s||4!==q(t))throw t}try{const t=await this.ldap.search({...r,paged:!1,sizeLimit:Math.min(s+1,2147483647)},this.base,i);a.add(t.searchEntries)}catch(t){if(4!==q(t))throw t}return{entries:a.entries,truncated:!0}}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=p(t),r=e?`(${this.mainAttribute}=*${s}*)`:`(${this.mainAttribute}=${s})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){if(E(this.schema,e),!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new C(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const s=JSON.stringify(e.default);if(s!==JSON.stringify(i))throw new C(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new C(this.invalidValueMessage(t));if(e.required&&!i&&!e.generated)throw new C(`Attribute "${t}" is required`)}const i=x(this.schema,e,this.server.loadedPlugins);if(i)throw new C(`Attribute "${i}" is required`);return!0}async validateChanges(t,e){if(N(this.schema,e),!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new C(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new C(this.invalidValueMessage(t));if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new C(this.invalidValueMessage(t));if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new C(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if(Array.isArray(e)&&0===e.length){if(i.required)throw new C(`Attribute "${t}" is required`);return!0}if("pointer"===i.type){if("string"!=typeof e)throw new C(`Attribute "${t}" must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>w(s,t)))throw new C(`Attribute "${t}" must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.system.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new C(`Attribute "${t}" points to non-existent DN: ${s}`)}catch(e){throw new C(`Attribute "${t}" points to invalid or non-existent DN: ${s}`)}}"pointer"!==i.type&&await j(t,i,e,async t=>(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0);const s=i.test??i.items?.test;if(s){const t="string"==typeof s?n(s):s;return function(t){return(Array.isArray(t)?t:[t]).map(t=>String(t))}(e).every(e=>t.test(e))}return!0}}export{LdapFlat as default};
|
|
1
|
+
import{assertModifyRequest as t}from"../lib/ldapActions.js";import{wantJson as e,jsonBody as i,created as s,tryMethod as r,multiStatus as a}from"../lib/expressFormatedResponses.js";import{getCompiledRegex as n,escapeDnValue as o,asyncHandler as h,escapeRegex as c,validateDnValue as d,isDummyMemberDn as u,normalizeDn as l,getParentDn as f,launchHooksChained as m,launchHooks as b,escapeLdapFilter as p,isDnInBranch as w,substringSearchFilter as g}from"../lib/utils.js";import{neverReturnAttributes as y,schemaAttribute as $,invalidValueMessage as A,modifiedAttributeNames as v,toDirectoryEntry as E,missingRequiredAttribute as x,toDirectoryChanges as j,checkDnValues as N}from"../config/schema.js";import{BadRequestError as C,ConflictError as O,NotFoundError as k,HttpError as D,UnprocessableEntityError as P}from"../lib/errors.js";import{extractLdapCode as q}from"../lib/ldapCodes.js";import{loadSchemaFile as _}from"../lib/schemaFile.js";import L from"./plugin.js";class LdapFlat extends L{constructor(t,e){if(super(t),this.ldap=t.ldap,this.base=e.base,this.mainAttribute=e.mainAttribute,this.objectClass=e.objectClass,this.defaultAttributes=e.defaultAttributes||{},this.singularName=e.singularName,this.pluralName=e.pluralName,this.hookPrefix=e.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(this.ldap.declareObjectClasses(this.base,this.objectClass),e.schemaPath)try{this.schema=_(e.schemaPath,{config:this.config}),this.logger.info(`${this.singularName} schema loaded from ${e.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${e.schemaPath}: ${t}`)}}clientForbiddenAttributes(){const t=new Set;if(!this.schema)return t;for(const[e,i]of Object.entries(this.schema.attributes))(e!==this.mainAttribute||i.generatedFrom)&&(i.generated||i.readOnly)&&t.add(e.toLowerCase());return t}hiddenAttributes(){return y([this.schema])}entryAttributes(){return["*",...Object.keys(this.schema?.attributes??{})]}project(t){const e=this.hiddenAttributes();if(0===e.size)return t;const i={};for(const[s,r]of Object.entries(t))e.has(s.split(";")[0].toLowerCase())||(i[s]=r);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,s]of Object.entries(t))e[i]=this.project(s);return e}rejectForbiddenInput(t){const e=this.clientForbiddenAttributes();if(0!==e.size)for(const i of t){if("dn"===i)continue;if(!e.has(i.split(";")[0].toLowerCase()))continue;const t=$(this.schema,i)?.[1];throw new C(t?.readOnly?`Attribute "${i}" is read-only and cannot be set`:`Attribute "${i}" is computed by the server and cannot be set`)}}async generateMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const s=t[i.attribute],r=Array.isArray(s)?s[0]:s;if(null==r||""===r)throw new C(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let a=String(r);if(i.extract){const t=n(i.extract).exec(a);t&&(a=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(a=a.toLowerCase()),i.strip&&(a=a.replace(n(i.strip,"g"),"")),!a)throw new C(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);if(!await this.rdnExists(a))return a;if("suffix"!==i.onCollision)throw new O(`${this.singularName} "${a}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const e=`${a}-${t}`;if(!await this.rdnExists(e))return e}throw new O(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${o(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}invalidValueMessage(t){return A(t,this.schema?.attributes[t])}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,h(async(t,i)=>{if(!e(t,i))return;const s={};let r,a;if(t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(s.filter=g(t.query.match,t.query.attribute)),t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(",")),void 0!==t.query.limit){const e=t.query.limit;if(r="string"==typeof e&&/^\d+$/.test(e)?+e:NaN,!Number.isSafeInteger(r)||r<1)throw new C('Query parameter "limit" must be a positive integer')}try{a=await this.listEntriesLimited({...s,limit:r,req:t})}catch(t){if(void 0!==r||4!==q(t))throw t;throw new P("Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`.")}a.truncated&&i.set("X-Result-Truncated","true"),i.json(this.projectList(a.entries))})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,h(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,h(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,h(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,h(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,h(async(t,e)=>this.apiMove(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,h(async(t,e)=>this.apiRename(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const e=this.resolveDn(s),r=await this.ldap.search({paged:!1,scope:"base",attributes:this.entryAttributes()},e,t);if(0===r.searchEntries.length)throw new k(`${this.singularName} not found`);i.json(this.project(r.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new k(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const r=this.schema?.attributes[this.mainAttribute],a=Boolean(r?.generated&&r.generatedFrom),n=a?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o=a?await this.generateMainAttribute(n):n[this.mainAttribute],h={...n};delete h[this.mainAttribute],delete h.dn,await this.addEntry(o,h,t);const c=await this.searchEntriesByName(o,!1);return s(e,this.project(c[o]))}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await r(i,this.deleteEntry.bind(this),s,t)}async apiModify(e,s){const a=i(e,s);if(!a)return;t(a),this.rejectForbiddenInput(v(a));const n=decodeURIComponent(e.params.id);await r(s,this.modifyEntry.bind(this),n,a,e)}async apiMove(t,s){if(!e(t,s))return;const r=i(t,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(t.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new C("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,t);s.json({success:!0,...o})}async apiRename(t,s){if(!e(t,s))return;const r=i(t,s,"newId");if(!r)return;const n=r.newId;if("string"!=typeof n)throw new C("Missing or invalid newId in request body");if(this.rejectForbiddenInput(Object.keys(r).filter(t=>"newId"!==t)),new RegExp(`^${c(this.mainAttribute)}=`,"i").test(n))throw new C(`newId must be a ${this.mainAttribute} value, not a DN`);try{d(n,this.mainAttribute)}catch(t){throw new C(t instanceof Error?t.message:"Invalid newId")}if(!await this._validateOneChange(this.mainAttribute,n))throw new C(this.invalidValueMessage(this.mainAttribute));const o=decodeURIComponent(t.params.id),h=this.resolveDn(o),f=this.resolveDn(n),m=this.config.group_dummy_user;if(m&&(u(h,m)||u(f,m)))throw new O(`${m} is the configured placeholder member and cannot be renamed`);const b=l(h)===l(f),p=await this.entryExists(h),w=b?p:await this.entryExists(f),g=this.requirePlugin("ldapEnterpriseRules");let y=[];if(b){if(!p)throw new k(`${this.singularName} not found`)}else if(p){if(w)throw new O(`${this.singularName} "${n}" already exists`);y=await(g?.findReferences(h))||[],await this.renameEntry(h,f,t)}else{if(!w)throw new k(`${this.singularName} not found`);y=await(g?.findReferences(h))||[]}const $=g&&y.length>0?await g.rewriteReferences(y,h,f,t):{updated:0,failed:[]},A={success:0===$.failed.length,dn:f,referencesUpdated:$.updated};0!==$.failed.length?a(s,{...A,referencesFailed:$.failed}):s.json(A)}async entryExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(new RegExp(`^${c(this.mainAttribute)}=`,"i").test(t)){const e=f(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new C(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${o(t)},${this.base}`}async addEntry(t,e={},i){let s;new RegExp(`^${this.mainAttribute}=`,"i").test(t)?(s=this.resolveDn(t),t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(d(t,this.mainAttribute),s=this.resolveDn(t)),await this.validateNewEntry(s,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let r,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};for(const[t,e]of Object.entries(this.schema?.attributes??{}))e.fixed&&void 0!==e.default&&"objectClass"!==t&&(a[t]=e.default);E(this.schema,a),this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,a]=await m(this.registeredHooks[`${this.hookPrefix}add`],[s,a]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{r=await this.ldap.add(s,a,i)}catch(t){const e=t instanceof D&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),t instanceof D)throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return b(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e,i){let s=this.resolveDn(t);const r=this.opNumber();if([s,e]=await m(this.registeredHooks[`${this.hookPrefix}modify`],[s,e,r]),e.add&&e.add[this.mainAttribute])throw new O(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new C(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new C(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new C(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(s,e);const a=await this.ldap.modify(s,e,i);return b(this.registeredHooks[`${this.hookPrefix}modifydone`],[s,e,r]),a}async renameEntry(t,e,i){/,/.test(t)||d(t,this.mainAttribute),/,/.test(e)||d(e,this.mainAttribute);let s=this.resolveDn(t),r=this.resolveDn(e);[s,r]=await m(this.registeredHooks[`${this.hookPrefix}rename`],[s,r]);const a=await(i?this.ldap.forRequest(i).rename(s,r):this.ldap.rename(s,r));return b(this.registeredHooks[`${this.hookPrefix}renamedone`],[s,r]),a}async deleteEntry(t,e){let i=this.resolveDn(t);i=await m(this.registeredHooks[`${this.hookPrefix}delete`],i);const s=await this.ldap.delete(i,e);return b(this.registeredHooks[`${this.hookPrefix}deletedone`],i),s}async moveEntry(t,e,i){const s=this.resolveDn(t),r=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[r]||!this.schema?.attributes[a])throw new C(`Schema for ${this.singularName} does not support move operation (missing ${r} or ${a})`);let n;try{n=await this.ldap.search({paged:!1,scope:"base",attributes:[r]},s,i)}catch(t){if(32===q(t))throw new k(`Entry ${s} not found`);throw t}if(!n.searchEntries||0===n.searchEntries.length)throw new k(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await m(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h,i),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const s=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!s.searchEntries||0===s.searchEntries.length)throw new k(`Organization ${t} not found`);const r=s.searchEntries[0];if(r[i]){const t=r[i];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){if(e instanceof k)throw e;const i=q(e);if(32===i)throw new k(`Organization ${t} not found`);if(34===i)throw new C(`${t} is not a valid DN`);throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries(t){return(await this.listEntriesLimited(t)).entries}async listEntriesLimited({filter:t,attributes:e,req:i,limit:s}){const r={paged:void 0===s||{pageSize:Math.min(s+2,100)},filter:t||"(objectClass=*)"};if(e&&e.length>0){const t=this.mainAttribute.toLowerCase();r.attributes=e.some(e=>e.toLowerCase()===t)?e:[...e,this.mainAttribute]}const a=function(t,e){const i={},s=new Set;return{entries:i,add(r){for(const a of r){const r=a[t],n=Array.isArray(r)?r[0]:r;if(null==n||""===n)continue;const o=String(n);if(!s.has(o)){if(void 0!==e&&s.size>=e)return!1;s.add(o)}i[o]=a}return!0}}}(this.mainAttribute,s);try{const t=await this.ldap.search(r,this.base,i);for await(const e of t)if(!a.add(e.searchEntries))return{entries:a.entries,truncated:!0};return{entries:a.entries,truncated:!1}}catch(t){if(void 0===s||4!==q(t))throw t}try{const t=await this.ldap.search({...r,paged:!1,sizeLimit:Math.min(s+1,2147483647)},this.base,i);a.add(t.searchEntries)}catch(t){if(4!==q(t))throw t}return{entries:a.entries,truncated:!0}}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=p(t),r=e?`(${this.mainAttribute}=*${s}*)`:`(${this.mainAttribute}=${s})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){if(E(this.schema,e),!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new C(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const s=JSON.stringify(e.default);if(s!==JSON.stringify(i))throw new C(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new C(this.invalidValueMessage(t));if(e.required&&!i&&!e.generated)throw new C(`Attribute "${t}" is required`)}const i=x(this.schema,e,this.server.loadedPlugins);if(i)throw new C(`Attribute "${i}" is required`);return!0}async validateChanges(t,e){if(j(this.schema,e),!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new C(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new C(this.invalidValueMessage(t));if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new C(this.invalidValueMessage(t));if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new C(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if(Array.isArray(e)&&0===e.length){if(i.required)throw new C(`Attribute "${t}" is required`);return!0}if("pointer"===i.type){if("string"!=typeof e)throw new C(`Attribute "${t}" must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>w(s,t)))throw new C(`Attribute "${t}" must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.system.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new C(`Attribute "${t}" points to non-existent DN: ${s}`)}catch(e){throw new C(`Attribute "${t}" points to invalid or non-existent DN: ${s}`)}}"pointer"!==i.type&&await N(t,i,e,async t=>(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0);const s=i.test??i.items?.test;if(s){const t="string"==typeof s?n(s):s;return function(t){return(Array.isArray(t)?t:[t]).map(t=>String(t))}(e).every(e=>t.test(e))}return!0}}export{LdapFlat as default};
|
package/dist/lib/ldapActions.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as s,Control as n}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{normalizeDn as o,isDnInBranch as c,launchHooksChained as l,launchHooks as h,escapeDnValue as d}from"./utils.js";import{changeContext as p}from"./changeContext.js";import{runOperation as u,outsideOperation as f}from"./operation.js";import{BadRequestError as g,NotFoundError as y,ConflictError as m}from"./errors.js";import{extractLdapCode as w,isSchemaRefusal as b}from"./ldapCodes.js";import{SchemaIndex as C,parseSchema as A}from"./ldapSchema.js";const v={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}},isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function assertModifyRequest(e){const t=Object.keys(e).filter(e=>!["add","replace","delete"].includes(e));if(t.length>0)throw new g(`A modification carries add, replace or delete, not ${t.join(", ")}`);const{add:i,replace:s,delete:n}=e;for(const[e,t]of[["add",i],["replace",s]])if(void 0!==t&&!isPlainObject(t))throw new g(`"${e}" must map attributes to values`);if(!(void 0===n||isPlainObject(n)||Array.isArray(n)&&n.every(e=>"string"==typeof e)))throw new g('"delete" must list attribute names or map attributes to values');if(0===[i,s,n].reduce((e,t)=>e+(t?Object.keys(t).length:0),0))throw new g("A modification names at least one attribute in add, replace or delete")}function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),s=w(t);return void 0===s?i:withCode(i,s)}function withCode(e,t){return e.code=t,e}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]}),$=["*","+","1.1"],dropSelectors=e=>{for(const t of e.searchEntries)for(const e of $){const i=t[e];Array.isArray(i)&&0===i.length&&delete t[e]}return e};function ldapRefusal(e,t,i){return withCode(new g(`${e}: ${String(i)}`),t)}function assertHasValue(e,t){if(Array.isArray(t)&&0===t.length)throw new g(`Attribute "${e}" has no value`)}function ldapWriteError(e,t,i){const s=w(i);return b(s)?ldapRefusal(`The directory refused this write to ${t}`,s,i):ldapError(e,i)}function ldapRenameError(e,t,i,s){const n=w(s);return 68===n?withCode(new m(`Entry ${i} already exists`),n):b(n)?ldapRefusal(`The directory refused to rename ${t} to ${i}`,n,s):ldapError(e,s)}class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.declaredClasses=new Map,this.schemaUnavailable=!1,this.classWarnings=new a({max:1e4}),this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=r(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=this.config.ldap_cache_ttl,n="string"==typeof s?parseInt(s,10)||0:s??0,o=1e3*Math.max(n,0);if(this.cacheEnabled=o>0,this.searchCache=new a({max:i,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new a({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_base,this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const e=[];for(let i=0;i<this.ldapUrls.length;i++){const s=(this.currentUrlIndex+i)%this.ldapUrls.length,n=this.ldapUrls[s];try{this.logger.debug(`Attempting connection to ${n}`);const e={...this.options,url:n,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await i.bind(this.dn,this.pwd),s!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${n}`),this.currentUrlIndex=s),i}catch(t){this.logger.warn(`Failed to connect to ${n}: ${String(t)}`),e.push(t)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${e.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let i=this.availableConnections.length-1;i>=0;i--){const s=this.availableConnections[i];if(e-s.createdAt>this.connectionTtl){t.push(s),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(s);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),s=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",n,i,s,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}invalidateCache(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),i=`,${t}`,s=[];for(const e of this.searchCache.keys()){const n=foldDn(e.split("\0")[0]);(n===t||n.endsWith(i))&&s.push(e)}for(const e of s)this.searchCache.delete(e)}get system(){return this}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i,s)=>this.modify(t,i,e,s),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}declareObjectClasses(e,t,i=!1){const s=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);if(!e||0===s.length)return;const n=JSON.stringify([o(e),s.map(e=>e.toLowerCase()).sort(),i]);this.declaredClasses.set(n,{base:e,classes:s,includeBase:i})}declaredFor(e){let t=0,i=[];for(const s of this.declaredClasses.values()){if(!c(e,s.base))continue;const n=o(s.base);if(!s.includeBase&&o(e)===n)continue;const a=n.split(",").length;a>t&&(t=a,i=[]),a===t&&i.push(...s.classes)}const s=new Set;return i.filter(e=>{const t=e.toLowerCase();return!s.has(t)&&(s.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t,i=[]){if(!1===this.config.ldap_flat_auto_repair)return[];const s=[...this.declaredFor(e)];for(const e of i)s.some(t=>t.toLowerCase()===e.toLowerCase())||s.push(e);if(0===s.length)return[];const n=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let a;try{a=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>a.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:i}=a.resolveAttributes(e);return new Set([...t,...i].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),r=[...valued(t.add),...valued(t.replace)],o=s.filter(e=>{const t=a.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const i=allowedBy([e]);return r.some(e=>i.has(attributeKey(e)))});if(0===o.length)return[];let c;try{c=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...a.resolveAttributes(o).must]},e)).searchEntries[0]}catch{return[]}if(!c)return[];const l=c.objectClass??c.objectclass,h=(Array.isArray(l)?l:l?[l]:[]).map(String),d=allowedBy(h);let p=r.filter(e=>a.getAttributeType(e)&&!d.has(attributeKey(e)));if(0===p.length)return"allowed";const u=new Set(n.map(attributeKey)),f=new Set([...valued(c).filter(e=>!u.has(attributeKey(e))),...r].map(attributeKey)),{auxiliary:g,structural:y}=a.missingClasses(s,h),m=[];for(const t of g){const i=t.names[0]??t.oid,s=allowedBy([i]),n=p.filter(e=>s.has(attributeKey(e)));if(0===n.length)continue;const r=a.resolveAttributes([i]).must.filter(e=>!f.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${i} for ${n.join(", ")}, and cannot be given it without ${r.join(", ")}`):(m.push(i),p=p.filter(e=>!n.includes(e)))}const w=y.filter(e=>{const t=allowedBy([e]);return p.some(e=>t.has(attributeKey(e)))});return w.length>0&&this.warnOnce(`${e} cannot hold ${p.join(", ")}: its schema declares ${w.join(", ")}, a structural class an existing entry cannot be given`),m.length>0&&this.logger.info(`${e}: adding ${m.join(", ")}, which the change needs and its schema declares`),m}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,i=Array.isArray(t)?t[0]:t;if(i)return String(i)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new y(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new y(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},i=new C(A({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:i,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${i.schema.objectClasses.length} object classes, ${i.schema.attributeTypes.length} attribute types`),i})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}filterForCaller(e,t,i){if(!this.parent.hooks.ldapsearchfilter)return e;const s=this.parent.hooks.ldapsearchfilter,one=async e=>{const[n]=await l(s,[e,t,i]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,i){let s={...v,...e};s=await l(this.parent.hooks.ldapsearchopts,s),[t,s]=await l(this.parent.hooks.ldapsearchrequest,[t,s,i]);const n=this.cacheEnabled&&!s.paged&&"base"===s.scope,a=n?this.getCacheKey(t,s):"",r=this.cacheGeneration;if(n){const e=this.searchCache.get(a);if(e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),i,s)}const o=await this.acquireConnection();try{let e=s.paged?async function*(e){for await(const t of e)yield dropSelectors(t)}(o.client.searchPaginated(t,s)):o.client.search(t,s).then(dropSelectors);if(e=await l(this.parent.hooks.ldapsearchresult,e),n){const t=e;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(t,i,s)}return s.paged?this.filterForCaller(this.wrapPaginatedSearch(e,o),i,s):this.filterForCaller(e,i,s)}finally{s.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,t,s){e=this.setDn(e),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const n={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?n[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?n[e]=i.map(e=>e.toString()):n[e]=i;[e,t]=await l(this.parent.hooks.ldapaddrequest,[e,n,s]);const a=[];for(const[e,s]of Object.entries(t)){assertHasValue(e,s);const t=Array.isArray(s)?s:[s];a.push(new i({type:e,values:t}))}const r=await this.acquireConnection();try{this.invalidateCache(e),await r.client.add(e,a),this.invalidateCache(e),h(this.parent.hooks.ldapadddone,[e,t],p(s))}catch(t){throw function(e,t){const i=w(t);return 68===i?withCode(new m(`Entry ${e} already exists`),i):20===i?ldapRefusal(`The directory refused this write to ${e}`,i,t):ldapWriteError("LDAP add error",e,t)}(e,t)}finally{this.releaseConnection(r)}return await h(this.parent.hooks.ldapaddafter,[e,t],p(s)),!0}async modify(e,t,i,s={}){const n=this.opNumber(),a=[];try{return await u(n,()=>this.applyModify(e,t,i,s,n,a))}finally{this.endOperation(a,this.parent.hooks.ldapmodifyend,n)}}async applyModify(e,t,a,r,o,c){e=this.setDn(e);const d=[];if([e,t]=await l(this.parent.hooks.ldapmodifyrequest,[e,t,o,a]),t.add)for(const[e,n]of Object.entries(t.add))assertHasValue(e,n),d.push(new s({operation:"add",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.replace)for(const[e,n]of Object.entries(t.replace))d.push(new s({operation:"replace",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&d.push(new s({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,n]of Object.entries(t.delete)){const t=new s({operation:"delete",modification:new i(n?{type:e,values:Array.isArray(n)?n:[n]}:{type:e})});d.push(t)}if(0===d.length){return Object.keys(t.add||{}).length>0||Object.keys(t.replace||{}).length>0||(Array.isArray(t.delete)?t.delete.length>0:Object.keys(t.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),c.push(h(this.parent.hooks.ldapmodifydone,[e,{},o],r.context??p(a))),!1}{const l=r.permissive?[new n("1.2.840.113556.1.4.1413",{critical:!1})]:void 0,send=async()=>{const t=await this.acquireConnection();try{await t.client.modify(e,d,l)}finally{this.releaseConnection(t)}};try{this.invalidateCache(e);try{await send()}catch(n){if(65!==w(n))throw n;const a=await this.classRepair(e,t,r.declaredClasses);if("allowed"!==a&&0===a.length)throw n;const o="allowed"===a?void 0:new s({operation:"add",modification:new i({type:"objectClass",values:a})});o&&d.push(o),this.invalidateCache(e);try{await send()}catch(e){if(!o||20!==w(e))throw e;d.splice(d.indexOf(o),1),await send()}}return this.invalidateCache(e),c.push(h(this.parent.hooks.ldapmodifydone,[e,t,o],r.context??p(a))),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(d)}`),function(e,t){const i=w(t);return 32===i?withCode(new y(`Entry ${e} not found`),i):16===i?withCode(new y(`Entry ${e} does not hold the value to delete`),i):65!==i&&20!==i?ldapWriteError("LDAP modify error",e,t):withCode(new m(`Entry ${e} cannot hold this change: ${String(t)}`),i)}(e,t)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=this.opNumber(),n=[];try{return await u(s,()=>this.applyRename(e,t,i,n))}finally{this.endOperation(n,this.parent.hooks.ldaprenameend,s)}}async applyRename(e,t,i,s){[e,t]=await l(this.parent.hooks.ldaprenamerequest,[e,t,i]);const n=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await n.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),s.push(h(this.parent.hooks.ldaprenamedone,[e,t],p(i))),!0}catch(i){throw ldapRenameError("LDAP rename error",e,t,i)}finally{this.releaseConnection(n)}}async move(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),f(()=>h(this.parent.hooks.ldaprenamedone,[e,t],p(i))),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(i){throw ldapRenameError("LDAP move error",e,t,i)}finally{this.releaseConnection(s)}}async delete(e,t,i={}){const s=(Array.isArray(e)?e:[e]).map(e=>this.setDn(e)),n=this.opNumber(),a=[];try{return await u(n,()=>this.applyDelete(s,t,i,a))}finally{this.endOperation(a,this.parent?.hooks.ldapdeleteend,n)}}async applyDelete(e,t,i,s){[e]=await l(this.parent?.hooks.ldapdeleterequest,[e,t]);const n=await this.acquireConnection();try{for(const a of e){try{this.invalidateCache(a),await n.client.del(a),this.invalidateCache(a)}catch(e){const t=w(e);throw 32===t?withCode(new y(`Entry ${a} not found`),t):ldapError("LDAP delete error",e)}s.push(h(this.parent.hooks.ldapdeletedone,a,i.context??p(t)))}return!0}finally{this.releaseConnection(n)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${d(e)},${this.base}`,e}endOperation(e,t,...i){t?.length&&Promise.all(e).then(()=>h(t,...i))}opNumber(){return this.parent.operationSequence++}}export{assertModifyRequest,ldapActions as default};
|
|
1
|
+
import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as s,Control as n}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{normalizeDn as o,isDnInBranch as c,launchHooksChained as l,launchHooks as h,escapeDnValue as d,getParentDn as p}from"./utils.js";import{changeContext as u}from"./changeContext.js";import{runOperation as f,outsideOperation as g}from"./operation.js";import{BadRequestError as y,NotFoundError as w,ConflictError as m}from"./errors.js";import{extractLdapCode as b,isSchemaRefusal as C}from"./ldapCodes.js";import{SchemaIndex as A,parseSchema as v}from"./ldapSchema.js";const $={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}},isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function assertModifyRequest(e){const t=Object.keys(e).filter(e=>!["add","replace","delete"].includes(e));if(t.length>0)throw new y(`A modification carries add, replace or delete, not ${t.join(", ")}`);const{add:i,replace:s,delete:n}=e;for(const[e,t]of[["add",i],["replace",s]])if(void 0!==t&&!isPlainObject(t))throw new y(`"${e}" must map attributes to values`);if(!(void 0===n||isPlainObject(n)||Array.isArray(n)&&n.every(e=>"string"==typeof e)))throw new y('"delete" must list attribute names or map attributes to values');if(0===[i,s,n].reduce((e,t)=>e+(t?Object.keys(t).length:0),0))throw new y("A modification names at least one attribute in add, replace or delete")}function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),s=b(t);return void 0===s?i:withCode(i,s)}function withCode(e,t){return e.code=t,e}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]}),j=["*","+","1.1"],dropSelectors=e=>{for(const t of e.searchEntries)for(const e of j){const i=t[e];Array.isArray(i)&&0===i.length&&delete t[e]}return e};function ldapRefusal(e,t,i){return withCode(new y(`${e}: ${String(i)}`),t)}function assertHasValue(e,t){if(Array.isArray(t)&&0===t.length)throw new y(`Attribute "${e}" has no value`)}function ldapWriteError(e,t,i){const s=b(i);return C(s)?ldapRefusal(`The directory refused this write to ${t}`,s,i):ldapError(e,i)}function ldapRenameError(e,t,i,s){const n=b(s);return 68===n?withCode(new m(`Entry ${i} already exists`),n):C(n)?ldapRefusal(`The directory refused to rename ${t} to ${i}`,n,s):ldapError(e,s)}class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.declaredClasses=new Map,this.schemaUnavailable=!1,this.classWarnings=new a({max:1e4}),this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=r(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=this.config.ldap_cache_ttl,n="string"==typeof s?parseInt(s,10)||0:s??0,o=1e3*Math.max(n,0);if(this.cacheEnabled=o>0,this.searchCache=new a({max:i,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new a({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_base,this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const e=[];for(let i=0;i<this.ldapUrls.length;i++){const s=(this.currentUrlIndex+i)%this.ldapUrls.length,n=this.ldapUrls[s];try{this.logger.debug(`Attempting connection to ${n}`);const e={...this.options,url:n,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await i.bind(this.dn,this.pwd),s!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${n}`),this.currentUrlIndex=s),i}catch(t){this.logger.warn(`Failed to connect to ${n}: ${String(t)}`),e.push(t)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${e.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let i=this.availableConnections.length-1;i>=0;i--){const s=this.availableConnections[i];if(e-s.createdAt>this.connectionTtl){t.push(s),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(s);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),s=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",n,i,s,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}invalidateCache(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),i=`,${t}`,s=[];for(const e of this.searchCache.keys()){const n=foldDn(e.split("\0")[0]);(n===t||n.endsWith(i))&&s.push(e)}for(const e of s)this.searchCache.delete(e)}get system(){return this}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i,s)=>this.modify(t,i,e,s),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}declareObjectClasses(e,t,i=!1){const s=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);if(!e||0===s.length)return;const n=JSON.stringify([o(e),s.map(e=>e.toLowerCase()).sort(),i]);this.declaredClasses.set(n,{base:e,classes:s,includeBase:i})}declaredFor(e){let t=0,i=[];for(const s of this.declaredClasses.values()){if(!c(e,s.base))continue;const n=o(s.base);if(!s.includeBase&&o(e)===n)continue;const a=n.split(",").length;a>t&&(t=a,i=[]),a===t&&i.push(...s.classes)}const s=new Set;return i.filter(e=>{const t=e.toLowerCase();return!s.has(t)&&(s.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t,i=[]){if(!1===this.config.ldap_flat_auto_repair)return[];const s=[...this.declaredFor(e)];for(const e of i)s.some(t=>t.toLowerCase()===e.toLowerCase())||s.push(e);if(0===s.length)return[];const n=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let a;try{a=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>a.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:i}=a.resolveAttributes(e);return new Set([...t,...i].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),r=[...valued(t.add),...valued(t.replace)],o=s.filter(e=>{const t=a.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const i=allowedBy([e]);return r.some(e=>i.has(attributeKey(e)))});if(0===o.length)return[];let c;try{c=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...a.resolveAttributes(o).must]},e)).searchEntries[0]}catch{return[]}if(!c)return[];const l=c.objectClass??c.objectclass,h=(Array.isArray(l)?l:l?[l]:[]).map(String),d=allowedBy(h);let p=r.filter(e=>a.getAttributeType(e)&&!d.has(attributeKey(e)));if(0===p.length)return"allowed";const u=new Set(n.map(attributeKey)),f=new Set([...valued(c).filter(e=>!u.has(attributeKey(e))),...r].map(attributeKey)),{auxiliary:g,structural:y}=a.missingClasses(s,h),w=[];for(const t of g){const i=t.names[0]??t.oid,s=allowedBy([i]),n=p.filter(e=>s.has(attributeKey(e)));if(0===n.length)continue;const r=a.resolveAttributes([i]).must.filter(e=>!f.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${i} for ${n.join(", ")}, and cannot be given it without ${r.join(", ")}`):(w.push(i),p=p.filter(e=>!n.includes(e)))}const m=y.filter(e=>{const t=allowedBy([e]);return p.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${p.join(", ")}: its schema declares ${m.join(", ")}, a structural class an existing entry cannot be given`),w.length>0&&this.logger.info(`${e}: adding ${w.join(", ")}, which the change needs and its schema declares`),w}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,i=Array.isArray(t)?t[0]:t;if(i)return String(i)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new w(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new w(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},i=new A(v({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:i,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${i.schema.objectClasses.length} object classes, ${i.schema.attributeTypes.length} attribute types`),i})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}filterForCaller(e,t,i){if(!this.parent.hooks.ldapsearchfilter)return e;const s=this.parent.hooks.ldapsearchfilter,one=async e=>{const[n]=await l(s,[e,t,i]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,i){let s={...$,...e};s=await l(this.parent.hooks.ldapsearchopts,s),[t,s]=await l(this.parent.hooks.ldapsearchrequest,[t,s,i]);const n=this.cacheEnabled&&!s.paged&&"base"===s.scope,a=n?this.getCacheKey(t,s):"",r=this.cacheGeneration;if(n){const e=this.searchCache.get(a);if(e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),i,s)}const o=await this.acquireConnection();try{let e=s.paged?async function*(e){for await(const t of e)yield dropSelectors(t)}(o.client.searchPaginated(t,s)):o.client.search(t,s).then(dropSelectors);if(e=await l(this.parent.hooks.ldapsearchresult,e),n){const t=e;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(t,i,s)}return s.paged?this.filterForCaller(this.wrapPaginatedSearch(e,o),i,s):this.filterForCaller(e,i,s)}finally{s.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,t,s){e=this.setDn(e),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const n={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?n[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?n[e]=i.map(e=>e.toString()):n[e]=i;[e,t]=await l(this.parent.hooks.ldapaddrequest,[e,n,s]);const a=[];for(const[e,s]of Object.entries(t)){assertHasValue(e,s);const t=Array.isArray(s)?s:[s];a.push(new i({type:e,values:t}))}const r=await this.acquireConnection();try{this.invalidateCache(e),await r.client.add(e,a),this.invalidateCache(e),h(this.parent.hooks.ldapadddone,[e,t],u(s))}catch(t){throw function(e,t){const i=b(t);return 68===i?withCode(new m(`Entry ${e} already exists`),i):32===i?withCode(new w(`${p(e)} does not exist`),i):20===i?ldapRefusal(`The directory refused this write to ${e}`,i,t):ldapWriteError("LDAP add error",e,t)}(e,t)}finally{this.releaseConnection(r)}return await h(this.parent.hooks.ldapaddafter,[e,t],u(s)),!0}async modify(e,t,i,s={}){const n=this.opNumber(),a=[];try{return await f(n,()=>this.applyModify(e,t,i,s,n,a))}finally{this.endOperation(a,this.parent.hooks.ldapmodifyend,n)}}async applyModify(e,t,a,r,o,c){e=this.setDn(e);const d=[];if([e,t]=await l(this.parent.hooks.ldapmodifyrequest,[e,t,o,a]),t.add)for(const[e,n]of Object.entries(t.add))assertHasValue(e,n),d.push(new s({operation:"add",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.replace)for(const[e,n]of Object.entries(t.replace))d.push(new s({operation:"replace",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&d.push(new s({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,n]of Object.entries(t.delete)){const t=new s({operation:"delete",modification:new i(n?{type:e,values:Array.isArray(n)?n:[n]}:{type:e})});d.push(t)}if(0===d.length){return Object.keys(t.add||{}).length>0||Object.keys(t.replace||{}).length>0||(Array.isArray(t.delete)?t.delete.length>0:Object.keys(t.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),c.push(h(this.parent.hooks.ldapmodifydone,[e,{},o],r.context??u(a))),!1}{const l=r.permissive?[new n("1.2.840.113556.1.4.1413",{critical:!1})]:void 0,send=async()=>{const t=await this.acquireConnection();try{await t.client.modify(e,d,l)}finally{this.releaseConnection(t)}};try{this.invalidateCache(e);try{await send()}catch(n){if(65!==b(n))throw n;const a=await this.classRepair(e,t,r.declaredClasses);if("allowed"!==a&&0===a.length)throw n;const o="allowed"===a?void 0:new s({operation:"add",modification:new i({type:"objectClass",values:a})});o&&d.push(o),this.invalidateCache(e);try{await send()}catch(e){if(!o||20!==b(e))throw e;d.splice(d.indexOf(o),1),await send()}}return this.invalidateCache(e),c.push(h(this.parent.hooks.ldapmodifydone,[e,t,o],r.context??u(a))),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(d)}`),function(e,t){const i=b(t);return 32===i?withCode(new w(`Entry ${e} not found`),i):16===i?withCode(new w(`Entry ${e} does not hold the value to delete`),i):65!==i&&20!==i?ldapWriteError("LDAP modify error",e,t):withCode(new m(`Entry ${e} cannot hold this change: ${String(t)}`),i)}(e,t)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=this.opNumber(),n=[];try{return await f(s,()=>this.applyRename(e,t,i,n))}finally{this.endOperation(n,this.parent.hooks.ldaprenameend,s)}}async applyRename(e,t,i,s){[e,t]=await l(this.parent.hooks.ldaprenamerequest,[e,t,i]);const n=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await n.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),s.push(h(this.parent.hooks.ldaprenamedone,[e,t],u(i))),!0}catch(i){throw ldapRenameError("LDAP rename error",e,t,i)}finally{this.releaseConnection(n)}}async move(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),g(()=>h(this.parent.hooks.ldaprenamedone,[e,t],u(i))),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(i){throw ldapRenameError("LDAP move error",e,t,i)}finally{this.releaseConnection(s)}}async delete(e,t,i={}){const s=(Array.isArray(e)?e:[e]).map(e=>this.setDn(e)),n=this.opNumber(),a=[];try{return await f(n,()=>this.applyDelete(s,t,i,a))}finally{this.endOperation(a,this.parent?.hooks.ldapdeleteend,n)}}async applyDelete(e,t,i,s){[e]=await l(this.parent?.hooks.ldapdeleterequest,[e,t]);const n=await this.acquireConnection();try{for(const a of e){try{this.invalidateCache(a),await n.client.del(a),this.invalidateCache(a)}catch(e){const t=b(e);throw 32===t?withCode(new w(`Entry ${a} not found`),t):ldapError("LDAP delete error",e)}s.push(h(this.parent.hooks.ldapdeletedone,a,i.context??u(t)))}return!0}finally{this.releaseConnection(n)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${d(e)},${this.base}`,e}endOperation(e,t,...i){t?.length&&Promise.all(e).then(()=>h(t,...i))}opNumber(){return this.parent.operationSequence++}}export{assertModifyRequest,ldapActions as default};
|
|
@@ -41,6 +41,9 @@ This creates three nomenclature entries:
|
|
|
41
41
|
- `cn=mailingList,ou=twakeMailboxType,ou=nomenclature,{ldap_base}`
|
|
42
42
|
- `cn=teamMailbox,ou=twakeMailboxType,ou=nomenclature,{ldap_base}`
|
|
43
43
|
|
|
44
|
+
With [team mailboxes in a branch of their own](#team-mailboxes-in-a-branch-of-their-own),
|
|
45
|
+
leave the last one out.
|
|
46
|
+
|
|
44
47
|
### LDAP-Rest Configuration
|
|
45
48
|
|
|
46
49
|
Add to your ldap-rest configuration:
|
|
@@ -66,6 +69,47 @@ DM_JAMES_TEAM_MAILBOX_ANY_DOMAIN=true
|
|
|
66
69
|
|
|
67
70
|
If `james-mailing-list-branches` is empty (default), team mailboxes can be created anywhere.
|
|
68
71
|
|
|
72
|
+
### Team mailboxes in a branch of their own
|
|
73
|
+
|
|
74
|
+
Twake Mail expands every group of its `mailingLists.baseDn` as a list: a team
|
|
75
|
+
mailbox kept there would deliver twice, to the shared mailbox and to each
|
|
76
|
+
member. The Twake schema `twake/teamMailboxes.json` keeps them apart, as a
|
|
77
|
+
flat entity of their own under `ou=teammailboxes,{ldap_base}`, outside the
|
|
78
|
+
group base as long as one is set:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
DM_LDAP_FLAT_SCHEMA=…,/app/node_modules/ldap-rest/static/schemas/twake/teamMailboxes.json
|
|
82
|
+
DM_LDAP_GROUP_BASE="ou=lists,ou=groups,dc=example,dc=com"
|
|
83
|
+
DM_JAMES_MAILING_LIST_BRANCHES="ou=lists,ou=groups,dc=example,dc=com"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`--ldap-group-base` has to be set, to the lists branch: left empty, the group
|
|
87
|
+
base is `--ldap-base`, which holds `ou=teammailboxes`, and the groups routes
|
|
88
|
+
list and edit the team mailboxes with the groups schema.
|
|
89
|
+
|
|
90
|
+
- Create the branch first: nothing creates it, and a team mailbox added
|
|
91
|
+
under a missing one answers `404`.
|
|
92
|
+
|
|
93
|
+
```ldif
|
|
94
|
+
dn: ou=teammailboxes,dc=example,dc=com
|
|
95
|
+
objectClass: organizationalUnit
|
|
96
|
+
ou: teammailboxes
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- Its entries are `twakeStaticGroup`s whose `twakeMailboxType` is fixed to
|
|
100
|
+
`teamMailbox`, as a value. Leave `cn=teamMailbox` out of the nomenclature
|
|
101
|
+
(see [Nomenclature Setup](#nomenclature-setup)): the groups form then offers
|
|
102
|
+
only groups and mailing lists, and a team mailbox has one place to be
|
|
103
|
+
created in.
|
|
104
|
+
- A team mailbox needs an address TMail accepts (letters, digits, `#`, `-`
|
|
105
|
+
and `_` before the `@`) and a member.
|
|
106
|
+
- `--james-mailing-list-branches` on the lists branch refuses a team mailbox
|
|
107
|
+
created there by another way.
|
|
108
|
+
|
|
109
|
+
To keep them elsewhere, copy the schema and change its `base`, outside
|
|
110
|
+
`--ldap-group-base`: the groups routes list and edit every group under it,
|
|
111
|
+
with the groups schema, where the mailbox type can be changed.
|
|
112
|
+
|
|
69
113
|
## Mailing lists
|
|
70
114
|
|
|
71
115
|
`twake/james` creates nothing in James for a mailing list: a group with a
|
|
@@ -83,6 +127,11 @@ members-only list.
|
|
|
83
127
|
|
|
84
128
|
### Creating a Team Mailbox
|
|
85
129
|
|
|
130
|
+
Where Twake Mail reads mailing lists from the groups branch, create team
|
|
131
|
+
mailboxes [in a branch of their own](#team-mailboxes-in-a-branch-of-their-own)
|
|
132
|
+
instead: the example below, a team mailbox among the groups, only suits a
|
|
133
|
+
deployment whose groups branch holds no mailing list.
|
|
134
|
+
|
|
86
135
|
```javascript
|
|
87
136
|
await dm.ldap.add('cn=sales-team,ou=groups,dc=example,dc=com', {
|
|
88
137
|
objectClass: ['top', 'groupOfNames', 'twakeStaticGroup'],
|
|
@@ -243,6 +292,11 @@ client offers only the members the server accepts.
|
|
|
243
292
|
|
|
244
293
|
You can transition groups between different mailbox types by modifying the `twakeMailboxType` attribute. The system automatically handles the cleanup of the old type and setup of the new type.
|
|
245
294
|
|
|
295
|
+
These transitions keep the group where it is. With
|
|
296
|
+
[team mailboxes in a branch of their own](#team-mailboxes-in-a-branch-of-their-own),
|
|
297
|
+
a mailing list does not become a team mailbox: create the team mailbox, then
|
|
298
|
+
delete the list.
|
|
299
|
+
|
|
246
300
|
### Supported Transitions
|
|
247
301
|
|
|
248
302
|
#### mailingList → teamMailbox
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ldap-rest",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"description": "Light directory manager",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Identity-Manager"
|
|
@@ -680,6 +680,10 @@
|
|
|
680
680
|
"import": "./static/schemas/twake/positions.json",
|
|
681
681
|
"require": "./static/schemas/twake/positions.json"
|
|
682
682
|
},
|
|
683
|
+
"./schema-twake-teammailboxes": {
|
|
684
|
+
"import": "./static/schemas/twake/teamMailboxes.json",
|
|
685
|
+
"require": "./static/schemas/twake/teamMailboxes.json"
|
|
686
|
+
},
|
|
683
687
|
"./schema-twake-users": {
|
|
684
688
|
"import": "./static/schemas/twake/users.json",
|
|
685
689
|
"require": "./static/schemas/twake/users.json"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entity": {
|
|
3
|
+
"name": "twakeTeamMailbox",
|
|
4
|
+
"mainAttribute": "cn",
|
|
5
|
+
"objectClass": ["top", "groupOfNames", "twakeStaticGroup"],
|
|
6
|
+
"singularName": "teamMailbox",
|
|
7
|
+
"pluralName": "teamMailboxes",
|
|
8
|
+
"base": "ou=teammailboxes,__ldap_base__",
|
|
9
|
+
"label": {
|
|
10
|
+
"en": "Team mailboxes",
|
|
11
|
+
"fr": "Boîtes partagées",
|
|
12
|
+
"mn": "Хамтын шуудангийн хайрцгууд"
|
|
13
|
+
},
|
|
14
|
+
"singularLabel": {
|
|
15
|
+
"en": "team mailbox",
|
|
16
|
+
"fr": "boîte partagée",
|
|
17
|
+
"mn": "хамтын шуудангийн хайрцаг"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"strict": true,
|
|
21
|
+
"attributes": {
|
|
22
|
+
"objectClass": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"items": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"test": "^[a-zA-Z][a-zA-Z0-9-]*$"
|
|
27
|
+
},
|
|
28
|
+
"default": ["top", "groupOfNames", "twakeStaticGroup"],
|
|
29
|
+
"required": true,
|
|
30
|
+
"fixed": true
|
|
31
|
+
},
|
|
32
|
+
"cn": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"searchable": true,
|
|
35
|
+
"test": "^[#a-zA-Z0-9_-]{1,64}$",
|
|
36
|
+
"hint": {
|
|
37
|
+
"en": "Letters, digits, # - and _, as the name of its address",
|
|
38
|
+
"fr": "Lettres, chiffres, # - et _, comme le nom de son adresse",
|
|
39
|
+
"mn": "Үсэг, тоо, # - болон _, хаягийн нэрийн адил"
|
|
40
|
+
},
|
|
41
|
+
"required": true,
|
|
42
|
+
"role": "identifier",
|
|
43
|
+
"label": {
|
|
44
|
+
"en": "Name",
|
|
45
|
+
"fr": "Nom",
|
|
46
|
+
"mn": "Нэр"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"description": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"searchable": true,
|
|
52
|
+
"required": false,
|
|
53
|
+
"label": {
|
|
54
|
+
"en": "Description",
|
|
55
|
+
"fr": "Description",
|
|
56
|
+
"mn": "Тайлбар"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"mail": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"searchable": true,
|
|
62
|
+
"test": "^[#a-zA-Z0-9_-]{1,255}@[^@\\s]+\\.[^@\\s]+$",
|
|
63
|
+
"hint": {
|
|
64
|
+
"en": "Expected an email address whose name holds only letters, digits, # - and _, e.g. support@example.org",
|
|
65
|
+
"fr": "Une adresse électronique est attendue, dont le nom ne contient que lettres, chiffres, # - et _, par ex. support@example.org",
|
|
66
|
+
"mn": "Имэйл хаяг оруулна уу, нэр нь зөвхөн үсэг, тоо, # - болон _ агуулна, жишээ нь support@example.org"
|
|
67
|
+
},
|
|
68
|
+
"required": true,
|
|
69
|
+
"unique": {
|
|
70
|
+
"attributes": ["mailAlternateAddress"],
|
|
71
|
+
"branches": ["__LDAP_BASE__"]
|
|
72
|
+
},
|
|
73
|
+
"mailDomainScope": "organization",
|
|
74
|
+
"allowSubdomains": true,
|
|
75
|
+
"role": "primaryEmail",
|
|
76
|
+
"label": {
|
|
77
|
+
"en": "Email",
|
|
78
|
+
"fr": "Adresse électronique",
|
|
79
|
+
"mn": "Имэйл хаяг"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"twakeMailboxType": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"default": "cn=teamMailbox,ou=twakeMailboxType,ou=nomenclature,__LDAP_BASE__",
|
|
85
|
+
"fixed": true,
|
|
86
|
+
"required": true,
|
|
87
|
+
"role": "mailboxType",
|
|
88
|
+
"label": {
|
|
89
|
+
"en": "Mailbox type",
|
|
90
|
+
"fr": "Type de boîte",
|
|
91
|
+
"mn": "Хайрцгийн төрөл"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"member": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"items": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"test": "^[a-zA-Z]+=.*,__LDAP_BASE__$",
|
|
99
|
+
"hint": {
|
|
100
|
+
"en": "Expected the DN of an entry of this directory",
|
|
101
|
+
"fr": "Le DN d'une entrée de cet annuaire est attendu",
|
|
102
|
+
"mn": "Энэ лавлахын бичлэгийн DN оруулна уу"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"required": true,
|
|
106
|
+
"role": "members",
|
|
107
|
+
"label": {
|
|
108
|
+
"en": "Members",
|
|
109
|
+
"fr": "Membres",
|
|
110
|
+
"mn": "Гишүүд"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"owner": {
|
|
114
|
+
"type": "array",
|
|
115
|
+
"items": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"test": "^[a-zA-Z]+=.*,__LDAP_BASE__$",
|
|
118
|
+
"hint": {
|
|
119
|
+
"en": "Expected the DN of an entry of this directory",
|
|
120
|
+
"fr": "Le DN d'une entrée de cet annuaire est attendu",
|
|
121
|
+
"mn": "Энэ лавлахын бичлэгийн DN оруулна уу"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"required": false,
|
|
125
|
+
"role": "owners",
|
|
126
|
+
"label": {
|
|
127
|
+
"en": "Owners",
|
|
128
|
+
"fr": "Propriétaires",
|
|
129
|
+
"mn": "Эзэмшигчид"
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"twakeDepartmentLink": {
|
|
133
|
+
"type": "pointer",
|
|
134
|
+
"branch": ["__LDAP_BASE__"],
|
|
135
|
+
"required": true,
|
|
136
|
+
"role": "organizationLink",
|
|
137
|
+
"label": {
|
|
138
|
+
"en": "Department",
|
|
139
|
+
"fr": "Organisation",
|
|
140
|
+
"mn": "Байгууллага"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"twakeDepartmentPath": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"test": "^[^\\r\\n]{1,4096}$",
|
|
146
|
+
"required": true,
|
|
147
|
+
"generated": true,
|
|
148
|
+
"role": "organizationPath",
|
|
149
|
+
"label": {
|
|
150
|
+
"en": "Department path",
|
|
151
|
+
"fr": "Chemin",
|
|
152
|
+
"mn": "Зам"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|