ldap-rest 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/plugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/options.js +1 -1
- package/dist/config/schema.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/lib/errors.js +1 -1
- package/dist/lib/ldap/callerView.js +1 -0
- package/dist/lib/ldap/pool.js +1 -0
- package/dist/lib/ldap/schema.js +1 -0
- package/dist/lib/ldap/searchCache.js +1 -0
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/parseConfig.js +1 -1
- package/dist/lib/services/index.js +1 -0
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/ldap/accountLifecycle.js +1 -1
- package/dist/plugins/ldap/enterpriseRules.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/passwordPolicy.js +1 -1
- package/dist/plugins/ldap/raw.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/storage/file.js +1 -1
- package/dist/plugins/twake/instances.js +1 -1
- package/dist/plugins/twake/lifecycleAttributes.js +1 -1
- package/dist/plugins/twake/lifecycleEvents.js +1 -1
- package/dist/plugins/twake/tombstone.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +13 -0
- package/dist/src/abstract/plugin.d.ts +21 -0
- package/dist/src/bin/index.d.ts +18 -0
- package/dist/src/config/options.d.ts +16 -1
- package/dist/src/lib/errors.d.ts +2 -1
- package/dist/src/lib/ldap/callerView.d.ts +45 -0
- package/dist/src/lib/ldap/pool.d.ts +108 -0
- package/dist/src/lib/ldap/schema.d.ts +157 -0
- package/dist/src/lib/ldap/searchCache.d.ts +112 -0
- package/dist/src/lib/ldapActions.d.ts +25 -262
- package/dist/src/lib/parseConfig.d.ts +26 -2
- package/dist/src/lib/services/index.d.ts +56 -0
- package/dist/src/lib/utils.d.ts +72 -3
- package/dist/src/plugins/ldap/enterpriseRules.d.ts +0 -14
- package/dist/src/plugins/ldap/passwordPolicy.d.ts +3 -2
- package/dist/src/plugins/scim/mapping.d.ts +16 -3
- package/dist/src/plugins/scim/types.d.ts +2 -0
- package/dist/src/plugins/twake/instances.d.ts +31 -0
- package/dist/src/plugins/twake/lifecycleAttributes.d.ts +13 -2
- package/dist/src/plugins/twake/tombstone.d.ts +9 -2
- package/docs/README.md +16 -2
- package/docs/client-development/api/rest-api.md +4 -0
- package/docs/development/contracts.md +109 -0
- package/docs/plugin-development/README.md +8 -1
- package/docs/plugin-development/dependencies.md +13 -2
- package/docs/plugin-development/hooks.md +2 -1
- package/docs/plugin-development/options.md +40 -13
- package/docs/plugin-development/services.md +95 -0
- package/docs/usage/operations.md +9 -0
- package/docs/usage/plugins/integrations/instances.md +11 -3
- package/docs/usage/plugins/integrations/lifecycle-events.md +4 -1
- package/docs/usage/plugins/integrations/scim.md +20 -18
- package/docs/usage/plugins/integrations/tombstone.md +10 -6
- package/docs/usage/plugins/ldap/flat-generic.md +8 -1
- package/docs/usage/plugins/ldap/groups.md +4 -1
- package/docs/usage/plugins/ldap/password-policy.md +7 -0
- package/docs/usage/upgrading.md +59 -0
- package/package.json +17 -12
- package/static/browser/ldap-tree-viewer/LdapTreeViewer.js.map +1 -1
- package/static/browser/ldap-tree-viewer.esm.js.map +1 -1
- package/static/browser/ldap-tree-viewer.js.map +1 -1
- package/static/browser/ldap-tree-viewer.min.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Type as t}from"@sinclair/typebox";import{assertModifyRequest as e}from"../lib/ldapActions.js";import{jsonBody as i,created as a,tryMethod as s,multiStatus as r,WriteResponse as n}from"../lib/expressFormatedResponses.js";import{getCompiledRegex as o,escapeDnValue as h,escapeRegex as d,validateDnValue as c,isDummyMemberDn as l,normalizeDn as u,namesByDn as m,getParentDn as p,assertedRdnValue as b,escapeLdapFilter as f,substringSearchFilter as w}from"../lib/utils.js";import{neverReturnAttributes as g,assertClientMaySet as y,modifiedAttributeNames as A,toDirectoryEntry as $}from"../config/schema.js";import{applyFixedDefaults as v,EntityValidator as E}from"../lib/entitySchema/validator.js";import{ConflictError as j,BadRequestError as N,NotFoundError as x,HttpError as C,isAuthzRefusal as D,UnprocessableEntityError as k}from"../lib/errors.js";import{extractLdapCode as O}from"../lib/ldapCodes.js";import{loadSchemaFile as S}from"../lib/schemaFile.js";import L from"./plugin.js";import{defineRoute as P}from"../lib/route.js";const _=t.Object({match:t.Optional(t.String()),attribute:t.Optional(t.String()),attributes:t.Optional(t.String()),limit:t.Optional(t.Integer({minimum:1,maximum:Number.MAX_SAFE_INTEGER}))}),I=t.Object({id:t.String({minLength:1})}),q=t.Object({targetOrgDn:t.String({minLength:1})}),z=t.Object({success:t.Boolean(),departmentPath:t.String(),departmentLink:t.String()}),F=t.Object({newId:t.String({minLength:1})});class LdapFlat extends L{base;ldap;mainAttribute;objectClass;defaultAttributes;schema;singularName;pluralName;hookPrefix;ownHooks;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.ownHooks=this.hookBus.prefixed(this.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=S(e.schemaPath,{config:this.config}),this.ldap.knowAttributeNames(Object.keys(this.schema.attributes)),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}`)}}hiddenAttributes(){return g([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[a,s]of Object.entries(t))e.has(a.split(";")[0].toLowerCase())||(i[a]=s);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,a]of Object.entries(t))e[i]=this.project(a);return e}rejectForbiddenInput(t){y(this.schema,t,this.mainAttribute)}async generateMainAttribute(t){const e=this.deriveMainAttribute(t),i=this.schema?.attributes[this.mainAttribute]?.generatedFrom;if(void 0!==e&&i){if(!await this.rdnExists(e))return e;if("suffix"!==i.onCollision)throw new j(`${this.singularName} "${e}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const i=`${e}-${t}`;if(!await this.rdnExists(i))return i}throw new j(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}}deriveMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const a=t[i.attribute],s=Array.isArray(a)?a[0]:a;if(null==s||""===s)throw new N(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let r=String(s);if(i.extract){const t=o(i.extract).exec(r);t&&(r=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(r=r.toLowerCase()),i.strip&&(r=r.replace(o(i.strip,"g"),"")),!r)throw new N(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);return r}candidateEntry(t,e){const i={...this.defaultAttributes,[this.mainAttribute]:t,...e,objectClass:this.objectClass};return v(this.schema,i),i}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${h(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}validator(){return new E(this.schema,{entryExists:async t=>{try{return(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}},loadedPlugins:this.server.loadedPlugins,singularName:this.singularName})}api(t){P(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,query:_,handler:async({req:t,res:e,query:i})=>{const a={};i.match&&i.attribute&&(a.filter=w(i.match,i.attribute)),i.attributes&&(a.attributes=i.attributes.split(","));const s=i.limit;let r;try{r=await this.listEntriesLimited({...a,limit:s,req:t})}catch(t){if(void 0!==s||4!==O(t))throw t;throw new k("Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`.")}r.truncated&&e.set("X-Result-Truncated","true"),e.json(this.projectList(r.entries))}}),P(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:I,handler:async({req:t,res:e})=>this.apiGet(t,e)}),P(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,handler:async({req:t,res:e})=>this.apiAdd(t,e)}),P(t,{method:"delete",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:I,responses:{200:n},handler:async({req:t,res:e})=>this.apiDelete(t,e)}),P(t,{method:"put",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:I,responses:{200:n},handler:async({req:t,res:e})=>this.apiModify(t,e)}),P(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,params:I,body:q,responses:{200:z},handler:async({req:t,res:e})=>this.apiMove(t,e)}),P(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,params:I,body:F,handler:async({req:t,res:e})=>this.apiRename(t,e)})}async apiGet(t,e){const i=decodeURIComponent(t.params.id);try{const a=this.resolveDn(i),s=await this.ldap.search({paged:!1,scope:"base",attributes:this.entryAttributes()},a,t);if(0===s.searchEntries.length)throw new x(`${this.singularName} not found`);e.json(this.project(s.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new x(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const s=this.schema?.attributes[this.mainAttribute],r=Boolean(s?.generated&&s.generatedFrom),n=r?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o={...n};delete o[this.mainAttribute],delete o.dn;const h=r?this.deriveMainAttribute(n):void 0;void 0!==h&&await this.ldap.authorize("ldapaddrequest",[this.resolveDn(h),this.candidateEntry(h,o),t]);const d=r?await this.generateMainAttribute(n):n[this.mainAttribute];await this.addEntry(d,o,t);const c=await this.ldap.system.search({paged:!1,scope:"base",attributes:[this.mainAttribute]},this.resolveDn(d));return a(e,this.project(c.searchEntries[0]))}async apiDelete(t,e){const i=decodeURIComponent(t.params.id);await s(e,this.deleteEntry.bind(this),i,t)}async apiModify(t,a){const r=i(t,a);if(!r)return;e(r),this.rejectForbiddenInput(A(r));const n=decodeURIComponent(t.params.id);await s(a,this.modifyEntry.bind(this),n,r,t)}async apiMove(t,e){const i=t.body,a=decodeURIComponent(t.params.id),{targetOrgDn:s}=i,r=await this.moveEntry(a,s,t);e.json({success:!0,...r})}async apiRename(t,e){const i=t.body,a=i.newId;if(this.rejectForbiddenInput(Object.keys(i).filter(t=>"newId"!==t)),new RegExp(`^${d(this.mainAttribute)}=`,"i").test(a))throw new N(`newId must be a ${this.mainAttribute} value, not a DN`);try{c(a,this.mainAttribute)}catch(t){throw new N(t instanceof Error?t.message:"Invalid newId")}await this.validator().validateValue(this.mainAttribute,a);const s=decodeURIComponent(t.params.id),n=this.resolveDn(s),o=this.resolveDn(a),h=this.config.group_dummy_user;if(h&&(l(n,h)||l(o,h)))throw new j(`${h} is the configured placeholder member and cannot be renamed`);await this.ldap.authorize("ldaprenamerequest",[n,o,t]);const m=u(n)===u(o),p=await this.entryExists(n),b=m?p:await this.entryExists(o),f=this.requirePlugin("ldapEnterpriseRules");let w=[];if(m){if(!p)throw new x(`${this.singularName} not found`)}else if(p){if(b)throw new j(`${this.singularName} "${a}" already exists`);w=await(f?.findReferences(n))||[],await this.renameEntry(n,o,t)}else{if(!b)throw new x(`${this.singularName} not found`);w=await(f?.findReferences(n))||[]}const g=f&&w.length>0?await f.rewriteReferences(w,n,o,t):{updated:0,failed:[]},y={success:0===g.failed.length,dn:o,referencesUpdated:g.updated};0!==g.failed.length?r(e,{...y,referencesFailed:g.failed}):e.json(y)}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(m(t,this.mainAttribute)){const e=p(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new N(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${h(t)},${this.base}`}async addEntry(t,e={},i){let a;if(await this.ldap.canonicalAttributes(e),m(t,this.mainAttribute)){a=this.resolveDn(t);const e=b(t,this.mainAttribute,this.base);if(null===e)throw new N(`${this.mainAttribute} must be a value, or a "${this.mainAttribute}=…,${this.base}" DN`);t=e}else a=this.resolveDn(t);try{c(t,this.mainAttribute)}catch(t){throw new N(t instanceof Error?t.message:"Invalid identifier")}let s=this.candidateEntry(t,e);await this.ldap.authorize("ldapaddrequest",[a,{...s},i]);const r=await this.validateNewEntry(a,{[this.mainAttribute]:t,...e,objectClass:this.objectClass});s={...this.candidateEntry(t,{}),...r},$(this.schema,s),this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[a,s]=await this.ownHooks.chain("add",[a,s]),this.logger.debug("Adding LDAP entry:",{dn:a,entry:JSON.stringify(s,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(s))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);let n;try{n=await this.ldap.add(a,s,i)}catch(t){const e=t instanceof C&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:a,entry:JSON.stringify(s,null,2),error:t}),t instanceof C||D(t))throw t;throw new Error(`Failed to add ${this.singularName} ${a}: ${String(t)}`,{cause:t})}return this.ownHooks.notify("adddone",a,s),n}async modifyEntry(t,e,i){await this.ldap.canonicalChanges(e);let a=this.resolveDn(t);const s=this.opNumber();if([a,e]=await this.ownHooks.chain("modify",[a,e,s]),e.add&&e.add[this.mainAttribute])throw new j(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new N(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new N(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new N(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.ldap.authorize("ldapmodifyrequest",[a,e,s,i]),await this.validateChanges(a,e);const r=await this.ldap.modify(a,e,i);return this.ownHooks.notify("modifydone",a,e,s),r}async renameEntry(t,e,i){/,/.test(t)||c(t,this.mainAttribute),/,/.test(e)||c(e,this.mainAttribute);let a=this.resolveDn(t),s=this.resolveDn(e);[a,s]=await this.ownHooks.chain("rename",[a,s]);const r=await this.ldap.rename(a,s,i);return this.ownHooks.notify("renamedone",a,s),r}async deleteEntry(t,e){let i=this.resolveDn(t);i=await this.ownHooks.chain("delete",i);const a=await this.ldap.delete(i,e);return this.ownHooks.notify("deletedone",i),a}async moveEntry(t,e,i){const a=this.resolveDn(t),s=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",r=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[s]||!this.schema?.attributes[r])throw new N(`Schema for ${this.singularName} does not support move operation (missing ${s} or ${r})`);let n;try{n=await this.ldap.search({paged:!1,scope:"base",attributes:[s]},a,i)}catch(t){if(32===O(t))throw new x(`Entry ${a} not found`);throw t}if(!n.searchEntries||0===n.searchEntries.length)throw new x(`Entry ${a} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await this.ownHooks.chain("move",[a,e,i]);const h={replace:{[s]:e,[r]: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 a=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!a.searchEntries||0===a.searchEntries.length)throw new x(`Organization ${t} not found`);const s=a.searchEntries[0];if(s[i]){const t=s[i];return Array.isArray(t)?String(t[0]):String(t)}const r=s.ou||s.o;if(r){return`/${Array.isArray(r)?String(r[0]):String(r)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){if(e instanceof x)throw e;const i=O(e);if(32===i)throw new x(`Organization ${t} not found`);if(34===i)throw new N(`${t} is not a valid DN`);throw new Error(`Failed to fetch organization ${t}: ${String(e)}`,{cause:e})}}async listEntries(t){return(await this.listEntriesLimited(t)).entries}async listEntriesLimited({filter:t,attributes:e,req:i,limit:a}){const s={paged:void 0===a||{pageSize:Math.min(a+2,100)},filter:t||"(objectClass=*)"};if(e&&e.length>0){const t=this.mainAttribute.toLowerCase();s.attributes=e.some(e=>e.toLowerCase()===t)?e:[...e,this.mainAttribute]}const r=function(t,e){const i={},a=new Set;return{entries:i,add(s){for(const r of s){const s=r[t],n=Array.isArray(s)?s[0]:s;if(null==n||""===n)continue;const o=String(n);if(!a.has(o)){if(void 0!==e&&a.size>=e)return!1;a.add(o)}i[o]=r}return!0}}}(this.mainAttribute,a);try{const t=await this.ldap.search(s,this.base,i);for await(const e of t)if(!r.add(e.searchEntries))return{entries:r.entries,truncated:!0};return{entries:r.entries,truncated:!1}}catch(t){if(void 0===a||4!==O(t))throw t}try{const t=await this.ldap.search({...s,paged:!1,sizeLimit:Math.min(a+1,2147483647)},this.base,i);r.add(t.searchEntries)}catch(t){if(4!==O(t))throw t}return{entries:r.entries,truncated:!0}}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const a=f(t),s=e?`(${this.mainAttribute}=*${a}*)`:`(${this.mainAttribute}=${a})`;return await this.listEntries({filter:s,attributes:i})}async validateNewEntry(t,e){return await this.validator().validateNew(e)}async validateChanges(t,e){return await this.validator().validateChanges(e),!0}}export{I as FlatEntryParams,_ as FlatListQuery,q as FlatMoveBody,z as FlatMoveResponse,F as FlatRenameBody,LdapFlat as default};
|
|
1
|
+
import{Type as t}from"@sinclair/typebox";import{assertModifyRequest as e}from"../lib/ldapActions.js";import{jsonBody as i,created as a,tryMethod as s,multiStatus as r,WriteResponse as n}from"../lib/expressFormatedResponses.js";import{getCompiledRegex as o,escapeDnValue as h,escapeRegex as d,validateDnValue as c,isDummyMemberDn as l,normalizeDn as u,namesByDn as m,getParentDn as p,isDirectChildOf as b,assertRfc4514Escapes as f,assertedRdnValue as w,escapeLdapFilter as g,substringSearchFilter as y}from"../lib/utils.js";import{neverReturnAttributes as A,assertClientMaySet as $,modifiedAttributeNames as v,toDirectoryEntry as E}from"../config/schema.js";import{applyFixedDefaults as j,EntityValidator as N}from"../lib/entitySchema/validator.js";import{ConflictError as x,BadRequestError as D,NotFoundError as O,HttpError as k,isAuthzRefusal as C,UnprocessableEntityError as S}from"../lib/errors.js";import{extractLdapCode as P}from"../lib/ldapCodes.js";import{loadSchemaFile as L}from"../lib/schemaFile.js";import _ from"./plugin.js";import{defineRoute as q}from"../lib/route.js";const z=t.Object({match:t.Optional(t.String()),attribute:t.Optional(t.String()),attributes:t.Optional(t.String()),limit:t.Optional(t.Integer({minimum:1,maximum:Number.MAX_SAFE_INTEGER}))}),F=t.Object({id:t.String({minLength:1})}),I=t.Object({targetOrgDn:t.String({minLength:1})}),M=t.Object({success:t.Boolean(),departmentPath:t.String(),departmentLink:t.String()}),H=t.Object({newId:t.String({minLength:1})});class LdapFlat extends _{base;ldap;mainAttribute;objectClass;defaultAttributes;schema;singularName;pluralName;hookPrefix;ownHooks;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.ownHooks=this.hookBus.prefixed(this.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=L(e.schemaPath,{config:this.config}),this.ldap.knowAttributeNames(Object.keys(this.schema.attributes)),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}`)}}hiddenAttributes(){return A([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[a,s]of Object.entries(t))e.has(a.split(";")[0].toLowerCase())||(i[a]=s);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,a]of Object.entries(t))e[i]=this.project(a);return e}rejectForbiddenInput(t){$(this.schema,t,this.mainAttribute)}async generateMainAttribute(t){const e=this.deriveMainAttribute(t),i=this.schema?.attributes[this.mainAttribute]?.generatedFrom;if(void 0!==e&&i){if(!await this.rdnExists(e))return e;if("suffix"!==i.onCollision)throw new x(`${this.singularName} "${e}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const i=`${e}-${t}`;if(!await this.rdnExists(i))return i}throw new x(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}}deriveMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const a=t[i.attribute],s=Array.isArray(a)?a[0]:a;if(null==s||""===s)throw new D(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let r=String(s);if(i.extract){const t=o(i.extract).exec(r);t&&(r=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(r=r.toLowerCase()),i.strip&&(r=r.replace(o(i.strip,"g"),"")),!r)throw new D(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);return r}candidateEntry(t,e){const i={...this.defaultAttributes,[this.mainAttribute]:t,...e,objectClass:this.objectClass};return j(this.schema,i),i}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${h(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}validator(){return new N(this.schema,{entryExists:async t=>{try{return(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}},loadedPlugins:this.server.loadedPlugins,singularName:this.singularName})}api(t){q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,query:z,handler:async({req:t,res:e,query:i})=>{const a={};i.match&&i.attribute&&(a.filter=y(i.match,i.attribute)),i.attributes&&(a.attributes=i.attributes.split(","));const s=i.limit;let r;try{r=await this.listEntriesLimited({...a,limit:s,req:t})}catch(t){if(void 0!==s||4!==P(t))throw t;throw new S("Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`.")}r.truncated&&e.set("X-Result-Truncated","true"),e.json(this.projectList(r.entries))}}),q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:F,handler:async({req:t,res:e})=>this.apiGet(t,e)}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,handler:async({req:t,res:e})=>this.apiAdd(t,e)}),q(t,{method:"delete",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:F,responses:{200:n},handler:async({req:t,res:e})=>this.apiDelete(t,e)}),q(t,{method:"put",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:F,responses:{200:n},handler:async({req:t,res:e})=>this.apiModify(t,e)}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,params:F,body:I,responses:{200:M},handler:async({req:t,res:e})=>this.apiMove(t,e)}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,params:F,body:H,handler:async({req:t,res:e})=>this.apiRename(t,e)})}async apiGet(t,e){const i=t.params.id;try{const a=this.resolveDn(i),s=await this.ldap.search({paged:!1,scope:"base",attributes:this.entryAttributes()},a,t);if(0===s.searchEntries.length)throw new O(`${this.singularName} not found`);e.json(this.project(s.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new O(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const s=this.schema?.attributes[this.mainAttribute],r=Boolean(s?.generated&&s.generatedFrom),n=r?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o={...n};delete o[this.mainAttribute],delete o.dn;const h=r?this.deriveMainAttribute(n):void 0;void 0!==h&&await this.ldap.authorize("ldapaddrequest",[this.resolveDn(h),this.candidateEntry(h,o),t]);const d=r?await this.generateMainAttribute(n):n[this.mainAttribute];await this.addEntry(d,o,t);const c=await this.ldap.system.search({paged:!1,scope:"base",attributes:[this.mainAttribute]},this.creationOf(d).dn);return a(e,this.project(c.searchEntries[0]))}async apiDelete(t,e){const i=t.params.id;await s(e,this.deleteEntry.bind(this),i,t)}async apiModify(t,a){const r=i(t,a);if(!r)return;e(r),this.rejectForbiddenInput(v(r));const n=t.params.id;await s(a,this.modifyEntry.bind(this),n,r,t)}async apiMove(t,e){const i=t.body,a=t.params.id,{targetOrgDn:s}=i,r=await this.moveEntry(a,s,t);e.json({success:!0,...r})}async apiRename(t,e){const i=t.body,a=i.newId;if(this.rejectForbiddenInput(Object.keys(i).filter(t=>"newId"!==t)),new RegExp(`^${d(this.mainAttribute)}=`,"i").test(a))throw new D(`newId must be a ${this.mainAttribute} value, not a DN`);try{c(a,this.mainAttribute)}catch(t){throw new D(t instanceof Error?t.message:"Invalid newId")}await this.validator().validateValue(this.mainAttribute,a);const s=t.params.id,n=this.resolveDn(s),o=this.resolveDn(a),h=this.config.group_dummy_user;if(h&&(l(n,h)||l(o,h)))throw new x(`${h} is the configured placeholder member and cannot be renamed`);await this.ldap.authorize("ldaprenamerequest",[n,o,t]);const m=u(n)===u(o),p=await this.entryExists(n),b=m?p:await this.entryExists(o),f=this.requirePlugin("ldapEnterpriseRules");let w=[];if(m){if(!p)throw new O(`${this.singularName} not found`)}else if(p){if(b)throw new x(`${this.singularName} "${a}" already exists`);w=await(f?.findReferences(n))||[],await this.renameEntry(n,o,t)}else{if(!b)throw new O(`${this.singularName} not found`);w=await(f?.findReferences(n))||[]}const g=f&&w.length>0?await f.rewriteReferences(w,n,o,t):{updated:0,failed:[]},y={success:0===g.failed.length,dn:o,referencesUpdated:g.updated};0!==g.failed.length?r(e,{...y,referencesFailed:g.failed}):e.json(y)}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(m(t,this.mainAttribute)){const e=p(t),i=e!==t;if(!b(t,this.base))throw new D(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${h(t)},${this.base}`}creationOf(t){if(!m(t,this.mainAttribute))return{name:t,dn:this.resolveDn(t)};f(t,`${this.mainAttribute} DN`),this.resolveDn(t);const e=w(t,this.mainAttribute,this.base);if(null===e)throw new D(`${this.mainAttribute} must be a value, or a "${this.mainAttribute}=…,${this.base}" DN`);return{name:e,dn:`${this.mainAttribute}=${h(e)},${this.base}`}}async addEntry(t,e={},i){if("string"!=typeof t)throw new D(`${this.mainAttribute} must be a string`);await this.ldap.canonicalAttributes(e);const a=this.creationOf(t);t=a.name;let s=a.dn;try{c(t,this.mainAttribute)}catch(t){throw new D(t instanceof Error?t.message:"Invalid identifier")}let r=this.candidateEntry(t,e);await this.ldap.authorize("ldapaddrequest",[s,{...r},i]);const n=await this.validateNewEntry(s,{[this.mainAttribute]:t,...e,objectClass:this.objectClass});r={...this.candidateEntry(t,{}),...n},E(this.schema,r),this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,r]=await this.ownHooks.chain("add",[s,r]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(r,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(r))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);let o;try{o=await this.ldap.add(s,r,i)}catch(t){const e=t instanceof k&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(r,null,2),error:t}),t instanceof k||C(t))throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${String(t)}`,{cause:t})}return this.ownHooks.notify("adddone",s,r),o}async modifyEntry(t,e,i){await this.ldap.canonicalChanges(e);let a=this.resolveDn(t);const s=this.opNumber();if([a,e]=await this.ownHooks.chain("modify",[a,e,s]),e.add&&e.add[this.mainAttribute])throw new x(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new D(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new D(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new D(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.ldap.authorize("ldapmodifyrequest",[a,e,s,i]),await this.validateChanges(a,e);const r=await this.ldap.modify(a,e,i);return this.ownHooks.notify("modifydone",a,e,s),r}async renameEntry(t,e,i){/,/.test(t)||c(t,this.mainAttribute),/,/.test(e)||c(e,this.mainAttribute);let a=this.resolveDn(t),s=this.resolveDn(e);[a,s]=await this.ownHooks.chain("rename",[a,s]);const r=await this.ldap.rename(a,s,i);return this.ownHooks.notify("renamedone",a,s),r}async deleteEntry(t,e){let i=this.resolveDn(t);i=await this.ownHooks.chain("delete",i);const a=await this.ldap.delete(i,e);return this.ownHooks.notify("deletedone",i),a}async moveEntry(t,e,i){f(e,"targetOrgDn");const a=this.resolveDn(t),s=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",r=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[s]||!this.schema?.attributes[r])throw new D(`Schema for ${this.singularName} does not support move operation (missing ${s} or ${r})`);let n;try{n=await this.ldap.search({paged:!1,scope:"base",attributes:[s]},a,i)}catch(t){if(32===P(t))throw new O(`Entry ${a} not found`);throw t}if(!n.searchEntries||0===n.searchEntries.length)throw new O(`Entry ${a} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await this.ownHooks.chain("move",[a,e,i]);const h={replace:{[s]:e,[r]: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 a=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!a.searchEntries||0===a.searchEntries.length)throw new O(`Organization ${t} not found`);const s=a.searchEntries[0];if(s[i]){const t=s[i];return Array.isArray(t)?String(t[0]):String(t)}const r=s.ou||s.o;if(r){return`/${Array.isArray(r)?String(r[0]):String(r)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){if(e instanceof O)throw e;const i=P(e);if(32===i)throw new O(`Organization ${t} not found`);if(34===i)throw new D(`${t} is not a valid DN`);throw new Error(`Failed to fetch organization ${t}: ${String(e)}`,{cause:e})}}async listEntries(t){return(await this.listEntriesLimited(t)).entries}async listEntriesLimited({filter:t,attributes:e,req:i,limit:a}){const s={paged:void 0===a||{pageSize:Math.min(a+2,100)},filter:t||"(objectClass=*)"};if(e&&e.length>0){const t=this.mainAttribute.toLowerCase();s.attributes=e.some(e=>e.toLowerCase()===t)?e:[...e,this.mainAttribute]}const r=function(t,e){const i={},a=new Set;return{entries:i,add(s){for(const r of s){const s=r[t],n=Array.isArray(s)?s[0]:s;if(null==n||""===n)continue;const o=String(n);if(!a.has(o)){if(void 0!==e&&a.size>=e)return!1;a.add(o)}i[o]=r}return!0}}}(this.mainAttribute,a);try{const t=await this.ldap.search(s,this.base,i);for await(const e of t)if(!r.add(e.searchEntries))return{entries:r.entries,truncated:!0};return{entries:r.entries,truncated:!1}}catch(t){if(void 0===a||4!==P(t))throw t}try{const t=await this.ldap.search({...s,paged:!1,sizeLimit:Math.min(a+1,2147483647)},this.base,i);r.add(t.searchEntries)}catch(t){if(4!==P(t))throw t}return{entries:r.entries,truncated:!0}}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const a=g(t),s=e?`(${this.mainAttribute}=*${a}*)`:`(${this.mainAttribute}=${a})`;return await this.listEntries({filter:s,attributes:i})}async validateNewEntry(t,e){return await this.validator().validateNew(e)}async validateChanges(t,e){return await this.validator().validateChanges(e),!0}}export{F as FlatEntryParams,z as FlatListQuery,I as FlatMoveBody,M as FlatMoveResponse,H as FlatRenameBody,LdapFlat as default};
|
package/dist/abstract/plugin.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{providedService as e}from"../lib/services/index.js";export{serviceToken}from"../lib/services/index.js";import{timedOutbound as r}from"../lib/metrics.js";import{neverReturnAttributes as s,withoutAttributes as i}from"../config/schema.js";import{readOptions as o}from"../config/options.js";import t from"../config/args.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{defineRoute,registeredRoutes}from"../lib/route.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";const n=new Set(t.map(e=>e[0].replace(/^--?/,"").replace(/-/g,"_")));class DmPlugin{static optionDefinitions={};server;config;hookBus;logger;registeredHooks={};hooks;dependencies;provides;roles;followedOperationalAttributes;configApiKey;fetch(e,s){return r(this.name,()=>fetch(e,s))}constructor(e){this.server=e,this.config=e.config,this.hookBus=e.hookBus,this.logger=e.logger,this.registeredHooks=e.hooks,this.options(this.constructor.optionDefinitions)}options(e){const r=this.constructor.optionDefinitions;if(e!==r)throw new Error(`${this.constructor.name}: options() takes the definitions the plugin declares`);return o(r,this.config,n,e=>this.logger.warn(e))}opNumber(){return this.server.operationSequence++}hideNeverReturn(e){const r=[],isSchema=e=>"object"==typeof e?.attributes;for(const e of Object.values(this.server.loadedPlugins)){const s=e;if(isSchema(s.schema)&&r.push(s.schema),Array.isArray(s.instances))for(const e of s.instances)isSchema(e.schema)&&r.push(e.schema)}const o=s(r);if(0===o.size||!e||"object"!=typeof e)return e;const project=e=>e&&"object"==typeof e&&!Array.isArray(e)?i(e,o):e;return Array.isArray(e)?e.map(project):"dn"in e?i(e,o):Object.fromEntries(Object.entries(e).map(([e,r])=>[e,project(r)]))}missingPluginsWarned;missingServicesWarned;service(r){const s=e(this.server.services,r);return void 0!==s?s:(this.missingServicesWarned||(this.missingServicesWarned=new Set),this.missingServicesWarned.has(r)||(this.missingServicesWarned.add(r),this.logger.warn(`${this.name}: service '${r.name}' is not provided — its features will be skipped`)),null)}requirePlugin(e){const r=this.server.loadedPlugins[e];return r||(this.missingPluginsWarned||(this.missingPluginsWarned=new Set),this.missingPluginsWarned.has(e)||(this.missingPluginsWarned.add(e),this.logger.warn(`${this.name}: required plugin '${e}' is not loaded — its features will be skipped`)),null)}}export{DmPlugin as default};
|
package/dist/bin/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as e}from"url";import{join as t,dirname as i}from"path";import s from"express";import o from"body-parser";import{ConfigParser as r}from"../lib/parseConfig.js";import{unknownEnvironmentOptions as n}from"../config/options.js";import{keepRawBody as a}from"../lib/rawBody.js";import h from"../config/args.js";import l from"../lib/ldapActions.js";import{buildLogger as u}from"../logger/winston.js";import{setLogger as d,setDebug as c,errorResponse as g,messageOf as p}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as f}from"../lib/auth/base.js";import{assertAuthzComposition as m,assertEnforceable as w,LDAP_JUDGING_HOOKS as b}from"../lib/authz/composition.js";import{registerAttributeTypes as y}from"../lib/utils.js";export{assertedRdnValue,asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,fillPattern,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,movedToTrash,namesByDn,normalizeDn,organizationLink,parseDn,rdnTypes,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";import{errorChain as v}from"../lib/errors.js";export{AuthzRefusedError,BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";import $,{recordHookOwner as P}from"../lib/hookBus.js";export{hookOwner,runAsHookOwner}from"../lib/hookBus.js";import{runWithLogContext as j,runForRequest as k}from"../lib/requestContext.js";import{requestId as S}from"../lib/changeContext.js";import{mountHealth as x,HEALTH_PATHS as M}from"../lib/health.js";import{httpRequestDuration as O,routeTemplate as E,secondsSince as C,unhandledRejections as D}from"../lib/metrics.js";import{registerProcessShutdown as _,shutdownTimeout as A}from"../lib/shutdown.js";import R from"../plugins/priority.json.js";const stackChain=e=>{const t=v(e).map(e=>e.stack).filter(e=>Boolean(e));return t.length?t.join("\nCaused by: "):void 0},L=b.filter(e=>"ldapsearchfilter"!==e);class DM{app;config;ready;get server(){return this.life.server}set server(e){this.life.server=e}hooks={};hookBus=new $(this.hooks);loadedPlugins={};ldap;operationSequence;logger;authenticators=[];_authState={dispatcherMounted:!1};_errorMiddlewareSetup=!1;bodyParsers=s.Router();_composed=!1;get composed(){return this._composed}life={unhandledRejections:0};inFlight=new Set;get stopping(){return void 0!==this.life.shutdown}get unhandledRejections(){return this.life.unhandledRejections}_errorMiddleware;pluginLoads=new Map;unknownCliOptions;authzHooks=new WeakSet;constructor(){const e=new r(h);this.config=e.parse(),this.unknownCliOptions=e.unknownOptions,this.app=s(),this.app.disable("x-powered-by"),this.app.use((e,t,i)=>{const s=process.hrtime.bigint();t.setHeader("X-Content-Type-Options","nosniff"),this.stopping&&t.setHeader("Connection","close"),this.inFlight.add(t),t.once("finish",()=>{O.observe({method:e.method,route:E(e),status:String(t.statusCode)},C(s)),this.stopping&&this.server?.closeIdleConnections()}),t.once("close",()=>this.inFlight.delete(t)),i()}),this.app.use((e,t,i)=>{const s=S(e);t.setHeader("X-Request-Id",s),j({requestId:s},i)}),this.app.use(this.bodyParsers),this.app.use(o.json({verify:a})),this.app.use(o.urlencoded({extended:!0,verify:a})),x(this.app,this),this.logger=u(this.config),this.ldap=new l(this),d(this.logger),c(this.config.debug),this.operationSequence=0;const t=[];if(this.config.plugin){const e=this.config.plugin.find(e=>e.includes("configApi"));let i=this.config.plugin.filter(e=>!e.includes("configApi"));const s=this.logger,o=(async()=>{for(const e of R){const t=this.resolveModule(DM.moduleOf(e)),o=i.filter(e=>this.resolveModule(DM.moduleOf(e))===t);if(0!==o.length){i=i.filter(e=>!o.includes(e));for(const e of o)s.debug(`Loading ${e} in the priority pass`),await this.loadPlugin(e)}}})();t.push(o),t.push(o.then(async()=>{const e=i.map(e=>(s.debug(`Loading ${e} in the parallel batch`),this.loadPlugin(e)));await Promise.all(e)})),e&&t.push(Promise.all(t).then(()=>this.loadPlugin(e)))}this.ready=new Promise((e,i)=>{t.length>0?Promise.all(t).then(async()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),await m(this),w(this),await this.learnAttributeTypes(),this._composed=!0,this.reportUnknownOptions(),this.afterLoad(),e()}).catch(e=>i(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),this._composed=!0,e())})}async learnAttributeTypes(){if(Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz")))try{y((await this.ldap.schemaIndex()).schema.attributeTypes)}catch(e){this.logger.warn(`Could not read the directory schema, DNs are compared with the usual attribute type names only: ${String(e)}`)}}routerStack(){const e=this.app;return(e.router??e._router)?.stack}withConfig(e){return Object.assign(Object.create(Object.getPrototypeOf(this)),this,{config:e})}reportUnknownOptions(){const e=Object.values(this.loadedPlugins).map(e=>e.constructor.optionDefinitions??{}),t=new Set(e.flatMap(e=>Object.keys(e))),i=[...this.unknownCliOptions.filter(e=>!t.has(e.replace(/^--?/,"").replace(/-/g,"_"))).map(e=>`option ${e}`),...n(h,process.env,e).map(e=>`environment variable ${e}`)];if(0===i.length)return;const s=`Unknown configuration: ${i.join(", ")} — no declared option reads it`;if(this.config.strict_options)throw new Error(s);this.logger.warn(s)}mountAuthDispatcher(e){const t=e.roles||[];t.includes("protect")||t.includes("logging")||this.mountAuthDispatcherNow()}mountAuthDispatcherNow(){this._authState.dispatcherMounted||(this._authState.dispatcherMounted=!0,this.app.use((e,t,i)=>{this.dispatchAuth(e,t,i)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}registerBodyParser(e,t){this.bodyParsers.use(e,t)}dispatchAuth(e,t,i){let s=-1,o=[];for(const t of this.authenticators)for(const i of t.pathPrefixes)f(i,e.path)&&(i.length>s?(s=i.length,o=[t]):i.length!==s||o.includes(t)||o.push(t));if(s<0&&(o=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===o.length)return i();const finish=s=>{if(s>=o.length)return k(e,i);const r=o[s];if(!r.afterChain)return finish(s+1);r.afterChain(e,t,()=>finish(s+1))},r=[];e.authenticators=r;const run=i=>{if(i>=o.length)return finish(0);const s=o[i];s.authenticate(e,t,()=>{s.vouchedFor(e)&&r.push(s.name),run(i+1)})};run(0)}afterLoad(){for(const e of Object.values(this.loadedPlugins))try{e.afterLoad?.()}catch(t){this.logger.error(`Plugin ${e.name}: afterLoad failed: ${String(t)}`)}}warnUnauthenticatedRoutes(){const e=Object.values(this.loadedPlugins).filter(e=>e.roles?.includes("auth"));if(0===e.length)return[];const t=[];for(const i of e){if(!i.pathPrefixes||0===i.pathPrefixes.length)return[];t.push(...i.pathPrefixes)}const i=this.routerStack(),s=new Set;for(const e of i||[]){const t=e.route?.path;"string"==typeof t?s.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&s.add(e))}const o=[...s].filter(e=>!M.includes(e)&&!t.some(t=>e===t||e.startsWith(`${t}/`))),r=Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz"));return o.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication`+(r?" nor authorization":"")+`: ${o.sort().join(", ")}`),o.sort()}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.routerStack();if(e){const t=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==t&&e.splice(t,1)}}this._errorMiddleware=(e,t,i,s)=>{const{status:o,body:r}=g(e,this.config.debug);if(o<500)return this.logger.warn(`Client error ${o} in request ${t.method} ${t.path}: ${r.error}`),void(i.headersSent||i.status(o).json(r));this.logger.error(`Server error ${o} in request ${t.method} ${t.path}: ${p(e)}`,{stack:stackChain(e),url:t.url,method:t.method}),i.headersSent||i.status(o).json(r)},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}exitOnCrash(){process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:stackChain(e)}),setTimeout(()=>process.exit(1),this.config.shutdown_timeout),this.shutdown().finally(()=>process.exit(1))}),process.on("unhandledRejection",e=>{this.life.unhandledRejections++,D.inc(),this.logger.error(`Unhandled promise rejection: ${e instanceof Error?e.message:String(e)}`,{stack:stackChain(e)})})}run(){return new Promise((e,t)=>{this.server=this.app.listen(this.config.port,i=>{i?(this.logger.error(`Error starting server: ${i}`),t(i)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}shutdown(){return this.life.shutdown??=this.letGo(),this.life.shutdown}async letGo(){const e=this.config.shutdown_timeout,t=Date.now(),i=Math.max(1e3,e-5e3);this.logger.notice(`Shutting down, ${i} ms for what is in flight`);const s=this.server;if(s){const e=new Promise(e=>s.close(()=>e()));s.closeIdleConnections();for(const e of this.inFlight)e.headersSent||e.setHeader("Connection","close");await DM.within(e,i),s.closeAllConnections()}const o=new Map;for(const[e,t]of Object.entries(this.loadedPlugins))o.set(e,Promise.resolve().then(()=>t.close?.()).catch(t=>{this.logger.warn(`${e}: close failed: ${t instanceof Error?t.message:String(t)}`)}).finally(()=>o.delete(e)));const r=Promise.all(o.values()).then(()=>this.ldap.close().catch(()=>{})),n=Math.max(1e3,e-(Date.now()-t)-1e3);await DM.within(r,n)||(this.logger.warn(`Shut down with a close still pending: ${[...o.keys()].join(", ")||"the directory connections"}`),this.ldap.close().catch(()=>{})),this.logger.notice("Shut down")}static async within(e,t){let i;const s=new Promise(e=>{i=setTimeout(()=>e(!1),t)});try{return await Promise.race([e.then(()=>!0),s])}finally{clearTimeout(i)}}shutdownOnSignals(){A.ms=this.config.shutdown_timeout,_(()=>this.shutdown())}static moduleOf(e){const t=e.indexOf(":");return-1===t?e:e.substring(0,t)}resolveModule(s){return s.startsWith("core/")?s.replace("core/",t(i(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js"):s.startsWith(".")?e(new URL(s,import.meta.url)):s}loadPlugin(e){let t,i;if(/:/.test(e)){let s=e.substring(e.indexOf(":")+1);if(e=e.substring(0,e.indexOf(":")),/:/.test(s)){t=s.substring(0,s.indexOf(":")),t||(t=void 0),s=s.substring(s.indexOf(":")+1);try{i=JSON.parse(s),"object"!=typeof i?(this.logger.error(`Overrides for plugin ${e} are not valid: ${s}`),i=void 0):this.logger.debug(`Overrides for plugin ${t||e}: ${s}`)}catch(t){this.logger.error(`Failed to parse overrides for plugin ${e}: ${t}, using ${s}`),i=void 0}}else t=s,t||(t=void 0)}else t=void 0;const s=i?void 0:`${this.resolveModule(e)}\0${t??""}`,o=void 0===s?void 0:this.pluginLoads.get(s);if(o)return this.logger.debug(`Plugin ${e} already loaded, skipping`),o;const r=this.importPlugin(e,t,i);if(void 0!==s){this.pluginLoads.set(s,r);const forget=()=>{this.pluginLoads.get(s)===r&&this.pluginLoads.delete(s)};r.then(e=>e||forget(),forget)}return r}importPlugin(e,t,i){return this.logger.debug(`Loading plugin ${e}`),e.startsWith("core/")&&(e=this.resolveModule(e)),new Promise((s,o)=>{import(e).then(async r=>{let n;if(r&&r.default&&(r=r.default),i){const e={...this.config,...i};n=new r(this.withConfig(e))}else n=new r(this);if(!n)return o(new Error(`Unable to load ${e}`));s(await this.registerPlugin(e,n,t)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(t=>o(new Error(`Failed to load plugin ${e}: ${t}`)))})}async admitToComposition(e){this.loadedPlugins[e.name]=e;try{await m(this,e)}finally{delete this.loadedPlugins[e.name]}}async registerPlugin(e,t,i){if(t.name||(t.name=e),i&&(t.name=i),this.loadedPlugins[t.name])return this.logger.warn(`Plugin ${e} not registered: the name "${t.name}" is already taken. This instance is dropped and its routes will not exist. To load the same plugin twice, give each one a name: --plugin '${e}:${t.name}2:{…}'`),!1;if(this.logger.debug(`Registering plugin ${e} as ${t.name}`),t.dependencies)for(const e in t.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${t.name} depends on ${e}, loading it first`),await this.loadPlugin(t.dependencies[e]));if(this._composed&&await this.admitToComposition(t),t.api&&(this.mountAuthDispatcher(t),this.logger.debug(`Plugin ${t.name} has API, registering it`),await t.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),t.hooks)for(const e in t.hooks){this.logger.debug(`Plugin ${t.name} has hook ${e}, registering it`);const i=t.hooks[e];if(void 0===i)continue;if("function"!=typeof i)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);const s=e;if(P(i,t.name,e),t.roles?.includes("authz")&&L.includes(s)){const e=this.hookBus.subscribers(s),t=e.findIndex(e=>!this.authzHooks.has(e));this.hookBus.insert(s,i,t<0?e.length:t),this.authzHooks.add(i)}else this.hookBus.subscribe(s,i)}return this.loadedPlugins[t.name]=t,!0}}export{DM,P as recordHookOwner,y as registerAttributeTypes};
|
|
1
|
+
import{fileURLToPath as e}from"url";import{join as t,dirname as i}from"path";import s from"express";import r from"body-parser";import{ConfigParser as o}from"../lib/parseConfig.js";import{unknownEnvironmentOptions as n}from"../config/options.js";import{keepRawBody as a}from"../lib/rawBody.js";import h from"../config/args.js";import l from"../lib/ldapActions.js";import{buildLogger as d}from"../logger/winston.js";import{setLogger as u,setDebug as c,errorResponse as g,messageOf as p}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as f}from"../lib/auth/base.js";import{assertAuthzComposition as m,assertEnforceable as w,LDAP_JUDGING_HOOKS as b}from"../lib/authz/composition.js";import{registerAttributeTypes as v}from"../lib/utils.js";export{assertRfc4514Escapes,assertedRdnValue,asyncHandler,canonicalDn,escapeDnValue,escapeLdapFilter,escapeRegex,fillPattern,getCompiledRegex,getParentDn,getRdn,hasOnlyRfc4514Escapes,isChildOf,isDirectChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,movedToTrash,namesByDn,normalizeDn,organizationLink,parseDirectoryDate,parseDn,rdnTypes,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";import{BadRequestError as y,errorChain as $}from"../lib/errors.js";export{AuthzRefusedError,BadGatewayError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";import P,{recordHookOwner as j}from"../lib/hookBus.js";export{hookOwner,runAsHookOwner}from"../lib/hookBus.js";import{runWithLogContext as k,runForRequest as O}from"../lib/requestContext.js";import{requestId as S}from"../lib/changeContext.js";import{mountHealth as M,HEALTH_PATHS as x}from"../lib/health.js";import{httpRequestDuration as D,routeTemplate as C,secondsSince as E,unhandledRejections as _}from"../lib/metrics.js";import{registerProcessShutdown as A,shutdownTimeout as R}from"../lib/shutdown.js";import T from"../plugins/priority.json.js";const rejectEmptyMediaType=(e,t,i)=>{const s=e.headers["content-type"];(e=>void 0!==e.headers["transfer-encoding"]||!Number.isNaN(Number(e.headers["content-length"])))(e)&&"string"==typeof s&&""===s.split(";",1)[0].trim()?i(new y("Content-Type names no media type")):i()},stackChain=e=>{const t=$(e).map(e=>e.stack).filter(e=>Boolean(e));return t.length?t.join("\nCaused by: "):void 0},L=b.filter(e=>"ldapsearchfilter"!==e);class DM{app;config;ready;get server(){return this.life.server}set server(e){this.life.server=e}hooks={};hookBus=new P(this.hooks);loadedPlugins={};services=new Map;serviceOwners=new Map;ldap;operationSequence;logger;authenticators=[];_authState={dispatcherMounted:!1};_errorMiddlewareSetup=!1;bodyParsers=s.Router();_composed=!1;get composed(){return this._composed}life={unhandledRejections:0};inFlight=new Set;get stopping(){return void 0!==this.life.shutdown}get unhandledRejections(){return this.life.unhandledRejections}_errorMiddleware;pluginLoads=new Map;unknownCliOptions;deprecationWarnings;authzHooks=new WeakSet;constructor(){const e=new o(h);this.config=e.parse(),this.unknownCliOptions=e.unknownOptions,this.deprecationWarnings=e.deprecationWarnings,this.app=s(),this.app.disable("x-powered-by"),this.app.use((e,t,i)=>{const s=process.hrtime.bigint();t.setHeader("X-Content-Type-Options","nosniff"),this.stopping&&t.setHeader("Connection","close"),this.inFlight.add(t),t.once("finish",()=>{D.observe({method:e.method,route:C(e),status:String(t.statusCode)},E(s)),this.stopping&&this.server?.closeIdleConnections()}),t.once("close",()=>this.inFlight.delete(t)),i()}),this.app.use((e,t,i)=>{const s=S(e);t.setHeader("X-Request-Id",s),k({requestId:s},i)}),this.app.use(rejectEmptyMediaType),this.app.use(this.bodyParsers),this.app.use(r.json({verify:a})),this.app.use(r.urlencoded({extended:!0,verify:a})),M(this.app,this),this.logger=d(this.config);for(const e of this.deprecationWarnings)this.logger.warn(e);this.ldap=new l(this),u(this.logger),c(this.config.debug),this.operationSequence=0;const t=[];if(this.config.plugin){const e=this.config.plugin.find(e=>e.includes("configApi"));let i=this.config.plugin.filter(e=>!e.includes("configApi"));const s=this.logger,r=(async()=>{for(const e of T){const t=this.resolveModule(DM.moduleOf(e)),r=i.filter(e=>this.resolveModule(DM.moduleOf(e))===t);if(0!==r.length){i=i.filter(e=>!r.includes(e));for(const e of r)s.debug(`Loading ${e} in the priority pass`),await this.loadPlugin(e)}}})();t.push(r),t.push(r.then(async()=>{const e=i.map(e=>(s.debug(`Loading ${e} in the parallel batch`),this.loadPlugin(e)));await Promise.all(e)})),e&&t.push(Promise.all(t).then(()=>this.loadPlugin(e)))}this.ready=new Promise((e,i)=>{t.length>0?Promise.all(t).then(async()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),await m(this),w(this),await this.learnAttributeTypes(),this._composed=!0,this.reportUnknownOptions(),this.afterLoad(),e()}).catch(e=>i(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),this._composed=!0,e())})}async learnAttributeTypes(){if(Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz")))try{v((await this.ldap.schemaIndex()).schema.attributeTypes)}catch(e){this.logger.warn(`Could not read the directory schema, DNs are compared with the usual attribute type names only: ${String(e)}`)}}routerStack(){const e=this.app;return(e.router??e._router)?.stack}withConfig(e){return Object.assign(Object.create(Object.getPrototypeOf(this)),this,{config:e})}reportUnknownOptions(){const e=Object.values(this.loadedPlugins).map(e=>e.constructor.optionDefinitions??{}),t=new Set(e.flatMap(e=>Object.entries(e).flatMap(([e,t])=>[e,...(t.aliases??[]).map(e=>e.replace(/^--?/,"").replace(/-/g,"_"))]))),i=[...this.unknownCliOptions.filter(e=>!t.has(e.replace(/^--?/,"").replace(/-/g,"_"))).map(e=>`option ${e}`),...n(h,process.env,e).map(e=>`environment variable ${e}`)];if(0===i.length)return;const s=`Unknown configuration: ${i.join(", ")} — no declared option reads it`;if(this.config.strict_options)throw new Error(s);this.logger.warn(s)}mountAuthDispatcher(e){const t=e.roles||[];t.includes("protect")||t.includes("logging")||this.mountAuthDispatcherNow()}mountAuthDispatcherNow(){this._authState.dispatcherMounted||(this._authState.dispatcherMounted=!0,this.app.use((e,t,i)=>{this.dispatchAuth(e,t,i)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}registerBodyParser(e,t){this.bodyParsers.use(e,t)}dispatchAuth(e,t,i){let s=-1,r=[];for(const t of this.authenticators)for(const i of t.pathPrefixes)f(i,e.path)&&(i.length>s?(s=i.length,r=[t]):i.length!==s||r.includes(t)||r.push(t));if(s<0&&(r=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===r.length)return i();const finish=s=>{if(s>=r.length)return O(e,i);const o=r[s];if(!o.afterChain)return finish(s+1);o.afterChain(e,t,()=>finish(s+1))},o=[];e.authenticators=o;const run=i=>{if(i>=r.length)return finish(0);const s=r[i];s.authenticate(e,t,()=>{s.vouchedFor(e)&&o.push(s.name),run(i+1)})};run(0)}afterLoad(){for(const e of Object.values(this.loadedPlugins))try{e.afterLoad?.()}catch(t){this.logger.error(`Plugin ${e.name}: afterLoad failed: ${String(t)}`)}}warnUnauthenticatedRoutes(){const e=Object.values(this.loadedPlugins).filter(e=>e.roles?.includes("auth"));if(0===e.length)return[];const t=[];for(const i of e){if(!i.pathPrefixes||0===i.pathPrefixes.length)return[];t.push(...i.pathPrefixes)}const i=this.routerStack(),s=new Set;for(const e of i||[]){const t=e.route?.path;"string"==typeof t?s.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&s.add(e))}const r=[...s].filter(e=>!x.includes(e)&&!t.some(t=>e===t||e.startsWith(`${t}/`))),o=Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz"));return r.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication`+(o?" nor authorization":"")+`: ${r.sort().join(", ")}`),r.sort()}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.routerStack();if(e){const t=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==t&&e.splice(t,1)}}this._errorMiddleware=(e,t,i,s)=>{const{status:r,body:o}=g(e,this.config.debug);if(r<500)return this.logger.warn(`Client error ${r} in request ${t.method} ${t.path}: ${o.error}`),void(i.headersSent||i.status(r).json(o));this.logger.error(`Server error ${r} in request ${t.method} ${t.path}: ${p(e)}`,{stack:stackChain(e),url:t.url,method:t.method}),i.headersSent||i.status(r).json(o)},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}exitOnCrash(){process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:stackChain(e)}),setTimeout(()=>process.exit(1),this.config.shutdown_timeout),this.shutdown().finally(()=>process.exit(1))}),process.on("unhandledRejection",e=>{this.life.unhandledRejections++,_.inc(),this.logger.error(`Unhandled promise rejection: ${e instanceof Error?e.message:String(e)}`,{stack:stackChain(e)})})}run(){return new Promise((e,t)=>{this.server=this.app.listen(this.config.port,i=>{i?(this.logger.error(`Error starting server: ${i}`),t(i)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}shutdown(){return this.life.shutdown??=this.letGo(),this.life.shutdown}async letGo(){const e=this.config.shutdown_timeout,t=Date.now(),i=Math.max(1e3,e-5e3);this.logger.notice(`Shutting down, ${i} ms for what is in flight`);const s=this.server;if(s){const e=new Promise(e=>s.close(()=>e()));s.closeIdleConnections();for(const e of this.inFlight)e.headersSent||e.setHeader("Connection","close");await DM.within(e,i),s.closeAllConnections()}const r=new Map;for(const[e,t]of Object.entries(this.loadedPlugins))r.set(e,Promise.resolve().then(()=>t.close?.()).catch(t=>{this.logger.warn(`${e}: close failed: ${t instanceof Error?t.message:String(t)}`)}).finally(()=>r.delete(e)));const o=Promise.all(r.values()).then(()=>this.ldap.close().catch(()=>{})),n=Math.max(1e3,e-(Date.now()-t)-1e3);await DM.within(o,n)||(this.logger.warn(`Shut down with a close still pending: ${[...r.keys()].join(", ")||"the directory connections"}`),this.ldap.close().catch(()=>{})),this.logger.notice("Shut down")}static async within(e,t){let i;const s=new Promise(e=>{i=setTimeout(()=>e(!1),t)});try{return await Promise.race([e.then(()=>!0),s])}finally{clearTimeout(i)}}shutdownOnSignals(){R.ms=this.config.shutdown_timeout,A(()=>this.shutdown())}static moduleOf(e){const t=e.indexOf(":");return-1===t?e:e.substring(0,t)}resolveModule(s){return s.startsWith("core/")?s.replace("core/",t(i(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js"):s.startsWith(".")?e(new URL(s,import.meta.url)):s}loadPlugin(e){let t,i;if(/:/.test(e)){let s=e.substring(e.indexOf(":")+1);if(e=e.substring(0,e.indexOf(":")),/:/.test(s)){t=s.substring(0,s.indexOf(":")),t||(t=void 0),s=s.substring(s.indexOf(":")+1);try{i=JSON.parse(s),"object"!=typeof i?(this.logger.error(`Overrides for plugin ${e} are not valid: ${s}`),i=void 0):this.logger.debug(`Overrides for plugin ${t||e}: ${s}`)}catch(t){this.logger.error(`Failed to parse overrides for plugin ${e}: ${t}, using ${s}`),i=void 0}}else t=s,t||(t=void 0)}else t=void 0;const s=i?void 0:`${this.resolveModule(e)}\0${t??""}`,r=void 0===s?void 0:this.pluginLoads.get(s);if(r)return this.logger.debug(`Plugin ${e} already loaded, skipping`),r;const o=this.importPlugin(e,t,i);if(void 0!==s){this.pluginLoads.set(s,o);const forget=()=>{this.pluginLoads.get(s)===o&&this.pluginLoads.delete(s)};o.then(e=>e||forget(),forget)}return o}importPlugin(e,t,i){return this.logger.debug(`Loading plugin ${e}`),e.startsWith("core/")&&(e=this.resolveModule(e)),new Promise((s,r)=>{import(e).then(async o=>{let n;if(o&&o.default&&(o=o.default),i){const e={...this.config,...i};n=new o(this.withConfig(e))}else n=new o(this);if(!n)return r(new Error(`Unable to load ${e}`));s(await this.registerPlugin(e,n,t)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(t=>r(new Error(`Failed to load plugin ${e}: ${t}`)))})}async admitToComposition(e){this.loadedPlugins[e.name]=e;try{await m(this,e)}finally{delete this.loadedPlugins[e.name]}}async registerPlugin(e,t,i){if(t.name||(t.name=e),i&&(t.name=i),this.loadedPlugins[t.name])return this.logger.warn(`Plugin ${e} not registered: the name "${t.name}" is already taken. This instance is dropped and its routes will not exist. To load the same plugin twice, give each one a name: --plugin '${e}:${t.name}2:{…}'`),!1;if(this.logger.debug(`Registering plugin ${e} as ${t.name}`),t.dependencies)for(const e in t.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${t.name} depends on ${e}, loading it first`),await this.loadPlugin(t.dependencies[e]));if(this._composed&&await this.admitToComposition(t),t.api&&(this.mountAuthDispatcher(t),this.logger.debug(`Plugin ${t.name} has API, registering it`),await t.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),t.hooks)for(const e in t.hooks){this.logger.debug(`Plugin ${t.name} has hook ${e}, registering it`);const i=t.hooks[e];if(void 0===i)continue;if("function"!=typeof i)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);const s=e;if(j(i,t.name,e),t.roles?.includes("authz")&&L.includes(s)){const e=this.hookBus.subscribers(s),t=e.findIndex(e=>!this.authzHooks.has(e));this.hookBus.insert(s,i,t<0?e.length:t),this.authzHooks.add(i)}else this.hookBus.subscribe(s,i)}return this.loadedPlugins[t.name]=t,this.registerServices(t),!0}registerServices(e){for(const t of e.provides??[]){const i=this.serviceOwners.get(t.token);void 0===i?(this.services.set(t.token,t.service),this.serviceOwners.set(t.token,e.name)):this.logger.warn(`Plugin ${e.name}: service '${t.token.name}' is already provided by ${i}; this one is ignored`)}}}export{y as BadRequestError,DM,j as recordHookOwner,v as registerAttributeTypes};
|
package/dist/config/options.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{splitValue as e,parseNumber as r}from"../lib/parseConfig.js";function defineOptions(e){return e}const
|
|
1
|
+
import{splitValue as e,parseNumber as r}from"../lib/parseConfig.js";function defineOptions(e){return e}const n={string:(e={})=>({type:"string",...e}),number:(e={})=>({type:"number",...e}),boolean:(e={})=>({type:"boolean",...e}),array:(e={})=>({type:"array",...e}),stringOrFalse:(e={})=>({type:"string",allowFalse:!0,...e}),json:(e={})=>({type:"json",...e})};function cliName(e){return`--${e.replace(/_/g,"-")}`}function cliKey(e){return e.replace(/^--?/,"").replace(/-/g,"_")}function emptyValue(e){switch(e){case"boolean":return!1;case"number":return 0;case"array":return[];case"json":return{};default:return""}}function toConfigEntries(e){return Object.entries(e).map(([e,r])=>{const n=void 0===r.default?emptyValue(r.type):r.default,t=r,o=t.plural?t.plural.startsWith("--")?t.plural:`--${t.plural}`:void 0,i=r.aliases?.map(e=>e.startsWith("--")?e:`--${e}`),s=[cliName(e),r.env??"",n,r.type,o,t.kind,Boolean(r.required)];return i?.length?(s[7]=i,r.envAliases?.length&&(s[8]=[...r.envAliases])):r.envAliases?.length&&(s[8]=[...r.envAliases]),s})}function checkValue(e,r,n){const t=cliName(e);if(r.required&&function(e){return void 0===e||""===e||Array.isArray(e)&&0===e.length}(n))throw new Error(`Option ${t} is required`);switch(r.type){case"string":if("string"!=typeof n&&(!1!==n||!r.allowFalse))throw new Error(`Option ${t} must be a string`);if("string"==typeof n&&r.choices&&!r.choices?.includes(n))throw new Error(`Option ${t} must be one of `+r.choices?.join(", "));break;case"number":!function(e,r,n){if("number"!=typeof n||!Number.isInteger(n))throw new Error(`Option ${e} must be an integer, got "${String(n)}"`);if(void 0!==r.min&&n<r.min)throw new Error(`Option ${e} must be at least ${r.min}, got ${n}`);if(void 0!==r.max&&n>r.max)throw new Error(`Option ${e} must be at most ${r.max}, got ${n}`)}(t,r,n);break;case"boolean":if("boolean"!=typeof n)throw new Error(`Option ${t} must be true or false`);break;case"array":if(!Array.isArray(n)||n.some(e=>"string"!=typeof e))throw new Error(`Option ${t} must be a list of strings`);break;case"json":if(null===n||"object"!=typeof n||Array.isArray(n))throw new Error(`Option ${t} must be a JSON object`)}}function unknownEnvironmentOptions(e,r=process.env,n=[]){const t=new Set(e.flatMap(e=>[e[1],...e[8]??[]]).filter(e=>Boolean(e)));for(const e of n)for(const r of Object.values(e))for(const e of[r.env,...r.envAliases??[]])e&&t.add(e);return Object.keys(r).filter(e=>e.startsWith("DM_")&&!e.startsWith("DM_TEST_")&&!t.has(e))}function valueFromEnvironment(n,t,o){if("array"===t.type){const r=e(o,t.kind,"plural");return r.length>0?r:void 0}if("number"===t.type){if(!o.trim())return;const e=r(o);if(Number.isNaN(e))throw new Error(`Option ${n}: environment value "${o}" is not a number`);return e}if("boolean"===t.type)return"true"===o.toLowerCase();if("json"===t.type){if(!o.trim())return;try{return JSON.parse(o)}catch(e){throw new Error(`Option ${n}: environment value is not JSON: ${String(e)}`)}}return o}function environmentIsSet(r,n){return void 0!==n&&("number"===r.type||"json"===r.type?""!==n.trim():"array"!==r.type||e(n,r.kind,"plural").length>0)}const t=new Set;function reportWarning(e,r){e&&!t.has(r)&&(t.add(r),e(r))}function readOptions(e,r,n=new Set,t){const o=r,i={};for(const[r,s]of Object.entries(e)){const e=[s.env,...s.envAliases??[]].filter(e=>void 0!==e&&environmentIsSet(s,process.env[e]))[0],a=void 0!==e&&e!==s.env,u=n.has(r)||r in o?void 0:(s.aliases??[]).find(e=>cliKey(e)in o),l=r in o||void 0!==u,c=l?o[r]??(void 0===u?void 0:o[cliKey(u)]):!n.has(r)&&e?valueFromEnvironment(r,s,process.env[e]):void 0;void 0!==u&&reportWarning(t,`Configuration: ${u.startsWith("--")?u:`--${u}`} is deprecated, use ${cliName(r)}`),a&&void 0!==e&&!l&&reportWarning(t,`Configuration: ${e} is deprecated, use ${s.env??cliName(r)}`);const p="string"===s.type&&!s.emptyIsValue&&""===c&&void 0!==s.default&&""!==s.default,f=void 0===c||p?void 0===s.default?emptyValue(s.type):Array.isArray(s.default)?s.default.slice():s.default:Array.isArray(c)?c.slice():c;checkValue(r,s,f),i[r]=f}return i}export{defineOptions,n as opt,readOptions,toConfigEntries,unknownEnvironmentOptions};
|
package/dist/config/schema.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BadRequestError as t}from"../lib/errors.js";import{
|
|
1
|
+
import{BadRequestError as t}from"../lib/errors.js";import{assertRfc4514Escapes as e,isDnInBranch as r,getCompiledRegex as n}from"../lib/utils.js";function localizedText(t,e="en"){if(null==t)return;if("string"==typeof t)return t||void 0;const r=e.split("-")[0].toLowerCase();for(const n of[e,r,"en"])if(t[n])return t[n];return Object.values(t).find(Boolean)}function invalidValueMessage(t,e){const r=localizedText(e?.hint||e?.items?.hint);return`Invalid value for attribute "${t}"${r?`: ${r}`:""}`}function tooLongMessage(t,e){return`Attribute "${t}" accepts at most ${e} characters`}function hasRole(t,e){return!!t?.role&&(Array.isArray(t.role)?t.role.includes(e):t.role===e)}function roleAttribute(t,e){if(t)for(const[r,n]of Object.entries(t.attributes))if(hasRole(n,e))return r}function schemaAttribute(t,e){if(!t)return;const r=e.split(";")[0],n=Object.hasOwn(t.attributes,r)?t.attributes[r]:void 0;if(n)return[r,n];const i=r.toLowerCase();for(const[e,r]of Object.entries(t.attributes))if(e.toLowerCase()===i)return[e,r]}function modifiedAttributeNames(t){return[...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete.map(String):Object.keys(t.delete||{})]}function toDirectoryValue(e,r,n){if(null===n||"string"==typeof n||Buffer.isBuffer(n))return n;const i=schemaAttribute(e,r)?.[1],asText=(e,n)=>{if("string"==typeof e||Buffer.isBuffer(e))return e;if("number"==typeof e&&Number.isFinite(e)&&("number"===n||"integer"===n))return String(e);if("boolean"==typeof e&&"boolean"===n)return e?"TRUE":"FALSE";throw new t(`Attribute "${r}" must be a string or an array of strings`)};return Array.isArray(n)?n.map(t=>asText(t,i?.items?.type??i?.type)):asText(n,i?.type)}function toDirectoryEntry(t,e){for(const[r,n]of Object.entries(e))e[r]=toDirectoryValue(t,r,n)}function toDirectoryChanges(t,e){e.add&&toDirectoryEntry(t,e.add),e.replace&&toDirectoryEntry(t,e.replace),e.delete&&!Array.isArray(e.delete)&&toDirectoryEntry(t,e.delete)}function neverReturnAttributes(t){const e=new Set;for(const r of t)if(r)for(const[t,n]of Object.entries(r.attributes))n.neverReturn&&e.add(t.toLowerCase());return e}function withoutAttributes(t,e){if(0===e.size)return t;const r={};for(const[n,i]of Object.entries(t))e.has(n.split(";")[0].toLowerCase())||(r[n]=i);return r}async function checkDnValues(n,i,o,s){const a="pointer"===i.type,u=a?i.branch:i.items?.branch,c=a||"pointer"===i.items?.type;if(!c&&!u?.length)return;if(a&&"string"!=typeof o)throw new t(`Attribute "${n}" must be a string (DN pointer)`);const f=(Array.isArray(o)?o:[o]).map(t=>String(t));if(f.includes(""))throw new t(`Attribute "${n}" must point to a DN`);for(const i of f){if(e(i,`Attribute "${n}"`),u?.length&&!u.some(t=>r(i,t)))throw new t(`Attribute "${n}" must point to a DN within allowed branches: ${u.join(", ")}`);let o=!1;if(c){try{o=await s(i)}catch{}if(!o)throw new t(`Attribute "${n}" points to invalid or non-existent DN: ${i}`)}}}const i=4096;function maxLengthOf(t){return t?.maxLength??t?.items?.maxLength??i}function matchesPattern(t,e){const r=t.test??t.items?.test;if(!r)return!0;const i=maxLengthOf(t),o="string"==typeof r?n(r):r;return(Array.isArray(e)?e:[e]).every(t=>String(t).length<=i&&o.test(String(t)))}function tooLongValue(t,e){const r=t.maxLength??t.items?.maxLength;if(void 0===r&&!(t.test??t.items?.test))return;const n=r??i;return(Array.isArray(e)?e:[e]).find(t=>String(t).length>n)}function assertClientMaySet(e,r,n){if(e)for(const i of r){if("dn"===i)continue;const r=schemaAttribute(e,i);if(!r)continue;const[o,s]=r;if((!n||o.toLowerCase()!==n.toLowerCase()||s.generatedFrom)&&(s.generated||s.readOnly))throw new t(s.readOnly?`Attribute "${i}" is read-only and cannot be set`:`Attribute "${i}" is computed by the server and cannot be set`)}}function missingRequiredAttribute(t,e,r){if(!t)return;const filledByAPlugin=(e,n)=>Object.values(r).some(r=>{const i=r.fillsGeneratedAttribute;return"function"==typeof i&&i.call(r,e,n,t)});for(const[r,n]of Object.entries(t.attributes)){const t=e[r],i=null==t||""===t||Array.isArray(t)&&0===t.length;if(n.required&&i&&(!n.generated||!n.generatedFrom&&!filledByAPlugin(r,n)))return r}}export{i as DEFAULT_MAX_LENGTH,assertClientMaySet,checkDnValues,hasRole,invalidValueMessage,localizedText,matchesPattern,maxLengthOf,missingRequiredAttribute,modifiedAttributeNames,neverReturnAttributes,roleAttribute,schemaAttribute,toDirectoryChanges,toDirectoryEntry,toDirectoryValue,tooLongMessage,tooLongValue,withoutAttributes};
|
package/dist/lib/authz/base.js
CHANGED
|
@@ -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{ServiceUnavailableError as i,AmbiguousIdentityError as n,AuthzRefusedError as a,BadRequestError as o}from"../errors.js";import{getParentDn as h,isDnInBranch as c,getRdn as u,organizationLink as l,normalizeDn as d,escapeLdapFilter as f,parseDn as g}from"../utils.js";import{authzFor as w,servesRequest as p}from"./composition.js";function mostSpecificBranchRule(t,e){let r=null,s=-1;for(const[i,n]of t){if(!c(e,i))continue;const t=g(i).length;t>s&&([r,s]=[n,t])}return r}class AuthzBase extends t{static widenedForLink=Symbol("widened for the link");static topOrganizationRead=Symbol("top organization");roles=["authz"];cacheTTL;static UNRESOLVED_POLICIES=["deny","allow"];constructor(t){super(t),r(this.config,this.constructor.name),w(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)}judgesByAttachment(){return this.filtersAttachedEntries()}globalBranches(){return[]}async searchRefusal(t,e){if(this.filtersAttachedEntries()){if(0===(await this.getAuthorizedBranches(t)).length)return"administers no branch";if(!this.globalBranches().some(t=>c(e,t)))return}return(await this.getUserPermissions(t,e)).read?void 0:`does not have read permission for branch ${e}`}sourcePermission(t){return"read"}async storedDn(t){try{const e=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["1.1"]},t),r=e.searchEntries?.[0]?.dn;if(r)return String(r)}catch(e){if(32!==e?.code)throw this.logger.error(`${this.name}: ${t} could not be read to be authorized: ${String(e)}`),new i("Directory unavailable")}return t}async storedParent(t){const e=h(t);return e===t?t:`${u(t)},${await this.storedDn(e)}`}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.system.search({paged:!1,scope:"base",attributes:[e]},t),s=l(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&&d(t)===e}transitOriginAttribute(){if(void 0===this.transitBranch())return;const t=String(this.config.authz_transit_origin_attribute??"").trim();return t.length>0?t:void 0}async recordTransitOrigin(t,e){const r=this.transitOriginAttribute(),s=this.config.ldap_organization_link_attribute;if(!r||!s)return e;const same=(t,e)=>t.split(";")[0].toLowerCase()===e.toLowerCase(),named=(t,e)=>void 0!==t&&Object.keys(t).some(t=>same(t,e)),deleted=t=>Array.isArray(e.delete)?e.delete.some(e=>same(e,t)):named(e.delete,t);let i,n;if(named(e.replace,s))i=l(AuthzBase.valuesOf(e.replace,s));else if(named(e.add,s))i=l(AuthzBase.valuesOf(e.add,s));else if(!deleted(s))return e;if(named(e.replace,r)||named(e.add,r)||deleted(r))return e;try{n=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[s,r]},t)).searchEntries[0]}catch{}if(!n)return e;const a=l(AuthzBase.valuesOf(n,s)),o=this.inTransit(a),h=this.inTransit(i);return!o&&h&&a?{...e,replace:{...e.replace,[r]:a}}:o&&!h&&AuthzBase.valuesOf(n,r).length>0?{...e,delete:Array.isArray(e.delete)?[...e.delete,r]:{...e.delete,[r]:[]}}:e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:d(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${f(t)})`,r=(await this.server.ldap.system.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new n(t,r.length);if(0===r.length)return null;const s=r[0].dn;return"string"==typeof s?s:String(s)}onlyRelinks(t){const e=[this.config.ldap_organization_link_attribute,this.config.ldap_organization_path_attribute].filter(t=>"string"==typeof t).map(t=>t.toLowerCase());return[...Object.keys(t.add??{}),...Object.keys(t.replace??{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete??{})].every(t=>e.includes(String(t).split(";")[0].toLowerCase()))}async refusingRule(t,e,r,s){const i=d(e);for(const n of await this.ruleBranches(t)){if(!(d(n)===i||s&&c(n,e)))continue;const a=await this.getUserPermissions(t,n);if(r.some(t=>!a[t]))return n}}shouldSkipAuthorization(t){return!t?.user||!p(t,this.config)}resolutionCache=new Map;static RESOLUTION_CACHE_MAX=1e4;resolutionCacheFullWarned=!1;fallbackWarned=!1;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,n=Date.now(),o=this.resolutionCache.get(i);let h;if(o&&n-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)n-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(i,{user:h,at:n}):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 a(`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 organizationLinks(t,e){if(e.some(t=>""===t.trim()))throw new o(`${t} cannot hold an empty value`);const r=[...new Map(e.map(t=>[d(t),t])).values()];if(r.length>1)throw new o(`${t} attaches an entry to one organization, not ${r.length}`);return r}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.system.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(d)),u=s.filter(t=>!o.has(d(t)));if(0===u.length)return;const l=this.config.ldap_top_organization,f=d(l||this.server.ldap.base),g=new Set;let w=h(r);const p=new Set;for(;w&&!p.has(d(w));){const t=d(w);if(p.add(t),t!==f&&!c(w,f))break;for(const t of await this.storedValues(w,i))g.add(d(t));if(t===f)break;const e=h(w);if(e===w)break;w=e}const m=u.filter(t=>!g.has(d(t)));if(0!==m.length&&!await this.mayLinkAnyDomain(t))throw new a(`User ${e.user} may not link ${r} to domain ${m[0]}: no organization above it is linked to it`)}async mayLinkAnyDomain(t){const e=this.config.ldap_top_organization;return!!e&&!0===(await this.getUserPermissions(t,e)).write}clearResolutionCache(){this.resolutionCache.clear()}hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,r,s])=>{if(this.shouldSkipAuthorization(s))return[t,await this.recordTransitOrigin(t,e),r,s];const i=await this.resolveCaller(s);if(!i)return[t,await this.recordTransitOrigin(t,e),r,s];const n=await this.storedDn(t),o=this.config.ldap_organization_link_attribute,h=o?AuthzBase.organizationLinks(o,[...AuthzBase.valuesOf(e.replace,o),...AuthzBase.valuesOf(e.add,o)]):[];if(o&&h.length>0){let r,c=this.extractBranchDn(n),u=!1;try{const e=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[o]},t)).searchEntries[0];e&&(u=!0,r=l(e[o]),r&&(c=r))}catch{}const d=await this.storedDn(h[0]),f=this.filtersAttachedEntries(),g=f&&u&&this.inTransit(r),w=f&&this.inTransit(d);if(!g){const t=w?"write":this.sourcePermission(c);if(!(await this.getUserPermissions(i,c))[t])throw new a(`User ${s.user} does not have ${t} permission for source branch ${c}`)}if(w){if(g){if(0===(await this.getAuthorizedBranches(i)).length)throw new a(`User ${s.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,d)).write)throw new a(`User ${s.user} does not have write permission for destination branch ${d}`)}if(!g&&!this.onlyRelinks(e)){const t=f?c:this.extractBranchDn(n);if(!(await this.getUserPermissions(i,t)).write)throw new a(`User ${s.user} does not have write permission for branch ${t}`)}}else{const t=await this.effectiveBranch(n);if(!(await this.getUserPermissions(i,t)).write)throw new a(`User ${s.user} does not have write permission for branch ${t}`)}const c=await this.refusingRule(i,n,["write"],!1);if(c)throw new a(`User ${s.user} does not have write permission for branch ${c}`);const u=this.domainLinkAttribute();return u&&await this.guardDomainLinks(i,s,t,[...AuthzBase.valuesOf(e.add,u),...AuthzBase.valuesOf(e.replace,u)],u,!1),[t,await this.recordTransitOrigin(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,o=n?AuthzBase.organizationLinks(n,AuthzBase.valuesOf(e,n)):[],h=await this.storedParent(t);i=o.length>0?await this.storedDn(o[0]):this.extractBranchDn(h);if(!(await this.getUserPermissions(s,i)).write)throw new a(`User ${r.user} does not have write permission for branch ${i}`);const c=await this.refusingRule(s,h,["write"],!1);if(c)throw new a(`User ${r.user} does not have write permission for branch ${c}`);const u=this.domainLinkAttribute();return u&&await this.guardDomainLinks(s,r,t,AuthzBase.valuesOf(e,u),u,!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,[AuthzBase.topOrganizationRead]:!0},r];const i=await this.searchRefusal(s,t);if(i)throw new a(`User ${r.user} ${i}`);if(this.filtersAttachedEntries()){const s=this.config.ldap_organization_link_attribute;if(s&&!AuthzBase.wantsAttribute(e,s)){const t=e.attributes,r=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...r,s],[AuthzBase.widenedForLink]:!0}}return[t,e,r]}return[t,e,r]},ldapsearchfilter:async([t,e,r])=>{const s=[t,e,r];if(this.shouldSkipAuthorization(e))return s;if(!t?.searchEntries?.length)return s;const i=await this.resolveCaller(e);if(!i)return s;if(!this.filtersAttachedEntries()){if(r?.[AuthzBase.topOrganizationRead])return s;const n=await this.ruleBranches(i),a=[],o=new Map,c=new Set(n.map(d));for(const r of t.searchEntries){const t=String(r.dn),s=d(h(t)),n=c.has(d(t));let u=n?void 0:o.get(s);void 0===u&&(u=!0===(await this.getUserPermissions(i,t)).read,n||o.set(s,u)),u?a.push(r):this.logger.debug(`${this.name}: hiding ${t} from ${e.user}, refused by a narrower branch rule`)}return t.searchEntries=a,s}const n=this.globalBranches();if(n.length){const r=new Map,s=[];for(const a of t.searchEntries){const t=n.find(t=>c(a.dn,t));void 0===t||(r.has(t)||r.set(t,!0===(await this.getUserPermissions(i,t)).read),r.get(t))?s.push(a):this.logger.debug(`${this.name}: hiding ${String(a.dn)} from ${e.user}, in the global branch ${t}`)}t.searchEntries=s}const a=this.config.ldap_organization_link_attribute;if(!a)return s;const o=await this.getAuthorizedBranches(i);if(0===o.length)return s;const u=this.transitKey(),l=new Map,reads=async t=>{if(e=t,!o.some(t=>c(e,t)))return!1;var e;const r=d(t);return l.has(r)||l.set(r,!0===(await this.getUserPermissions(i,t)).read),l.get(r)},f=[];for(const r of t.searchEntries){const t=r[a],s=null==t?[]:(Array.isArray(t)?t:[t]).map(String);if(0===s.length){f.push(r);continue}let i=!1;for(const t of s)if(await reads(t)){i=!0;break}i||s.some(t=>this.inTransit(t,u))?f.push(r):this.logger.debug(`${this.name}: hiding ${String(r.dn)} from ${e.user}, attached to ${s[0]}`)}if(t.searchEntries=f,r?.[AuthzBase.widenedForLink])for(const e of t.searchEntries)delete e[a];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=await this.storedDn(t),n=await this.storedParent(e),o=this.extractBranchDn(i),h=this.extractBranchDn(n),c=await this.getUserPermissions(s,o),u=this.sourcePermission(o);if(!c[u])throw new a(`User ${r.user} does not have ${u} permission for source branch ${o}`);const l=await this.refusingRule(s,i,["read","write"],!0);if(l)throw new a(`User ${r.user} does not have read and write permission for branch ${l}`);if(!(await this.getUserPermissions(s,h)).write)throw new a(`User ${r.user} does not have write permission for destination branch ${h}`);const d=await this.refusingRule(s,n,["write"],!1);if(d)throw new a(`User ${r.user} does not have write permission for branch ${d}`);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.storedDn(t),i=await this.effectiveBranch(s);if(!(await this.getUserPermissions(r,i)).delete)throw new a(`User ${e.user} does not have delete permission for branch ${i}`);const n=await this.refusingRule(r,s,["delete"],!0);if(n)throw new a(`User ${e.user} does not have delete permission for branch ${n}`)}return[t,e]}}}export{AuthzBase as default,mostSpecificBranchRule};
|
|
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{BadRequestError as i,ServiceUnavailableError as n,AmbiguousIdentityError as a,AuthzRefusedError as o}from"../errors.js";import{getParentDn as h,isDnInBranch as c,getRdn as u,organizationLink as l,normalizeDn as d,escapeLdapFilter as f,parseDn as g}from"../utils.js";import{authzFor as w,servesRequest as p}from"./composition.js";function mostSpecificBranchRule(t,e){let r=null,s=-1;for(const[i,n]of t){if(!c(e,i))continue;const t=g(i).length;t>s&&([r,s]=[n,t])}return r}class AuthzBase extends t{static widenedForLink=Symbol("widened for the link");static topOrganizationRead=Symbol("top organization");roles=["authz"];cacheTTL;static UNRESOLVED_POLICIES=["deny","allow"];constructor(t){super(t),r(this.config,this.constructor.name),w(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)}judgesByAttachment(){return this.filtersAttachedEntries()}globalBranches(){return[]}async searchRefusal(t,e){if(this.filtersAttachedEntries()){if(0===(await this.getAuthorizedBranches(t)).length)return"administers no branch";if(!this.globalBranches().some(t=>c(e,t)))return}return(await this.getUserPermissions(t,e)).read?void 0:`does not have read permission for branch ${e}`}sourcePermission(t){return"read"}async storedDn(t){try{const e=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["1.1"]},t),r=e.searchEntries?.[0]?.dn;if(r)return String(r)}catch(e){if(34===e?.code)throw new i(`Invalid DN: ${t}`);if(32!==e?.code)throw this.logger.error(`${this.name}: ${t} could not be read to be authorized: ${String(e)}`),new n("Directory unavailable")}return t}async storedParent(t){const e=h(t);return e===t?t:`${u(t)},${await this.storedDn(e)}`}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.system.search({paged:!1,scope:"base",attributes:[e]},t),s=l(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&&d(t)===e}transitOriginAttribute(){if(void 0===this.transitBranch())return;const t=String(this.config.authz_transit_origin_attribute??"").trim();return t.length>0?t:void 0}async recordTransitOrigin(t,e){const r=this.transitOriginAttribute(),s=this.config.ldap_organization_link_attribute;if(!r||!s)return e;const same=(t,e)=>t.split(";")[0].toLowerCase()===e.toLowerCase(),named=(t,e)=>void 0!==t&&Object.keys(t).some(t=>same(t,e)),deleted=t=>Array.isArray(e.delete)?e.delete.some(e=>same(e,t)):named(e.delete,t);let i,n;if(named(e.replace,s))i=l(AuthzBase.valuesOf(e.replace,s));else if(named(e.add,s))i=l(AuthzBase.valuesOf(e.add,s));else if(!deleted(s))return e;if(named(e.replace,r)||named(e.add,r)||deleted(r))return e;try{n=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[s,r]},t)).searchEntries[0]}catch{}if(!n)return e;const a=l(AuthzBase.valuesOf(n,s)),o=this.inTransit(a),h=this.inTransit(i);return!o&&h&&a?{...e,replace:{...e.replace,[r]:a}}:o&&!h&&AuthzBase.valuesOf(n,r).length>0?{...e,delete:Array.isArray(e.delete)?[...e.delete,r]:{...e.delete,[r]:[]}}:e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:d(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${f(t)})`,r=(await this.server.ldap.system.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new a(t,r.length);if(0===r.length)return null;const s=r[0].dn;return"string"==typeof s?s:String(s)}onlyRelinks(t){const e=[this.config.ldap_organization_link_attribute,this.config.ldap_organization_path_attribute].filter(t=>"string"==typeof t).map(t=>t.toLowerCase());return[...Object.keys(t.add??{}),...Object.keys(t.replace??{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete??{})].every(t=>e.includes(String(t).split(";")[0].toLowerCase()))}async refusingRule(t,e,r,s){const i=d(e);for(const n of await this.ruleBranches(t)){if(!(d(n)===i||s&&c(n,e)))continue;const a=await this.getUserPermissions(t,n);if(r.some(t=>!a[t]))return n}}shouldSkipAuthorization(t){return!t?.user||!p(t,this.config)}resolutionCache=new Map;static RESOLUTION_CACHE_MAX=1e4;resolutionCacheFullWarned=!1;fallbackWarned=!1;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,n=Date.now(),a=this.resolutionCache.get(i);let h;if(a&&n-a.at<this.cacheTTL)h=a.user;else{if(h=await this.resolveUser(i),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[t,e]of this.resolutionCache)n-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(i,{user:h,at:n}):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 o(`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 organizationLinks(t,e){if(e.some(t=>""===t.trim()))throw new i(`${t} cannot hold an empty value`);const r=[...new Map(e.map(t=>[d(t),t])).values()];if(r.length>1)throw new i(`${t} attaches an entry to one organization, not ${r.length}`);return r}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.system.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(d)),u=s.filter(t=>!a.has(d(t)));if(0===u.length)return;const l=this.config.ldap_top_organization,f=d(l||this.server.ldap.base),g=new Set;let w=h(r);const p=new Set;for(;w&&!p.has(d(w));){const t=d(w);if(p.add(t),t!==f&&!c(w,f))break;for(const t of await this.storedValues(w,i))g.add(d(t));if(t===f)break;const e=h(w);if(e===w)break;w=e}const m=u.filter(t=>!g.has(d(t)));if(0!==m.length&&!await this.mayLinkAnyDomain(t))throw new o(`User ${e.user} may not link ${r} to domain ${m[0]}: no organization above it is linked to it`)}async mayLinkAnyDomain(t){const e=this.config.ldap_top_organization;return!!e&&!0===(await this.getUserPermissions(t,e)).write}clearResolutionCache(){this.resolutionCache.clear()}hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,r,s])=>{if(this.shouldSkipAuthorization(s))return[t,await this.recordTransitOrigin(t,e),r,s];const i=await this.resolveCaller(s);if(!i)return[t,await this.recordTransitOrigin(t,e),r,s];const n=await this.storedDn(t),a=this.config.ldap_organization_link_attribute,h=a?AuthzBase.organizationLinks(a,[...AuthzBase.valuesOf(e.replace,a),...AuthzBase.valuesOf(e.add,a)]):[];if(a&&h.length>0){let r,c=this.extractBranchDn(n),u=!1;try{const e=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[a]},t)).searchEntries[0];e&&(u=!0,r=l(e[a]),r&&(c=r))}catch{}const d=await this.storedDn(h[0]),f=this.filtersAttachedEntries(),g=f&&u&&this.inTransit(r),w=f&&this.inTransit(d);if(!g){const t=w?"write":this.sourcePermission(c);if(!(await this.getUserPermissions(i,c))[t])throw new o(`User ${s.user} does not have ${t} permission for source branch ${c}`)}if(w){if(g){if(0===(await this.getAuthorizedBranches(i)).length)throw new o(`User ${s.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,d)).write)throw new o(`User ${s.user} does not have write permission for destination branch ${d}`)}if(!g&&!this.onlyRelinks(e)){const t=f?c:this.extractBranchDn(n);if(!(await this.getUserPermissions(i,t)).write)throw new o(`User ${s.user} does not have write permission for branch ${t}`)}}else{const t=await this.effectiveBranch(n);if(!(await this.getUserPermissions(i,t)).write)throw new o(`User ${s.user} does not have write permission for branch ${t}`)}const c=await this.refusingRule(i,n,["write"],!1);if(c)throw new o(`User ${s.user} does not have write permission for branch ${c}`);const u=this.domainLinkAttribute();return u&&await this.guardDomainLinks(i,s,t,[...AuthzBase.valuesOf(e.add,u),...AuthzBase.valuesOf(e.replace,u)],u,!1),[t,await this.recordTransitOrigin(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,a=n?AuthzBase.organizationLinks(n,AuthzBase.valuesOf(e,n)):[],h=await this.storedParent(t);i=a.length>0?await this.storedDn(a[0]):this.extractBranchDn(h);if(!(await this.getUserPermissions(s,i)).write)throw new o(`User ${r.user} does not have write permission for branch ${i}`);const c=await this.refusingRule(s,h,["write"],!1);if(c)throw new o(`User ${r.user} does not have write permission for branch ${c}`);const u=this.domainLinkAttribute();return u&&await this.guardDomainLinks(s,r,t,AuthzBase.valuesOf(e,u),u,!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,[AuthzBase.topOrganizationRead]:!0},r];const i=await this.searchRefusal(s,t);if(i)throw new o(`User ${r.user} ${i}`);if(this.filtersAttachedEntries()){const s=this.config.ldap_organization_link_attribute;if(s&&!AuthzBase.wantsAttribute(e,s)){const t=e.attributes,r=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...r,s],[AuthzBase.widenedForLink]:!0}}return[t,e,r]}return[t,e,r]},ldapsearchfilter:async([t,e,r])=>{const s=[t,e,r];if(this.shouldSkipAuthorization(e))return s;if(!t?.searchEntries?.length)return s;const i=await this.resolveCaller(e);if(!i)return s;if(!this.filtersAttachedEntries()){if(r?.[AuthzBase.topOrganizationRead])return s;const n=await this.ruleBranches(i),a=[],o=new Map,c=new Set(n.map(d));for(const r of t.searchEntries){const t=String(r.dn),s=d(h(t)),n=c.has(d(t));let u=n?void 0:o.get(s);void 0===u&&(u=!0===(await this.getUserPermissions(i,t)).read,n||o.set(s,u)),u?a.push(r):this.logger.debug(`${this.name}: hiding ${t} from ${e.user}, refused by a narrower branch rule`)}return t.searchEntries=a,s}const n=this.globalBranches();if(n.length){const r=new Map,s=[];for(const a of t.searchEntries){const t=n.find(t=>c(a.dn,t));void 0===t||(r.has(t)||r.set(t,!0===(await this.getUserPermissions(i,t)).read),r.get(t))?s.push(a):this.logger.debug(`${this.name}: hiding ${String(a.dn)} from ${e.user}, in the global branch ${t}`)}t.searchEntries=s}const a=this.config.ldap_organization_link_attribute;if(!a)return s;const o=await this.getAuthorizedBranches(i);if(0===o.length)return s;const u=this.transitKey(),l=new Map,reads=async t=>{if(e=t,!o.some(t=>c(e,t)))return!1;var e;const r=d(t);return l.has(r)||l.set(r,!0===(await this.getUserPermissions(i,t)).read),l.get(r)},f=[];for(const r of t.searchEntries){const t=r[a],s=null==t?[]:(Array.isArray(t)?t:[t]).map(String);if(0===s.length){f.push(r);continue}let i=!1;for(const t of s)if(await reads(t)){i=!0;break}i||s.some(t=>this.inTransit(t,u))?f.push(r):this.logger.debug(`${this.name}: hiding ${String(r.dn)} from ${e.user}, attached to ${s[0]}`)}if(t.searchEntries=f,r?.[AuthzBase.widenedForLink])for(const e of t.searchEntries)delete e[a];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=await this.storedDn(t),n=await this.storedParent(e),a=this.extractBranchDn(i),h=this.extractBranchDn(n),c=await this.getUserPermissions(s,a),u=this.sourcePermission(a);if(!c[u])throw new o(`User ${r.user} does not have ${u} permission for source branch ${a}`);const l=await this.refusingRule(s,i,["read","write"],!0);if(l)throw new o(`User ${r.user} does not have read and write permission for branch ${l}`);if(!(await this.getUserPermissions(s,h)).write)throw new o(`User ${r.user} does not have write permission for destination branch ${h}`);const d=await this.refusingRule(s,n,["write"],!1);if(d)throw new o(`User ${r.user} does not have write permission for branch ${d}`);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.storedDn(t),i=await this.effectiveBranch(s);if(!(await this.getUserPermissions(r,i)).delete)throw new o(`User ${e.user} does not have delete permission for branch ${i}`);const n=await this.refusingRule(r,s,["delete"],!0);if(n)throw new o(`User ${e.user} does not have delete permission for branch ${n}`)}return[t,e]}}}export{AuthzBase as default,mostSpecificBranchRule};
|
package/dist/lib/errors.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class HttpError extends Error{statusCode;refusal;constructor(r,t=500,o){super(r),this.statusCode=t,this.refusal=o,this.name="HttpError"}}class BadRequestError extends HttpError{constructor(r="Bad request"){super(r,400),this.name="BadRequestError"}}class UnauthorizedError extends HttpError{constructor(r="Unauthorized"){super(r,401),this.name="UnauthorizedError"}}const r="Token does not have permission on this branch",errorBody=(t,o)=>{if(o===r)return{error:o,code:"forbidden"};const e=errorChain(t).find(r=>r instanceof HttpError)?.refusal;return e?{error:o,...e}:{error:o}};class ForbiddenError extends HttpError{constructor(r="Forbidden"){super(r,403),this.name="ForbiddenError"}}class AuthzRefusedError extends ForbiddenError{authzRefusal=!0;constructor(r){super(r),this.name="AuthzRefusedError"}}const errorChain=r=>{const t=[];let o=r;for(;o instanceof Error&&!t.includes(o)&&(t.push(o),32!==t.length);)o=o.cause;return t},isAuthzRefusal=r=>errorChain(r).some(r=>!0===r.authzRefusal),statusCodeOf=r=>{const t
|
|
1
|
+
class HttpError extends Error{statusCode;refusal;constructor(r,t=500,o){super(r),this.statusCode=t,this.refusal=o,this.name="HttpError"}}class BadRequestError extends HttpError{constructor(r="Bad request"){super(r,400),this.name="BadRequestError"}}class UnauthorizedError extends HttpError{constructor(r="Unauthorized"){super(r,401),this.name="UnauthorizedError"}}const r="Token does not have permission on this branch",errorBody=(t,o)=>{if(o===r)return{error:o,code:"forbidden"};const e=errorChain(t).find(r=>r instanceof HttpError)?.refusal;return e?{error:o,...e}:{error:o}};class ForbiddenError extends HttpError{constructor(r="Forbidden"){super(r,403),this.name="ForbiddenError"}}class AuthzRefusedError extends ForbiddenError{authzRefusal=!0;constructor(r){super(r),this.name="AuthzRefusedError"}}const errorChain=r=>{const t=[];let o=r;for(;o instanceof Error&&!t.includes(o)&&(t.push(o),32!==t.length);)o=o.cause;return t},isAuthzRefusal=r=>errorChain(r).some(r=>!0===r.authzRefusal),statusCodeOf=r=>{for(const t of errorChain(r)){const{statusCode:r,status:o}=t;if("number"==typeof r)return r;if("number"==typeof o&&o>=400&&o<600)return o}return 500};class AmbiguousIdentityError extends AuthzRefusedError{identity;count;constructor(r,t){super(`${r} names ${t} directory entries, so no single one can be taken for the caller`),this.identity=r,this.count=t,this.name="AmbiguousIdentityError"}}class NotFoundError extends HttpError{constructor(r="Not found"){super(r,404),this.name="NotFoundError"}}class ConflictError extends HttpError{constructor(r="Conflict",t){super(r,409,t),this.name="ConflictError"}}class UriTooLongError extends HttpError{constructor(r="URI Too Long"){super(r,414),this.name="UriTooLongError"}}class UnprocessableEntityError extends HttpError{constructor(r="Unprocessable Entity"){super(r,422),this.name="UnprocessableEntityError"}}class TooManyRequestsError extends HttpError{constructor(r="Too Many Requests"){super(r,429),this.name="TooManyRequestsError"}}class BadGatewayError extends HttpError{constructor(r="Bad Gateway"){super(r,502),this.name="BadGatewayError"}}class ServiceUnavailableError extends HttpError{constructor(r="Service Unavailable"){super(r,503),this.name="ServiceUnavailableError"}}class GatewayTimeoutError extends HttpError{constructor(r="Gateway Timeout"){super(r,504),this.name="GatewayTimeoutError"}}export{r as AUTHZ_REFUSED,AmbiguousIdentityError,AuthzRefusedError,BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UnprocessableEntityError,UriTooLongError,errorBody,errorChain,isAuthzRefusal,statusCodeOf};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isAuthzRefusal as t}from"../errors.js";import{extractLdapCode as r}from"../ldapCodes.js";import{currentRequest as e}from"../requestContext.js";class CallerView{search;hasFilter;filterHook;constructor(t){this.search=t.search,this.hasFilter=t.hasFilter,this.filterHook=t.filter}async readableBy(s,a){if(a??=e(),!a)return!0;try{const t=await this.search({paged:!1,scope:"base",attributes:["1.1"]},s,a);return(t.searchEntries?.length??0)>0}catch(e){if(t(e)||403===e?.statusCode||32===r(e))return!1;throw e}}filter(t,r,e){if(!this.hasFilter())return t;const one=async t=>await this.filterHook(t,r,e)??t;if("function"==typeof t[Symbol.asyncIterator]){const r=t;return async function*(){for await(const t of r)yield await one(t)}()}return one(t)}}export{CallerView as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Client as t}from"ldapts";import{outsideAnyRequest as n}from"../requestContext.js";import{timedLdap as i,ldapPoolWait as e,secondsSince as o}from"../metrics.js";class LdapPool{urls;dn;pwd;options;poolSize;connectionTtl;logger;clientFactory;connectionPool=[];availableConnections=[];waitingResolvers=[];opening=0;isCleaningUp=!1;currentUrlIndex=0;closed=!1;constructor(n){this.urls=n.urls,this.dn=n.dn,this.pwd=n.pwd,this.options=n.options,this.poolSize=n.poolSize,this.connectionTtl=n.connectionTtl,this.logger=n.logger,this.clientFactory=n.clientFactory??(n=>new t(n)),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`),this.logger.info(`LDAP failover configured with ${this.urls.length} URL(s): ${this.urls.join(", ")}`)}async createConnection(){const t=[],n=this.urls;for(let e=0;e<n.length;e++){const o=(this.currentUrlIndex+e)%n.length,s=n[o];try{this.logger.debug(`Attempting connection to ${s}`);const t={...this.options,url:s,tlsOptions:s.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},e=await this.clientFactory(t);return await i("bind",()=>e.bind(this.dn,this.pwd)),o!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${n[this.currentUrlIndex]} to ${s}`),this.currentUrlIndex=o),e}catch(n){this.logger.warn(`Failed to connect to ${s}: ${String(n)}`),t.push(n)}}throw this.logger.error(`LDAP connection failed for all ${n.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${t.map(t=>t.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const t=Date.now(),n=[];for(let i=this.availableConnections.length-1;i>=0;i--){const e=this.availableConnections[i];if(t-e.createdAt>this.connectionTtl){n.push(e),this.availableConnections.splice(i,1);const t=this.connectionPool.indexOf(e);-1!==t&&this.connectionPool.splice(t,1)}}for(const t of n)t.client.unbind().catch(t=>{this.logger.debug(`Error unbinding expired connection: ${String(t)}`)});n.length>0&&this.logger.debug(`Cleaned up ${n.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async ping(t){if(this.closed)throw new Error("LDAP connections closed");const n=[],i=this.urls;for(let e=0;e<i.length;e++){const o=i[(this.currentUrlIndex+e)%i.length],s={...this.options,url:o,timeout:t,connectTimeout:t,tlsOptions:o.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0};let r;try{return r=await this.clientFactory(s),await r.bind(this.dn,this.pwd),void await r.search("",{scope:"base",attributes:["1.1"]})}catch(t){n.push(`${o}: ${String(t)}`)}finally{await(r?.unbind().catch(()=>{}))}}throw new Error(`No directory answered: ${n.join(", ")}`)}async close(){this.closed=!0,this.refuseWaiters(new Error("LDAP connections closed"));const t=this.connectionPool.splice(0);this.availableConnections.length=0,await Promise.allSettled(t.map(t=>t.client.unbind()))}async acquire(){const t=process.hrtime.bigint();try{return await this.takeConnection()}finally{e.observe(o(t))}}async withConnection(t){const n=await this.acquire();try{return await t(n.client)}finally{this.release(n)}}assertOpen(){if(this.closed)throw new Error("LDAP connections closed")}stats(){return{busy:this.connectionPool.length-this.availableConnections.length,idle:this.availableConnections.length,waiting:this.waitingResolvers.length,opening:this.opening,max:this.poolSize}}async takeConnection(){if(this.assertOpen(),this.cleanupExpiredConnections(),this.availableConnections.length>0){const t=this.availableConnections.pop();return t.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),t}if(this.connectionPool.length+this.opening<this.poolSize){this.opening++;try{const t=await n(()=>this.createConnection());if(this.closed)throw await t.unbind().catch(()=>{}),new Error("LDAP connections closed");const i={client:t,createdAt:Date.now(),inUse:!0};return this.connectionPool.push(i),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),i}catch(t){throw this.refuseWaiters(t instanceof Error?t:new Error(String(t))),t}finally{this.opening--}}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((t,n)=>{const i=globalThis.setTimeout(()=>{const t=this.waitingResolvers.indexOf(settle);-1!==t&&this.waitingResolvers.splice(t,1),n(new Error("LDAP connection pool timeout after 30s"))},3e4),settle=e=>{globalThis.clearTimeout(i),"error"in e?n(e.error):t("retry"in e?this.takeConnection():e.connection)};this.waitingResolvers.push(settle)})}refuseWaiters(t){for(const n of this.waitingResolvers.splice(0))n({error:t})}wakeWaiter(){this.waitingResolvers.shift()?.({retry:!0})}release(t){if(this.closed)t.client.unbind().catch(()=>{});else{if(Date.now()-t.createdAt>this.connectionTtl){const n=this.connectionPool.indexOf(t);return-1!==n&&this.connectionPool.splice(n,1),t.client.unbind().catch(()=>{}),this.logger.debug("Released expired LDAP connection (discarded)"),void this.wakeWaiter()}if(this.waitingResolvers.length>0){const n=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void n({connection:t})}t.inUse=!1,this.availableConnections.push(t),this.logger.debug("Released LDAP connection back to pool")}}}export{LdapPool as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LRUCache as e}from"lru-cache";import{NotFoundError as t,BadRequestError as s}from"../errors.js";import{SchemaIndex as a,parseSchema as r}from"../ldapSchema.js";import{normalizeDn as n,isDnInBranch as i,knownAttributeName as c}from"../utils.js";class DirectorySchema{logger;search;autoRepair;schemaCacheTtl;declaredClasses=new Map;schemaCache;schemaLoading;schemaUnavailable=!1;spellings=new Map;spelledTypes=new Map;classWarnings=new e({max:1e4});constructor(e){this.logger=e.logger,this.search=e.search,this.autoRepair=e.autoRepair,this.schemaCacheTtl=e.schemaCacheTtl}declareObjectClasses(e,t,s=!1){const a=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);if(!e||0===a.length)return;const r=JSON.stringify([n(e),a.map(e=>e.toLowerCase()).sort(),s]);this.declaredClasses.set(r,{base:e,classes:a,includeBase:s})}declaredFor(e){let t=0,s=[];for(const a of this.declaredClasses.values()){if(!i(e,a.base))continue;const r=n(a.base);if(!a.includeBase&&n(e)===r)continue;const c=r.split(",").length;c>t&&(t=c,s=[]),c===t&&s.push(...a.classes)}const a=new Set;return s.filter(e=>{const t=e.toLowerCase();return!a.has(t)&&(a.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t,s=[]){if(!this.autoRepair())return[];const a=[...this.declaredFor(e)];for(const e of s)a.some(t=>t.toLowerCase()===e.toLowerCase())||a.push(e);if(0===a.length)return[];const r=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 n;try{n=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=>n.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:s}=n.resolveAttributes(e);return new Set([...t,...s].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),i=[...valued(t.add),...valued(t.replace)],c=a.filter(e=>{const t=n.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const s=allowedBy([e]);return i.some(e=>s.has(attributeKey(e)))});if(0===c.length)return[];let o;try{o=(await this.search({paged:!1,scope:"base",attributes:["objectClass",...n.resolveAttributes(c).must]},e)).searchEntries[0]}catch{return[]}if(!o)return[];const h=o.objectClass??o.objectclass,l=(Array.isArray(h)?h:h?[h]:[]).map(String),d=allowedBy(l);let u=i.filter(e=>n.getAttributeType(e)&&!d.has(attributeKey(e)));if(0===u.length)return"allowed";const m=new Set(r.map(attributeKey)),g=new Set([...valued(o).filter(e=>!m.has(attributeKey(e))),...i].map(attributeKey)),{auxiliary:b,structural:f}=n.missingClasses(a,l),p=[];for(const t of b){const s=t.names[0]??t.oid,a=allowedBy([s]),r=u.filter(e=>a.has(attributeKey(e)));if(0===r.length)continue;const i=n.resolveAttributes([s]).must.filter(e=>!g.has(attributeKey(e)));i.length>0?this.warnOnce(`${e} needs ${s} for ${r.join(", ")}, and cannot be given it without ${i.join(", ")}`):(p.push(s),u=u.filter(e=>!r.includes(e)))}const y=f.filter(e=>{const t=allowedBy([e]);return u.some(e=>t.has(attributeKey(e)))});return y.length>0&&this.warnOnce(`${e} cannot hold ${u.join(", ")}: its schema declares ${y.join(", ")}, a structural class an existing entry cannot be given`),p.length>0&&this.logger.info(`${e}: adding ${p.join(", ")}, which the change needs and its schema declares`),p}async subschemaDn(){try{const e=await this.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,s=Array.isArray(t)?t[0]:t;if(s)return String(s)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=this.schemaCacheTtl();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(),s=(await this.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(s=>{if(32===s?.code)throw new t(`Subschema entry ${e} not found`);throw s})).searchEntries[0];if(!s)throw new t(`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)},n=new a(r({objectClasses:list(s.objectClasses),attributeTypes:list(s.attributeTypes),ldapSyntaxes:list(s.ldapSyntaxes),matchingRules:list(s.matchingRules)}));return this.schemaCache={index:n,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${n.schema.objectClasses.length} object classes, ${n.schema.attributeTypes.length} attribute types`),n})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}knowAttributeNames(e){for(const t of e){if(!t||t.includes(";"))continue;const e=t.toLowerCase();this.spellings.has(e)||this.spellings.set(e,t);const s=(c(t)??t).toLowerCase(),a=this.spelledTypes.get(s);void 0===a?this.spelledTypes.set(s,t):a!==t&&this.logger.warn(`Attribute "${t}" is also named "${a}" in the configuration or a schema: plugins comparing with one name miss the other`)}}async canonicalAttributes(e){if(!e)return;const t=Object.keys(e),s=await this.canonicalNames(t);if(t.every((e,t)=>e===s[t]))return;const a=t.map(t=>e[t]);for(const s of t)delete e[s];s.forEach((t,s)=>e[t]=a[s])}async canonicalChanges(e){e&&(await this.canonicalAttributes(e.add),await this.canonicalAttributes(e.replace),Array.isArray(e.delete)?e.delete=await this.canonicalNames(e.delete):await this.canonicalAttributes(e.delete))}async canonicalNames(e){if(0===e.length)return e;let t;try{t=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){this.schemaUnavailable||this.logger.warn(`The directory schema could not be read (${String(e)}): attribute names are resolved from the standard types only, a name given as another OID is refused, and object classes are not repaired`),this.schemaUnavailable=!0}const a=new Map;return e.map(e=>{const r=this.canonicalName(e,t),n=a.get(r.toLowerCase());if(void 0!==n)throw new s(`"${n}" and "${e}" name the same attribute`);return a.set(r.toLowerCase(),e),r})}canonicalName(e,t){const[a,...r]=e.split(";"),n=a.replace(/^oid\./i,""),i=r.find(e=>"binary"!==e.toLowerCase());if(void 0!==i)throw new s(`Attribute option "${i}" of "${e}" is not accepted`);if(!/^(?:[A-Za-z][A-Za-z0-9-]*|\d+(?:\.\d+)+)$/.test(n))throw new s(`"${e}" is not an attribute description`);const o=t?.getAttributeType(n),h=t?void 0:c(n);if(!t&&!h&&/^\d+(\.\d+)+$/.test(n))throw new s(`Attribute "${e}" cannot be named by its OID here`);const l=(o?[...o.names,o.oid]:[n,...h?[h]:[]]).map(e=>this.spellings.get(e.toLowerCase())).find(Boolean)??o?.names[0]??h??n;return r.length?`${l};binary`:l}}export{DirectorySchema as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash as e}from"node:crypto";import{LRUCache as t}from"lru-cache";import{normalizeDn as r}from"../utils.js";const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>{try{return r(e)}catch{return e.trim().toLowerCase().split(",").map(e=>e.trim()).join(",")}},s=new Set(["member","uniquemember"]),isMembershipAttribute=e=>s.has(e.toLowerCase()),membersNamed=e=>Object.entries(e??{}).flatMap(([e,t])=>isMembershipAttribute(e)?[t].flat().map(e=>Buffer.isBuffer(e)?e.toString():e).filter(e=>"string"==typeof e):[]),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]});class SearchCache{enabled;logger;searchCache;cacheGeneration=0;constructor(e){this.logger=e.logger,this.enabled=e.ttl>0,this.searchCache=new t({max:e.max,ttl:this.enabled?e.ttl:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.enabled?`LDAP search cache initialized: max=${e.max}, ttl=${e.ttl/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)")}get generation(){return this.cacheGeneration}key(e,t){const r=this.getAttributeSignature(t.attributes),s=t.explicitBufferAttributes?.length?JSON.stringify([...t.explicitBufferAttributes].sort()):"-",i="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",i,r,s,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}get(e){const t=this.searchCache.get(e);return t?cloneSearchResult(t):void 0}set(e,t){this.searchCache.set(e,cloneSearchResult(t))}invalidate(e){if(!this.enabled)return;this.cacheGeneration++;const t=foldDn(e),r=`,${t}`,s=[];for(const e of this.searchCache.keys()){const i=foldDn(e.split("\0")[0]);(i===t||i.endsWith(r))&&s.push(e)}for(const e of s)this.searchCache.delete(e)}invalidateReferences(e){if(!this.enabled)return;this.cacheGeneration++;const t=foldDn(e),r=`,${t}`,names=e=>{if("string"!=typeof e)return!1;const s=foldDn(e);return s===t||s.endsWith(r)},s=[];for(const[e,t]of this.searchCache.entries())t.searchEntries.some(e=>Object.values(e).some(e=>Array.isArray(e)?e.some(names):names(e)))&&s.push(e);for(const e of s)this.searchCache.delete(e)}invalidateMembership(e,t){if([t.add,t.replace,t.delete].flatMap(e=>Array.isArray(e)?e:Object.keys(e??{})).some(isMembershipAttribute)){this.invalidateReferences(e);for(const e of[t.add,t.replace,t.delete])for(const t of membersNamed(Array.isArray(e)?{}:e))this.invalidate(t)}}getAttributeSignature(e){return e&&0!==e.length?JSON.stringify([...e].sort()):"*"}}export{SearchCache as default,digestKey,membersNamed};
|
package/dist/lib/ldapActions.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as n,Control as s}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{normalizeDn as o,isDnInBranch as c,knownAttributeName as l,rdnTypes as h,escapeDnValue as d,getParentDn as u}from"./utils.js";import{changeContext as f}from"./changeContext.js";import p,{hookOwner as g}from"./hookBus.js";import{runOperation as y,outsideOperation as w}from"./operation.js";import{outsideAnyRequest as m,outsideRequest as b,currentRequest as A}from"./requestContext.js";import{timedLdap as C,ldapPoolWait as v,secondsSince as $,ldapSearchCache as L,ldapResult as D,ldapOperationDuration as j}from"./metrics.js";import{BadRequestError as S,isAuthzRefusal as P,NotFoundError as E,ConflictError as x}from"./errors.js";import{extractLdapCode as R,isSchemaRefusal as O}from"./ldapCodes.js";import{SchemaIndex as k,parseSchema as _}from"./ldapSchema.js";const U={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}},B={ldapaddrequest:2,ldapmodifyrequest:3,ldaprenamerequest:2,ldapdeleterequest:1},q=new Set(["search","add","modify","rename","move","delete"]),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 S(`A modification carries add, replace or delete, not ${t.join(", ")}`);const{add:i,replace:n,delete:s}=e;for(const[e,t]of[["add",i],["replace",n]])if(void 0!==t&&!isPlainObject(t))throw new S(`"${e}" must map attributes to values`);if(!(void 0===s||isPlainObject(s)||Array.isArray(s)&&s.every(e=>"string"==typeof e)))throw new S('"delete" must list attribute names or map attributes to values');if(0===[i,n,s].reduce((e,t)=>e+(t?Object.keys(t).length:0),0))throw new S("A modification names at least one attribute in add, replace or delete")}function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),n=R(t);return void 0===n?i:withCode(i,n)}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(","),T=new Set(["member","uniquemember"]),isMembershipAttribute=e=>T.has(e.toLowerCase()),membersNamed=e=>Object.entries(e??{}).flatMap(([e,t])=>isMembershipAttribute(e)?[t].flat().map(e=>Buffer.isBuffer(e)?e.toString():e).filter(e=>"string"==typeof e):[]),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]}),W=["*","+","1.1"],dropSelectors=e=>{for(const t of e.searchEntries)for(const e of W){const i=t[e];Array.isArray(i)&&0===i.length&&delete t[e]}return e};function ldapRefusal(e,t,i){return withCode(new S(`${e}: ${String(i)}`),t)}function assertHasValue(e,t){if((e=>{const absent=e=>null==e;return Array.isArray(e)?0===e.length||e.some(e=>absent(e)):absent(e)})(t))throw new S(`Attribute "${e}" has no value`)}function ldapWriteError(e,t,i){const n=R(i);return O(n)?ldapRefusal(`The directory refused this write to ${t}`,n,i):ldapError(e,i)}function ldapRenameError(e,t,i,n){const s=R(n);return 68===s?withCode(new x(`Entry ${i} already exists`),s):O(s)?ldapRefusal(`The directory refused to rename ${t} to ${i}`,s,n):ldapError(e,n)}class ldapActions{config;options;dn;pwd;base;parent;logger;searchCache;cacheEnabled;cacheGeneration=0;queryLimit;connectionPool=[];poolSize;connectionTtl;ldapUrls;currentUrlIndex=0;attrSignatureCache;waitingResolvers=[];availableConnections=[];opening=0;isCleaningUp=!1;declaredClasses=new Map;schemaCache;schemaLoading;schemaUnavailable=!1;spellings=new Map;spelledTypes=new Map;classWarnings=new a({max:1e4});constructor(e){this.parent=e,this.logger=e.logger,this.config=e.config;for(const[e,t]of Object.entries(this.config))/_attributes?$/.test(e)&&this.knowAttributeNames([t].flat().filter(e=>"string"==typeof e));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,n=this.config.ldap_cache_ttl,s="string"==typeof n?parseInt(n,10)||0:n??0,o=1e3*Math.max(s,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 bind DN is not defined, please set --ldap-dn (or DM_LDAP_DN)");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined, please set --ldap-pwd (or DM_LDAP_PWD)");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 n=(this.currentUrlIndex+i)%this.ldapUrls.length,s=this.ldapUrls[n];try{this.logger.debug(`Attempting connection to ${s}`);const e={...this.options,url:s,tlsOptions:s.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await C("bind",()=>i.bind(this.dn,this.pwd)),n!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${s}`),this.currentUrlIndex=n),i}catch(t){this.logger.warn(`Failed to connect to ${s}: ${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 n=this.availableConnections[i];if(e-n.createdAt>this.connectionTtl){t.push(n),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(n);-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 ping(e){if(this.closed)throw new Error("LDAP connections closed");const i=[];for(let n=0;n<this.ldapUrls.length;n++){const s=this.ldapUrls[(this.currentUrlIndex+n)%this.ldapUrls.length],a=new t({...this.options,url:s,timeout:e,connectTimeout:e,tlsOptions:s.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0});try{return await a.bind(this.dn,this.pwd),void await a.search("",{scope:"base",attributes:["1.1"]})}catch(e){i.push(`${s}: ${String(e)}`)}finally{await a.unbind().catch(()=>{})}}throw new Error(`No directory answered: ${i.join(", ")}`)}closed=!1;async close(){this.closed=!0,this.refuseWaiters(new Error("LDAP connections closed"));const e=this.connectionPool.splice(0);this.availableConnections.length=0,await Promise.allSettled(e.map(e=>e.client.unbind()))}async acquireConnection(){const e=process.hrtime.bigint();try{return await this.takeConnection()}finally{v.observe($(e))}}poolStats(){return{busy:this.connectionPool.length-this.availableConnections.length,idle:this.availableConnections.length,waiting:this.waitingResolvers.length,opening:this.opening,max:this.poolSize}}async takeConnection(){if(this.closed)throw new Error("LDAP connections closed");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.opening<this.poolSize){this.opening++;try{const e=await m(()=>this.createConnection());if(this.closed)throw await e.unbind().catch(()=>{}),new Error("LDAP connections closed");const t={client:e,createdAt:Date.now(),inUse:!0};return this.connectionPool.push(t),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),t}catch(e){throw this.refuseWaiters(e instanceof Error?e:new Error(String(e))),e}finally{this.opening--}}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(settle);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),settle=n=>{globalThis.clearTimeout(i),"error"in n?t(n.error):e("retry"in n?this.takeConnection():n.connection)};this.waitingResolvers.push(settle)})}refuseWaiters(e){for(const t of this.waitingResolvers.splice(0))t({error:e})}wakeWaiter(){this.waitingResolvers.shift()?.({retry:!0})}releaseConnection(e){if(this.closed)e.client.unbind().catch(()=>{});else{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(()=>{}),this.logger.debug("Released expired LDAP connection (discarded)"),void this.wakeWaiter()}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t({connection: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),n=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",s="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",s,i,n,!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}`,n=[];for(const e of this.searchCache.keys()){const s=foldDn(e.split("\0")[0]);(s===t||s.endsWith(i))&&n.push(e)}for(const e of n)this.searchCache.delete(e)}invalidateReferences(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),i=`,${t}`,names=e=>{if("string"!=typeof e)return!1;const n=foldDn(e);return n===t||n.endsWith(i)},n=[];for(const[e,t]of this.searchCache.entries())t.searchEntries.some(e=>Object.values(e).some(e=>Array.isArray(e)?e.some(names):names(e)))&&n.push(e);for(const e of n)this.searchCache.delete(e)}get system(){return this.systemView??=new Proxy(this,{get:(e,t)=>{const i=Reflect.get(e,t,e);return"function"==typeof i&&q.has(t)?(...t)=>b(()=>i.apply(e,t)):i}}),this.systemView}systemView;forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i,n)=>this.modify(t,i,e,n),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}async readableBy(e,t){if(t??=A(),!t)return!0;try{const i=await this.search({paged:!1,scope:"base",attributes:["1.1"]},e,t);return(i.searchEntries?.length??0)>0}catch(e){if(P(e)||403===e?.statusCode||32===R(e))return!1;throw e}}async authorize(e,t){const i=[...t];"ldapaddrequest"===e&&await this.canonicalAttributes(i[1]),"ldapmodifyrequest"===e&&await this.canonicalChanges(i[1]);const n=B[e];i[n]??=A();const[s,...a]=i,r="ldapdeleterequest"===e?[[s].flat().map(e=>this.setDn(String(e))),...a]:[this.setDn(String(s)),...a];"ldaprenamerequest"===e&&(r[1]=this.setDn(String(r[1])));const o=this.parent.hookBus.subscribers(e).filter(e=>{const t=g(e);return Boolean(t&&this.parent.loadedPlugins[t.plugin]?.roles?.includes("authz"))}),c=new p({[e]:o});await c.chain(e,r)}declareObjectClasses(e,t,i=!1){const n=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);if(!e||0===n.length)return;const s=JSON.stringify([o(e),n.map(e=>e.toLowerCase()).sort(),i]);this.declaredClasses.set(s,{base:e,classes:n,includeBase:i})}declaredFor(e){let t=0,i=[];for(const n of this.declaredClasses.values()){if(!c(e,n.base))continue;const s=o(n.base);if(!n.includeBase&&o(e)===s)continue;const a=s.split(",").length;a>t&&(t=a,i=[]),a===t&&i.push(...n.classes)}const n=new Set;return i.filter(e=>{const t=e.toLowerCase();return!n.has(t)&&(n.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 n=[...this.declaredFor(e)];for(const e of i)n.some(t=>t.toLowerCase()===e.toLowerCase())||n.push(e);if(0===n.length)return[];const s=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=n.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 u=r.filter(e=>a.getAttributeType(e)&&!d.has(attributeKey(e)));if(0===u.length)return"allowed";const f=new Set(s.map(attributeKey)),p=new Set([...valued(c).filter(e=>!f.has(attributeKey(e))),...r].map(attributeKey)),{auxiliary:g,structural:y}=a.missingClasses(n,h),w=[];for(const t of g){const i=t.names[0]??t.oid,n=allowedBy([i]),s=u.filter(e=>n.has(attributeKey(e)));if(0===s.length)continue;const r=a.resolveAttributes([i]).must.filter(e=>!p.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${i} for ${s.join(", ")}, and cannot be given it without ${r.join(", ")}`):(w.push(i),u=u.filter(e=>!s.includes(e)))}const m=y.filter(e=>{const t=allowedBy([e]);return u.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${u.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 E(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new E(`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 k(_({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}}knowAttributeNames(e){for(const t of e){if(!t||t.includes(";"))continue;const e=t.toLowerCase();this.spellings.has(e)||this.spellings.set(e,t);const i=(l(t)??t).toLowerCase(),n=this.spelledTypes.get(i);void 0===n?this.spelledTypes.set(i,t):n!==t&&this.logger.warn(`Attribute "${t}" is also named "${n}" in the configuration or a schema: plugins comparing with one name miss the other`)}}async canonicalAttributes(e){if(!e)return;const t=Object.keys(e),i=await this.canonicalNames(t);if(t.every((e,t)=>e===i[t]))return;const n=t.map(t=>e[t]);for(const i of t)delete e[i];i.forEach((t,i)=>e[t]=n[i])}async canonicalChanges(e){e&&(await this.canonicalAttributes(e.add),await this.canonicalAttributes(e.replace),Array.isArray(e.delete)?e.delete=await this.canonicalNames(e.delete):await this.canonicalAttributes(e.delete))}async canonicalNames(e){if(0===e.length)return e;let t;try{t=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){this.schemaUnavailable||this.logger.warn(`The directory schema could not be read (${String(e)}): attribute names are resolved from the standard types only, a name given as another OID is refused, and object classes are not repaired`),this.schemaUnavailable=!0}const i=new Map;return e.map(e=>{const n=this.canonicalName(e,t),s=i.get(n.toLowerCase());if(void 0!==s)throw new S(`"${s}" and "${e}" name the same attribute`);return i.set(n.toLowerCase(),e),n})}canonicalName(e,t){const[i,...n]=e.split(";"),s=i.replace(/^oid\./i,""),a=n.find(e=>"binary"!==e.toLowerCase());if(void 0!==a)throw new S(`Attribute option "${a}" of "${e}" is not accepted`);if(!/^(?:[A-Za-z][A-Za-z0-9-]*|\d+(?:\.\d+)+)$/.test(s))throw new S(`"${e}" is not an attribute description`);const r=t?.getAttributeType(s),o=t?void 0:l(s);if(!t&&!o&&/^\d+(\.\d+)+$/.test(s))throw new S(`Attribute "${e}" cannot be named by its OID here`);const c=(r?[...r.names,r.oid]:[s,...o?[o]:[]]).map(e=>this.spellings.get(e.toLowerCase())).find(Boolean)??r?.names[0]??o??s;return n.length?`${c};binary`:c}filterForCaller(e,t,i){if(!this.parent.hookBus.has("ldapsearchfilter"))return e;const one=async e=>{const[n]=await this.parent.hookBus.chain("ldapsearchfilter",[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){i??=A();let n={...U,...e};n=await this.parent.hookBus.chain("ldapsearchopts",n),[t,n]=await this.parent.hookBus.chain("ldapsearchrequest",[t,n,i]);const s=this.cacheEnabled&&!n.paged&&"base"===n.scope,a=s?this.getCacheKey(t,n):"",r=this.cacheGeneration;if(s){const e=this.searchCache.get(a);if(L.inc({result:e?"hit":"miss"}),e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),i,n)}const o=await this.acquireConnection();try{const e=n.paged?async function*(e){for await(const t of e)yield dropSelectors(t)}(o.client.searchPaginated(t,n)):C("search",()=>o.client.search(t,n)).then(dropSelectors),c=await this.parent.hookBus.chain("ldapsearchresult",e);if(s){const e=c;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(e)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(e,i,n)}return n.paged?this.filterForCaller(this.wrapPaginatedSearch(c,o),i,n):this.filterForCaller(c,i,n)}finally{n.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){let i=0,n="ok";try{for(;;){const t=process.hrtime.bigint();let s;try{s=await e.next()}catch(e){throw n=D(e),e}finally{i+=$(t)}if(s.done)return;yield s.value}}finally{try{await e.return(void 0)}catch{}finally{j.observe({operation:"search",result:n},i),this.releaseConnection(t)}}}async add(e,t,n){n??=A(),e=this.setDn(e),await this.canonicalAttributes(t),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const s={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?s[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?s[e]=i.map(e=>e.toString()):s[e]=i;[e,t]=await this.parent.hookBus.chain("ldapaddrequest",[e,s,n]);const a=membersNamed(t),r=[];for(const[e,n]of Object.entries(t)){assertHasValue(e,n);const t=Array.isArray(n)?n:[n];r.push(new i({type:e,values:t}))}const o=await this.acquireConnection();try{this.invalidateCache(e);for(const e of a)this.invalidateCache(e);await C("add",()=>o.client.add(e,r)),this.invalidateCache(e);for(const e of a)this.invalidateCache(e);this.parent.hookBus.notify("ldapadddone",e,t,f(n))}catch(t){throw function(e,t){const i=R(t);return 68===i?withCode(new x(`Entry ${e} already exists`),i):32===i?withCode(new E(`${u(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(o)}return await this.parent.hookBus.notifyAndWait("ldapaddafter",e,t,f(n)),!0}async modify(e,t,i,n={}){i??=A();const s=this.opNumber(),a=[];try{return await y(s,()=>this.applyModify(e,t,i,n,s,a))}finally{this.endOperation(a,"ldapmodifyend",s)}}async applyModify(e,t,a,r,o,c){e=this.setDn(e);const l=[];if(await this.canonicalChanges(t),[e,t]=await this.parent.hookBus.chain("ldapmodifyrequest",[e,t,o,a]),t.add)for(const[e,s]of Object.entries(t.add))assertHasValue(e,s),l.push(new n({operation:"add",modification:new i({type:e,values:Array.isArray(s)?s:[s]})}));if(t.replace)for(const[e,s]of Object.entries(t.replace))l.push(new n({operation:"replace",modification:new i({type:e,values:Array.isArray(s)?s:[s]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)l.push(new n({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,s]of Object.entries(t.delete)){const t=new n({operation:"delete",modification:new i(s?{type:e,values:Array.isArray(s)?s:[s]}:{type:e})});l.push(t)}const h=[t.add,t.replace,t.delete].flatMap(e=>Array.isArray(e)?e:Object.keys(e??{})).some(isMembershipAttribute),invalidate=()=>{if(this.invalidateCache(e),h){this.invalidateReferences(e);for(const e of[t.add,t.replace,t.delete])for(const t of membersNamed(Array.isArray(e)?{}:e))this.invalidateCache(t)}};if(0===l.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(this.parent.hookBus.notifyAndWait("ldapmodifydone",e,{},o,r.context??f(a))),!1}{const h=r.permissive?[new s("1.2.840.113556.1.4.1413",{critical:!1})]:void 0,send=async()=>{const t=await this.acquireConnection();try{await C("modify",()=>t.client.modify(e,l,h))}finally{this.releaseConnection(t)}};try{invalidate();try{await send()}catch(s){if(65!==R(s))throw s;const a=await this.classRepair(e,t,r.declaredClasses);if("allowed"!==a&&0===a.length)throw s;const o="allowed"===a?void 0:new n({operation:"add",modification:new i({type:"objectClass",values:a})});o&&l.push(o),this.invalidateCache(e);try{await send()}catch(e){if(!o||20!==R(e))throw e;l.splice(l.indexOf(o),1),await send()}}return invalidate(),c.push(this.parent.hookBus.notifyAndWait("ldapmodifydone",e,t,o,r.context??f(a))),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(l)}`),function(e,t){const i=R(t);return 32===i?withCode(new E(`Entry ${e} not found`),i):16===i?withCode(new E(`Entry ${e} does not hold the value to delete`),i):65!==i&&20!==i?ldapWriteError("LDAP modify error",e,t):withCode(new x(`Entry ${e} cannot hold this change: ${String(t)}`),i)}(e,t)}}}async rename(e,t,i){i??=A(),e=this.setDn(e),t=this.setDn(t);const n=h(e),s=h(t);if(1!==s.length||!n.includes(s[0]))throw new S(`${t}: an entry is renamed by one value of the attribute it is named by`);const a=this.opNumber(),r=[];try{return await y(a,()=>this.applyRename(e,t,i,r))}finally{this.endOperation(r,"ldaprenameend",a)}}async applyRename(e,t,i,n){[e,t]=await this.parent.hookBus.chain("ldaprenamerequest",[e,t,i]);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),await C("rename",()=>s.client.modifyDN(e,t)),this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),n.push(this.parent.hookBus.notifyAndWait("ldaprenamedone",e,t,f(i))),!0}catch(i){throw ldapRenameError("LDAP rename error",e,t,i)}finally{this.releaseConnection(s)}}async move(e,t,i){i??=A(),e=this.setDn(e),t=this.setDn(t);const n=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),await C("rename",()=>n.client.modifyDN(e,t)),this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),w(()=>this.parent.hookBus.notify("ldaprenamedone",e,t,f(i))),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(i){throw ldapRenameError("LDAP move error",e,t,i)}finally{this.releaseConnection(n)}}async delete(e,t,i={}){t??=A();const n=(Array.isArray(e)?e:[e]).map(e=>this.setDn(e)),s=this.opNumber(),a=[];try{return await y(s,()=>this.applyDelete(n,t,i,a))}finally{this.endOperation(a,"ldapdeleteend",s)}}async applyDelete(e,t,i,n){[e]=await this.parent.hookBus.chain("ldapdeleterequest",[e,t]);const s=await this.acquireConnection();try{for(const a of e){try{this.invalidateCache(a),this.invalidateReferences(a),await C("delete",()=>s.client.del(a)),this.invalidateCache(a),this.invalidateReferences(a)}catch(e){const t=R(e);throw 32===t?withCode(new E(`Entry ${a} not found`),t):ldapError("LDAP delete error",e)}n.push(this.parent.hookBus.notifyAndWait("ldapdeletedone",a,i.context??f(t)))}return!0}finally{this.releaseConnection(s)}}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){this.parent.hookBus.has(t)&&Promise.all(e).then(()=>this.parent.hookBus.notify(t,i))}opNumber(){return this.parent.operationSequence++}}export{assertModifyRequest,ldapActions as default};
|
|
1
|
+
import{Attribute as e,Change as t,Control as a}from"ldapts";import i from"p-limit";import{rdnTypes as s,escapeDnValue as r,getParentDn as n}from"./utils.js";import{changeContext as o}from"./changeContext.js";import c,{hookOwner as h}from"./hookBus.js";import{runOperation as l,outsideOperation as d}from"./operation.js";import{outsideRequest as p,currentRequest as f}from"./requestContext.js";import{ldapSearchCache as u,timedLdap as y,ldapResult as m,secondsSince as w,ldapOperationDuration as g}from"./metrics.js";import b from"./ldap/pool.js";import A,{digestKey as v,membersNamed as D}from"./ldap/searchCache.js";import _ from"./ldap/schema.js";import $ from"./ldap/callerView.js";import{BadRequestError as C,NotFoundError as k,ConflictError as j}from"./errors.js";import{extractLdapCode as B,isSchemaRefusal as q}from"./ldapCodes.js";const E={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}},L={ldapaddrequest:2,ldapmodifyrequest:3,ldaprenamerequest:2,ldapdeleterequest:1},R=new Set(["search","add","modify","rename","move","delete"]),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 C(`A modification carries add, replace or delete, not ${t.join(", ")}`);const{add:a,replace:i,delete:s}=e;for(const[e,t]of[["add",a],["replace",i]])if(void 0!==t&&!isPlainObject(t))throw new C(`"${e}" must map attributes to values`);if(!(void 0===s||isPlainObject(s)||Array.isArray(s)&&s.every(e=>"string"==typeof e)))throw new C('"delete" must list attribute names or map attributes to values');if(0===[a,i,s].reduce((e,t)=>e+(t?Object.keys(t).length:0),0))throw new C("A modification names at least one attribute in add, replace or delete")}function ldapError(e,t){const a=new Error(`${e}: ${String(t)}`),i=B(t);return void 0===i?a:withCode(a,i)}function withCode(e,t){return e.code=t,e}const P=["*","+","1.1"],dropSelectors=e=>{for(const t of e.searchEntries)for(const e of P){const a=t[e];Array.isArray(a)&&0===a.length&&delete t[e]}return e};function ldapRefusal(e,t,a){return withCode(new C(`${e}: ${String(a)}`),t)}function assertHasValue(e,t){if((e=>{const absent=e=>null==e;return Array.isArray(e)?0===e.length||e.some(e=>absent(e)):absent(e)})(t))throw new C(`Attribute "${e}" has no value`)}function ldapWriteError(e,t,a){const i=B(a);return q(i)?ldapRefusal(`The directory refused this write to ${t}`,i,a):ldapError(e,a)}function ldapRenameError(e,t,a,i){const s=B(i);return 68===s?withCode(new j(`Entry ${a} already exists`),s):q(s)?ldapRefusal(`The directory refused to rename ${t} to ${a}`,s,i):ldapError(e,i)}class ldapActions{config;base;parent;logger;cache;queryLimit;pool;schema;caller;constructor(e){this.parent=e,this.logger=e.logger,this.config=e.config,this.schema=new _({logger:this.logger,search:(e,t)=>this.system.search(e,t),autoRepair:()=>!1!==this.config.ldap_flat_auto_repair,schemaCacheTtl:()=>1e3*(this.config.ldap_raw_schema_cache_ttl??3600)}),this.caller=new $({search:(e,t,a)=>this.search(e,t,a),hasFilter:()=>this.parent.hookBus.has("ldapsearchfilter"),filter:async(e,t,a)=>{const[i]=await this.parent.hookBus.chain("ldapsearchfilter",[e,t,a]);return i??e}});for(const[e,t]of Object.entries(this.config))/_attributes?$/.test(e)&&this.knowAttributeNames([t].flat().filter(e=>"string"==typeof e));const t=this.config.ldap_concurrency||10;this.queryLimit=i(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const a="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,r="string"==typeof s?parseInt(s,10)||0:s??0,n=1e3*Math.max(r,0);if(this.cache=new A({max:a,ttl:n,logger:this.logger}),!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 bind DN is not defined, please set --ldap-dn (or DM_LDAP_DN)");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined, please set --ldap-pwd (or DM_LDAP_PWD)");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;const o=e.config.ldap_url,c={url:o[0],timeout:0,connectTimeout:0,strictDN:!1};o[0].startsWith("ldaps://")&&(c.tlsOptions={minVersion:"TLSv1.2"}),this.pool=new b({urls:o,dn:e.config.ldap_dn,pwd:e.config.ldap_pwd,options:c,poolSize:e.config.ldap_pool_size||5,connectionTtl:1e3*(e.config.ldap_connection_ttl||60),logger:this.logger})}async ping(e){await this.pool.ping(e)}async close(){await this.pool.close()}poolStats(){return this.pool.stats()}invalidateCache(e){this.cache.invalidate(e)}get system(){return this.systemView??=new Proxy(this,{get:(e,t)=>{const a=Reflect.get(e,t,e);return"function"==typeof a&&R.has(t)?(...t)=>p(()=>a.apply(e,t)):a}}),this.systemView}systemView;forRequest(e){return{search:(t,a)=>this.search(t,a??this.base,e),add:(t,a)=>this.add(t,a,e),modify:(t,a,i)=>this.modify(t,a,e,i),rename:(t,a)=>this.rename(t,a,e),delete:t=>this.delete(t,e)}}async readableBy(e,t){return this.caller.readableBy(e,t)}async authorize(e,t){const a=[...t];"ldapaddrequest"===e&&await this.canonicalAttributes(a[1]),"ldapmodifyrequest"===e&&await this.canonicalChanges(a[1]);const i=L[e];a[i]??=f();const[s,...r]=a,n="ldapdeleterequest"===e?[[s].flat().map(e=>this.setDn(String(e))),...r]:[this.setDn(String(s)),...r];"ldaprenamerequest"===e&&(n[1]=this.setDn(String(n[1])));const o=this.parent.hookBus.subscribers(e).filter(e=>{const t=h(e);return Boolean(t&&this.parent.loadedPlugins[t.plugin]?.roles?.includes("authz"))}),l=new c({[e]:o});await l.chain(e,n)}declareObjectClasses(e,t,a=!1){this.schema.declareObjectClasses(e,t,a)}async subschemaDn(){return this.schema.subschemaDn()}async schemaIndex(){return this.schema.schemaIndex()}knowAttributeNames(e){this.schema.knowAttributeNames(e)}async canonicalAttributes(e){await this.schema.canonicalAttributes(e)}async canonicalChanges(e){await this.schema.canonicalChanges(e)}filterForCaller(e,t,a){return this.caller.filter(e,t,a)}async search(e,t=this.base,a){a??=f();let i={...E,...e};i=await this.parent.hookBus.chain("ldapsearchopts",i),[t,i]=await this.parent.hookBus.chain("ldapsearchrequest",[t,i,a]);const s=this.cache.enabled&&!i.paged&&"base"===i.scope,r=s?this.cache.key(t,i):"",n=this.cache.generation;if(s){const e=this.cache.get(r);if(u.inc({result:e?"hit":"miss"}),e)return this.logger.debug(`LDAP search cache hit: ${v(r)}`),this.filterForCaller(e,a,i)}if(i.paged)return this.pool.assertOpen(),this.filterForCaller(this.pagedSearch(t,i),a,i);const o=await this.pool.acquire();try{const e=y("search",()=>o.client.search(t,i)).then(dropSelectors).catch(e=>{throw function(e,t){const a=B(t);return q(a)?ldapRefusal(`The directory refused this read of ${e}`,a,t):t instanceof Error?t:new Error(String(t))}(t,e)}),c=await this.parent.hookBus.chain("ldapsearchresult",e);if(s){const e=c;n===this.cache.generation?(this.cache.set(r,e),this.logger.debug(`LDAP search cached: ${v(r)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${v(r)}`)}return this.filterForCaller(c,a,i)}finally{this.pool.release(o)}}async*pagedSearch(e,t){const a=await this.pool.acquire();let i,s=!1,r=0,n="ok";try{const o=async function*(e){for await(const t of e)yield dropSelectors(t)}(a.client.searchPaginated(e,t));for(i=await this.parent.hookBus.chain("ldapsearchresult",o);;){const e=process.hrtime.bigint();let t;try{s=!0,t=await i.next()}catch(e){throw n=m(e),e}finally{r+=w(e)}if(t.done)return;yield t.value}}finally{try{await(i?.return(void 0))}catch{}finally{s&&g.observe({operation:"search",result:n},r),this.pool.release(a)}}}async add(t,a,i){i??=f(),t=this.setDn(t),await this.canonicalAttributes(a),a.objectClass&&0!==a.objectClass.length||!this.config.user_class||(a.objectClass=this.config.user_class);const s={};for(const[e,t]of Object.entries(a))Buffer.isBuffer(t)?s[e]=t.toString():Array.isArray(t)&&t.length>0&&Buffer.isBuffer(t[0])?s[e]=t.map(e=>e.toString()):s[e]=t;[t,a]=await this.parent.hookBus.chain("ldapaddrequest",[t,s,i]);const r=D(a),c=[];for(const[t,i]of Object.entries(a)){assertHasValue(t,i);const a=Array.isArray(i)?i:[i];c.push(new e({type:t,values:a}))}const h=await this.pool.acquire();try{this.cache.invalidate(t);for(const e of r)this.cache.invalidate(e);await y("add",()=>h.client.add(t,c)),this.cache.invalidate(t);for(const e of r)this.cache.invalidate(e);this.parent.hookBus.notify("ldapadddone",t,a,o(i))}catch(e){throw function(e,t){const a=B(t);return 68===a?withCode(new j(`Entry ${e} already exists`),a):32===a?withCode(new k(`${n(e)} does not exist`),a):20===a?ldapRefusal(`The directory refused this write to ${e}`,a,t):ldapWriteError("LDAP add error",e,t)}(t,e)}finally{this.pool.release(h)}return await this.parent.hookBus.notifyAndWait("ldapaddafter",t,a,o(i)),!0}async modify(e,t,a,i={}){a??=f();const s=this.opNumber(),r=[];try{return await l(s,()=>this.applyModify(e,t,a,i,s,r))}finally{this.endOperation(r,"ldapmodifyend",s)}}async applyModify(i,s,r,n,c,h){i=this.setDn(i);const l=[];if(await this.canonicalChanges(s),[i,s]=await this.parent.hookBus.chain("ldapmodifyrequest",[i,s,c,r]),s.add)for(const[a,i]of Object.entries(s.add))assertHasValue(a,i),l.push(new t({operation:"add",modification:new e({type:a,values:Array.isArray(i)?i:[i]})}));if(s.replace)for(const[a,i]of Object.entries(s.replace))l.push(new t({operation:"replace",modification:new e({type:a,values:Array.isArray(i)?i:[i]})}));if(s.delete)if(Array.isArray(s.delete))for(const a of s.delete)l.push(new t({operation:"delete",modification:new e({type:a,values:[]})}));else for(const[a,i]of Object.entries(s.delete)){const s=new t({operation:"delete",modification:new e(i?{type:a,values:Array.isArray(i)?i:[i]}:{type:a})});l.push(s)}const invalidate=()=>{this.cache.invalidate(i),this.cache.invalidateMembership(i,s)};if(0===l.length){return Object.keys(s.add||{}).length>0||Object.keys(s.replace||{}).length>0||(Array.isArray(s.delete)?s.delete.length>0:Object.keys(s.delete||{}).length>0)?this.logger.warn(`Modify on ${i} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${i} had nothing to apply`),h.push(this.parent.hookBus.notifyAndWait("ldapmodifydone",i,{},c,n.context??o(r))),!1}{const d=n.permissive?[new a("1.2.840.113556.1.4.1413",{critical:!1})]:void 0,send=()=>this.pool.withConnection(e=>y("modify",()=>e.modify(i,l,d)));try{invalidate();try{await send()}catch(a){if(65!==B(a))throw a;const r=await this.schema.classRepair(i,s,n.declaredClasses);if("allowed"!==r&&0===r.length)throw a;const o="allowed"===r?void 0:new t({operation:"add",modification:new e({type:"objectClass",values:r})});o&&l.push(o),this.cache.invalidate(i);try{await send()}catch(e){if(!o||20!==B(e))throw e;l.splice(l.indexOf(o),1),await send()}}return invalidate(),h.push(this.parent.hookBus.notifyAndWait("ldapmodifydone",i,s,c,n.context??o(r))),!0}catch(e){throw this.logger.warn(`Changes that failed: ${i}, ${JSON.stringify(l)}`),function(e,t){const a=B(t);return 32===a?withCode(new k(`Entry ${e} not found`),a):16===a?withCode(new k(`Entry ${e} does not hold the value to delete`),a):65!==a&&20!==a?ldapWriteError("LDAP modify error",e,t):withCode(new j(`Entry ${e} cannot hold this change: ${String(t)}`),a)}(i,e)}}}async rename(e,t,a){a??=f(),e=this.setDn(e),t=this.setDn(t);const i=s(e),r=s(t);if(1!==r.length||!i.includes(r[0]))throw new C(`${t}: an entry is renamed by one value of the attribute it is named by`);const n=this.opNumber(),o=[];try{return await l(n,()=>this.applyRename(e,t,a,o))}finally{this.endOperation(o,"ldaprenameend",n)}}async applyRename(e,t,a,i){[e,t]=await this.parent.hookBus.chain("ldaprenamerequest",[e,t,a]);const s=await this.pool.acquire();try{return this.cache.invalidate(e),this.cache.invalidate(t),this.cache.invalidateReferences(e),await y("rename",()=>s.client.modifyDN(e,t)),this.cache.invalidate(e),this.cache.invalidate(t),this.cache.invalidateReferences(e),i.push(this.parent.hookBus.notifyAndWait("ldaprenamedone",e,t,o(a))),!0}catch(a){throw ldapRenameError("LDAP rename error",e,t,a)}finally{this.pool.release(s)}}async move(e,t,a){a??=f(),e=this.setDn(e),t=this.setDn(t);const i=await this.pool.acquire();try{return this.cache.invalidate(e),this.cache.invalidate(t),this.cache.invalidateReferences(e),await y("rename",()=>i.client.modifyDN(e,t)),this.cache.invalidate(e),this.cache.invalidate(t),this.cache.invalidateReferences(e),d(()=>this.parent.hookBus.notify("ldaprenamedone",e,t,o(a))),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(a){throw ldapRenameError("LDAP move error",e,t,a)}finally{this.pool.release(i)}}async delete(e,t,a={}){t??=f();const i=(Array.isArray(e)?e:[e]).map(e=>this.setDn(e)),s=this.opNumber(),r=[];try{return await l(s,()=>this.applyDelete(i,t,a,r))}finally{this.endOperation(r,"ldapdeleteend",s)}}async applyDelete(e,t,a,i){return[e]=await this.parent.hookBus.chain("ldapdeleterequest",[e,t]),await this.pool.withConnection(async s=>{for(const r of e){try{this.cache.invalidate(r),this.cache.invalidateReferences(r),await y("delete",()=>s.del(r)),this.cache.invalidate(r),this.cache.invalidateReferences(r)}catch(e){const t=B(e);throw 32===t?withCode(new k(`Entry ${r} not found`),t):ldapWriteError("LDAP delete error",r,e)}i.push(this.parent.hookBus.notifyAndWait("ldapdeletedone",r,a.context??o(t)))}}),!0}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${r(e)},${this.base}`,e}endOperation(e,t,a){this.parent.hookBus.has(t)&&Promise.all(e).then(()=>this.parent.hookBus.notify(t,a))}opNumber(){return this.parent.operationSequence++}}export{assertModifyRequest,ldapActions as default};
|
package/dist/lib/parseConfig.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class ConfigParser{config;unknownArgs=[];get unknownOptions(){return[...this.unknownArgs]}constructor(e){this.config=e}parse(e=process.argv){const r={},t=this.parseCliArgs(e);for(const e of this.config){const n=this.getKeyFromCliArg(e[0]);let i=e[2],s=!0;if(void 0!==
|
|
1
|
+
class ConfigParser{config;unknownArgs=[];deprecated=[];get unknownOptions(){return[...this.unknownArgs]}get deprecationWarnings(){return[...this.deprecated]}warnOnce(e){this.deprecated.includes(e)||this.deprecated.push(e)}findEntry(e){const r=this.config.find(r=>r[0]===e);if(r)return r;const t=this.config.find(r=>r[7]?.includes(e));return t&&this.warnOnce(`Configuration: ${e} is deprecated, use ${t[0]}`),t}envNameOf(e,r){const t=e[1]||void 0;if(t){if(this.environmentIsSet(e,process.env[t]))return t;if("array"!==e[3]&&r.has(e[0]))return t;for(const r of e[8]??[])if(r&&this.environmentIsSet(e,process.env[r]))return this.warnOnce(`Configuration: ${r} is deprecated, use ${t}`),r;return t}}environmentIsSet(e,r){return void 0!==r&&("array"===e[3]?splitValue(r,e[5],"plural").length>0:"number"!==e[3]&&"json"!==e[3]||""!==r.trim())}constructor(e){this.config=e,this.checkAliases()}checkAliases(){const e=new Set;for(const r of this.config)for(const t of[r[0],r[4],r[1]])t&&e.add(t);const r=new Set;for(const t of this.config)for(const n of[...t[7]??[],...t[8]??[]]){if(e.has(n))throw new Error(`Configuration: alias ${n} of ${t[0]} is already a declared name`);if(r.has(n))throw new Error(`Configuration: alias ${n} of ${t[0]} is declared twice`);r.add(n)}}parse(e=process.argv){this.deprecated.length=0;const r={},t=this.parseCliArgs(e);for(const e of this.config){const n=this.getKeyFromCliArg(e[0]);let i=e[2],s=!0;const o=this.envNameOf(e,t);if(void 0!==o){const r=process.env[o],t="array"===e[3]&&void 0!==r?splitValue(r,e[5],"plural"):[],n=void 0!==r&&("array"===e[3]&&0===t.length||("number"===e[3]||"json"===e[3])&&""===r.trim());if(n&&e[6])throw new Error(`Error in environment variable ${o}: the value is empty. Set it, or leave the variable out`);if(void 0!==r&&!n)if(s=!1,"boolean"===e[3])i="true"===r.toLowerCase();else if("number"===e[3]){if(i=parseNumber(r),Number.isNaN(i))throw new Error(`Error in environment variable ${o}: "${r}" is not a number`)}else if("array"===e[3])i=t;else if("json"===e[3])try{i=JSON.parse(r)}catch(e){throw new Error(`Error parsing JSON from environment variable ${o}: ${e}`)}else i=r}if(t.has(e[0])){const r=t.get(e[0]);if("boolean"===e[3])i=!0;else if("number"===e[3])i=r;else if("array"===e[3])i=(s?[]:i).concat(r.flatMap(r=>splitCliValue(e[0],r,e[5],"singular"))),s=!1;else if("json"===e[3])try{i=JSON.parse(r)}catch(r){throw new Error(`Error parsing JSON from command line argument ${e[0]}: ${r}`)}else i=r;t.delete(e[0])}if("array"===e[3]&&e[4]&&t.has(e[4])){const r=t.get(e[4]);i=(s?[]:i).concat(splitCliValue(e[4],r,e[5],"plural")),t.delete(e[4])}r[n]=i}return this.unknownArgs=[],t.forEach((e,t)=>{if(r[t])throw new Error(`Error in command line: ${t} redefined`);this.unknownArgs.push(t),r[t.replace(/^-+/,"").replace(/-/g,"_")]=e}),r}parseCliArgs(e){const r=new Map;for(let t=2;t<e.length;t++){const n=e[t];if(n.startsWith("--")||n.startsWith("-")){const i=this.findEntry(n),s=i?i[0]:n;if(i&&"boolean"===i[3])r.set(s,!0);else if(i&&"number"===i[3]){if(!e[t+1]?.trim())throw new Error(`Error in command line: ${n} has an empty value. Leave it out to keep the default`);const i=parseNumber(e[t+1]);if(Number.isNaN(i))throw new Error(`Error in command line: ${n} takes a number, got "${e[t+1]}"`);r.set(s,i),t++}else if(i&&"array"===i[3]){const o=r.get(s)||[],a=e[t+1];o.push(a),r.set(s,o);const l=e[t+2];if(void 0!==l&&!l.startsWith("-"))throw new Error(`Error in command line: ${n} takes one value, got "${a}" followed by "${l}". Repeat ${n} for each value`+(i[4]?`, or give them all to ${i[4]}`:""))}else{const n=e[t+1];r.set(s,n),t++}}}return r}getKeyFromCliArg(e){return e.startsWith("--")?e.substring(2).replace(/-/g,"_"):e.startsWith("-")?e.substring(1).replace(/-/g,"_"):e}}function splitValue(e,r,t){let n;if("singular"===t)"identifiers"===r&&(n=/,/);else if("dns"===r)n=/;/;else{const t="phrases"===r?"\\r\\n":"\\s";n=new RegExp(`[${e.includes(";")?";":","}${t}]+`)}return n?e.split(n).map(e=>e.trim()).filter(e=>e.length>0):[e.trim()].filter(e=>e.length>0)}function parseNumber(e){const r=e.trim();return/^-?\d+$/.test(r)?Number(r):NaN}function splitCliValue(e,r,t,n){const i=void 0===r?[]:splitValue(r,t,n);if(0===i.length)throw new Error(`Error in command line: ${e} has an empty value. Leave it out to keep the default`);return i}export{ConfigParser,parseNumber,splitValue};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const serviceToken=e=>{const t={name:e,with:e=>({token:t,service:e})};return t},providedService=(e,t)=>e.get(t);export{providedService,serviceToken};
|
package/dist/lib/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BadRequestError as e}from"./errors.js";import"./expressFormatedResponses.js";import"./metrics.js";import"./requestContext.js";const n=new Map;function getCompiledRegex(e,t){const r=t?`${e}:${t}`:e;let o=n.get(r);return o||(o=new RegExp(e,t),n.set(r,o)),o}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const transformSchemas=(e,n)=>e.toString().replace(/__(\S+)__/g,(e,t)=>{if(!t||"string"!=typeof t)return e;const r=t.trim().toLowerCase(),o=n;return o[r]?"object"!=typeof o[r]?o[r]:JSON.stringify(o[r]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function substringSearchFilter(n,t){const r=/^[a-zA-Z][a-zA-Z0-9-]*$/,o=t.split(",").map(e=>e.trim()).filter(Boolean);if(0===o.length||!o.every(e=>r.test(e)))throw new e("Invalid LDAP attribute name");const i=escapeLdapFilter(n),s=o.map(e=>`(${e}=*${i}*)`);return 1===s.length?s[0]:`(|${s.join("")})`}function fillPattern(e,n,t){return e.split(n).join(t)}function escapeDnValue(e){let n=e.replace(/[\\,+"<>;=]/g,e=>`\\${e}`).replace(/[\0-\x1f\x7f]/g,e=>`\\${hexByte(e.charCodeAt(0))}`);return e.startsWith(" ")&&(n=`\\${n}`),e.length>1&&e.endsWith(" ")&&(n=`${n.slice(0,-1)}\\ `),e.startsWith("#")&&(n=`\\${n}`),n}const t=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0});function unescapeDnValue(n){if("string"!=typeof n)throw new e("DN value must be a string");let t="",r=0,o=[];const flushBytes=()=>{if(o.length){for(const e of utf8Chars(o))t+="number"==typeof e?String.fromCharCode(e):e;o=[]}};for(;r<n.length;)if("\\"===n[r]&&r+1<n.length){const e=n[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<n.length){const e=n.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){o.push(parseInt(e,16)),r+=3;continue}}flushBytes(),t+=e,r+=2}else flushBytes(),t+=n[r],r++;return flushBytes(),t}function validateDnValue(e,n){if(null==e||"string"!=typeof e)throw new Error(`${n} must be a string`);if(0===e.trim().length)throw new Error(`${n} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${n} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${n} contains invalid invisible characters`)}function asciiSpace(e){return" "===e||e>="\t"&&e<="\r"}function trimUnescaped(e){let n=0,t=e.length;for(;n<t&&asciiSpace(e[n]);)n++;for(;t>n&&asciiSpace(e[t-1]);)t--;if(t<e.length){let r=0;for(let o=t-1;o>=n&&"\\"===e[o];o--)r++;r%2==1&&t++}return e.slice(n,t)}function parseDn(n){if("string"!=typeof n)throw new e("DN must be a string");const t=[];let r="",o=!1,i=!1;for(let e=0;e<n.length;e++){const s=n[e];o?(r+=s,o=!1):"\\"===s?(r+=s,o=!0):'"'===s&&(i||opensValue(r))?(r+=s,i=!i):i||","!==s&&";"!==s?r+=s:(t.push(trimUnescaped(r)),r="")}if(i)throw new e("DN has an unbalanced quote");return r&&t.push(trimUnescaped(r)),t}function opensValue(e){const n=e.trimEnd();if(!n.endsWith("="))return!1;let t=0;for(let e=n.length-2;e>=0&&"\\"===n[e];e--)t++;return t%2==0}function isDnSyntax(e){let n;try{n=parseDn(e)}catch{return!1}if(0===n.length)return!1;const t=/^\s*(?:[A-Za-z][A-Za-z0-9-]*|(?:OID\.)?\d+(?:\.\d+)+)\s*=/i;return n.every(e=>{if(!e)return!1;const n=[];let r="",o=!1;for(const t of e){if(o)o=!1;else if("\\"===t)o=!0;else if("+"===t){n.push(r),r="";continue}r+=t}return n.push(r),n.every(e=>t.test(e))})}function getParentDn(e){const n=parseDn(e);return n.length<=1?e:n.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function rdnValue(e){const[n]=parseDn(e);return rdnValueOf(n)}function rdnValueOf(e){if(!e)return"";const n=e.indexOf("=");if(n<=0)return"";let t=n+1;for(;t<e.length&&asciiSpace(e[t]);)t++;return unescapeDnValue(e.slice(t))}function assertedRdnValue(e,n,t){const[r,...o]=parseDn(e),i=r?r.indexOf("="):-1;return i<=0||r.slice(0,i).trim().toLowerCase()!==n.toLowerCase()||splitRdn(r).length>1||void 0!==t&&normalizeDn(o.join(","))!==normalizeDn(t)?null:rdnValueOf(r)}function namesByDn(e,n){try{const[t]=parseDn(e),r=t?t.indexOf("="):-1;return r>0&&t.slice(0,r).trim().toLowerCase()===n.toLowerCase()}catch{return!1}}function isChildOf(e,n){try{return isDnInBranch(e,n)&&!isDnInBranch(n,e)}catch{return!1}}const r=/((?:\\[0-9a-fA-F]{2})+)|\\([^])/g;function hexByte(e){return e.toString(16).padStart(2,"0")}function utf8Chars(e){const n=[],r=Uint8Array.from(e);for(let o=0;o<e.length;){const i=utf8Length(e[o]);let s;if(i&&o+i<=e.length)try{s=t.decode(r.subarray(o,o+i))}catch{}void 0===s?n.push(e[o++]):(n.push(...s),o+=i)}return n}function utf8Length(e){return e<128?1:e>>5==6?2:e>>4==14?3:e>>3==30?4:0}const o=new Map,i=new Map;function registerAttributeTypes(e){for(const{oid:n,names:t}of e){const e=(t[0]??n).toLowerCase();for(const r of[n,...t])o.set(r.toLowerCase(),e),i.set(r.toLowerCase(),t[0]??n)}}function knownAttributeName(e){return i.get(e.toLowerCase().replace(/^oid\./,""))}function rdnTypes(e){return splitRdn(parseDn(e)[0]??"").map(e=>{const n=e.indexOf("=");return-1===n?"":canonicalType(e.slice(0,n).trim())})}function canonicalType(e){const n=e.toLowerCase().replace(/^oid\./,"");return o.get(n)??n}function splitRdn(e){const n=[];let t="",r=!1,o=!1;for(const i of e){if(r)r=!1;else if("\\"===i)r=!0;else if('"'===i&&(o||opensValue(t)))o=!o;else if("+"===i&&!o){n.push(t),t="";continue}t+=i}return n.push(t),n}function normalizeDn(e){return normalizedRdns(e).join(",")}function normalizedRdns(e){return parseDn(e).map(e=>splitRdn(e).map(e=>{const n=e.indexOf("=");return-1===n?e.trim().toLowerCase():`${canonicalType(e.slice(0,n).trim())}=${function(e){const n=trimUnescaped(e);if(/^#(?:[0-9a-fA-F]{2})+$/.test(n))return n;let t=0;for(let e=n.length-2;e>0&&"\\"===n[e];e--)t++;e=n.length>=2&&n.startsWith('"')&&n.endsWith('"')&&t%2==0?n.slice(1,-1):n;const o=[];let i=0;const plain=e=>{for(const n of e)o.push(n)};for(const n of e.matchAll(r)){plain(e.slice(i,n.index)),i=n.index+n[0].length;const[,t,r]=n;if(void 0!==r){o.push(r);continue}const s=t.split("\\").filter(Boolean).map(e=>parseInt(e,16));for(const e of utf8Chars(s))"number"==typeof e?o.push(`\\${hexByte(e)}`):plain(e)}i<e.length&&plain(e.slice(i));const kept=e=>3===e.length&&"\\"===e[0],s=[];let a="";for(const e of o)kept(e)?(s.push(...a.toLowerCase().normalize("NFKC"),e),a=""):a+=e;s.push(...a.toLowerCase().normalize("NFKC"));const c=s.filter((e,n)=>" "!==e||n>0&&" "!==s[n-1]);let l=0,u=c.length;for(;l<u&&" "===c[l];)l++;for(;u>l&&" "===c[u-1];)u--;let f="";for(let e=l;e<u;e++){const n=c[e];kept(n)?f+=n:n.charCodeAt(0)<32||127===n.charCodeAt(0)?f+=`\\${hexByte(n.charCodeAt(0))}`:',+"\\<>;='.includes(n)||e===l&&"#"===n?f+=`\\${n}`:f+=n}return f}(e.slice(n+1)).toLowerCase()}`}).sort().join("+")).filter(e=>e.length>0)}function isDummyMemberDn(e,n){if(!n||"string"!=typeof e)return!1;if(e===n)return!0;try{return normalizeDn(e)===normalizeDn(n)}catch{return!1}}function organizationLink(e){for(const n of Array.isArray(e)?e:[e]){if(null==n)continue;const e=String(n).trim();if(e.length>0)return e}}function isDnInBranch(e,n){const t=normalizedRdns(n);if(0===t.length)return!1;const r=normalizedRdns(e);if(r.length<t.length)return!1;return r.slice(r.length-t.length).every((e,n)=>e===t[n])}function movedToTrash(e,n,t){return!!e&&isDnInBranch(t,e)&&!isDnInBranch(n,e)}registerAttributeTypes([{oid:"2.5.4.0",names:["objectClass"]},{oid:"2.5.4.3",names:["cn","commonName"]},{oid:"2.5.4.4",names:["sn","surname"]},{oid:"2.5.4.5",names:["serialNumber"]},{oid:"2.5.4.6",names:["c","countryName"]},{oid:"2.5.4.7",names:["l","localityName"]},{oid:"2.5.4.8",names:["st","stateOrProvinceName"]},{oid:"2.5.4.9",names:["street","streetAddress"]},{oid:"2.5.4.10",names:["o","organizationName"]},{oid:"2.5.4.11",names:["ou","organizationalUnitName"]},{oid:"2.5.4.12",names:["title"]},{oid:"2.5.4.42",names:["givenName","gn"]},{oid:"2.5.4.43",names:["initials"]},{oid:"2.5.4.46",names:["dnQualifier"]},{oid:"2.5.4.65",names:["pseudonym"]},{oid:"0.9.2342.19200300.100.1.1",names:["uid","userid"]},{oid:"0.9.2342.19200300.100.1.3",names:["mail","rfc822Mailbox"]},{oid:"0.9.2342.19200300.100.1.25",names:["dc","domainComponent"]},{oid:"2.16.840.1.113730.3.1.241",names:["displayName"]},{oid:"2.16.840.1.113730.3.1.3",names:["employeeNumber"]},{oid:"1.3.6.1.1.1.1.0",names:["uidNumber"]},{oid:"1.3.6.1.1.1.1.1",names:["gidNumber"]}]);const asyncHandler=e=>(n,t,r)=>{Promise.resolve(e(n,t,r)).catch(r)};export{assertedRdnValue,asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,fillPattern,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,movedToTrash,namesByDn,normalizeDn,organizationLink,parseDn,rdnTypes,rdnValue,registerAttributeTypes,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue};
|
|
1
|
+
import{BadRequestError as e}from"./errors.js";import"./expressFormatedResponses.js";import"./metrics.js";import"./requestContext.js";const n=new Map;function getCompiledRegex(e,t){const r=t?`${e}:${t}`:e;let i=n.get(r);return i||(i=new RegExp(e,t),n.set(r,i)),i}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const transformSchemas=(e,n)=>e.toString().replace(/__(\S+)__/g,(e,t)=>{if(!t||"string"!=typeof t)return e;const r=t.trim().toLowerCase(),i=n;return i[r]?"object"!=typeof i[r]?i[r]:JSON.stringify(i[r]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function substringSearchFilter(n,t){const r=/^[a-zA-Z][a-zA-Z0-9-]*$/,i=t.split(",").map(e=>e.trim()).filter(Boolean);if(0===i.length||!i.every(e=>r.test(e)))throw new e("Invalid LDAP attribute name");const o=escapeLdapFilter(n),s=i.map(e=>`(${e}=*${o}*)`);return 1===s.length?s[0]:`(|${s.join("")})`}function fillPattern(e,n,t){return e.split(n).join(t)}function escapeDnValue(e){let n=e.replace(/[\\,+"<>;=]/g,e=>`\\${e}`).replace(/[\0-\x1f\x7f]/g,e=>`\\${hexByte(e.charCodeAt(0))}`);return e.startsWith(" ")&&(n=`\\${n}`),e.length>1&&e.endsWith(" ")&&(n=`${n.slice(0,-1)}\\ `),e.startsWith("#")&&(n=`\\${n}`),n}const t=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0});function unescapeDnValue(n){if("string"!=typeof n)throw new e("DN value must be a string");let t="",r=0,i=[];const flushBytes=()=>{if(i.length){for(const e of utf8Chars(i))t+="number"==typeof e?String.fromCharCode(e):e;i=[]}};for(;r<n.length;)if("\\"===n[r]&&r+1<n.length){const e=n[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<n.length){const e=n.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){i.push(parseInt(e,16)),r+=3;continue}}flushBytes(),t+=e,r+=2}else flushBytes(),t+=n[r],r++;return flushBytes(),t}function validateDnValue(e,n){if(null==e||"string"!=typeof e)throw new Error(`${n} must be a string`);if(0===e.trim().length)throw new Error(`${n} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${n} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${n} contains invalid invisible characters`)}function asciiSpace(e){return" "===e||e>="\t"&&e<="\r"}function trimUnescaped(e){let n=0,t=e.length;for(;n<t&&asciiSpace(e[n]);)n++;for(;t>n&&asciiSpace(e[t-1]);)t--;if(t<e.length){let r=0;for(let i=t-1;i>=n&&"\\"===e[i];i--)r++;r%2==1&&t++}return e.slice(n,t)}function parseDn(n){if("string"!=typeof n)throw new e("DN must be a string");const t=[];let r="",i=!1,o=!1;for(let e=0;e<n.length;e++){const s=n[e];i?(r+=s,i=!1):"\\"===s?(r+=s,i=!0):'"'===s&&(o||opensValue(r))?(r+=s,o=!o):o||","!==s&&";"!==s?r+=s:(t.push(trimUnescaped(r)),r="")}if(o)throw new e("DN has an unbalanced quote");return r&&t.push(trimUnescaped(r)),t}function opensValue(e){const n=e.trimEnd();if(!n.endsWith("="))return!1;let t=0;for(let e=n.length-2;e>=0&&"\\"===n[e];e--)t++;return t%2==0}function isDnSyntax(e){let n;try{n=parseDn(e)}catch{return!1}if(0===n.length)return!1;const t=/^\s*(?:[A-Za-z][A-Za-z0-9-]*|(?:OID\.)?\d+(?:\.\d+)+)\s*=/i;return n.every(e=>{if(!e)return!1;const n=[];let r="",i=!1;for(const t of e){if(i)i=!1;else if("\\"===t)i=!0;else if("+"===t){n.push(r),r="";continue}r+=t}return n.push(r),n.every(e=>t.test(e))})}const r=new Set(['"',"+",",",";","<",">","\\","#","="," "]);function hasOnlyRfc4514Escapes(e){for(let n=0;n<e.length;n++){if("\\"!==e[n])continue;const t=e[n+1];if(void 0===t)return!1;if(r.has(t))n++;else{if(!/^[0-9a-fA-F]{2}$/.test(e.slice(n+1,n+3)))return!1;n+=2}}return!0}function assertRfc4514Escapes(n,t){if(!hasOnlyRfc4514Escapes(n))throw new e(`${t} uses an escape RFC 4514 does not define`)}function getParentDn(e){const n=parseDn(e);return n.length<=1?e:n.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function rdnValue(e){const[n]=parseDn(e);return rdnValueOf(n)}function unquotedValue(e){let n=0;for(let t=e.length-2;t>0&&"\\"===e[t];t--)n++;return e.length>=2&&e.startsWith('"')&&e.endsWith('"')&&n%2==0?e.slice(1,-1):e}function rdnValueOf(e){if(!e)return"";const n=e.indexOf("=");if(n<=0)return"";let t=n+1;for(;t<e.length&&asciiSpace(e[t]);)t++;return unescapeDnValue(unquotedValue(e.slice(t)))}function dropSpaces(e){const n=e.filter((n,t)=>" "!==n||" "!==e[t-1]);let t=0,r=n.length;for(;t<r&&" "===n[t];)t++;for(;r>t&&" "===n[r-1];)r--;return n.slice(t,r)}function assertedRdnValue(e,n,t){const[r]=parseDn(e),i=r?r.indexOf("="):-1;return i<=0||r.slice(0,i).trim().toLowerCase()!==n.toLowerCase()||splitRdn(r).length>1?null:void 0===t||isDirectChildOf(e,t)?dropSpaces([...rdnValueOf(r).normalize("NFKC")]).join(""):null}function namesByDn(e,n){try{const[t]=parseDn(e),r=t?t.indexOf("="):-1;return r>0&&t.slice(0,r).trim().toLowerCase()===n.toLowerCase()}catch{return!1}}function isChildOf(e,n){try{return isDnInBranch(e,n)&&!isDnInBranch(n,e)}catch{return!1}}function isDirectChildOf(e,n){try{const[,...t]=parseDn(e);return t.length>0&&normalizeDn(t.join(","))===normalizeDn(n)}catch{return!1}}const i=/((?:\\[0-9a-fA-F]{2})+)|\\([^])/g;function hexByte(e){return e.toString(16).padStart(2,"0")}function utf8Chars(e){const n=[],r=Uint8Array.from(e);for(let i=0;i<e.length;){const o=utf8Length(e[i]);let s;if(o&&i+o<=e.length)try{s=t.decode(r.subarray(i,i+o))}catch{}void 0===s?n.push(e[i++]):(n.push(...s),i+=o)}return n}function utf8Length(e){return e<128?1:e>>5==6?2:e>>4==14?3:e>>3==30?4:0}const o=new Map,s=new Map;function registerAttributeTypes(e){for(const{oid:n,names:t}of e){const e=(t[0]??n).toLowerCase();for(const r of[n,...t])o.set(r.toLowerCase(),e),s.set(r.toLowerCase(),t[0]??n)}}function knownAttributeName(e){return s.get(e.toLowerCase().replace(/^oid\./,""))}function rdnTypes(e){return splitRdn(parseDn(e)[0]??"").map(e=>{const n=e.indexOf("=");return-1===n?"":canonicalType(e.slice(0,n).trim())})}function canonicalType(e){const n=e.toLowerCase().replace(/^oid\./,"");return o.get(n)??n}function splitRdn(e){const n=[];let t="",r=!1,i=!1;for(const o of e){if(r)r=!1;else if("\\"===o)r=!0;else if('"'===o&&(i||opensValue(t)))i=!i;else if("+"===o&&!i){n.push(t),t="";continue}t+=o}return n.push(t),n}function normalizeDn(e){return normalizedRdns(e).join(",")}function canonicalDn(e){return normalizedRdns(e,!1).join(",")}function normalizedRdns(e,n=!0){return parseDn(e).map(e=>splitRdn(e).map(e=>{const t=e.indexOf("=");if(-1===t)return e.trim().toLowerCase();const r=function(e,n=!0){const t=trimUnescaped(e);if(/^#(?:[0-9a-fA-F]{2})+$/.test(t))return t;e=unquotedValue(t);const r=[];let o=0;const plain=e=>{for(const n of e)r.push(n)};for(const n of e.matchAll(i)){plain(e.slice(o,n.index)),o=n.index+n[0].length;const[,t,i]=n;if(void 0!==i){r.push(i);continue}const s=t.split("\\").filter(Boolean).map(e=>parseInt(e,16));for(const e of utf8Chars(s))"number"==typeof e?r.push(`\\${hexByte(e)}`):plain(e)}o<e.length&&plain(e.slice(o));const kept=e=>3===e.length&&"\\"===e[0],fold=e=>(n?e.toLowerCase():e).normalize("NFKC"),s=[];let a="";for(const e of r)kept(e)?(s.push(...fold(a),e),a=""):a+=e;s.push(...fold(a));const c=dropSpaces(s);let u="";for(const[e,n]of c.entries())kept(n)?u+=n:n.charCodeAt(0)<32||127===n.charCodeAt(0)?u+=`\\${hexByte(n.charCodeAt(0))}`:',+"\\<>;='.includes(n)||0===e&&"#"===n?u+=`\\${n}`:u+=n;return u}(e.slice(t+1),n);return`${canonicalType(e.slice(0,t).trim())}=${n?r.toLowerCase():r}`}).sort().join("+")).filter(e=>e.length>0)}function isDummyMemberDn(e,n){if(!n||"string"!=typeof e)return!1;if(e===n)return!0;try{return normalizeDn(e)===normalizeDn(n)}catch{return!1}}function organizationLink(e){for(const n of Array.isArray(e)?e:[e]){if(null==n)continue;const e=String(n).trim();if(e.length>0)return e}}function isDnInBranch(e,n){const t=normalizedRdns(n);if(0===t.length)return!1;const r=normalizedRdns(e);if(r.length<t.length)return!1;return r.slice(r.length-t.length).every((e,n)=>e===t[n])}function movedToTrash(e,n,t){return!!e&&isDnInBranch(t,e)&&!isDnInBranch(n,e)}function parseDirectoryDate(e){const n=String(e).trim(),t=/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})?(\d{2})?(?:[.,](\d+))?(Z|[+-]\d{2}(?::?\d{2})?)?$/.exec(n);if(!t){const e=new Date(n);return isNaN(e.getTime())?null:e}const[,r,i,o,s,a,c,u,l]=t,[f,d,p,m,h,g]=[r,i,o,s,a,c].map(e=>void 0===e?0:Number(e));if(d<1||d>12||m>23||h>59||g>60)return null;const D=new Date(0);if(D.setUTCFullYear(f,d-1,p),D.getUTCMonth()!==d-1||D.getUTCDate()!==p)return null;D.setUTCHours(m,h,g);let y=D.getTime();if(void 0!==u){const e=void 0!==c?1e3:void 0!==a?6e4:36e5;y+=Math.round(Number(`0.${u}`)*e)}if(l&&"Z"!==l){const e=l.slice(1).replace(":",""),n=Number(e.slice(0,2)),t=e.length>2?Number(e.slice(2,4)):0;if(n>23||t>59)return null;y-=("-"===l[0]?-1:1)*(60*n+t)*6e4}return new Date(y)}registerAttributeTypes([{oid:"2.5.4.0",names:["objectClass"]},{oid:"2.5.4.3",names:["cn","commonName"]},{oid:"2.5.4.4",names:["sn","surname"]},{oid:"2.5.4.5",names:["serialNumber"]},{oid:"2.5.4.6",names:["c","countryName"]},{oid:"2.5.4.7",names:["l","localityName"]},{oid:"2.5.4.8",names:["st","stateOrProvinceName"]},{oid:"2.5.4.9",names:["street","streetAddress"]},{oid:"2.5.4.10",names:["o","organizationName"]},{oid:"2.5.4.11",names:["ou","organizationalUnitName"]},{oid:"2.5.4.12",names:["title"]},{oid:"2.5.4.42",names:["givenName","gn"]},{oid:"2.5.4.43",names:["initials"]},{oid:"2.5.4.46",names:["dnQualifier"]},{oid:"2.5.4.65",names:["pseudonym"]},{oid:"0.9.2342.19200300.100.1.1",names:["uid","userid"]},{oid:"0.9.2342.19200300.100.1.3",names:["mail","rfc822Mailbox"]},{oid:"0.9.2342.19200300.100.1.25",names:["dc","domainComponent"]},{oid:"2.16.840.1.113730.3.1.241",names:["displayName"]},{oid:"2.16.840.1.113730.3.1.3",names:["employeeNumber"]},{oid:"1.3.6.1.1.1.1.0",names:["uidNumber"]},{oid:"1.3.6.1.1.1.1.1",names:["gidNumber"]}]);const asyncHandler=e=>(n,t,r)=>{Promise.resolve(e(n,t,r)).catch(r)};export{assertRfc4514Escapes,assertedRdnValue,asyncHandler,canonicalDn,escapeDnValue,escapeLdapFilter,escapeRegex,fillPattern,getCompiledRegex,getParentDn,getRdn,hasOnlyRfc4514Escapes,isChildOf,isDirectChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,movedToTrash,namesByDn,normalizeDn,organizationLink,parseDirectoryDate,parseDn,rdnTypes,rdnValue,registerAttributeTypes,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomBytes as e}from"crypto";import{Type as t}from"@sinclair/typebox";import s from"../../abstract/plugin.js";import{roleAttribute as o}from"../../config/schema.js";import{BadRequestError as a,NotFoundError as n}from"../../lib/errors.js";import{defineRoute as r}from"../../lib/route.js";const c=t.Object({state:t.String()}),i=t.Object({password:t.Optional(t.String()),forceChange:t.Optional(t.Boolean())}),d=t.Object({success:t.Boolean(),state:t.String()}),l=t.Object({success:t.Boolean(),generated:t.Boolean(),forceChange:t.Boolean(),password:t.Optional(t.String())}),p="abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%*-_=+";function generatePassword(t=16){const s=67*Math.floor(256/67);let o="";for(;o.length<t;)for(const a of e(t))if(!(a>=s)&&(o+=p[a%67],o.length===t))break;return o}class LdapAccountLifecycle extends s{name="ldapAccountLifecycle";roles=["api"];dependencies={ldapFlatGeneric:"core/ldap/flatGeneric"};api(e){const t=this.server.loadedPlugins.ldapFlatGeneric;if(!t?.instances?.length)return void this.logger.warn("accountLifecycle: no flat entity is loaded, no endpoint registered");const s=`${this.config.api_prefix}/v1/ldap`;for(const a of t.instances){const t=o(a.schema,"accountStatus"),n=t?a.schema?.attributes[t]?.states:void 0;t&&n&&Object.keys(n).length>0&&(r(e,{method:"post",path:`${s}/${a.pluralName}/:id/status`,body:c,responses:{200:d},handler:async({req:e,res:s})=>this.setStatus(a,t,n,e,s)}),this.logger.info(`accountLifecycle: ${a.pluralName} states = ${Object.keys(n).join(", ")}`));const p=o(a.schema,"password");p&&r(e,{method:"post",path:`${s}/${a.pluralName}/:id/password`,body:i,responses:{200:l},handler:async({req:e,res:t})=>this.resetPassword(a,p,e,t)})}}async setStatus(e,t,s,o,n){const r=o.body;if(!Object.hasOwn(s,r.state))throw new a(`Unknown state "${r.state}" for ${e.singularName}; known states: ${Object.keys(s).join(", ")}`);const c=s[r.state],i=
|
|
1
|
+
import{randomBytes as e}from"crypto";import{Type as t}from"@sinclair/typebox";import s from"../../abstract/plugin.js";import{roleAttribute as o}from"../../config/schema.js";import{BadRequestError as a,NotFoundError as n}from"../../lib/errors.js";import{defineRoute as r}from"../../lib/route.js";const c=t.Object({state:t.String()}),i=t.Object({password:t.Optional(t.String()),forceChange:t.Optional(t.Boolean())}),d=t.Object({success:t.Boolean(),state:t.String()}),l=t.Object({success:t.Boolean(),generated:t.Boolean(),forceChange:t.Boolean(),password:t.Optional(t.String())}),p="abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%*-_=+";function generatePassword(t=16){const s=67*Math.floor(256/67);let o="";for(;o.length<t;)for(const a of e(t))if(!(a>=s)&&(o+=p[a%67],o.length===t))break;return o}class LdapAccountLifecycle extends s{name="ldapAccountLifecycle";roles=["api"];dependencies={ldapFlatGeneric:"core/ldap/flatGeneric"};api(e){const t=this.server.loadedPlugins.ldapFlatGeneric;if(!t?.instances?.length)return void this.logger.warn("accountLifecycle: no flat entity is loaded, no endpoint registered");const s=`${this.config.api_prefix}/v1/ldap`;for(const a of t.instances){const t=o(a.schema,"accountStatus"),n=t?a.schema?.attributes[t]?.states:void 0;t&&n&&Object.keys(n).length>0&&(r(e,{method:"post",path:`${s}/${a.pluralName}/:id/status`,body:c,responses:{200:d},handler:async({req:e,res:s})=>this.setStatus(a,t,n,e,s)}),this.logger.info(`accountLifecycle: ${a.pluralName} states = ${Object.keys(n).join(", ")}`));const p=o(a.schema,"password");p&&r(e,{method:"post",path:`${s}/${a.pluralName}/:id/password`,body:i,responses:{200:l},handler:async({req:e,res:t})=>this.resetPassword(a,p,e,t)})}}async setStatus(e,t,s,o,n){const r=o.body;if(!Object.hasOwn(s,r.state))throw new a(`Unknown state "${r.state}" for ${e.singularName}; known states: ${Object.keys(s).join(", ")}`);const c=s[r.state],i=o.params.id;await this.modify(e,i,{replace:{[t]:c}},o),n.json({success:!0,state:r.state})}async resetPassword(e,t,s,n){const r=s.body,c=r.password;if(""===c)throw new a("Field password must be a non-empty string; omit it to have one generated");if(void 0!==c){if([...c].length>256)throw new a("Field password must be at most 256 characters");if(/[\u0000-\u001f\u007f-\u009f]/.test(c))throw new a("Field password must not contain control characters")}const i=void 0===c,d=i?generatePassword():c,l={[t]:d},p=o(e.schema,"passwordReset"),f=!1!==r.forceChange;if(p){const t=e.schema?.attributes[p],s=t?.states,o=f?s?.required??"TRUE":s?.cleared??"FALSE";l[p]=o}const h=s.params.id;await this.modify(e,h,{replace:l},s),n.json({success:!0,generated:i,forceChange:Boolean(p)&&f,...i?{password:d}:{}})}async modify(e,t,s,o){try{await e.modifyEntry(t,s,o)}catch(t){if(32===t.code)throw new n(`${e.singularName} not found`);throw t}}}export{i as PasswordBody,l as PasswordResponse,c as StatusBody,d as StatusResponse,LdapAccountLifecycle as default,generatePassword};
|