ldap-rest 0.21.0 → 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};
@@ -1 +1 @@
1
- import{fileURLToPath as e}from"url";import{join as _,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls","identifiers",!0],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(a(e(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuotaSize"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes","identifiers"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes","identifiers"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes","identifiers"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(a(e(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes","identifiers"],["--static-path","DM_STATIC_PATH",_(a(e(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases","dns"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes","identifiers"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-retry-attempts","DM_JAMES_RETRY_ATTEMPTS",5,"number"],["--james-retry-delay","DM_JAMES_RETRY_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches","dns"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--james-sync-forwards","DM_JAMES_SYNC_FORWARDS",!1,"boolean"],["--james-team-mailbox-any-domain","DM_JAMES_TEAM_MAILBOX_ANY_DOMAIN",!1,"boolean"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-space-base","DM_TWAKE_SPACE_BASE",""],["--twake-space-class","DM_TWAKE_SPACE_CLASS",["top","twakeSpace"],"array",null,"identifiers"],["--twake-space-display-name-attribute","DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-space-admin-attribute","DM_TWAKE_SPACE_ADMIN_ATTRIBUTE","twakeSpaceAdmin"],["--twake-space-editor-attribute","DM_TWAKE_SPACE_EDITOR_ATTRIBUTE","twakeSpaceEditor"],["--twake-space-viewer-attribute","DM_TWAKE_SPACE_VIEWER_ATTRIBUTE","twakeSpaceViewer"],["--twake-space-user-role-attribute","DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE",""],["--twake-space-exchange","DM_TWAKE_SPACE_EXCHANGE","space"],["--twake-space-sync-queue","DM_TWAKE_SPACE_SYNC_QUEUE","twake.space.sync.requested.ldap-rest"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array",null,"dns"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array",null,"identifiers"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array",null,"identifiers"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array",null,"dns"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array",null,"dns"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes","identifiers"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes","identifiers"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps","phrases"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs","phrases"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array",null,"identifiers"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies","identifiers"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-base","DM_PPOLICY_BASE",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes","identifiers"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes","identifiers"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
1
+ import{fileURLToPath as e}from"url";import{join as _,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls","identifiers",!0],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(a(e(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuotaSize"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes","identifiers"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes","identifiers"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes","identifiers"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(a(e(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes","identifiers"],["--static-path","DM_STATIC_PATH",_(a(e(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases","dns"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes","identifiers"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-retry-attempts","DM_JAMES_RETRY_ATTEMPTS",5,"number"],["--james-retry-delay","DM_JAMES_RETRY_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches","dns"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--james-sync-forwards","DM_JAMES_SYNC_FORWARDS",!1,"boolean"],["--james-team-mailbox-any-domain","DM_JAMES_TEAM_MAILBOX_ANY_DOMAIN",!1,"boolean"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-space-base","DM_TWAKE_SPACE_BASE",""],["--twake-space-class","DM_TWAKE_SPACE_CLASS",["top","twakeSpace"],"array",null,"identifiers"],["--twake-space-display-name-attribute","DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-space-admin-attribute","DM_TWAKE_SPACE_ADMIN_ATTRIBUTE","twakeSpaceAdmin"],["--twake-space-editor-attribute","DM_TWAKE_SPACE_EDITOR_ATTRIBUTE","twakeSpaceEditor"],["--twake-space-viewer-attribute","DM_TWAKE_SPACE_VIEWER_ATTRIBUTE","twakeSpaceViewer"],["--twake-space-user-role-attribute","DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE",""],["--twake-space-exchange","DM_TWAKE_SPACE_EXCHANGE","space"],["--twake-space-sync-queue","DM_TWAKE_SPACE_SYNC_QUEUE","twake.space.sync.requested.ldap-rest"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array",null,"dns"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array",null,"identifiers"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array",null,"identifiers"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array",null,"dns"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array",null,"dns"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes","identifiers"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes","identifiers"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps","phrases"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs","phrases"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array",null,"identifiers"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--authz-global-branch","DM_AUTHZ_GLOBAL_BRANCHES",[],"array","--authz-global-branches","dns"],["--authz-global-organization","DM_AUTHZ_GLOBAL_ORGANIZATION",""],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies","identifiers"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-base","DM_PPOLICY_BASE",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes","identifiers"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes","identifiers"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
@@ -1 +1 @@
1
- import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{assertIdentityMode as r,identityFor as s}from"../auth/base.js";import{AmbiguousIdentityError as i,ForbiddenError as n}from"../errors.js";import{organizationLink as a,getParentDn as o,normalizeDn as h,escapeLdapFilter as c,isDnInBranch as u}from"../utils.js";import{authzFor as l,servesRequest as d}from"./composition.js";class AuthzBase extends t{constructor(t){super(t),this.roles=["authz"],this.resolutionCache=new Map,this.resolutionCacheFullWarned=!1,this.fallbackWarned=!1,this.hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,r,s])=>{if(this.shouldSkipAuthorization(s))return[t,e,r,s];const i=await this.resolveCaller(s);if(!i)return[t,e,r,s];const n=this.config.ldap_organization_link_attribute;if(n&&e.replace?.[n]){let r,o=this.extractBranchDn(t),h=!1;try{const e=(await this.server.ldap.search({paged:!1,scope:"base",attributes:[n]},t)).searchEntries[0];e&&(h=!0,r=a(e[n]),r&&(o=r))}catch{}const c=e.replace[n],u=Array.isArray(c)?String(c[0]):String(c),l=this.filtersAttachedEntries(),d=l&&h&&this.inTransit(r),f=l&&this.inTransit(u);if(!d){const t=f?"write":"read";if(!(await this.getUserPermissions(i,o))[t])throw new Error(`[authz-forbidden] User ${s.user} does not have ${t} permission for source branch ${o}`)}if(f){if(d){if(0===(await this.getAuthorizedBranches(i)).length)throw new Error(`[authz-forbidden] User ${s.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,u)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${u}`)}}else{const e=await this.effectiveBranch(t);if(!(await this.getUserPermissions(i,e)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${e}`)}const o=this.domainLinkAttribute();return o&&await this.guardDomainLinks(i,s,t,[...AuthzBase.valuesOf(e.add,o),...AuthzBase.valuesOf(e.replace,o)],o,!1),[t,e,r,s]},ldapaddrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];let i;const n=this.config.ldap_organization_link_attribute;if(n&&e[n]){const t=e[n];i=Array.isArray(t)?String(t[0]):String(t)}else i=this.extractBranchDn(t);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${i}`);const a=this.domainLinkAttribute();return a&&await this.guardDomainLinks(s,r,t,AuthzBase.valuesOf(e,a),a,!0),[t,e,r]},ldapsearchrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];if(t===this.config.ldap_top_organization&&"base"===e.scope)return[t,e,r];if(this.filtersAttachedEntries()){if(0===(await this.getAuthorizedBranches(s)).length)throw new Error(`[authz-forbidden] User ${r.user} administers no branch`);const i=this.config.ldap_organization_link_attribute;if(i&&!AuthzBase.wantsAttribute(e,i)){const t=e.attributes,r=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...r,i]},AuthzBase.widenedForLink.add(e)}return[t,e,r]}if(!(await this.getUserPermissions(s,t)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${t}`);return[t,e,r]},ldapsearchfilter:async([t,e,r])=>{const s=[t,e,r];if(!this.filtersAttachedEntries())return s;const i=this.config.ldap_organization_link_attribute;if(!i||this.shouldSkipAuthorization(e))return s;if(!t?.searchEntries?.length)return s;const n=await this.resolveCaller(e);if(!n)return s;const a=await this.getAuthorizedBranches(n);if(0===a.length)return s;const within=t=>a.some(e=>u(t,e)),o=this.transitKey();if(t.searchEntries=t.searchEntries.filter(t=>{const r=t[i];if(null==r)return!0;const s=(Array.isArray(r)?r:[r]).map(String);return 0===s.length||(!!s.some(within)||(!!s.some(t=>this.inTransit(t,o))||(this.logger.debug(`${this.name}: hiding ${String(t.dn)} from ${e.user}, attached to ${s[0]}`),!1)))}),r&&AuthzBase.widenedForLink.has(r))for(const e of t.searchEntries)delete e[i];return[t,e,r]},getOrganisationTop:async([t,e])=>{if(this.shouldSkipAuthorization(t))return[t,e];const r=await this.resolveCaller(t);if(!r)return[t,e];const s=await this.getAuthorizedBranches(r);if(s.length>0){const e=[];for(const r of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},r,t);1===s.searchEntries.length&&e.push(s.searchEntries[0])}catch(t){this.logger.warn(`Failed to fetch authorized branch ${r}: ${t}`)}if(1===e.length)return[t,e[0]];if(e.length>1)return[t,e[0]]}return[t,e]},ldaprenamerequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];const i=this.extractBranchDn(t),n=this.extractBranchDn(e);if(!(await this.getUserPermissions(s,i)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for source branch ${i}`);if(!(await this.getUserPermissions(s,n)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for destination branch ${n}`);return[t,e,r]},ldapdeleterequest:async([t,e])=>{if(this.shouldSkipAuthorization(e))return[t,e];const r=await this.resolveCaller(e);if(!r)return[t,e];const s=Array.isArray(t)?t:[t];for(const t of s){const s=await this.effectiveBranch(t);if(!(await this.getUserPermissions(r,s)).delete)throw new Error(`[authz-forbidden] User ${e.user} does not have delete permission for branch ${s}`)}return[t,e]}},r(this.config,this.constructor.name),l(this.config,this.constructor.name);const e=this.config.authz_unresolved_user??"deny";if(!AuthzBase.UNRESOLVED_POLICIES.includes(e))throw new Error(`${this.constructor.name}: unknown --authz-unresolved-user "${e}". Known: ${AuthzBase.UNRESOLVED_POLICIES.join(", ")}.`);this.config.authz_transit_branch&&!this.filtersAttachedEntries()&&this.logger.warn(`${this.constructor.name}: --authz-transit-branch is ignored, entries are not judged by their attachment here (--authz-filter-attached-entries)`)}static wantsAttribute(t,e){const r=t?.attributes;if(void 0===r)return!0;const s=(Array.isArray(r)?r:[r]).map(t=>String(t).toLowerCase());return 0===s.length||(!!s.includes("*")||s.includes(e.toLowerCase()))}extractBranchDn(t){return o(t)}filtersAttachedEntries(){return Boolean(this.config.authz_filter_attached_entries)}async effectiveBranch(t){const e=this.config.ldap_organization_link_attribute;if(!e||!this.filtersAttachedEntries())return this.extractBranchDn(t);try{const r=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e]},t),s=a(r.searchEntries?.[0]?.[e]);if(s)return s}catch{}return this.extractBranchDn(t)}transitBranch(){if(!this.filtersAttachedEntries())return;const t=String(this.config.authz_transit_branch??"").trim();return t.length>0?t:void 0}inTransit(t,e=this.transitKey()){return void 0===t||void 0!==e&&h(t)===e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:h(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${c(t)})`,r=(await this.server.ldap.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new i(t,r.length);if(0===r.length)return null;const s=r[0].dn;return"string"==typeof s?s:String(s)}shouldSkipAuthorization(t){return!t?.user||!d(t,this.config)}async resolveCaller(t){const{value:e,fellBack:r}=s(t,this.config);r&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so permissions are read for req.user instead. Rules written on logins will not match`));const i=e,a=Date.now(),o=this.resolutionCache.get(i);let h;if(o&&a-o.at<this.cacheTTL)h=o.user;else{if(h=await this.resolveUser(i),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[t,e]of this.resolutionCache)a-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(i,{user:h,at:a}):this.resolutionCacheFullWarned||(this.resolutionCacheFullWarned=!0,this.logger.warn(`${this.name}: more than ${AuthzBase.RESOLUTION_CACHE_MAX} identities resolved within one cache window, so resolutions are no longer cached and each request costs a lookup`))}if(h)return h;if("allow"===this.config.authz_unresolved_user)return this.logger.warn(`User ${i} could not be resolved by ${this.name}; allowed by --authz-unresolved-user allow`),null;throw new n(`[authz-forbidden] User ${i} could not be resolved by ${this.name}, so no permission can be read for them`)}domainLinkAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"domainLink")||this.config.enterprise_domain_link_attribute||void 0}static valuesOf(t,e){if(!t)return[];const r=e.toLowerCase(),s=[];for(const[e,i]of Object.entries(t))if(e.split(";")[0].toLowerCase()===r&&null!=i)for(const t of Array.isArray(i)?i:[i])s.push(String(t));return s}async storedValues(t,e){try{const r=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e]},t);return AuthzBase.valuesOf(r.searchEntries?.[0],e)}catch{return[]}}async guardDomainLinks(t,e,r,s,i,n){if(0===s.length)return;const a=new Set((n?[]:await this.storedValues(r,i)).map(h)),c=s.filter(t=>!a.has(h(t)));if(0===c.length)return;const l=this.config.ldap_top_organization,d=h(l||this.server.ldap.base),f=new Set;let g=o(r);const w=new Set;for(;g&&!w.has(h(g));){const t=h(g);if(w.add(t),t!==d&&!u(g,d))break;for(const t of await this.storedValues(g,i))f.add(h(t));if(t===d)break;const e=o(g);if(e===g)break;g=e}const p=c.filter(t=>!f.has(h(t)));if(!(0===p.length||l&&(await this.getUserPermissions(t,l)).write))throw new Error(`[authz-forbidden] User ${e.user} may not link ${r} to domain ${p[0]}: no organization above it is linked to it`)}clearResolutionCache(){this.resolutionCache.clear()}}AuthzBase.widenedForLink=new WeakSet,AuthzBase.UNRESOLVED_POLICIES=["deny","allow"],AuthzBase.RESOLUTION_CACHE_MAX=1e4;export{AuthzBase as default};
1
+ import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{assertIdentityMode as r,identityFor as s}from"../auth/base.js";import{AmbiguousIdentityError as i,ForbiddenError as n}from"../errors.js";import{isDnInBranch as a,organizationLink as o,getParentDn as h,normalizeDn as c,escapeLdapFilter as u}from"../utils.js";import{authzFor as l,servesRequest as d}from"./composition.js";class AuthzBase extends t{constructor(t){super(t),this.roles=["authz"],this.resolutionCache=new Map,this.resolutionCacheFullWarned=!1,this.fallbackWarned=!1,this.hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,r,s])=>{if(this.shouldSkipAuthorization(s))return[t,e,r,s];const i=await this.resolveCaller(s);if(!i)return[t,e,r,s];const n=this.config.ldap_organization_link_attribute;if(n&&e.replace?.[n]){let r,a=this.extractBranchDn(t),h=!1;try{const e=(await this.server.ldap.search({paged:!1,scope:"base",attributes:[n]},t)).searchEntries[0];e&&(h=!0,r=o(e[n]),r&&(a=r))}catch{}const c=e.replace[n],u=Array.isArray(c)?String(c[0]):String(c),l=this.filtersAttachedEntries(),d=l&&h&&this.inTransit(r),f=l&&this.inTransit(u);if(!d){const t=f?"write":"read";if(!(await this.getUserPermissions(i,a))[t])throw new Error(`[authz-forbidden] User ${s.user} does not have ${t} permission for source branch ${a}`)}if(f){if(d){if(0===(await this.getAuthorizedBranches(i)).length)throw new Error(`[authz-forbidden] User ${s.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,u)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${u}`)}}else{const e=await this.effectiveBranch(t);if(!(await this.getUserPermissions(i,e)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${e}`)}const a=this.domainLinkAttribute();return a&&await this.guardDomainLinks(i,s,t,[...AuthzBase.valuesOf(e.add,a),...AuthzBase.valuesOf(e.replace,a)],a,!1),[t,e,r,s]},ldapaddrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];let i;const n=this.config.ldap_organization_link_attribute;if(n&&e[n]){const t=e[n];i=Array.isArray(t)?String(t[0]):String(t)}else i=this.extractBranchDn(t);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${i}`);const a=this.domainLinkAttribute();return a&&await this.guardDomainLinks(s,r,t,AuthzBase.valuesOf(e,a),a,!0),[t,e,r]},ldapsearchrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];if(t===this.config.ldap_top_organization&&"base"===e.scope)return[t,e,r];if(this.filtersAttachedEntries()){if(0===(await this.getAuthorizedBranches(s)).length)throw new Error(`[authz-forbidden] User ${r.user} administers no branch`);if(this.globalBranches().some(e=>a(t,e))&&!(await this.getUserPermissions(s,t)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${t}`);const i=this.config.ldap_organization_link_attribute;if(i&&!AuthzBase.wantsAttribute(e,i)){const t=e.attributes,r=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...r,i]},AuthzBase.widenedForLink.add(e)}return[t,e,r]}if(!(await this.getUserPermissions(s,t)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${t}`);return[t,e,r]},ldapsearchfilter:async([t,e,r])=>{const s=[t,e,r];if(!this.filtersAttachedEntries())return s;if(this.shouldSkipAuthorization(e))return s;if(!t?.searchEntries?.length)return s;const i=await this.resolveCaller(e);if(!i)return s;const n=this.globalBranches();if(n.length){const r=new Map,s=[];for(const o of t.searchEntries){const t=n.find(t=>a(o.dn,t));void 0===t||(r.has(t)||r.set(t,!0===(await this.getUserPermissions(i,t)).read),r.get(t))?s.push(o):this.logger.debug(`${this.name}: hiding ${String(o.dn)} from ${e.user}, in the global branch ${t}`)}t.searchEntries=s}const o=this.config.ldap_organization_link_attribute;if(!o)return s;const h=await this.getAuthorizedBranches(i);if(0===h.length)return s;const within=t=>h.some(e=>a(t,e)),c=this.transitKey();if(t.searchEntries=t.searchEntries.filter(t=>{const r=t[o];if(null==r)return!0;const s=(Array.isArray(r)?r:[r]).map(String);return 0===s.length||(!!s.some(within)||(!!s.some(t=>this.inTransit(t,c))||(this.logger.debug(`${this.name}: hiding ${String(t.dn)} from ${e.user}, attached to ${s[0]}`),!1)))}),r&&AuthzBase.widenedForLink.has(r))for(const e of t.searchEntries)delete e[o];return[t,e,r]},getOrganisationTop:async([t,e])=>{if(this.shouldSkipAuthorization(t))return[t,e];const r=await this.resolveCaller(t);if(!r)return[t,e];const s=await this.getAuthorizedBranches(r);if(s.length>0){const e=[];for(const r of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},r,t);1===s.searchEntries.length&&e.push(s.searchEntries[0])}catch(t){this.logger.warn(`Failed to fetch authorized branch ${r}: ${t}`)}if(1===e.length)return[t,e[0]];if(e.length>1)return[t,e[0]]}return[t,e]},ldaprenamerequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];const i=this.extractBranchDn(t),n=this.extractBranchDn(e);if(!(await this.getUserPermissions(s,i)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for source branch ${i}`);if(!(await this.getUserPermissions(s,n)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for destination branch ${n}`);return[t,e,r]},ldapdeleterequest:async([t,e])=>{if(this.shouldSkipAuthorization(e))return[t,e];const r=await this.resolveCaller(e);if(!r)return[t,e];const s=Array.isArray(t)?t:[t];for(const t of s){const s=await this.effectiveBranch(t);if(!(await this.getUserPermissions(r,s)).delete)throw new Error(`[authz-forbidden] User ${e.user} does not have delete permission for branch ${s}`)}return[t,e]}},r(this.config,this.constructor.name),l(this.config,this.constructor.name);const e=this.config.authz_unresolved_user??"deny";if(!AuthzBase.UNRESOLVED_POLICIES.includes(e))throw new Error(`${this.constructor.name}: unknown --authz-unresolved-user "${e}". Known: ${AuthzBase.UNRESOLVED_POLICIES.join(", ")}.`);this.config.authz_transit_branch&&!this.filtersAttachedEntries()&&this.logger.warn(`${this.constructor.name}: --authz-transit-branch is ignored, entries are not judged by their attachment here (--authz-filter-attached-entries)`)}static wantsAttribute(t,e){const r=t?.attributes;if(void 0===r)return!0;const s=(Array.isArray(r)?r:[r]).map(t=>String(t).toLowerCase());return 0===s.length||(!!s.includes("*")||s.includes(e.toLowerCase()))}extractBranchDn(t){return h(t)}filtersAttachedEntries(){return Boolean(this.config.authz_filter_attached_entries)}globalBranches(){return[]}async effectiveBranch(t){const e=this.config.ldap_organization_link_attribute;if(!e||!this.filtersAttachedEntries())return this.extractBranchDn(t);try{const r=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e]},t),s=o(r.searchEntries?.[0]?.[e]);if(s)return s}catch{}return this.extractBranchDn(t)}transitBranch(){if(!this.filtersAttachedEntries())return;const t=String(this.config.authz_transit_branch??"").trim();return t.length>0?t:void 0}inTransit(t,e=this.transitKey()){return void 0===t||void 0!==e&&c(t)===e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:c(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${u(t)})`,r=(await this.server.ldap.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new i(t,r.length);if(0===r.length)return null;const s=r[0].dn;return"string"==typeof s?s:String(s)}shouldSkipAuthorization(t){return!t?.user||!d(t,this.config)}async resolveCaller(t){const{value:e,fellBack:r}=s(t,this.config);r&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so permissions are read for req.user instead. Rules written on logins will not match`));const i=e,a=Date.now(),o=this.resolutionCache.get(i);let h;if(o&&a-o.at<this.cacheTTL)h=o.user;else{if(h=await this.resolveUser(i),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[t,e]of this.resolutionCache)a-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(i,{user:h,at:a}):this.resolutionCacheFullWarned||(this.resolutionCacheFullWarned=!0,this.logger.warn(`${this.name}: more than ${AuthzBase.RESOLUTION_CACHE_MAX} identities resolved within one cache window, so resolutions are no longer cached and each request costs a lookup`))}if(h)return h;if("allow"===this.config.authz_unresolved_user)return this.logger.warn(`User ${i} could not be resolved by ${this.name}; allowed by --authz-unresolved-user allow`),null;throw new n(`[authz-forbidden] User ${i} could not be resolved by ${this.name}, so no permission can be read for them`)}domainLinkAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"domainLink")||this.config.enterprise_domain_link_attribute||void 0}static valuesOf(t,e){if(!t)return[];const r=e.toLowerCase(),s=[];for(const[e,i]of Object.entries(t))if(e.split(";")[0].toLowerCase()===r&&null!=i)for(const t of Array.isArray(i)?i:[i])s.push(String(t));return s}async storedValues(t,e){try{const r=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e]},t);return AuthzBase.valuesOf(r.searchEntries?.[0],e)}catch{return[]}}async guardDomainLinks(t,e,r,s,i,n){if(0===s.length)return;const o=new Set((n?[]:await this.storedValues(r,i)).map(c)),u=s.filter(t=>!o.has(c(t)));if(0===u.length)return;const l=this.config.ldap_top_organization,d=c(l||this.server.ldap.base),f=new Set;let g=h(r);const w=new Set;for(;g&&!w.has(c(g));){const t=c(g);if(w.add(t),t!==d&&!a(g,d))break;for(const t of await this.storedValues(g,i))f.add(c(t));if(t===d)break;const e=h(g);if(e===g)break;g=e}const p=u.filter(t=>!f.has(c(t)));if(!(0===p.length||l&&(await this.getUserPermissions(t,l)).write))throw new Error(`[authz-forbidden] User ${e.user} may not link ${r} to domain ${p[0]}: no organization above it is linked to it`)}clearResolutionCache(){this.resolutionCache.clear()}}AuthzBase.widenedForLink=new WeakSet,AuthzBase.UNRESOLVED_POLICIES=["deny","allow"],AuthzBase.RESOLUTION_CACHE_MAX=1e4;export{AuthzBase as default};
@@ -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};
@@ -1 +1 @@
1
- import s from"../../lib/authz/base.js";import{escapeLdapFilter as e}from"../../lib/utils.js";class AuthzLinid1 extends s{constructor(s){super(s),this.name="authzLinid1",this.permissionsCache=new Map,this.cacheTTL=3e5;const e=this.config.authz_local_admin_attribute||"twakeLocalAdminLink";this.logger.info(`AuthzLinid1: Authorization based on ${e} attribute enabled`)}filtersAttachedEntries(){return!0}async resolveUser(s){return this.getUserDn(s)}async getUserDn(s){return this.findUserDn(s)}async getUserPermissions(s,e){const t=Date.now(),i=this.permissionsCache.get(s);if(i&&t-i.timestamp<this.cacheTTL){const s=this.findBranchPermissions(i.branches,e);if(s)return s}await this.refreshUserPermissions(s);const r=this.permissionsCache.get(s);if(r){const s=this.findBranchPermissions(r.branches,e);if(s)return s}return{read:!1,write:!1,delete:!1}}async refreshUserPermissions(s){const t=new Map;try{const i=`(${this.config.authz_local_admin_attribute||"twakeLocalAdminLink"}=${e(s)})`,r=this.config.ldap_top_organization||this.config.ldap_base||"",n=await this.server.ldap.search({paged:!1,filter:i,attributes:["dn"],scope:"sub"},r);if(n.searchEntries)for(const s of n.searchEntries){const e="string"==typeof s.dn?s.dn:String(s.dn);t.set(e,{read:!0,write:!0,delete:!0})}}catch(e){this.logger.error(`Failed to refresh permissions for user ${s}: ${e}`)}this.permissionsCache.set(s,{branches:t,timestamp:Date.now()})}async getAuthorizedBranches(s){const e=Date.now(),t=this.permissionsCache.get(s);(!t||e-t.timestamp>=this.cacheTTL)&&await this.refreshUserPermissions(s);const i=this.permissionsCache.get(s);return i?Array.from(i.branches.keys()):[]}findBranchPermissions(s,e){if(s.has(e))return s.get(e)||null;for(const[t,i]of s.entries())if(e.toLowerCase().endsWith(`,${t.toLowerCase()}`))return i;return null}}export{AuthzLinid1 as default};
1
+ import s from"../../lib/authz/base.js";import{isDnInBranch as t,escapeLdapFilter as e}from"../../lib/utils.js";class AuthzLinid1 extends s{constructor(s){super(s),this.name="authzLinid1",this.permissionsCache=new Map,this.cacheTTL=3e5;const e=this.config.authz_local_admin_attribute||"twakeLocalAdminLink";this.logger.info(`AuthzLinid1: Authorization based on ${e} attribute enabled`);const i=this.globalOrganization(),r=i&&this.globalBranches().find(s=>t(i,s));if(r)throw new Error(`AuthzLinid1: --authz-global-organization ${i} lies in the global branch ${r}`)}globalOrganization(){return this.config.authz_global_organization||this.config.ldap_top_organization||void 0}filtersAttachedEntries(){return!0}globalBranches(){return this.config.authz_global_branch??[]}async resolveUser(s){return this.getUserDn(s)}async getUserDn(s){return this.findUserDn(s)}async getUserPermissions(s,e){const i=this.globalOrganization();if(this.globalBranches().some(s=>t(e,s)))return i&&!t(i,e)?this.getUserPermissions(s,i):{read:!1,write:!1,delete:!1};const r=Date.now(),n=this.permissionsCache.get(s);if(n&&r-n.timestamp<this.cacheTTL){const s=this.findBranchPermissions(n.branches,e);if(s)return s}await this.refreshUserPermissions(s);const a=this.permissionsCache.get(s);if(a){const s=this.findBranchPermissions(a.branches,e);if(s)return s}return{read:!1,write:!1,delete:!1}}async refreshUserPermissions(s){const t=new Map;try{const i=`(${this.config.authz_local_admin_attribute||"twakeLocalAdminLink"}=${e(s)})`,r=this.config.ldap_top_organization||this.config.ldap_base||"",n=await this.server.ldap.search({paged:!1,filter:i,attributes:["dn"],scope:"sub"},r);if(n.searchEntries)for(const s of n.searchEntries){const e="string"==typeof s.dn?s.dn:String(s.dn);t.set(e,{read:!0,write:!0,delete:!0})}}catch(t){this.logger.error(`Failed to refresh permissions for user ${s}: ${t}`)}this.permissionsCache.set(s,{branches:t,timestamp:Date.now()})}async getAuthorizedBranches(s){const t=Date.now(),e=this.permissionsCache.get(s);(!e||t-e.timestamp>=this.cacheTTL)&&await this.refreshUserPermissions(s);const i=this.permissionsCache.get(s);return i?Array.from(i.branches.keys()):[]}findBranchPermissions(s,t){if(s.has(t))return s.get(t)||null;for(const[e,i]of s.entries())if(t.toLowerCase().endsWith(`,${e.toLowerCase()}`))return i;return null}}export{AuthzLinid1 as default};
@@ -1 +1 @@
1
- import{isDeepStrictEqual as e}from"node:util";import i from"../../abstract/plugin.js";import{asyncHandler as t,normalizeDn as s,escapeLdapFilter as a,validateDnValue as o,escapeDnValue as r}from"../../lib/utils.js";import{jsonBody as n}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as c}from"../../lib/ldapCodes.js";import{NotFoundError as l,ConflictError as d,BadRequestError as p}from"../../lib/errors.js";const u=["pwdChangedTime","pwdAccountLockedTime","pwdFailureTime","pwdGraceUseTime","pwdReset","pwdPolicySubentry"],w={pwdMinAge:"integer",pwdMaxAge:"integer",pwdInHistory:"integer",pwdCheckQuality:"integer",pwdMinLength:"integer",pwdMaxLength:"integer",pwdExpireWarning:"integer",pwdGraceAuthNLimit:"integer",pwdGraceExpiry:"integer",pwdLockout:"boolean",pwdLockoutDuration:"integer",pwdMaxFailure:"integer",pwdFailureCountInterval:"integer",pwdMustChange:"boolean",pwdAllowUserChange:"boolean",pwdSafeModify:"boolean",pwdMinDelay:"integer",pwdMaxDelay:"integer",pwdMaxIdle:"integer",pwdMaxRecordedFailure:"integer",pwdCheckModuleArg:"text",pwdUseCheckModule:"boolean"},h=["pwdCheckModuleArg","pwdUseCheckModule"],y=[...Object.keys(w),"pwdAttribute","pwdCheckModule"],isWritablePolicyAttribute=e=>Object.prototype.hasOwnProperty.call(w,e),values=e=>(Array.isArray(e)?e:void 0===e?[]:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>""!==e),first=e=>values(e)[0],userEntry=e=>({dn:e.dn,userPassword:first(e.userPassword),uid:first(e.uid),cn:first(e.cn),displayName:first(e.displayName),mail:first(e.mail),pwdChangedTime:first(e.pwdChangedTime),pwdAccountLockedTime:first(e.pwdAccountLockedTime),pwdFailureTime:values(e.pwdFailureTime),pwdGraceUseTime:values(e.pwdGraceUseTime),pwdReset:first(e.pwdReset),pwdPolicySubentry:first(e.pwdPolicySubentry)}),typedPolicy=e=>{const i={};for(const t of y){const s=values(e[t]);if(0!==s.length)switch(w[t]){case"integer":{const e=parseInt(s[0],10);isNaN(e)||(i[t]=e);break}case"boolean":i[t]="TRUE"===s[0];break;default:i[t]=1===s.length?s[0]:s}}return i},g=/[!@#$%^&*()_+\-=[\]{}|;:'",.<>?/]/;class PasswordPolicy extends i{constructor(){super(...arguments),this.name="ldapPasswordPolicy",this.roles=["api","configurable"],this.policyCache=new Map,this.discoveredDefault=null,this.warnedAmbiguousDefault=!1,this.warnedMissingBase=!1,this.POLICY_CACHE_TTL=6e4}api(e){const i=`${this.config.api_prefix}/v1`;e.get(`${i}/password-policy`,t(async(e,i)=>{const t=await this.readPolicyConfig();i.json(t)})),e.get(`${i}/users/:id/password-status`,t(async(e,i)=>{const t=this.param(e,"id"),s=await this.getPasswordStatus(t,e);i.json(s)})),e.post(`${i}/users/:id/unlock`,t(async(e,i)=>{const t=this.param(e,"id");await this.unlockAccount(t,e),i.json({success:!0,message:"Account unlocked"})})),e.get(`${i}/password-policy/expiring-soon`,t(async(e,i)=>{const t=parseInt(e.query.days)||this.config.ppolicy_warn_days||14,s=await this.findExpiringPasswords(t,e);i.json({warningDays:t,users:s})})),e.get(`${i}/password-policy/locked-accounts`,t(async(e,i)=>{const t=await this.findLockedAccounts(e);i.json({accounts:t})})),e.get(`${i}/password-policies`,t(async(e,i)=>{i.json({policies:await this.listPolicies(e)})})),e.post(`${i}/password-policies`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const s=await this.createPolicy(t,e);i.status(201).json(s)})),e.get(`${i}/password-policies/:cn`,t(async(e,i)=>{const t=await this.findPolicy(this.param(e,"cn"),e);i.json(await this.toPolicyRecord(t))})),e.patch(`${i}/password-policies/:cn`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const s=await this.updatePolicy(this.param(e,"cn"),t,e);i.json(s)})),e.delete(`${i}/password-policies/:cn`,t(async(e,i)=>{await this.deletePolicy(this.param(e,"cn"),e),i.json({success:!0})})),e.put(`${i}/users/:id/password-policy`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const{policy:s}=t;if(null!==s&&("string"!=typeof s||!s))throw new p("policy must be a policy name or null");i.json(await this.assignPolicy(this.param(e,"id"),s,e))})),this.config.ppolicy_validate_complexity&&e.post(`${i}/password/validate`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const{password:s}=t;if(!s)throw new p("password required");const a=this.validateComplexity(s);i.json(a)}))}async getPasswordStatus(e,i){const t=this.resolveUserDn(e),s=await this.readUser(t,e,["*",...u],i),a=await this.policyFor(s);return this.calculateStatus(t,s,a)}async readUser(e,i,t,s){try{const a=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:t,paged:!1},e,s);if(!a.searchEntries||0===a.searchEntries.length)throw new l(`User not found: ${i}`);return userEntry(a.searchEntries[0])}catch(e){if(e instanceof Error&&("NoSuchObjectError"===e.name||e.message.includes("0x20")||e.message.includes("Code: 32")))throw new l(`User not found: ${i}`);throw e}}calculateStatus(e,i,t){const s=Date.now(),a=this.parseGeneralizedTime(i.pwdChangedTime);let o=null,r=!1,n=null;t.pwdMaxAge&&a&&(o=new Date(a.getTime()+1e3*t.pwdMaxAge),r=s>o.getTime(),n=Math.ceil((o.getTime()-s)/864e5));let c=!1;const l=this.config.ppolicy_warn_days||14;null!==n&&n>0&&(c=n<=l);const d=i.pwdAccountLockedTime;let p=!!d;if(p&&!("000001010000Z"===d)&&t.pwdLockoutDuration){const e=this.parseGeneralizedTime(d);if(e){s>new Date(e.getTime()+1e3*t.pwdLockoutDuration).getTime()&&(p=!1)}}const u=i.pwdFailureTime,w=Array.isArray(u)?u.length:0,h=i.pwdGraceUseTime,y=Array.isArray(h)?h.length:0,g="TRUE"===i.pwdReset;return{dn:e,passwordSet:!!i.userPassword,lastChanged:a?.toISOString()||null,expiresAt:o?.toISOString()||null,daysUntilExpiration:n,isExpired:r,isExpiringSoon:c,mustChange:g,isLocked:p,lockedAt:d&&this.parseGeneralizedTime(d)?.toISOString()||null,failureCount:w,graceLoginsUsed:y,policy:t.dn??null}}async unlockAccount(e,i){const t=this.resolveUserDn(e),s=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["pwdAccountLockedTime","pwdFailureTime"],paged:!1},t,i);if(!s.searchEntries||0===s.searchEntries.length)throw new l(`User not found: ${e}`);const a=userEntry(s.searchEntries[0]),o={};a.pwdAccountLockedTime&&(o.pwdAccountLockedTime=[]),a.pwdFailureTime?.length&&(o.pwdFailureTime=[]),Object.keys(o).length>0?(await this.server.ldap.modify(t,{delete:o},i),this.logger.info(`Account unlocked: ${t}`)):this.logger.info(`Account was not locked: ${t}`)}async findExpiringPasswords(e,i){const t=this.config.ldap_users_base||this.config.ldap_base,s=await this.server.ldap.search({filter:"(pwdChangedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdChangedTime","pwdPolicySubentry"],paged:!1},t,i),a=Date.now(),o=24*e*60*60*1e3,r=[];for(const e of s.searchEntries){const i=userEntry(e),t=this.parseGeneralizedTime(i.pwdChangedTime);if(!t)continue;const s=await this.policyFor(i);if(!s.pwdMaxAge)continue;const n=new Date(t.getTime()+1e3*s.pwdMaxAge),c=n.getTime()-a;c>0&&c<o&&r.push({dn:i.dn,uid:i.uid,displayName:i.displayName||i.cn,mail:i.mail,expiresAt:n.toISOString(),daysUntilExpiration:Math.ceil(c/864e5)})}return r.sort((e,i)=>e.daysUntilExpiration-i.daysUntilExpiration)}async findLockedAccounts(e){const i=this.config.ldap_users_base||this.config.ldap_base;return(await this.server.ldap.search({filter:"(pwdAccountLockedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdAccountLockedTime","pwdFailureTime"],paged:!1},i,e)).searchEntries.map(e=>{const i=userEntry(e);return{dn:i.dn,uid:i.uid,displayName:i.displayName||i.cn,mail:i.mail,lockedAt:this.parseGeneralizedTime(i.pwdAccountLockedTime)?.toISOString(),failureCount:Array.isArray(i.pwdFailureTime)?i.pwdFailureTime.length:0}})}async policyFor(e){if(e.pwdPolicySubentry){const i=await this.readPolicyAt(e.pwdPolicySubentry);if(i)return i}return this.readPolicyConfig()}async readPolicyConfig(){const e=this.config.ppolicy_default_dn||await this.discoverDefaultDn(),i=e?await this.readPolicyAt(e):null;return this.lastDefaultPolicy=i??{},this.lastDefaultPolicy}async discoverDefaultDn(){if(this.discoveredDefault&&Date.now()-this.discoveredDefault.time<this.POLICY_CACHE_TTL)return this.discoveredDefault.dn;let e=null;try{const i=(await this.server.ldap.search({filter:"(objectClass=pwdPolicy)",scope:"sub",attributes:["1.1"],paged:!1},this.policyBase())).searchEntries.map(e=>e.dn);1===i.length?e=i[0]:i.length>1&&!this.warnedAmbiguousDefault&&(this.warnedAmbiguousDefault=!0,this.logger.warn(`${i.length} password policies found and no --ppolicy-default-dn: accounts without pwdPolicySubentry are reported without a policy`))}catch(e){this.logger.debug(`No ppolicy found: ${e}`)}return this.discoveredDefault={dn:e,time:Date.now()},e}async readPolicyAt(e){const i=s(e),t=this.policyCache.get(i);if(t&&Date.now()-t.time<this.POLICY_CACHE_TTL)return t.policy;let a,o=null;try{const i=await this.server.ldap.search({filter:"(objectClass=pwdPolicy)",scope:"base",attributes:y,paged:!1},e);i.searchEntries.length>0&&(o=((e,i)=>{const t=typedPolicy(e);return{dn:i,pwdMaxAge:t.pwdMaxAge,pwdMinAge:t.pwdMinAge,pwdInHistory:t.pwdInHistory,pwdCheckQuality:t.pwdCheckQuality,pwdMinLength:t.pwdMinLength,pwdMaxFailure:t.pwdMaxFailure,pwdLockout:!0===t.pwdLockout,pwdLockoutDuration:t.pwdLockoutDuration,pwdGraceAuthNLimit:t.pwdGraceAuthNLimit,pwdExpireWarning:t.pwdExpireWarning,pwdMustChange:!0===t.pwdMustChange,pwdAllowUserChange:!1!==t.pwdAllowUserChange}})(i.searchEntries[0],e))}catch(e){a=String(e)}const r=this.config.ppolicy_default_dn;return!o&&r&&s(r)===i?this.logger.warn(`--ppolicy-default-dn ${e} holds no password policy${a?`: ${a}`:""}`):a&&this.logger.debug(`Failed to read ppolicy from ${e}: ${a}`),this.policyCache.set(i,{policy:o,time:Date.now()}),o}forgetPolicies(){this.policyCache.clear(),this.discoveredDefault=null}policyBase(){return this.config.ppolicy_base||this.config.ldap_base}async searchPolicies(e,i){try{return(await this.server.ldap.search({filter:`(&(objectClass=pwdPolicy)${e})`,scope:"sub",attributes:["objectClass","cn",...y],paged:!1},this.policyBase(),i)).searchEntries}catch(e){if(32!==c(e))throw e;return this.warnedMissingBase||(this.warnedMissingBase=!0,this.logger.warn(`${this.policyBase()} does not exist: no password policy is found there, check --ppolicy-base`)),[]}}async findPolicy(e,i){const t=await this.searchPolicies(`(cn=${a(e)})`,i);if(0===t.length)throw new l(`No password policy named ${e}`);if(t.length>1)throw new d(`Several password policies are named ${e}`);return t[0]}async listPolicies(e){const i=[];for(const t of await this.searchPolicies("",e))i.push(await this.toPolicyRecord(t));return i.sort((e,i)=>e.cn.localeCompare(i.cn))}async toPolicyRecord(e){const i=(await this.readPolicyConfig()).dn;return{...{dn:e.dn,cn:values(e.cn)[0]??"",isDefault:!!i&&s(i)===s(e.dn)},...typedPolicy(e)}}policyValue(e,i){switch(w[e]){case"integer":if("number"!=typeof i||!Number.isInteger(i)||i<0||"pwdCheckQuality"===e&&i>2)throw new p("pwdCheckQuality"===e?"pwdCheckQuality must be 0, 1 or 2":`${e} must be a non-negative integer`);return String(i);case"boolean":if("boolean"!=typeof i)throw new p(`${e} must be a boolean`);return i?"TRUE":"FALSE";default:if("string"!=typeof i||""===i)throw new p(`${e} must be a non-empty string`);return i}}async createPolicy(e,i){const{cn:t,...s}=e;if("string"!=typeof t)throw new p("cn required");try{o(t,"cn")}catch(e){throw new p(e.message)}const a=await this.findPolicy(t,i).catch(e=>{if(e instanceof l)return null;throw e});if(a)throw new d(`A password policy is named ${t}: ${a.dn}`);const n=["top","person","pwdPolicy"],u={objectClass:n,cn:t,sn:t,pwdAttribute:"userPassword"};for(const[e,i]of Object.entries(s)){if(!isWritablePolicyAttribute(e))throw new p(`${e} cannot be written`);null!==i&&(u[e]=this.policyValue(e,i))}h.some(e=>e in u)&&n.push("pwdPolicyChecker");try{await this.server.ldap.add(`cn=${r(t)},${this.policyBase()}`,u,i)}catch(e){if(32!==c(e))throw e;throw new l(`${this.policyBase()} does not exist: create it or change --ppolicy-base`)}return this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(t,i))}async updatePolicy(i,t,a){const o=await this.findPolicy(i,a),r=await this.toPolicyRecord(o),n={},c=[];for(const[i,a]of Object.entries(t)){if(!isWritablePolicyAttribute(i)){if("dn"===i?"string"==typeof a&&s(a)===s(r.dn):Object.prototype.hasOwnProperty.call(r,i)&&e(r[i],a))continue;throw new p(`${i} cannot be written`)}null!==a?n[i]=this.policyValue(i,a):void 0!==r[i]&&c.push(i)}const l={};if(Object.keys(n).length>0&&(l.replace=n),c.length>0&&(l.delete=c),!l.replace&&!l.delete)return r;const d=values(o.objectClass).map(e=>e.toLowerCase());return h.some(e=>e in n)&&!d.includes("pwdpolicychecker")&&(l.add={objectClass:"pwdPolicyChecker"}),await this.server.ldap.modify(o.dn,l,a),this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(i,a))}async deletePolicy(e,i){const t=await this.findPolicy(e,i),o=(await this.readPolicyConfig()).dn;if(o&&s(o)===s(t.dn))throw new d(`${t.dn} is the default password policy`);if((await this.server.ldap.search({filter:`(pwdPolicySubentry=${a(t.dn)})`,scope:"sub",attributes:["1.1"],sizeLimit:1,paged:!1},this.config.ldap_base)).searchEntries.length>0)throw new d(`${t.dn} is still applied to at least one entry`);await this.server.ldap.delete(t.dn,i),this.forgetPolicies()}async assignPolicy(e,i,t){const s=this.resolveUserDn(e);if(null===i){try{await this.server.ldap.modify(s,{delete:["pwdPolicySubentry"]},t)}catch(e){if(16!==c(e))throw e}return{dn:s,policy:null}}const a=await this.findPolicy(i,t);return await this.server.ldap.modify(s,{replace:{pwdPolicySubentry:a.dn}},t),{dn:s,policy:a.dn}}param(e,i){const t=e.params[i];return Array.isArray(t)?t[0]:t}parseGeneralizedTime(e){if(!e)return null;const i=e.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);if(!i)return null;const[,t,s,a,o,r,n]=i;return new Date(Date.UTC(+t,+s-1,+a,+o,+r,+n))}validateComplexity(e){const i=[],t=this.config,s=t.ppolicy_min_length||12;return e.length<s&&i.push(`Minimum ${s} characters required`),t.ppolicy_require_uppercase&&!/[A-Z]/.test(e)&&i.push("At least one uppercase letter required"),t.ppolicy_require_lowercase&&!/[a-z]/.test(e)&&i.push("At least one lowercase letter required"),t.ppolicy_require_digit&&!/\d/.test(e)&&i.push("At least one digit required"),t.ppolicy_require_special&&!g.test(e)&&i.push("At least one special character required"),{valid:0===i.length,errors:i}}resolveUserDn(e){if(e.includes("="))return e;const i=this.config.ldap_users_base||this.config.ldap_base;return`${this.config.ldap_user_main_attribute||"uid"}=${r(e)},${i}`}getConfigApiData(){const e=this.config.api_prefix||"/api",i=!!this.config.ppolicy_validate_complexity;return{name:this.name,enabled:!0,endpoints:{getPolicy:`${e}/v1/password-policy`,getUserStatus:`${e}/v1/users/:id/password-status`,unlockUser:`${e}/v1/users/:id/unlock`,getExpiringSoon:`${e}/v1/password-policy/expiring-soon`,getLockedAccounts:`${e}/v1/password-policy/locked-accounts`,listPolicies:`${e}/v1/password-policies`,policy:`${e}/v1/password-policies/:cn`,assignPolicy:`${e}/v1/users/:id/password-policy`,validatePassword:i?`${e}/v1/password/validate`:void 0},config:{warnDays:this.config.ppolicy_warn_days||14,validateComplexity:i,...i&&{complexityRules:{minLength:this.config.ppolicy_min_length||12,requireUppercase:this.config.ppolicy_require_uppercase??!0,requireLowercase:this.config.ppolicy_require_lowercase??!0,requireDigit:this.config.ppolicy_require_digit??!0,requireSpecial:this.config.ppolicy_require_special??!0}}},ldapPolicy:this.lastDefaultPolicy}}}export{PasswordPolicy as default};
1
+ import{isDeepStrictEqual as e}from"node:util";import i from"../../abstract/plugin.js";import{asyncHandler as t,normalizeDn as s,escapeLdapFilter as a,validateDnValue as o,escapeDnValue as r}from"../../lib/utils.js";import{jsonBody as n}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as c}from"../../lib/ldapCodes.js";import{NotFoundError as l,ConflictError as d,BadRequestError as p}from"../../lib/errors.js";const u=["pwdChangedTime","pwdAccountLockedTime","pwdFailureTime","pwdGraceUseTime","pwdReset","pwdPolicySubentry"],w={pwdMinAge:"integer",pwdMaxAge:"integer",pwdInHistory:"integer",pwdCheckQuality:"integer",pwdMinLength:"integer",pwdMaxLength:"integer",pwdExpireWarning:"integer",pwdGraceAuthNLimit:"integer",pwdGraceExpiry:"integer",pwdLockout:"boolean",pwdLockoutDuration:"integer",pwdMaxFailure:"integer",pwdFailureCountInterval:"integer",pwdMustChange:"boolean",pwdAllowUserChange:"boolean",pwdSafeModify:"boolean",pwdMinDelay:"integer",pwdMaxDelay:"integer",pwdMaxIdle:"integer",pwdMaxRecordedFailure:"integer",pwdCheckModuleArg:"text",pwdUseCheckModule:"boolean"},h=["pwdCheckModuleArg","pwdUseCheckModule"],y=[...Object.keys(w),"pwdAttribute","pwdCheckModule"],isWritablePolicyAttribute=e=>Object.prototype.hasOwnProperty.call(w,e),values=e=>(Array.isArray(e)?e:void 0===e?[]:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>""!==e),first=e=>values(e)[0],userEntry=e=>({dn:e.dn,userPassword:first(e.userPassword),uid:first(e.uid),cn:first(e.cn),displayName:first(e.displayName),mail:first(e.mail),pwdChangedTime:first(e.pwdChangedTime),pwdAccountLockedTime:first(e.pwdAccountLockedTime),pwdFailureTime:values(e.pwdFailureTime),pwdGraceUseTime:values(e.pwdGraceUseTime),pwdReset:first(e.pwdReset),pwdPolicySubentry:first(e.pwdPolicySubentry)}),typedPolicy=e=>{const i={};for(const t of y){const s=values(e[t]);if(0!==s.length)switch(w[t]){case"integer":{const e=parseInt(s[0],10);isNaN(e)||(i[t]=e);break}case"boolean":i[t]="TRUE"===s[0];break;default:i[t]=1===s.length?s[0]:s}}return i},g=/[!@#$%^&*()_+\-=[\]{}|;:'",.<>?/]/;class PasswordPolicy extends i{constructor(){super(...arguments),this.name="ldapPasswordPolicy",this.roles=["api","configurable"],this.policyCache=new Map,this.discoveredDefault=null,this.warnedAmbiguousDefault=!1,this.warnedMissingBase=!1,this.POLICY_CACHE_TTL=6e4}api(e){const i=`${this.config.api_prefix}/v1`;e.get(`${i}/password-policy`,t(async(e,i)=>{const t=await this.readPolicyConfig();if(t.dn)try{await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["1.1"],paged:!1},t.dn,e)}catch(e){if(32!==c(e))throw e;return this.forgetPolicies(),void i.json({})}i.json(t)})),e.get(`${i}/users/:id/password-status`,t(async(e,i)=>{const t=this.param(e,"id"),s=await this.getPasswordStatus(t,e);i.json(s)})),e.post(`${i}/users/:id/unlock`,t(async(e,i)=>{const t=this.param(e,"id");await this.unlockAccount(t,e),i.json({success:!0,message:"Account unlocked"})})),e.get(`${i}/password-policy/expiring-soon`,t(async(e,i)=>{const t=parseInt(e.query.days)||this.config.ppolicy_warn_days||14,s=await this.findExpiringPasswords(t,e);i.json({warningDays:t,users:s})})),e.get(`${i}/password-policy/locked-accounts`,t(async(e,i)=>{const t=await this.findLockedAccounts(e);i.json({accounts:t})})),e.get(`${i}/password-policies`,t(async(e,i)=>{i.json({policies:await this.listPolicies(e)})})),e.post(`${i}/password-policies`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const s=await this.createPolicy(t,e);i.status(201).json(s)})),e.get(`${i}/password-policies/:cn`,t(async(e,i)=>{const t=await this.findPolicy(this.param(e,"cn"),e);i.json(await this.toPolicyRecord(t))})),e.patch(`${i}/password-policies/:cn`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const s=await this.updatePolicy(this.param(e,"cn"),t,e);i.json(s)})),e.delete(`${i}/password-policies/:cn`,t(async(e,i)=>{await this.deletePolicy(this.param(e,"cn"),e),i.json({success:!0})})),e.put(`${i}/users/:id/password-policy`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const{policy:s}=t;if(null!==s&&("string"!=typeof s||!s))throw new p("policy must be a policy name or null");i.json(await this.assignPolicy(this.param(e,"id"),s,e))})),this.config.ppolicy_validate_complexity&&e.post(`${i}/password/validate`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const{password:s}=t;if(!s)throw new p("password required");const a=this.validateComplexity(s);i.json(a)}))}async getPasswordStatus(e,i){const t=this.resolveUserDn(e),s=await this.readUser(t,e,["*",...u],i),a=await this.policyFor(s);return this.calculateStatus(t,s,a)}async readUser(e,i,t,s){try{const a=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:t,paged:!1},e,s);if(!a.searchEntries||0===a.searchEntries.length)throw new l(`User not found: ${i}`);return userEntry(a.searchEntries[0])}catch(e){if(e instanceof Error&&("NoSuchObjectError"===e.name||e.message.includes("0x20")||e.message.includes("Code: 32")))throw new l(`User not found: ${i}`);throw e}}calculateStatus(e,i,t){const s=Date.now(),a=this.parseGeneralizedTime(i.pwdChangedTime);let o=null,r=!1,n=null;t.pwdMaxAge&&a&&(o=new Date(a.getTime()+1e3*t.pwdMaxAge),r=s>o.getTime(),n=Math.ceil((o.getTime()-s)/864e5));let c=!1;const l=this.config.ppolicy_warn_days||14;null!==n&&n>0&&(c=n<=l);const d=i.pwdAccountLockedTime;let p=!!d;if(p&&!("000001010000Z"===d)&&t.pwdLockoutDuration){const e=this.parseGeneralizedTime(d);if(e){s>new Date(e.getTime()+1e3*t.pwdLockoutDuration).getTime()&&(p=!1)}}const u=i.pwdFailureTime,w=Array.isArray(u)?u.length:0,h=i.pwdGraceUseTime,y=Array.isArray(h)?h.length:0,g="TRUE"===i.pwdReset;return{dn:e,passwordSet:!!i.userPassword,lastChanged:a?.toISOString()||null,expiresAt:o?.toISOString()||null,daysUntilExpiration:n,isExpired:r,isExpiringSoon:c,mustChange:g,isLocked:p,lockedAt:d&&this.parseGeneralizedTime(d)?.toISOString()||null,failureCount:w,graceLoginsUsed:y,policy:t.dn??null}}async unlockAccount(e,i){const t=this.resolveUserDn(e),s=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["pwdAccountLockedTime","pwdFailureTime"],paged:!1},t,i);if(!s.searchEntries||0===s.searchEntries.length)throw new l(`User not found: ${e}`);const a=userEntry(s.searchEntries[0]),o={};a.pwdAccountLockedTime&&(o.pwdAccountLockedTime=[]),a.pwdFailureTime?.length&&(o.pwdFailureTime=[]),Object.keys(o).length>0?(await this.server.ldap.modify(t,{delete:o},i),this.logger.info(`Account unlocked: ${t}`)):this.logger.info(`Account was not locked: ${t}`)}async findExpiringPasswords(e,i){const t=this.config.ldap_users_base||this.config.ldap_base,s=await this.server.ldap.search({filter:"(pwdChangedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdChangedTime","pwdPolicySubentry"],paged:!1},t,i),a=Date.now(),o=24*e*60*60*1e3,r=[];for(const e of s.searchEntries){const i=userEntry(e),t=this.parseGeneralizedTime(i.pwdChangedTime);if(!t)continue;const s=await this.policyFor(i);if(!s.pwdMaxAge)continue;const n=new Date(t.getTime()+1e3*s.pwdMaxAge),c=n.getTime()-a;c>0&&c<o&&r.push({dn:i.dn,uid:i.uid,displayName:i.displayName||i.cn,mail:i.mail,expiresAt:n.toISOString(),daysUntilExpiration:Math.ceil(c/864e5)})}return r.sort((e,i)=>e.daysUntilExpiration-i.daysUntilExpiration)}async findLockedAccounts(e){const i=this.config.ldap_users_base||this.config.ldap_base;return(await this.server.ldap.search({filter:"(pwdAccountLockedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdAccountLockedTime","pwdFailureTime"],paged:!1},i,e)).searchEntries.map(e=>{const i=userEntry(e);return{dn:i.dn,uid:i.uid,displayName:i.displayName||i.cn,mail:i.mail,lockedAt:this.parseGeneralizedTime(i.pwdAccountLockedTime)?.toISOString(),failureCount:Array.isArray(i.pwdFailureTime)?i.pwdFailureTime.length:0}})}async policyFor(e){if(e.pwdPolicySubentry){const i=await this.readPolicyAt(e.pwdPolicySubentry);if(i)return i}return this.readPolicyConfig()}async readPolicyConfig(){const e=this.config.ppolicy_default_dn||await this.discoverDefaultDn();return(e?await this.readPolicyAt(e):null)??{}}async discoverDefaultDn(){if(this.discoveredDefault&&Date.now()-this.discoveredDefault.time<this.POLICY_CACHE_TTL)return this.discoveredDefault.dn;let e=null;try{const i=(await this.server.ldap.search({filter:"(objectClass=pwdPolicy)",scope:"sub",attributes:["1.1"],paged:!1},this.policyBase())).searchEntries.map(e=>e.dn);1===i.length?e=i[0]:i.length>1&&!this.warnedAmbiguousDefault&&(this.warnedAmbiguousDefault=!0,this.logger.warn(`${i.length} password policies found and no --ppolicy-default-dn: accounts without pwdPolicySubentry are reported without a policy`))}catch(e){this.logger.debug(`No ppolicy found: ${e}`)}return this.discoveredDefault={dn:e,time:Date.now()},e}async readPolicyAt(e){const i=s(e),t=this.policyCache.get(i);if(t&&Date.now()-t.time<this.POLICY_CACHE_TTL)return t.policy;let a,o=null;try{const i=await this.server.ldap.search({filter:"(objectClass=pwdPolicy)",scope:"base",attributes:y,paged:!1},e);i.searchEntries.length>0&&(o=((e,i)=>{const t=typedPolicy(e);return{dn:i,pwdMaxAge:t.pwdMaxAge,pwdMinAge:t.pwdMinAge,pwdInHistory:t.pwdInHistory,pwdCheckQuality:t.pwdCheckQuality,pwdMinLength:t.pwdMinLength,pwdMaxFailure:t.pwdMaxFailure,pwdLockout:!0===t.pwdLockout,pwdLockoutDuration:t.pwdLockoutDuration,pwdGraceAuthNLimit:t.pwdGraceAuthNLimit,pwdExpireWarning:t.pwdExpireWarning,pwdMustChange:!0===t.pwdMustChange,pwdAllowUserChange:!1!==t.pwdAllowUserChange}})(i.searchEntries[0],e))}catch(e){a=String(e)}const r=this.config.ppolicy_default_dn;return!o&&r&&s(r)===i?this.logger.warn(`--ppolicy-default-dn ${e} holds no password policy${a?`: ${a}`:""}`):a&&this.logger.debug(`Failed to read ppolicy from ${e}: ${a}`),this.policyCache.set(i,{policy:o,time:Date.now()}),o}forgetPolicies(){this.policyCache.clear(),this.discoveredDefault=null}policyBase(){return this.config.ppolicy_base||this.config.ldap_base}async searchPolicies(e,i){try{return(await this.server.ldap.search({filter:`(&(objectClass=pwdPolicy)${e})`,scope:"sub",attributes:["objectClass","cn",...y],paged:!1},this.policyBase(),i)).searchEntries}catch(e){if(32!==c(e))throw e;return this.warnedMissingBase||(this.warnedMissingBase=!0,this.logger.warn(`${this.policyBase()} does not exist: no password policy is found there, check --ppolicy-base`)),[]}}async findPolicy(e,i){const t=await this.searchPolicies(`(cn=${a(e)})`,i);if(0===t.length)throw new l(`No password policy named ${e}`);if(t.length>1)throw new d(`Several password policies are named ${e}`);return t[0]}async listPolicies(e){const i=[];for(const t of await this.searchPolicies("",e))i.push(await this.toPolicyRecord(t));return i.sort((e,i)=>e.cn.localeCompare(i.cn))}async toPolicyRecord(e){const i=(await this.readPolicyConfig()).dn;return{...{dn:e.dn,cn:values(e.cn)[0]??"",isDefault:!!i&&s(i)===s(e.dn)},...typedPolicy(e)}}policyValue(e,i){switch(w[e]){case"integer":if("number"!=typeof i||!Number.isInteger(i)||i<0||"pwdCheckQuality"===e&&i>2)throw new p("pwdCheckQuality"===e?"pwdCheckQuality must be 0, 1 or 2":`${e} must be a non-negative integer`);return String(i);case"boolean":if("boolean"!=typeof i)throw new p(`${e} must be a boolean`);return i?"TRUE":"FALSE";default:if("string"!=typeof i||""===i)throw new p(`${e} must be a non-empty string`);return i}}async createPolicy(e,i){const{cn:t,...s}=e;if("string"!=typeof t)throw new p("cn required");try{o(t,"cn")}catch(e){throw new p(e.message)}const a=await this.findPolicy(t,i).catch(e=>{if(e instanceof l)return null;throw e});if(a)throw new d(`A password policy is named ${t}: ${a.dn}`);const n=["top","person","pwdPolicy"],u={objectClass:n,cn:t,sn:t,pwdAttribute:"userPassword"};for(const[e,i]of Object.entries(s)){if(!isWritablePolicyAttribute(e))throw new p(`${e} cannot be written`);null!==i&&(u[e]=this.policyValue(e,i))}h.some(e=>e in u)&&n.push("pwdPolicyChecker");try{await this.server.ldap.add(`cn=${r(t)},${this.policyBase()}`,u,i)}catch(e){if(32!==c(e))throw e;throw new l(`${this.policyBase()} does not exist: create it or change --ppolicy-base`)}return this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(t,i))}async updatePolicy(i,t,a){const o=await this.findPolicy(i,a),r=await this.toPolicyRecord(o),n={},c=[];for(const[i,a]of Object.entries(t)){if(!isWritablePolicyAttribute(i)){if("dn"===i?"string"==typeof a&&s(a)===s(r.dn):Object.prototype.hasOwnProperty.call(r,i)&&e(r[i],a))continue;throw new p(`${i} cannot be written`)}null!==a?n[i]=this.policyValue(i,a):void 0!==r[i]&&c.push(i)}const l={};if(Object.keys(n).length>0&&(l.replace=n),c.length>0&&(l.delete=c),!l.replace&&!l.delete)return r;const d=values(o.objectClass).map(e=>e.toLowerCase());return h.some(e=>e in n)&&!d.includes("pwdpolicychecker")&&(l.add={objectClass:"pwdPolicyChecker"}),await this.server.ldap.modify(o.dn,l,a),this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(i,a))}async deletePolicy(e,i){const t=await this.findPolicy(e,i),o=(await this.readPolicyConfig()).dn;if(o&&s(o)===s(t.dn))throw new d(`${t.dn} is the default password policy`);if((await this.server.ldap.search({filter:`(pwdPolicySubentry=${a(t.dn)})`,scope:"sub",attributes:["1.1"],sizeLimit:1,paged:!1},this.config.ldap_base)).searchEntries.length>0)throw new d(`${t.dn} is still applied to at least one entry`);await this.server.ldap.delete(t.dn,i),this.forgetPolicies()}async assignPolicy(e,i,t){const s=this.resolveUserDn(e);if(null===i){try{await this.server.ldap.modify(s,{delete:["pwdPolicySubentry"]},t)}catch(e){if(16!==c(e))throw e}return{dn:s,policy:null}}const a=await this.findPolicy(i,t);return await this.server.ldap.modify(s,{replace:{pwdPolicySubentry:a.dn}},t),{dn:s,policy:a.dn}}param(e,i){const t=e.params[i];return Array.isArray(t)?t[0]:t}parseGeneralizedTime(e){if(!e)return null;const i=e.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);if(!i)return null;const[,t,s,a,o,r,n]=i;return new Date(Date.UTC(+t,+s-1,+a,+o,+r,+n))}validateComplexity(e){const i=[],t=this.config,s=t.ppolicy_min_length||12;return e.length<s&&i.push(`Minimum ${s} characters required`),t.ppolicy_require_uppercase&&!/[A-Z]/.test(e)&&i.push("At least one uppercase letter required"),t.ppolicy_require_lowercase&&!/[a-z]/.test(e)&&i.push("At least one lowercase letter required"),t.ppolicy_require_digit&&!/\d/.test(e)&&i.push("At least one digit required"),t.ppolicy_require_special&&!g.test(e)&&i.push("At least one special character required"),{valid:0===i.length,errors:i}}resolveUserDn(e){if(e.includes("="))return e;const i=this.config.ldap_users_base||this.config.ldap_base;return`${this.config.ldap_user_main_attribute||"uid"}=${r(e)},${i}`}getConfigApiData(){const e=this.config.api_prefix||"/api",i=!!this.config.ppolicy_validate_complexity;return{name:this.name,enabled:!0,endpoints:{getPolicy:`${e}/v1/password-policy`,getUserStatus:`${e}/v1/users/:id/password-status`,unlockUser:`${e}/v1/users/:id/unlock`,getExpiringSoon:`${e}/v1/password-policy/expiring-soon`,getLockedAccounts:`${e}/v1/password-policy/locked-accounts`,listPolicies:`${e}/v1/password-policies`,policy:`${e}/v1/password-policies/:cn`,assignPolicy:`${e}/v1/users/:id/password-policy`,validatePassword:i?`${e}/v1/password/validate`:void 0},config:{warnDays:this.config.ppolicy_warn_days||14,validateComplexity:i,...i&&{complexityRules:{minLength:this.config.ppolicy_min_length||12,requireUppercase:this.config.ppolicy_require_uppercase??!0,requireLowercase:this.config.ppolicy_require_lowercase??!0,requireDigit:this.config.ppolicy_require_digit??!0,requireSpecial:this.config.ppolicy_require_special??!0}}}}}}export{PasswordPolicy as default};
@@ -146,6 +146,14 @@ export interface Config {
146
146
  authz_dynamic_reload_endpoint?: boolean;
147
147
  authz_dynamic_bypass?: string[];
148
148
  authz_local_admin_attribute?: string;
149
+ /**
150
+ * Branches outside the organization tree, such as the password policies,
151
+ * that only the administrators of `authz_global_organization` read and
152
+ * write
153
+ */
154
+ authz_global_branch?: string[];
155
+ /** The organization holding the global branches; the top one by default */
156
+ authz_global_organization?: string;
149
157
  rate_limit_window_ms?: number;
150
158
  rate_limit_max?: number;
151
159
  crowdsec_url?: string;
@@ -58,6 +58,11 @@ export default abstract class AuthzBase extends DmPlugin {
58
58
  * `--authz-filter-attached-entries`, off by default.
59
59
  */
60
60
  protected filtersAttachedEntries(): boolean;
61
+ /**
62
+ * Branches the broad read of `filtersAttachedEntries` leaves out: there,
63
+ * only a caller holding the branch reads. None unless a plugin names some.
64
+ */
65
+ protected globalBranches(): string[];
61
66
  /**
62
67
  * The branch a write is judged against.
63
68
  *
@@ -17,6 +17,8 @@ export default class AuthzLinid1 extends AuthzBase {
17
17
  name: string;
18
18
  permissionsCache: Map<string, CachedPermissions>;
19
19
  constructor(server: DM);
20
+ /** The organization holding the global branches */
21
+ private globalOrganization;
20
22
  /**
21
23
  * Always: the branches this plugin grants are organizations, and what an
22
24
  * administrator manages in them is the accounts and groups whose
@@ -25,6 +27,7 @@ export default class AuthzLinid1 extends AuthzBase {
25
27
  * administrator is refused every account.
26
28
  */
27
29
  protected filtersAttachedEntries(): boolean;
30
+ protected globalBranches(): string[];
28
31
  /**
29
32
  * Resolve user - for authzLinid1, convert uid to userDn via LDAP
30
33
  */
@@ -239,7 +239,6 @@ export default class PasswordPolicy extends DmPlugin {
239
239
  private discoveredDefault;
240
240
  private warnedAmbiguousDefault;
241
241
  private warnedMissingBase;
242
- private lastDefaultPolicy;
243
242
  private readonly POLICY_CACHE_TTL;
244
243
  api(app: Express): void;
245
244
  /**
@@ -108,7 +108,8 @@ way:
108
108
  `;` is split on `;` too.
109
109
  - Options holding DNs or DN expressions (`--ldap-raw-base`,
110
110
  `--james-mailing-list-branch`, `--twake-tombstone-dn`,
111
- `--twake-tombstone-group-bases`, `--twake-instance-dn`): split on `;` only,
111
+ `--twake-tombstone-group-bases`, `--twake-instance-dn`,
112
+ `--authz-global-branches`): split on `;` only,
112
113
  never on `,` nor spaces, as a DN holds both.
113
114
  `DM_LDAP_RAW_BASE="ou=My Unit,dc=example,dc=com"` is one DN; separate
114
115
  several DNs with `;`.
@@ -436,9 +437,11 @@ whichever of the two names it selects. A value other than `req.user` or
436
437
 
437
438
  #### `core/auth/authzLinid1`
438
439
 
439
- | CLI | Env | Default | Description |
440
- | ------------------------------- | -------------------------------- | --------------------- | --------------------- |
441
- | `--authz-local-admin-attribute` | `DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE` | `twakeLocalAdminLink` | Local admin attribute |
440
+ | CLI | Env | Default | Description |
441
+ | --------------------------------------------------- | -------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
442
+ | `--authz-local-admin-attribute` | `DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE` | `twakeLocalAdminLink` | Local admin attribute |
443
+ | `--authz-global-branch` / `--authz-global-branches` | `DM_AUTHZ_GLOBAL_BRANCHES` | | Branches outside the tree only the administrators of `--authz-global-organization` read and write, [details](plugins/auth/authz-linid1.md#global-branches) |
444
+ | `--authz-global-organization` | `DM_AUTHZ_GLOBAL_ORGANIZATION` | `--ldap-top-organization` | The organization holding the global branches |
442
445
 
443
446
  ### Security Plugins
444
447
 
@@ -117,6 +117,29 @@ organization schema's `domainLink` role, or
117
117
  rule belongs to the base of the authorization plugins, so `authzPerBranch`
118
118
  applies it too.
119
119
 
120
+ ### Global branches
121
+
122
+ What is stored outside the organization tree is reference data: every
123
+ administrator reads it, and nobody writes it, since nobody administers its
124
+ parent. `--authz-global-branches` names branches held instead by the
125
+ administrators of one organization, `--authz-global-organization`, the top
126
+ organization by default, and of the organizations above it:
127
+
128
+ ```bash
129
+ --authz-global-branches "ou=ppolicies,dc=example,dc=com" \
130
+ --authz-global-organization "ou=Headquarters,ou=organization,dc=example,dc=com"
131
+ ```
132
+
133
+ They read, write and delete there; any other administrator is refused, even
134
+ a search started above the branch leaves its entries out. That is how only
135
+ the administrators of the whole directory manage the
136
+ [password policies](../ldap/password-policy.md#managing-policies). A global
137
+ branch is no branch of theirs in `GET /authz/scope`, so it adds nothing to
138
+ the organizations they are offered.
139
+
140
+ `--authz-global-organization` must lie outside the global branches: nobody
141
+ would administer it otherwise, and the server refuses to start.
142
+
120
143
  ## Configuration
121
144
 
122
145
  No configuration file needed - permissions are read from LDAP. Simply load the plugin:
@@ -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
@@ -83,7 +83,9 @@ Enable local password complexity validation (for client-side feedback before sub
83
83
  GET /api/v1/password-policy
84
84
  ```
85
85
 
86
- Returns the current ppolicy configuration from LDAP.
86
+ Returns the current ppolicy configuration from LDAP. The caller must be
87
+ allowed to read the policy entry: an authorization plugin keeping it from
88
+ them answers `403`, as for [`/password-policies`](#managing-policies).
87
89
 
88
90
  **Response (200):**
89
91
 
@@ -348,6 +350,13 @@ the OpenLDAP administrator's guide, applied to `userPassword`. A policy of
348
350
  the same name answers `409`. `--ppolicy-base` must exist: otherwise creating
349
351
  answers `404` and listing finds nothing, with a warning in the log.
350
352
 
353
+ **Who manages them:** writes go through the authorization plugins like any
354
+ other. With `authzLinid1`, nobody writes outside the organization tree, and
355
+ every administrator reads there: name `--ppolicy-base` in
356
+ `--authz-global-branches` so that only the administrators of the whole
357
+ directory read and change the policies
358
+ ([global branches](../auth/authz-linid1.md#global-branches)).
359
+
351
360
  **Deleting a policy** is refused (`409`) while it is the default one or an
352
361
  entry's `pwdPolicySubentry` names it. The default one is only known through
353
362
  `--ppolicy-default-dn` when there are several: without it, nothing stops
@@ -516,19 +525,15 @@ The plugin automatically exports its configuration via the `configApi` plugin. W
516
525
  "requireDigit": true,
517
526
  "requireSpecial": true
518
527
  }
519
- },
520
- "ldapPolicy": {
521
- "dn": "cn=default,ou=policies,dc=example,dc=com",
522
- "pwdMaxAge": 7776000,
523
- "pwdMinLength": 12,
524
- "pwdLockout": true
525
528
  }
526
529
  }
527
530
  }
528
531
  }
529
532
  ```
530
533
 
531
- **Note:** The `ldapPolicy` field contains the LDAP ppolicy configuration and is populated after the first request to any passwordPolicy endpoint.
534
+ The policies themselves are not part of it: read them through
535
+ [`GET /password-policy`](#get-password-policy-configuration), which
536
+ authorization plugins judge.
532
537
 
533
538
  ## See Also
534
539
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.21.0",
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
+ }