ldap-rest 0.8.2 → 0.9.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.
Files changed (79) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/bin/index.js +1 -1
  3. package/dist/config/args.js +1 -1
  4. package/dist/lib/auth/base.js +1 -1
  5. package/dist/lib/authz/base.js +1 -1
  6. package/dist/lib/authz/composition.js +1 -0
  7. package/dist/lib/expressFormatedResponses.js +1 -1
  8. package/dist/lib/ldapActions.js +1 -1
  9. package/dist/lib/ldapCodes.js +1 -0
  10. package/dist/lib/parseConfig.js +1 -1
  11. package/dist/lib/storage/store.js +1 -0
  12. package/dist/lib/utils.js +1 -1
  13. package/dist/plugins/auth/authzDynamic.js +1 -1
  14. package/dist/plugins/auth/authzPerBranch.js +1 -1
  15. package/dist/plugins/auth/authzPerRoute.js +1 -1
  16. package/dist/plugins/auth/authzScope.js +1 -1
  17. package/dist/plugins/auth/hmac.js +1 -1
  18. package/dist/plugins/auth/llng.js +1 -1
  19. package/dist/plugins/auth/openidconnect.js +1 -1
  20. package/dist/plugins/auth/token.js +1 -1
  21. package/dist/plugins/auth/totp.js +1 -1
  22. package/dist/plugins/bcl.js +1 -0
  23. package/dist/plugins/ldap/organizations.js +1 -1
  24. package/dist/plugins/priority.json.js +1 -1
  25. package/dist/plugins/scim/baseResolver.js +1 -1
  26. package/dist/plugins/scim/errors.js +1 -1
  27. package/dist/plugins/scim/groups.js +1 -1
  28. package/dist/plugins/scim/list.js +1 -1
  29. package/dist/plugins/scim/users.js +1 -1
  30. package/dist/plugins/storage/file.js +1 -0
  31. package/dist/plugins/storage/ldap.js +1 -0
  32. package/dist/plugins/storage.js +1 -0
  33. package/dist/src/abstract/ldapFlat.d.ts +7 -0
  34. package/dist/src/abstract/plugin.d.ts +20 -0
  35. package/dist/src/bin/index.d.ts +50 -8
  36. package/dist/src/config/args.d.ts +36 -0
  37. package/dist/src/hooks.d.ts +55 -0
  38. package/dist/src/lib/auth/base.d.ts +175 -9
  39. package/dist/src/lib/authz/base.d.ts +133 -4
  40. package/dist/src/lib/authz/composition.d.ts +145 -0
  41. package/dist/src/lib/expressFormatedResponses.d.ts +9 -2
  42. package/dist/src/lib/ldapActions.d.ts +30 -0
  43. package/dist/src/lib/ldapCodes.d.ts +28 -0
  44. package/dist/src/lib/storage/store.d.ts +84 -0
  45. package/dist/src/lib/utils.d.ts +19 -0
  46. package/dist/src/plugins/auth/authzDynamic.d.ts +76 -0
  47. package/dist/src/plugins/auth/authzPerBranch.d.ts +11 -0
  48. package/dist/src/plugins/auth/authzPerRoute.d.ts +26 -0
  49. package/dist/src/plugins/auth/authzScope.d.ts +44 -3
  50. package/dist/src/plugins/auth/hmac.d.ts +2 -0
  51. package/dist/src/plugins/auth/llng.d.ts +39 -0
  52. package/dist/src/plugins/auth/openidconnect.d.ts +66 -3
  53. package/dist/src/plugins/auth/rateLimit.d.ts +3 -1
  54. package/dist/src/plugins/auth/token.d.ts +2 -0
  55. package/dist/src/plugins/auth/totp.d.ts +2 -0
  56. package/dist/src/plugins/bcl.d.ts +65 -0
  57. package/dist/src/plugins/ldap/organizations.d.ts +90 -1
  58. package/dist/src/plugins/scim/baseResolver.d.ts +3 -1
  59. package/dist/src/plugins/scim/errors.d.ts +2 -10
  60. package/dist/src/plugins/storage/file.d.ts +19 -0
  61. package/dist/src/plugins/storage/ldap.d.ts +17 -0
  62. package/dist/src/plugins/storage.d.ts +44 -0
  63. package/docs/plugin-development/README.md +12 -1
  64. package/docs/plugin-development/dependencies.md +40 -15
  65. package/docs/usage/configuration.md +89 -13
  66. package/docs/usage/plugins/README.md +17 -12
  67. package/docs/usage/plugins/auth/README.md +129 -0
  68. package/docs/usage/plugins/auth/authz-dynamic.md +42 -0
  69. package/docs/usage/plugins/auth/authz-linid1.md +32 -0
  70. package/docs/usage/plugins/auth/authz-per-branch.md +8 -0
  71. package/docs/usage/plugins/auth/authz-per-route.md +6 -0
  72. package/docs/usage/plugins/auth/authz-scope.md +69 -16
  73. package/docs/usage/plugins/auth/back-channel-logout.md +86 -0
  74. package/docs/usage/plugins/auth/llng.md +29 -0
  75. package/docs/usage/plugins/auth/oidc.md +52 -0
  76. package/docs/usage/plugins/ldap/organizations.md +37 -0
  77. package/docs/usage/plugins/utilities/storage.md +94 -0
  78. package/docs/usage/upgrading.md +289 -0
  79. package/package.json +17 -1
@@ -1 +1 @@
1
- import t from"fs";import{wantJson as e,jsonBody as i,created as s,tryMethod as r,multiStatus as a}from"../lib/expressFormatedResponses.js";import{transformSchemas as n,getCompiledRegex as o,escapeDnValue as h,asyncHandler as d,escapeRegex as c,validateDnValue as u,isDummyMemberDn as l,normalizeDn as f,getParentDn as m,launchHooksChained as b,launchHooks as p,escapeLdapFilter as g,isDnInBranch as w,substringSearchFilter as y}from"../lib/utils.js";import{neverReturnAttributes as $,schemaAttribute as A,modifiedAttributeNames as v,missingRequiredAttribute as E,checkDnValues as x}from"../config/schema.js";import{BadRequestError as N,ConflictError as j,NotFoundError as k,HttpError as D}from"../lib/errors.js";import O from"./plugin.js";class LdapFlat extends O{constructor(e,i){if(super(e),this.ldap=e.ldap,this.base=i.base,this.mainAttribute=i.mainAttribute,this.objectClass=i.objectClass,this.defaultAttributes=i.defaultAttributes||{},this.singularName=i.singularName,this.pluralName=i.pluralName,this.hookPrefix=i.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(n(e,this.config)),this.logger.info(`${this.singularName} schema loaded from ${i.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${i.schemaPath}: ${t}`)}}clientForbiddenAttributes(){const t=new Set;if(!this.schema)return t;for(const[e,i]of Object.entries(this.schema.attributes))(e!==this.mainAttribute||i.generatedFrom)&&(i.generated||i.readOnly)&&t.add(e.toLowerCase());return t}hiddenAttributes(){return $([this.schema])}project(t){const e=this.hiddenAttributes();if(0===e.size)return t;const i={};for(const[s,r]of Object.entries(t))e.has(s.split(";")[0].toLowerCase())||(i[s]=r);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,s]of Object.entries(t))e[i]=this.project(s);return e}rejectForbiddenInput(t){const e=this.clientForbiddenAttributes();if(0!==e.size)for(const i of t){if("dn"===i)continue;if(!e.has(i.split(";")[0].toLowerCase()))continue;const t=A(this.schema,i)?.[1];throw new N(t?.readOnly?`Attribute "${i}" is read-only and cannot be set`:`Attribute "${i}" is computed by the server and cannot be set`)}}async generateMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const s=t[i.attribute],r=Array.isArray(s)?s[0]:s;if(null==r||""===r)throw new N(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let a=String(r);if(i.extract){const t=o(i.extract).exec(a);t&&(a=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(a=a.toLowerCase()),i.strip&&(a=a.replace(o(i.strip,"g"),"")),!a)throw new N(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);if(!await this.rdnExists(a))return a;if("suffix"!==i.onCollision)throw new j(`${this.singularName} "${a}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const e=`${a}-${t}`;if(!await this.rdnExists(e))return e}throw new j(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}async rdnExists(t){try{const e=await this.ldap.search({paged:!1,scope:"base"},`${this.mainAttribute}=${h(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}invalidValueMessage(t){const e=this.schema?.attributes[t],i=e?.hint||e?.items?.hint;return i?`Invalid value for attribute "${t}": ${i}`:`Invalid value for attribute "${t}"`}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,d(async(t,i)=>{if(!e(t,i))return;const s={};t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(s.filter=y(t.query.match,t.query.attribute)),t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(","));const r=await this.listEntries({...s,req:t});i.json(this.projectList(r))})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,d(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,d(async(t,e)=>this.apiMove(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,d(async(t,e)=>this.apiRename(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const e=this.resolveDn(s),r=await this.ldap.search({paged:!1,scope:"base"},e,t);if(0===r.searchEntries.length)throw new k(`${this.singularName} not found`);i.json(this.project(r.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new k(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const r=this.schema?.attributes[this.mainAttribute],a=Boolean(r?.generated&&r.generatedFrom),n=a?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o=a?await this.generateMainAttribute(n):n[this.mainAttribute],h={...n};delete h[this.mainAttribute],delete h.dn,await this.addEntry(o,h,t);const d=await this.searchEntriesByName(o,!1);return s(e,this.project(d[o]))}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await r(i,this.deleteEntry.bind(this),s,t)}async apiModify(t,e){const s=i(t,e);if(!s)return;this.rejectForbiddenInput(v(s));const a=decodeURIComponent(t.params.id);await r(e,this.modifyEntry.bind(this),a,s,t)}async apiMove(t,s){if(!e(t,s))return;const r=i(t,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(t.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new N("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,t);s.json({success:!0,...o})}async apiRename(t,s){if(!e(t,s))return;const r=i(t,s,"newId");if(!r)return;const n=r.newId;if("string"!=typeof n)throw new N("Missing or invalid newId in request body");if(this.rejectForbiddenInput(Object.keys(r).filter(t=>"newId"!==t)),new RegExp(`^${c(this.mainAttribute)}=`,"i").test(n))throw new N(`newId must be a ${this.mainAttribute} value, not a DN`);try{u(n,this.mainAttribute)}catch(t){throw new N(t instanceof Error?t.message:"Invalid newId")}if(!await this._validateOneChange(this.mainAttribute,n))throw new N(this.invalidValueMessage(this.mainAttribute));const o=decodeURIComponent(t.params.id),h=this.resolveDn(o),d=this.resolveDn(n),m=this.config.group_dummy_user;if(m&&(l(h,m)||l(d,m)))throw new j(`${m} is the configured placeholder member and cannot be renamed`);const b=f(h)===f(d),p=await this.entryExists(h),g=b?p:await this.entryExists(d),w=this.requirePlugin("ldapEnterpriseRules");let y=[];if(b){if(!p)throw new k(`${this.singularName} not found`)}else if(p){if(g)throw new j(`${this.singularName} "${n}" already exists`);y=await(w?.findReferences(h))||[],await this.renameEntry(h,d,t)}else{if(!g)throw new k(`${this.singularName} not found`);y=await(w?.findReferences(h))||[]}const $=w&&y.length>0?await w.rewriteReferences(y,h,d,t):{updated:0,failed:[]},A={success:0===$.failed.length,dn:d,referencesUpdated:$.updated};0!==$.failed.length?a(s,{...A,referencesFailed:$.failed}):s.json(A)}async entryExists(t){try{const e=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(new RegExp(`^${c(this.mainAttribute)}=`,"i").test(t)){const e=m(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 s;new RegExp(`^${this.mainAttribute}=`,"i").test(t)?(s=this.resolveDn(t),t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(u(t,this.mainAttribute),s=this.resolveDn(t)),await this.validateNewEntry(s,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let r,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,a]=await b(this.registeredHooks[`${this.hookPrefix}add`],[s,a]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{r=await this.ldap.add(s,a,i)}catch(t){const e=t instanceof D&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),t instanceof D)throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return p(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e,i){let s=this.resolveDn(t);const r=this.opNumber();if([s,e]=await b(this.registeredHooks[`${this.hookPrefix}modify`],[s,e,r]),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.validateChanges(s,e);const a=await this.ldap.modify(s,e,i);return p(this.registeredHooks[`${this.hookPrefix}modifydone`],[s,e,r]),a}async renameEntry(t,e,i){/,/.test(t)||u(t,this.mainAttribute),/,/.test(e)||u(e,this.mainAttribute);let s=this.resolveDn(t),r=this.resolveDn(e);[s,r]=await b(this.registeredHooks[`${this.hookPrefix}rename`],[s,r]);const a=await(i?this.ldap.forRequest(i).rename(s,r):this.ldap.rename(s,r));return p(this.registeredHooks[`${this.hookPrefix}renamedone`],[s,r]),a}async deleteEntry(t,e){let i=this.resolveDn(t);i=await b(this.registeredHooks[`${this.hookPrefix}delete`],i);const s=await this.ldap.delete(i,e);return p(this.registeredHooks[`${this.hookPrefix}deletedone`],i),s}async moveEntry(t,e,i){const s=this.resolveDn(t),r=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[r]||!this.schema?.attributes[a])throw new N(`Schema for ${this.singularName} does not support move operation (missing ${r} or ${a})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[r]},s,i);if(!n.searchEntries||0===n.searchEntries.length)throw new k(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await b(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const s=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!s.searchEntries||0===s.searchEntries.length)throw new k(`Organization ${t} not found`);const r=s.searchEntries[0];if(r[i]){const t=r[i];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){if(e instanceof k)throw e;if(32===e.code)throw new k(`Organization ${t} not found`);throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e,req:i}){const s={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(s.attributes=e);const r=await this.ldap.search(s,this.base,i),a={};for await(const t of r)t.searchEntries.forEach(t=>{if(t[this.mainAttribute]){const e=t[this.mainAttribute];let i;i=Array.isArray(e)?"string"==typeof e[0]?e[0]:String(e[0]):"string"==typeof e?e:String(e),a[i]=t}});return a}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=g(t),r=e?`(${this.mainAttribute}=*${s}*)`:`(${this.mainAttribute}=${s})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new N(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const s=JSON.stringify(e.default);if(s!==JSON.stringify(i))throw new N(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.required&&!i&&!e.generated)throw new N(`Attribute "${t}" is required`)}const i=E(this.schema,e,this.server.loadedPlugins);if(i)throw new N(`Attribute "${i}" is required`);return!0}async validateChanges(t,e){if(!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new N(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new N(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if("pointer"===i.type){if("string"!=typeof e)throw new N(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>w(s,t)))throw new N(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new N(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new N(`Field ${t} points to invalid or non-existent DN: ${s}`)}}"pointer"!==i.type&&await x(t,i,e,async t=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0);const s=i.test??i.items?.test;if(s){const t="string"==typeof s?o(s):s;return function(t){return(Array.isArray(t)?t:[t]).map(t=>String(t))}(e).every(e=>t.test(e))}return!0}}export{LdapFlat as default};
1
+ import t from"fs";import{wantJson as e,jsonBody as i,created as s,tryMethod as r,multiStatus as a}from"../lib/expressFormatedResponses.js";import{transformSchemas as n,getCompiledRegex as o,escapeDnValue as h,asyncHandler as d,escapeRegex as c,validateDnValue as u,isDummyMemberDn as l,normalizeDn as f,getParentDn as m,launchHooksChained as b,launchHooks as p,escapeLdapFilter as g,isDnInBranch as w,substringSearchFilter as y}from"../lib/utils.js";import{neverReturnAttributes as $,schemaAttribute as A,modifiedAttributeNames as v,missingRequiredAttribute as E,checkDnValues as x}from"../config/schema.js";import{BadRequestError as N,ConflictError as j,NotFoundError as k,HttpError as D}from"../lib/errors.js";import O from"./plugin.js";class LdapFlat extends O{constructor(e,i){if(super(e),this.ldap=e.ldap,this.base=i.base,this.mainAttribute=i.mainAttribute,this.objectClass=i.objectClass,this.defaultAttributes=i.defaultAttributes||{},this.singularName=i.singularName,this.pluralName=i.pluralName,this.hookPrefix=i.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(n(e,this.config)),this.logger.info(`${this.singularName} schema loaded from ${i.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${i.schemaPath}: ${t}`)}}clientForbiddenAttributes(){const t=new Set;if(!this.schema)return t;for(const[e,i]of Object.entries(this.schema.attributes))(e!==this.mainAttribute||i.generatedFrom)&&(i.generated||i.readOnly)&&t.add(e.toLowerCase());return t}hiddenAttributes(){return $([this.schema])}project(t){const e=this.hiddenAttributes();if(0===e.size)return t;const i={};for(const[s,r]of Object.entries(t))e.has(s.split(";")[0].toLowerCase())||(i[s]=r);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,s]of Object.entries(t))e[i]=this.project(s);return e}rejectForbiddenInput(t){const e=this.clientForbiddenAttributes();if(0!==e.size)for(const i of t){if("dn"===i)continue;if(!e.has(i.split(";")[0].toLowerCase()))continue;const t=A(this.schema,i)?.[1];throw new N(t?.readOnly?`Attribute "${i}" is read-only and cannot be set`:`Attribute "${i}" is computed by the server and cannot be set`)}}async generateMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const s=t[i.attribute],r=Array.isArray(s)?s[0]:s;if(null==r||""===r)throw new N(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let a=String(r);if(i.extract){const t=o(i.extract).exec(a);t&&(a=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(a=a.toLowerCase()),i.strip&&(a=a.replace(o(i.strip,"g"),"")),!a)throw new N(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);if(!await this.rdnExists(a))return a;if("suffix"!==i.onCollision)throw new j(`${this.singularName} "${a}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const e=`${a}-${t}`;if(!await this.rdnExists(e))return e}throw new j(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${h(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}invalidValueMessage(t){const e=this.schema?.attributes[t],i=e?.hint||e?.items?.hint;return i?`Invalid value for attribute "${t}": ${i}`:`Invalid value for attribute "${t}"`}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,d(async(t,i)=>{if(!e(t,i))return;const s={};t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(s.filter=y(t.query.match,t.query.attribute)),t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(","));const r=await this.listEntries({...s,req:t});i.json(this.projectList(r))})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,d(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,d(async(t,e)=>this.apiMove(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,d(async(t,e)=>this.apiRename(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const e=this.resolveDn(s),r=await this.ldap.search({paged:!1,scope:"base"},e,t);if(0===r.searchEntries.length)throw new k(`${this.singularName} not found`);i.json(this.project(r.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new k(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const r=this.schema?.attributes[this.mainAttribute],a=Boolean(r?.generated&&r.generatedFrom),n=a?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o=a?await this.generateMainAttribute(n):n[this.mainAttribute],h={...n};delete h[this.mainAttribute],delete h.dn,await this.addEntry(o,h,t);const d=await this.searchEntriesByName(o,!1);return s(e,this.project(d[o]))}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await r(i,this.deleteEntry.bind(this),s,t)}async apiModify(t,e){const s=i(t,e);if(!s)return;this.rejectForbiddenInput(v(s));const a=decodeURIComponent(t.params.id);await r(e,this.modifyEntry.bind(this),a,s,t)}async apiMove(t,s){if(!e(t,s))return;const r=i(t,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(t.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new N("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,t);s.json({success:!0,...o})}async apiRename(t,s){if(!e(t,s))return;const r=i(t,s,"newId");if(!r)return;const n=r.newId;if("string"!=typeof n)throw new N("Missing or invalid newId in request body");if(this.rejectForbiddenInput(Object.keys(r).filter(t=>"newId"!==t)),new RegExp(`^${c(this.mainAttribute)}=`,"i").test(n))throw new N(`newId must be a ${this.mainAttribute} value, not a DN`);try{u(n,this.mainAttribute)}catch(t){throw new N(t instanceof Error?t.message:"Invalid newId")}if(!await this._validateOneChange(this.mainAttribute,n))throw new N(this.invalidValueMessage(this.mainAttribute));const o=decodeURIComponent(t.params.id),h=this.resolveDn(o),d=this.resolveDn(n),m=this.config.group_dummy_user;if(m&&(l(h,m)||l(d,m)))throw new j(`${m} is the configured placeholder member and cannot be renamed`);const b=f(h)===f(d),p=await this.entryExists(h),g=b?p:await this.entryExists(d),w=this.requirePlugin("ldapEnterpriseRules");let y=[];if(b){if(!p)throw new k(`${this.singularName} not found`)}else if(p){if(g)throw new j(`${this.singularName} "${n}" already exists`);y=await(w?.findReferences(h))||[],await this.renameEntry(h,d,t)}else{if(!g)throw new k(`${this.singularName} not found`);y=await(w?.findReferences(h))||[]}const $=w&&y.length>0?await w.rewriteReferences(y,h,d,t):{updated:0,failed:[]},A={success:0===$.failed.length,dn:d,referencesUpdated:$.updated};0!==$.failed.length?a(s,{...A,referencesFailed:$.failed}):s.json(A)}async entryExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(new RegExp(`^${c(this.mainAttribute)}=`,"i").test(t)){const e=m(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 s;new RegExp(`^${this.mainAttribute}=`,"i").test(t)?(s=this.resolveDn(t),t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(u(t,this.mainAttribute),s=this.resolveDn(t)),await this.validateNewEntry(s,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let r,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,a]=await b(this.registeredHooks[`${this.hookPrefix}add`],[s,a]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{r=await this.ldap.add(s,a,i)}catch(t){const e=t instanceof D&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),t instanceof D)throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return p(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e,i){let s=this.resolveDn(t);const r=this.opNumber();if([s,e]=await b(this.registeredHooks[`${this.hookPrefix}modify`],[s,e,r]),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.validateChanges(s,e);const a=await this.ldap.modify(s,e,i);return p(this.registeredHooks[`${this.hookPrefix}modifydone`],[s,e,r]),a}async renameEntry(t,e,i){/,/.test(t)||u(t,this.mainAttribute),/,/.test(e)||u(e,this.mainAttribute);let s=this.resolveDn(t),r=this.resolveDn(e);[s,r]=await b(this.registeredHooks[`${this.hookPrefix}rename`],[s,r]);const a=await(i?this.ldap.forRequest(i).rename(s,r):this.ldap.rename(s,r));return p(this.registeredHooks[`${this.hookPrefix}renamedone`],[s,r]),a}async deleteEntry(t,e){let i=this.resolveDn(t);i=await b(this.registeredHooks[`${this.hookPrefix}delete`],i);const s=await this.ldap.delete(i,e);return p(this.registeredHooks[`${this.hookPrefix}deletedone`],i),s}async moveEntry(t,e,i){const s=this.resolveDn(t),r=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[r]||!this.schema?.attributes[a])throw new N(`Schema for ${this.singularName} does not support move operation (missing ${r} or ${a})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[r]},s,i);if(!n.searchEntries||0===n.searchEntries.length)throw new k(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await b(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const s=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!s.searchEntries||0===s.searchEntries.length)throw new k(`Organization ${t} not found`);const r=s.searchEntries[0];if(r[i]){const t=r[i];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){if(e instanceof k)throw e;if(32===e.code)throw new k(`Organization ${t} not found`);throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e,req:i}){const s={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(s.attributes=e);const r=await this.ldap.search(s,this.base,i),a={};for await(const t of r)t.searchEntries.forEach(t=>{if(t[this.mainAttribute]){const e=t[this.mainAttribute];let i;i=Array.isArray(e)?"string"==typeof e[0]?e[0]:String(e[0]):"string"==typeof e?e:String(e),a[i]=t}});return a}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=g(t),r=e?`(${this.mainAttribute}=*${s}*)`:`(${this.mainAttribute}=${s})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new N(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const s=JSON.stringify(e.default);if(s!==JSON.stringify(i))throw new N(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.required&&!i&&!e.generated)throw new N(`Attribute "${t}" is required`)}const i=E(this.schema,e,this.server.loadedPlugins);if(i)throw new N(`Attribute "${i}" is required`);return!0}async validateChanges(t,e){if(!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new N(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new N(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if("pointer"===i.type){if("string"!=typeof e)throw new N(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>w(s,t)))throw new N(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.system.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new N(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new N(`Field ${t} points to invalid or non-existent DN: ${s}`)}}"pointer"!==i.type&&await x(t,i,e,async t=>(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0);const s=i.test??i.items?.test;if(s){const t="string"==typeof s?o(s):s;return function(t){return(Array.isArray(t)?t:[t]).map(t=>String(t))}(e).every(e=>t.test(e))}return!0}}export{LdapFlat as default};
package/dist/bin/index.js CHANGED
@@ -1 +1 @@
1
- import{fileURLToPath as e}from"url";import{join as t,dirname as r}from"path";import i from"express";import s from"body-parser";import{parseConfig as o}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as h}from"../logger/winston.js";import{setLogger as l}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as u,claimedPrefixes as d}from"../lib/auth/base.js";import g from"../plugins/priority.json.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,isDummyMemberDn,launchHooks,launchHooksChained,normalizeDn,parseDn,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this.authenticators=[],this._authState={dispatcherMounted:!1},this._errorMiddlewareSetup=!1,this.config=o(n),this.app=i(),this.app.use(s.json()),this.app.use(s.urlencoded({extended:!0})),this.logger=h(this.config),this.ldap=new a(this),l(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const t=this.config.plugin.find(e=>e.includes("configApi"));let r=this.config.plugin.filter(e=>!e.includes("configApi"));const i=(async()=>{for(const e of g)r.includes(e)&&(r=r.filter(t=>t!==e),await this.loadPlugin(e))})();e.push(i),e.push(i.then(async()=>{const e=r.map(e=>this.loadPlugin(e));await Promise.all(e)})),t&&e.push(Promise.all(e).then(()=>this.loadPlugin(t)))}this.ready=new Promise((t,r)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),t()}).catch(e=>r(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),t())})}routerStack(){const e=this.app;return(e.router??e._router)?.stack}withConfig(e){return Object.assign(Object.create(Object.getPrototypeOf(this)),this,{config:e})}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,r)=>{this.dispatchAuth(e,t,r)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}dispatchAuth(e,t,r){let i=-1,s=[];for(const t of this.authenticators)for(const r of t.pathPrefixes)u(r,e.path)&&(r.length>i?(i=r.length,s=[t]):r.length!==i||s.includes(t)||s.push(t));if(i<0&&(s=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===s.length)return r();const run=i=>{if(i>=s.length)return r();s[i].authenticate(e,t,()=>run(i+1))};run(0)}claimedAuthPrefixes(e){return d(this.loadedPlugins,e)}warnUnauthenticatedRoutes(){const e=Object.values(this.loadedPlugins).filter(e=>e.roles?.includes("auth"));if(0===e.length)return[];const t=[];for(const r of e){if(!r.pathPrefixes||0===r.pathPrefixes.length)return[];t.push(...r.pathPrefixes)}const r=this.routerStack(),i=new Set;for(const e of r||[]){const t=e.route?.path;"string"==typeof t?i.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&i.add(e))}const s=[...i].filter(e=>!t.some(t=>e===t||e.startsWith(`${t}/`)));return s.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication: ${s.sort().join(", ")}`),s.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,r,i)=>{let s="statusCode"in e?e.statusCode:500,o=e.message;if(500===s&&/\[authz-forbidden\]/.test(e.message)?(s=403,o="Token does not have permission on this branch"):403===s&&/\[authz-forbidden\]/.test(e.message)&&(o="Token does not have permission on this branch"),s>=400&&s<500)return this.logger.warn(`Client error ${s} in request ${t.method} ${t.path}: ${o}`),r.headersSent?void 0:r.status(s).json({error:o});this.logger.error(`Server error ${s} in request ${t.method} ${t.path}: ${e.message}`,{stack:e.stack,url:t.url,method:t.method}),r.headersSent||r.status(s).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,t)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:t})}),new Promise((e,t)=>{this.server=this.app.listen(this.config.port,r=>{r?(this.logger.error(`Error starting server: ${r}`),t(r)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(i){let s,o;if(/:/.test(i)){let e=i.substring(i.indexOf(":")+1);if(i=i.substring(0,i.indexOf(":")),/:/.test(e)){s=e.substring(0,e.indexOf(":")),s||(s=void 0),e=e.substring(e.indexOf(":")+1);try{o=JSON.parse(e),"object"!=typeof o?(this.logger.error(`Overrides for plugin ${i} are not valid: ${e}`),o=void 0):this.logger.debug(`Overrides for plugin ${s||i}: ${e}`)}catch(t){this.logger.error(`Failed to parse overrides for plugin ${i}: ${t}, using ${e}`),o=void 0}}else s=e,s||(s=void 0)}else s=void 0;return this.logger.debug(`Loading plugin ${i}`),i.startsWith("core/")&&(i=i.replace("core/",t(r(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((e,t)=>{import(i).then(async r=>{let n;if(r&&r.default&&(r=r.default),o){const e={...this.config,...o};n=new r(this.withConfig(e))}else n=new r(this);if(!n)return t(new Error(`Unable to load ${i}`));e(await this.registerPlugin(i,n,s)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(e=>t(new Error(`Failed to load plugin ${i}: ${e}`)))})}async registerPlugin(e,t,r){if(t.name||(t.name=e),r&&(t.name=r),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(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 r=t.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!r||"function"!=typeof r)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);this.hooks[e].push(r)}return this.loadedPlugins[t.name]=t,!0}}export{DM};
1
+ import{fileURLToPath as e}from"url";import{join as t,dirname as r}from"path";import i from"express";import s from"body-parser";import{parseConfig as o}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as h}from"../logger/winston.js";import{setLogger as l}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as u}from"../lib/auth/base.js";import{assertAuthzComposition as d}from"../lib/authz/composition.js";import{recordHookOwner as g}from"../lib/utils.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,hookOwner,isChildOf,isDnInBranch,isDummyMemberDn,launchHooks,launchHooksChained,normalizeDn,parseDn,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";import c from"../plugins/priority.json.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this.authenticators=[],this._authState={dispatcherMounted:!1},this._errorMiddlewareSetup=!1,this._composed=!1,this.config=o(n),this.app=i(),this.app.use(s.json()),this.app.use(s.urlencoded({extended:!0})),this.logger=h(this.config),this.ldap=new a(this),l(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const t=this.config.plugin.find(e=>e.includes("configApi"));let r=this.config.plugin.filter(e=>!e.includes("configApi"));const i=this.logger,s=(async()=>{for(const e of c){const t=this.resolveModule(DM.moduleOf(e)),s=r.filter(e=>this.resolveModule(DM.moduleOf(e))===t);if(0!==s.length){r=r.filter(e=>!s.includes(e));for(const e of s)i.debug(`Loading ${e} in the priority pass`),await this.loadPlugin(e)}}})();e.push(s),e.push(s.then(async()=>{const e=r.map(e=>(i.debug(`Loading ${e} in the parallel batch`),this.loadPlugin(e)));await Promise.all(e)})),t&&e.push(Promise.all(e).then(()=>this.loadPlugin(t)))}this.ready=new Promise((t,r)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),d(this),this._composed=!0,this.afterLoad(),t()}).catch(e=>r(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),this._composed=!0,t())})}routerStack(){const e=this.app;return(e.router??e._router)?.stack}withConfig(e){return Object.assign(Object.create(Object.getPrototypeOf(this)),this,{config:e})}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,r)=>{this.dispatchAuth(e,t,r)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}dispatchAuth(e,t,r){let i=-1,s=[];for(const t of this.authenticators)for(const r of t.pathPrefixes)u(r,e.path)&&(r.length>i?(i=r.length,s=[t]):r.length!==i||s.includes(t)||s.push(t));if(i<0&&(s=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===s.length)return r();const finish=i=>{if(i>=s.length)return r();const o=s[i];if(!o.afterChain)return finish(i+1);o.afterChain(e,t,()=>finish(i+1))},o=[];e.authenticators=o;const run=r=>{if(r>=s.length)return finish(0);const i=s[r];i.authenticate(e,t,()=>{i.vouchedFor(e)&&o.push(i.name),run(r+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 r of e){if(!r.pathPrefixes||0===r.pathPrefixes.length)return[];t.push(...r.pathPrefixes)}const r=this.routerStack(),i=new Set;for(const e of r||[]){const t=e.route?.path;"string"==typeof t?i.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&i.add(e))}const s=[...i].filter(e=>!t.some(t=>e===t||e.startsWith(`${t}/`)));return s.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication: ${s.sort().join(", ")}`),s.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,r,i)=>{let s="statusCode"in e?e.statusCode:500,o=e.message;if(500===s&&/\[authz-forbidden\]/.test(e.message)?(s=403,o="Token does not have permission on this branch"):403===s&&/\[authz-forbidden\]/.test(e.message)&&(o="Token does not have permission on this branch"),s>=400&&s<500)return this.logger.warn(`Client error ${s} in request ${t.method} ${t.path}: ${o}`),void(r.headersSent||r.status(s).json({error:o}));this.logger.error(`Server error ${s} in request ${t.method} ${t.path}: ${e.message}`,{stack:e.stack,url:t.url,method:t.method}),r.headersSent||r.status(s).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,t)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:t})}),new Promise((e,t)=>{this.server=this.app.listen(this.config.port,r=>{r?(this.logger.error(`Error starting server: ${r}`),t(r)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}static moduleOf(e){const t=e.indexOf(":");return-1===t?e:e.substring(0,t)}resolveModule(i){return i.startsWith("core/")?i.replace("core/",t(r(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js"):i.startsWith(".")?e(new URL(i,import.meta.url)):i}loadPlugin(e){let t,r;if(/:/.test(e)){let i=e.substring(e.indexOf(":")+1);if(e=e.substring(0,e.indexOf(":")),/:/.test(i)){t=i.substring(0,i.indexOf(":")),t||(t=void 0),i=i.substring(i.indexOf(":")+1);try{r=JSON.parse(i),"object"!=typeof r?(this.logger.error(`Overrides for plugin ${e} are not valid: ${i}`),r=void 0):this.logger.debug(`Overrides for plugin ${t||e}: ${i}`)}catch(t){this.logger.error(`Failed to parse overrides for plugin ${e}: ${t}, using ${i}`),r=void 0}}else t=i,t||(t=void 0)}else t=void 0;return this.logger.debug(`Loading plugin ${e}`),e.startsWith("core/")&&(e=this.resolveModule(e)),new Promise((i,s)=>{import(e).then(async o=>{let n;if(o&&o.default&&(o=o.default),r){const e={...this.config,...r};n=new o(this.withConfig(e))}else n=new o(this);if(!n)return s(new Error(`Unable to load ${e}`));i(await this.registerPlugin(e,n,t)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(t=>s(new Error(`Failed to load plugin ${e}: ${t}`)))})}admitToComposition(e){this.loadedPlugins[e.name]=e;try{d(this,e)}finally{delete this.loadedPlugins[e.name]}}async registerPlugin(e,t,r){if(t.name||(t.name=e),r&&(t.name=r),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&&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 r=t.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!r||"function"!=typeof r)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);this.hooks[e].push(r),g(r,t.name,e)}return this.loadedPlugins[t.name]=t,!0}}export{DM,g as recordHookOwner};
@@ -1 +1 @@
1
- import{fileURLToPath as _}from"url";import{join as a,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",a(e(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",a(e(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",a(e(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
1
+ import{fileURLToPath as _}from"url";import{join as a,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",a(e(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",a(e(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",a(e(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
@@ -1 +1 @@
1
- import t from"../../abstract/plugin.js";import{serverError as e}from"../expressFormatedResponses.js";import{launchHooksChained as r}from"../utils.js";function stripTrailingSlashes(t){let e=t.length;for(;e>0&&"/"===t[e-1];)e--;return t.slice(0,e)}function prefixCoversPath(t,e){return e===t||e.startsWith(`${t}/`)}function claimedPrefixes(t,e){const r=[];for(const s of Object.values(t)){if(s.name===e||!s.roles?.includes("auth"))continue;const t=s.pathPrefixes;t?.length&&r.push(...t)}return r}class AuthBase extends t{get pathPrefixes(){if(this._pathPrefixes)return this._pathPrefixes;const t=this.config.auth_path_prefix,e=Array.isArray(t)?t:t?[t]:[],r=[];for(const t of e){if("string"!=typeof t)throw new Error(`${this.name}: auth_path_prefix must contain strings, got ${JSON.stringify(t)}`);const e=t.trim(),s=stripTrailingSlashes(e);if(0===s.length){if(0===e.length)throw new Error(`${this.name}: auth_path_prefix contains an empty entry`);return this.logger.info(`${this.name}: auth_path_prefix contains "${e}", which covers the whole server: this authentication guards every path`),this._pathPrefixes=[]}if(!s.startsWith("/"))throw new Error(`${this.name}: auth_path_prefix entry "${t}" must start with "/", otherwise it matches no request and guards nothing`);r.push(s)}return this._pathPrefixes=r}async authenticate(t,s,i){try{[t,s]=await r(this.server.hooks.beforeAuth,[t,s])}catch(t){return e(s,t)}this.authMethod(t,s,async()=>{try{this.hooks?.onAuth&&([t,s]=await r(this.server.hooks.afterAuth,[t,s])),i()}catch(t){e(s,t)}})}api(t){const e=this.pathPrefixes;this.server.registerAuthenticator(this),this.logger.info(0===e.length?`${this.name}: authentication guards every path not claimed by another plugin`:`${this.name}: authentication restricted to ${e.join(", ")}`)}}export{claimedPrefixes,AuthBase as default,prefixCoversPath};
1
+ import e from"../../abstract/plugin.js";import{serverError as t}from"../expressFormatedResponses.js";import{launchHooksChained as r}from"../utils.js";function stripTrailingSlashes(e){let t=e.length;for(;t>0&&"/"===e[t-1];)t--;return e.slice(0,t)}function prefixCoversPath(e,t){return t===e||t.startsWith(`${e}/`)}const i=["req.user","req.userName"];function assertIdentityMode(e,t){const r=e.authz_identity??"req.user";if(!i.includes(r))throw new Error(`${t}: unknown --authz-identity "${r}". Known: ${i.join(", ")}.`)}function identityFor(e,t){return e?"req.userName"!==t.authz_identity?{value:e.user,fellBack:!1}:e.userName?{value:e.userName,fellBack:!1}:{value:e.user,fellBack:Boolean(e.user)}:{fellBack:!1}}function identityCoverage(e){const t=new Set,r=[];for(const i of Object.values(e)){if(!i.roles?.includes("auth"))continue;const e=i.knownIdentities?.();if(e)for(const r of e)t.add(r);else r.push(i.name)}return{known:t,unverifiable:r}}function warnUnmatchedRuleKeys(e,t,r,i){if(0===e.length)return;const{known:n,unverifiable:s}=identityCoverage(t);if(e.filter(e=>n.has(e)).length>0)return;if(0===n.size&&0===s.length)return;const a=s.length?` ${s.join(", ")} cannot be checked before a login, so this may be right`:"";i.warn(`${r}: none of its configured identities (${e.join(", ")}) is one the loaded authenticators can publish${n.size?` (${[...n].join(", ")})`:""}.${a||" Every rule is inert."}`)}class AuthBase extends e{constructor(){super(...arguments),this.passedThrough=new WeakSet}get pathPrefixes(){if(this._pathPrefixes)return this._pathPrefixes;const e=this.config.auth_path_prefix,t=Array.isArray(e)?e:e?[e]:[],r=[];for(const e of t){if("string"!=typeof e)throw new Error(`${this.name}: auth_path_prefix must contain strings, got ${JSON.stringify(e)}`);const t=e.trim(),i=stripTrailingSlashes(t);if(0===i.length){if(0===t.length)throw new Error(`${this.name}: auth_path_prefix contains an empty entry`);return this.logger.info(`${this.name}: auth_path_prefix contains "${t}", which covers the whole server: this authentication guards every path`),this._pathPrefixes=[]}if(!i.startsWith("/"))throw new Error(`${this.name}: auth_path_prefix entry "${e}" must start with "/", otherwise it matches no request and guards nothing`);r.push(i)}return this._pathPrefixes=r}async authenticate(e,i,n){try{[e,i]=await r(this.server.hooks.beforeAuth,[e,i])}catch(e){return t(i,e)}this.authMethod(e,i,async()=>{try{[e,i]=await r(this.server.hooks.afterAuth,[e,i]),n()}catch(e){t(i,e)}})}publishIdentity(e,t,r){e.user=t,e.userName=r||t}knownIdentities(){}identitySource(){return"req.user and req.userName: the name this plugin knows the caller by"}afterLoad(){this.logger.info(`${this.name}: ${this.identitySource()}`)}passThrough(e,t){this.passedThrough.add(e),t()}vouchedFor(e){return!this.passedThrough.has(e)}api(e){const t=this.pathPrefixes;this.server.registerAuthenticator(this),this.logger.info(0===t.length?`${this.name}: authentication guards every path not claimed by another plugin`:`${this.name}: authentication restricted to ${t.join(", ")}`)}}export{assertIdentityMode,AuthBase as default,identityCoverage,identityFor,prefixCoversPath,warnUnmatchedRuleKeys};
@@ -1 +1 @@
1
- import r from"../../abstract/plugin.js";import{getParentDn as e}from"../utils.js";class AuthzBase extends r{constructor(){super(...arguments),this.roles=["authz"],this.hooks={ldapmodifyrequest:async([r,e,t,s])=>{if(this.shouldSkipAuthorization(s))return[r,e,t,s];const i=await this.resolveUser(s.user);if(!i)return this.logger.warn(`User ${s.user} could not be resolved`),[r,e,t,s];const o=this.config.ldap_organization_link_attribute;if(o&&e.replace?.[o]){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[o]},r);if(e.searchEntries.length>0){const r=e.searchEntries[0][o],t=Array.isArray(r)?String(r[0]):String(r);if(!(await this.getUserPermissions(i,t)).read)throw new Error(`[authz-forbidden] User ${s.user} does not have read permission for source branch ${t}`)}}catch(e){const t=this.extractBranchDn(r);if(!(await this.getUserPermissions(i,t)).read)throw new Error(`[authz-forbidden] User ${s.user} does not have read permission for source branch ${t}`)}const t=e.replace[o],n=Array.isArray(t)?String(t[0]):String(t);if(!(await this.getUserPermissions(i,n)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${n}`)}else{const e=this.extractBranchDn(r);if(!(await this.getUserPermissions(i,e)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${e}`)}return[r,e,t,s]},ldapaddrequest:async([r,e,t])=>{if(this.shouldSkipAuthorization(t))return[r,e,t];const s=await this.resolveUser(t.user);if(!s)return this.logger.warn(`User ${t.user} could not be resolved`),[r,e,t];let i;const o=this.config.ldap_organization_link_attribute;if(o&&e[o]){const r=e[o];i=Array.isArray(r)?String(r[0]):String(r)}else i=this.extractBranchDn(r);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${t.user} does not have write permission for branch ${i}`);return[r,e,t]},ldapsearchrequest:async([r,e,t])=>{if(this.shouldSkipAuthorization(t))return[r,e,t];const s=await this.resolveUser(t.user);if(!s)return this.logger.warn(`User ${t.user} could not be resolved`),[r,e,t];if(r===this.config.ldap_top_organization&&"base"===e.scope)return[r,e,t];if(!(await this.getUserPermissions(s,r)).read)throw new Error(`[authz-forbidden] User ${t.user} does not have read permission for branch ${r}`);return[r,e,t]},getOrganisationTop:async([r,e])=>{if(this.shouldSkipAuthorization(r))return[r,e];const t=await this.resolveUser(r.user);if(!t)return[r,e];const s=await this.getAuthorizedBranches(t);if(s.length>0){const e=[];for(const t of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},t,r);1===s.searchEntries.length&&e.push(s.searchEntries[0])}catch(r){this.logger.warn(`Failed to fetch authorized branch ${t}: ${r}`)}if(1===e.length)return[r,e[0]];if(e.length>1)return[r,e[0]]}return[r,e]},ldaprenamerequest:async([r,e,t])=>{if(this.shouldSkipAuthorization(t))return[r,e,t];const s=await this.resolveUser(t.user);if(!s)return this.logger.warn(`User ${t.user} could not be resolved`),[r,e,t];const i=this.extractBranchDn(r),o=this.extractBranchDn(e);if(!(await this.getUserPermissions(s,i)).read)throw new Error(`[authz-forbidden] User ${t.user} does not have read permission for source branch ${i}`);if(!(await this.getUserPermissions(s,o)).write)throw new Error(`[authz-forbidden] User ${t.user} does not have write permission for destination branch ${o}`);return[r,e,t]},ldapdeleterequest:async([r,e])=>{if(this.shouldSkipAuthorization(e))return[r,e];const t=await this.resolveUser(e.user);if(!t)return this.logger.warn(`User ${e.user} could not be resolved`),[r,e];const s=Array.isArray(r)?r:[r];for(const r of s){const s=this.extractBranchDn(r);if(!(await this.getUserPermissions(t,s)).delete)throw new Error(`[authz-forbidden] User ${e.user} does not have delete permission for branch ${s}`)}return[r,e]}}}extractBranchDn(r){return e(r)}shouldSkipAuthorization(r){return!r?.user}}export{AuthzBase as default};
1
+ import e from"../../abstract/plugin.js";import{assertIdentityMode as t,identityFor as r}from"../auth/base.js";import{ForbiddenError as s}from"../errors.js";import{getParentDn as i,isDnInBranch as n}from"../utils.js";import{authzFor as a,servesRequest as o}from"./composition.js";class AuthzBase extends e{constructor(e){super(e),this.roles=["authz"],this.resolutionCache=new Map,this.resolutionCacheFullWarned=!1,this.fallbackWarned=!1,this.hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([e,t,r,s])=>{if(this.shouldSkipAuthorization(s))return[e,t,r,s];const i=await this.resolveCaller(s);if(!i)return[e,t,r,s];const n=this.config.ldap_organization_link_attribute;if(n&&t.replace?.[n]){let r=this.extractBranchDn(e);try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[n]},e),s=t.searchEntries[0]?.[n],i=Array.isArray(s)?s[0]:s;null!=i&&""!==String(i)&&(r=String(i))}catch{}if(!(await this.getUserPermissions(i,r)).read)throw new Error(`[authz-forbidden] User ${s.user} does not have read permission for source branch ${r}`);const a=t.replace[n],o=Array.isArray(a)?String(a[0]):String(a);if(!(await this.getUserPermissions(i,o)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${o}`)}else{const t=await this.effectiveBranch(e);if(!(await this.getUserPermissions(i,t)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${t}`)}return[e,t,r,s]},ldapaddrequest:async([e,t,r])=>{if(this.shouldSkipAuthorization(r))return[e,t,r];const s=await this.resolveCaller(r);if(!s)return[e,t,r];let i;const n=this.config.ldap_organization_link_attribute;if(n&&t[n]){const e=t[n];i=Array.isArray(e)?String(e[0]):String(e)}else i=this.extractBranchDn(e);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${i}`);return[e,t,r]},ldapsearchrequest:async([e,t,r])=>{if(this.shouldSkipAuthorization(r))return[e,t,r];const s=await this.resolveCaller(r);if(!s)return[e,t,r];if(e===this.config.ldap_top_organization&&"base"===t.scope)return[e,t,r];if(this.config.authz_filter_attached_entries){if(0===(await this.getAuthorizedBranches(s)).length)throw new Error(`[authz-forbidden] User ${r.user} administers no branch`);const i=this.config.ldap_organization_link_attribute;if(i&&!AuthzBase.wantsAttribute(t,i)){const e=t.attributes,r=Array.isArray(e)?e.map(String):[String(e)];t={...t,attributes:[...r,i]},AuthzBase.widenedForLink.add(t)}return[e,t,r]}if(!(await this.getUserPermissions(s,e)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${e}`);return[e,t,r]},ldapsearchfilter:async([e,t,r])=>{const s=[e,t,r];if(!this.config.authz_filter_attached_entries)return s;const i=this.config.ldap_organization_link_attribute;if(!i||this.shouldSkipAuthorization(t))return s;if(!e?.searchEntries?.length)return s;const a=await this.resolveCaller(t);if(!a)return s;const o=await this.getAuthorizedBranches(a);if(0===o.length)return s;const within=e=>o.some(t=>n(e,t));if(e.searchEntries=e.searchEntries.filter(e=>{const r=e[i];if(null==r)return!0;const s=(Array.isArray(r)?r:[r]).map(String);return 0===s.length||(!!s.some(within)||(this.logger.debug(`${this.name}: hiding ${String(e.dn)} from ${t.user}, attached to ${s[0]}`),!1))}),r&&AuthzBase.widenedForLink.has(r))for(const t of e.searchEntries)delete t[i];return[e,t,r]},getOrganisationTop:async([e,t])=>{if(this.shouldSkipAuthorization(e))return[e,t];const r=await this.resolveCaller(e);if(!r)return[e,t];const s=await this.getAuthorizedBranches(r);if(s.length>0){const t=[];for(const r of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},r,e);1===s.searchEntries.length&&t.push(s.searchEntries[0])}catch(e){this.logger.warn(`Failed to fetch authorized branch ${r}: ${e}`)}if(1===t.length)return[e,t[0]];if(t.length>1)return[e,t[0]]}return[e,t]},ldaprenamerequest:async([e,t,r])=>{if(this.shouldSkipAuthorization(r))return[e,t,r];const s=await this.resolveCaller(r);if(!s)return[e,t,r];const i=this.extractBranchDn(e),n=this.extractBranchDn(t);if(!(await this.getUserPermissions(s,i)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for source branch ${i}`);if(!(await this.getUserPermissions(s,n)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for destination branch ${n}`);return[e,t,r]},ldapdeleterequest:async([e,t])=>{if(this.shouldSkipAuthorization(t))return[e,t];const r=await this.resolveCaller(t);if(!r)return[e,t];const s=Array.isArray(e)?e:[e];for(const e of s){const s=await this.effectiveBranch(e);if(!(await this.getUserPermissions(r,s)).delete)throw new Error(`[authz-forbidden] User ${t.user} does not have delete permission for branch ${s}`)}return[e,t]}},t(this.config,this.constructor.name),a(this.config,this.constructor.name);const r=this.config.authz_unresolved_user??"deny";if(!AuthzBase.UNRESOLVED_POLICIES.includes(r))throw new Error(`${this.constructor.name}: unknown --authz-unresolved-user "${r}". Known: ${AuthzBase.UNRESOLVED_POLICIES.join(", ")}.`)}static wantsAttribute(e,t){const r=e?.attributes;if(void 0===r)return!0;const s=(Array.isArray(r)?r:[r]).map(e=>String(e).toLowerCase());return 0===s.length||(!!s.includes("*")||s.includes(t.toLowerCase()))}extractBranchDn(e){return i(e)}async effectiveBranch(e){const t=this.config.ldap_organization_link_attribute;if(!t||!this.config.authz_filter_attached_entries)return this.extractBranchDn(e);try{const r=await this.server.ldap.search({paged:!1,scope:"base",attributes:[t]},e),s=(e=>{if(null==e)return;const t=Array.isArray(e)?e[0]:e,r=String(t);return r.length>0?r:void 0})(r.searchEntries?.[0]?.[t]);if(s)return s}catch{}return this.extractBranchDn(e)}shouldSkipAuthorization(e){return!e?.user||!o(e,this.config)}async resolveCaller(e){const{value:t,fellBack:i}=r(e,this.config);i&&!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 n=t,a=Date.now(),o=this.resolutionCache.get(n);let h;if(o&&a-o.at<this.cacheTTL)h=o.user;else{if(h=await this.resolveUser(n),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[e,t]of this.resolutionCache)a-t.at>=this.cacheTTL&&this.resolutionCache.delete(e);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(n,{user:h,at:a}):this.resolutionCacheFullWarned||(this.resolutionCacheFullWarned=!0,this.logger.warn(`${this.name}: more than ${AuthzBase.RESOLUTION_CACHE_MAX} identities resolved within one cache window, so resolutions are no longer cached and each request costs a lookup`))}if(h)return h;if("allow"===this.config.authz_unresolved_user)return this.logger.warn(`User ${n} could not be resolved by ${this.name}; allowed by --authz-unresolved-user allow`),null;throw new s(`[authz-forbidden] User ${n} could not be resolved by ${this.name}, so no permission can be read for them`)}clearResolutionCache(){this.resolutionCache.clear()}}AuthzBase.widenedForLink=new WeakSet,AuthzBase.UNRESOLVED_POLICIES=["deny","allow"],AuthzBase.RESOLUTION_CACHE_MAX=1e4;export{AuthzBase as default};
@@ -0,0 +1 @@
1
+ const e=["ldapsearchrequest","ldapaddrequest","ldapmodifyrequest","ldapdeleterequest","ldaprenamerequest","ldapsearchfilter"],t="everyone",n=new WeakMap;function authzFor(e,t){if(n.has(e))return n.get(e);const o=function(e,t){const n=e.authz_for;if(null==n||""===n)return;const o=Array.isArray(n)?n:[n],s=[];for(const e of o){if("string"!=typeof e||""===e.trim())throw new Error(`${t}: authz_for must list authentication plugin names, got `+JSON.stringify(e));s.push(e.trim())}return s.length>0?s:void 0}(e,t);return n.set(e,o),o}function servesRequest(e,t){const n=authzFor(t,"authz_for");if(!n)return!0;const o=e?.authenticators;return!o||0===o.length||o.some(e=>n.includes(e))}function runTogether(e,t){return 0===e.pathPrefixes.length&&0===t.pathPrefixes.length||e.pathPrefixes.some(e=>t.pathPrefixes.includes(e))}function ldapJudges(n){const o=[];for(const s of Object.values(n.loadedPlugins)){if(!s.roles?.includes("authz"))continue;const n=e.filter(e=>Boolean(s.hooks?.[e]));if(0===n.length)continue;const r=s.roles.includes("auth")?[s.name]:authzFor(s.config,s.name)??t;o.push({plugin:s,hooks:n,population:r})}return o}function overlap(e,n,o){if(e===t||n===t)return"same";if(e.some(e=>n.includes(e)))return"same";const byName=e=>o.find(t=>t.name===e);for(const t of e)for(const e of n){const n=byName(t),o=byName(e);if(n&&o&&runTogether(n,o))return"together"}}function describePopulation(e,n){return e!==t?`requests authenticated by ${e.join(", ")}`:n.length>0?`every authenticated request (${n.map(e=>e.name).join(", ")})`:"every authenticated request"}function assertAuthzComposition(e,t){const n=[...e.authenticators];var o;t&&(o=t,Boolean(o.roles?.includes("auth"))&&Array.isArray(o.pathPrefixes))&&!n.some(e=>e.name===t.name)&&n.push(t);const s=n.map(e=>e.name),concerns=e=>!t||e===t,r=Object.values(e.loadedPlugins).filter(concerns);for(const n of r){if(!n.roles?.includes("authz"))continue;if(n.roles.includes("auth")){const t=n.config.authz_for,o=e.config.authz_for;void 0!==t&&JSON.stringify(t)!==JSON.stringify(o)&&"[]"!==JSON.stringify(t)&&e.logger.warn(`${n.name}: authz_for is ignored here. This plugin judges the requests it authenticates itself, and no others`);continue}const o=(authzFor(n.config,n.name)??[]).filter(e=>!s.includes(e));if(o.length>0)throw new Error(`${n.name}: authz_for names ${o.join(", ")}, which `+(o.length>1?"are not loaded authentication plugins":"is not a loaded authentication plugin")+` (loaded: ${s.join(", ")||"none"}). It would judge no request of theirs.`+(t?" Register the authentication plugins before the authorization plugins that name them.":""))}const i=t?void 0:authzFor(e.config,"--authz-for");i&&!Object.values(e.loadedPlugins).some(t=>t.roles?.includes("authz")&&!t.roles.includes("auth")&&t.config.authz_for===e.config.authz_for)&&e.logger.warn(`--authz-for names ${i.join(", ")}, and no loaded plugin reads it: it scopes nothing. authzDynamic judges the requests it authenticates itself; the option is for authzPerBranch, authzLinid1 and authzPerRoute`);const a=ldapJudges(e);for(const t of a)concerns(t.plugin)&&e.logger.info(`${t.plugin.name} judges ${t.hooks.join(", ")} for `+describePopulation(t.population,n));const u=Boolean(e.config.authz_combine),h=[];for(let t=0;t<a.length;t++)for(let o=t+1;o<a.length;o++){const s=a[t],r=a[o];if(!concerns(s.plugin)&&!concerns(r.plugin))continue;const i=overlap(s.population,r.population,n);if(!i)continue;const l=`${s.plugin.name} and ${r.plugin.name}`;"together"===i?e.logger.warn(`${l} judge the requests of authenticators that run on the same paths: a request carrying both credentials is judged by both, and the first refusal wins`):u?e.logger.warn(`${l} both judge the LDAP operations of the same requests, and compose as an AND (--authz-combine): the first refusal wins`):h.push(l)}if(h.length>0)throw new Error(`${h.join("; ")} both judge the LDAP operations of the same requests, and would compose as an AND: the first refusal wins, and the 403 does not say whose it was. Give each an authz_for naming the authentication plugins it serves, so that no request is judged by both, or set --authz-combine to make the AND deliberate.`);for(const e of r)e.assertComposition?.()}export{t as EVERYONE,e as LDAP_JUDGING_HOOKS,assertAuthzComposition,authzFor,describePopulation,ldapJudges,overlap,runTogether,servesRequest};
@@ -1 +1 @@
1
- let r;const setLogger=t=>{r=t},getLogger=()=>r,tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(t,e,n)=>{r?.warn(`Client error ${t}: ${n}`),e.status(t).json({error:n})},ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},multiStatus=(r,t)=>{r.status(207).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),forbidden=(r,t="Forbidden")=>_rejectResponse(403,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(t,e)=>{const n=e instanceof Error?e.message:String(e);let s=e instanceof Error&&"statusCode"in e?e.statusCode:500;if(500===s&&/\[authz-forbidden\]/.test(n)&&(s=403),s>=400&&s<500){const e=403===s&&/\[authz-forbidden\]/.test(n)?"Token does not have permission on this branch":n;return r.warn(`Client error ${s}: ${e}`),void t.status(s).json({error:e})}e instanceof Error?r.error(e.message,e):"string"==typeof e?r.error(e):r.error("Server error",e),t.status(s).json({error:"check logs"})},wantJson=(r,t)=>!1!==r.accepts("json")||(badRequest(t),!1),jsonBody=(r,t,...e)=>{try{if(!wantJson(r,t))return!1;const n=r.body;if(e.length>0)for(let r=0;r<e.length;r++)if(null==n[e[r]])return badRequest(t,"Bad content"),!1;return n}catch(r){return badRequest(t,"Bad content"),!1}};export{badRequest,created,forbidden,getLogger,jsonBody,multiStatus,notFound,ok,serverError,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
1
+ let r;const setLogger=t=>{r=t},getLogger=()=>r,tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(t,e,n)=>{r?.warn(`Client error ${t}: ${n}`),e.status(t).json({error:n})},ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},multiStatus=(r,t)=>{r.status(207).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),forbidden=(r,t="Forbidden")=>_rejectResponse(403,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(t,e)=>{const n=e instanceof Error?e.message:String(e);let s=e instanceof Error&&"statusCode"in e?e.statusCode:500;if(500===s&&/\[authz-forbidden\]/.test(n)&&(s=403),s>=400&&s<500){const e=403===s&&/\[authz-forbidden\]/.test(n)?"Token does not have permission on this branch":n;return r?.warn(`Client error ${s}: ${e}`),void t.status(s).json({error:e})}e instanceof Error?r?.error(e.message,e):"string"==typeof e?r?.error(e):r?.error("Server error",e),t.status(s).json({error:"check logs"})},wantJson=(r,t)=>!1!==r.accepts("json")||(badRequest(t),!1),jsonBody=(r,t,...e)=>{try{if(!wantJson(r,t))return!1;const n=r.body;if(e.length>0)for(let r=0;r<e.length;r++)if(null==n[e[r]])return badRequest(t,"Bad content"),!1;return n}catch(r){return badRequest(t,"Bad content"),!1}};export{badRequest,created,forbidden,getLogger,jsonBody,multiStatus,notFound,ok,serverError,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
@@ -1 +1 @@
1
- import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as n}from"ldapts";import{LRUCache as o}from"lru-cache";import s from"p-limit";import{launchHooksChained as a,launchHooks as r,escapeDnValue as c}from"./utils.js";import{ConflictError as l}from"./errors.js";const h={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),n=t?.code;return"number"==typeof n&&(i.code=n),i}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]});class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=s(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,a="string"==typeof n?parseInt(n,10)||0:n??0,r=1e3*Math.max(a,0);if(this.cacheEnabled=r>0,this.searchCache=new o({max:i,ttl:this.cacheEnabled?r:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${r/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new o({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");e.config.ldap_base?this.base=e.config.ldap_base:(this.base=e.config.ldap_dn.split(",",2)[1],this.logger.warn(`LDAP base is not defined, using "${this.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,o=this.ldapUrls[n];try{this.logger.debug(`Attempting connection to ${o}`);const e={...this.options,url:o,tlsOptions:o.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await i.bind(this.dn,this.pwd),n!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${o}`),this.currentUrlIndex=n),i}catch(t){this.logger.warn(`Failed to connect to ${o}: ${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 acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),n=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",o="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",o,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 o=foldDn(e.split("\0")[0]);(o===t||o.endsWith(i))&&n.push(e)}for(const e of n)this.searchCache.delete(e)}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i)=>this.modify(t,i,e),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}async search(e,t=this.base,i){let n={...h,...e};n=await a(this.parent.hooks.ldapsearchopts,n),[t,n]=await a(this.parent.hooks.ldapsearchrequest,[t,n,i]);const o=this.cacheEnabled&&!n.paged&&"base"===n.scope,s=o?this.getCacheKey(t,n):"",r=this.cacheGeneration;if(o){const e=this.searchCache.get(s);if(e)return this.logger.debug(`LDAP search cache hit: ${digestKey(s)}`),cloneSearchResult(e)}const c=await this.acquireConnection();try{let e=n.paged?c.client.searchPaginated(t,n):c.client.search(t,n);if(e=await a(this.parent.hooks.ldapsearchresult,e),o){const t=e;return r===this.cacheGeneration?(this.searchCache.set(s,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(s)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(s)}`),t}return n.paged?this.wrapPaginatedSearch(e,c):e}finally{n.paged||this.releaseConnection(c)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,t,n){e=this.setDn(e),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const o={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?o[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?o[e]=i.map(e=>e.toString()):o[e]=i;[e,t]=await a(this.parent.hooks.ldapaddrequest,[e,o,n]);const s=[];for(const[e,t]of Object.entries(o)){const n=Array.isArray(t)?t:[t];s.push(new i({type:e,values:n}))}const c=await this.acquireConnection();try{return this.invalidateCache(e),await c.client.add(e,s),this.invalidateCache(e),r(this.parent.hooks.ldapadddone,[e,t]).catch(e=>{this.logger.error(`Hook ldapadddone failed: ${String(e)}`)}),!0}catch(t){throw function(e,t){const i=ldapError("LDAP add error",t);if(68!==i.code)return i;const n=new l(`Entry ${e} already exists`);return n.code=68,n}(e,t)}finally{this.releaseConnection(c)}}async modify(e,t,o){e=this.setDn(e);const s=[],c=this.opNumber();if([e,t]=await a(this.parent.hooks.ldapmodifyrequest,[e,t,c,o]),t.add)for(const[e,o]of Object.entries(t.add))s.push(new n({operation:"add",modification:new i({type:e,values:Array.isArray(o)?o:[o]})}));if(t.replace)for(const[e,o]of Object.entries(t.replace))s.push(new n({operation:"replace",modification:new i({type:e,values:Array.isArray(o)?o:[o]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&s.push(new n({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,o]of Object.entries(t.delete)){const t=new n({operation:"delete",modification:new i(o?{type:e,values:Array.isArray(o)?o:[o]}:{type:e})});s.push(t)}if(0===s.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`),r(this.parent.hooks.ldapmodifydone,[e,{},c]).catch(e=>{this.logger.error(`Hook ldapmodifydone failed: ${String(e)}`)}),!1}{const i=await this.acquireConnection();try{return this.invalidateCache(e),await i.client.modify(e,s),this.invalidateCache(e),r(this.parent.hooks.ldapmodifydone,[e,t,c]).catch(e=>{this.logger.error(`Hook ldapmodifydone failed: ${String(e)}`)}),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(s)}`),ldapError("LDAP modify error",t)}finally{this.releaseConnection(i)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t),[e,t]=await a(this.parent.hooks.ldaprenamerequest,[e,t,i]);const n=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await n.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),r(this.parent.hooks.ldaprenamedone,[e,t]).catch(e=>{this.logger.error(`Hook ldaprenamedone failed: ${String(e)}`)}),!0}catch(e){throw ldapError("LDAP rename error",e)}finally{this.releaseConnection(n)}}async move(e,t){e=this.setDn(e),t=this.setDn(t);const i=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await i.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw ldapError("LDAP move error",e)}finally{this.releaseConnection(i)}}async delete(e,t){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),[e]=await a(this.parent?.hooks.ldapdeleterequest,[e,t]);const i=await this.acquireConnection();try{for(const t of e){try{this.invalidateCache(t),await i.client.del(t),this.invalidateCache(t)}catch(e){throw ldapError("LDAP delete error",e)}r(this.parent.hooks.ldapdeletedone,t).catch(e=>{this.logger.error(`Hook ldapdeletedone failed: ${String(e)}`)})}return!0}finally{this.releaseConnection(i)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${c(e)},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
1
+ import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as n}from"ldapts";import{LRUCache as s}from"lru-cache";import o from"p-limit";import{launchHooksChained as a,launchHooks as r,escapeDnValue as c}from"./utils.js";import{ConflictError as l}from"./errors.js";const h={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),n=t?.code;return"number"==typeof n&&(i.code=n),i}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]});class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=o(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,a="string"==typeof n?parseInt(n,10)||0:n??0,r=1e3*Math.max(a,0);if(this.cacheEnabled=r>0,this.searchCache=new s({max:i,ttl:this.cacheEnabled?r:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${r/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new s({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");e.config.ldap_base?this.base=e.config.ldap_base:(this.base=e.config.ldap_dn.split(",",2)[1],this.logger.warn(`LDAP base is not defined, using "${this.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 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 acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),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)}get system(){return this}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i)=>this.modify(t,i,e),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}filterForCaller(e,t,i){if(!this.parent.hooks.ldapsearchfilter)return e;const n=this.parent.hooks.ldapsearchfilter,one=async e=>{const[s]=await a(n,[e,t,i]);return s??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,i){let n={...h,...e};n=await a(this.parent.hooks.ldapsearchopts,n),[t,n]=await a(this.parent.hooks.ldapsearchrequest,[t,n,i]);const s=this.cacheEnabled&&!n.paged&&"base"===n.scope,o=s?this.getCacheKey(t,n):"",r=this.cacheGeneration;if(s){const e=this.searchCache.get(o);if(e)return this.logger.debug(`LDAP search cache hit: ${digestKey(o)}`),this.filterForCaller(cloneSearchResult(e),i,n)}const c=await this.acquireConnection();try{let e=n.paged?c.client.searchPaginated(t,n):c.client.search(t,n);if(e=await a(this.parent.hooks.ldapsearchresult,e),s){const t=e;return r===this.cacheGeneration?(this.searchCache.set(o,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(o)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(o)}`),this.filterForCaller(t,i,n)}return n.paged?this.filterForCaller(this.wrapPaginatedSearch(e,c),i,n):this.filterForCaller(e,i,n)}finally{n.paged||this.releaseConnection(c)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,t,n){e=this.setDn(e),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 a(this.parent.hooks.ldapaddrequest,[e,s,n]);const o=[];for(const[e,t]of Object.entries(s)){const n=Array.isArray(t)?t:[t];o.push(new i({type:e,values:n}))}const c=await this.acquireConnection();try{return this.invalidateCache(e),await c.client.add(e,o),this.invalidateCache(e),r(this.parent.hooks.ldapadddone,[e,t]),!0}catch(t){throw function(e,t){const i=ldapError("LDAP add error",t);if(68!==i.code)return i;const n=new l(`Entry ${e} already exists`);return n.code=68,n}(e,t)}finally{this.releaseConnection(c)}}async modify(e,t,s){e=this.setDn(e);const o=[],c=this.opNumber();if([e,t]=await a(this.parent.hooks.ldapmodifyrequest,[e,t,c,s]),t.add)for(const[e,s]of Object.entries(t.add))o.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))o.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)e&&o.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})});o.push(t)}if(0===o.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`),r(this.parent.hooks.ldapmodifydone,[e,{},c]),!1}{const i=await this.acquireConnection();try{return this.invalidateCache(e),await i.client.modify(e,o),this.invalidateCache(e),r(this.parent.hooks.ldapmodifydone,[e,t,c]),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(o)}`),ldapError("LDAP modify error",t)}finally{this.releaseConnection(i)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t),[e,t]=await a(this.parent.hooks.ldaprenamerequest,[e,t,i]);const n=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await n.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),r(this.parent.hooks.ldaprenamedone,[e,t]),!0}catch(e){throw ldapError("LDAP rename error",e)}finally{this.releaseConnection(n)}}async move(e,t){e=this.setDn(e),t=this.setDn(t);const i=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await i.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw ldapError("LDAP move error",e)}finally{this.releaseConnection(i)}}async delete(e,t){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),[e]=await a(this.parent?.hooks.ldapdeleterequest,[e,t]);const i=await this.acquireConnection();try{for(const t of e){try{this.invalidateCache(t),await i.client.del(t),this.invalidateCache(t)}catch(e){throw ldapError("LDAP delete error",e)}r(this.parent.hooks.ldapdeletedone,t)}return!0}finally{this.releaseConnection(i)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${c(e)},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
@@ -0,0 +1 @@
1
+ function extractLdapCode(e){if(null==e)return;if("object"==typeof e&&"code"in e){const t=e.code;if("number"==typeof t)return t}const t=e instanceof Error?e.message:"string"==typeof e?e:"";return/noSuchObject|No such object|code:?\s*(32|0x20)/i.test(t)?32:/entryAlreadyExists|Already[_ ]?Exists|code:?\s*(68|0x44)/i.test(t)?68:/noSuchAttribute|No such attribute|code:?\s*(16|0x10)/i.test(t)?16:/sizeLimitExceeded|Size Limit Exceeded|code:?\s*(4|0x4)\b/i.test(t)?4:/undefinedAttributeType|Undefined attribute type|code:?\s*(17|0x11)/i.test(t)?17:/objectClassViolation|Object Class Violation|code:?\s*(65|0x41)/i.test(t)?65:void 0}export{extractLdapCode};
@@ -1 +1 @@
1
- class ConfigParser{constructor(r){this.config=r}parse(r=process.argv){const e={},s=this.parseCliArgs(r);for(const r of this.config){const t=this.getKeyFromCliArg(r[0]);let n=r[2];if(void 0!==r[1]){const e=process.env[r[1]];if(void 0!==e)if("boolean"===r[3])n="true"===e.toLowerCase();else if("number"===r[3])n=parseInt(e);else if("array"===r[3]){const r=e.indexOf(";")>0?";":",";n=e.split(new RegExp(`[${r}\\s]+`)).map(r=>r.trim()).filter(r=>r.length>0)}else if("json"===r[3])try{n=JSON.parse(e)}catch(e){throw new Error(`Error parsing JSON from environment variable ${r[1]}: ${e}`)}else n=e}if(s.has(r[0])){const e=s.get(r[0]);if("boolean"===r[3])n=!0;else if("number"===r[3])n=parseInt(e);else if("array"===r[3])n=Array.isArray(n)?n.concat(e):e;else if("json"===r[3])try{n=JSON.parse(e)}catch(e){throw new Error(`Error parsing JSON from command line argument ${r[0]}: ${e}`)}else n=e;s.delete(r[0])}if("array"===r[3]&&r[4]&&s.has(r[4])){const e=s.get(r[4])||"";n=Array.isArray(n)?n.concat(e.split(/[,\s]+/).filter(r=>r.length>0)):e.split(/[,\s]+/).filter(r=>r.length>0),s.delete(r[4])}e[t]=n}return s.forEach((r,s)=>{if(e[s])throw new Error(`Error in command line: ${s} redefined`);e[s.replace(/^-+/,"").replace(/-/g,"_")]=r}),e}parseCliArgs(r){const e=new Map;for(let s=2;s<r.length;s++){const t=r[s];if(t.startsWith("--")||t.startsWith("-")){const n=this.config.find(r=>r[0]===t);if(n&&"boolean"===n[3])e.set(t,!0);else if(n&&"number"===n[3])e.set(t,parseInt(r[s+1]));else if(n&&"array"===n[3]){const n=e.get(t)||[],i=r[s+1];n.push(i),e.set(t,n)}else{const n=r[s+1];e.set(t,n),s++}}}return e}getKeyFromCliArg(r){return r.startsWith("--")?r.substring(2).replace(/-/g,"_"):r.startsWith("-")?r.substring(1).replace(/-/g,"_"):r}}function parseConfig(r,e=process.argv){return new ConfigParser(r).parse(e)}export{ConfigParser,parseConfig};
1
+ class ConfigParser{constructor(r){this.config=r}parse(r=process.argv){const e={},s=this.parseCliArgs(r);for(const r of this.config){const t=this.getKeyFromCliArg(r[0]);let o=r[2];if(void 0!==r[1]){const e=process.env[r[1]];if(void 0!==e)if("boolean"===r[3])o="true"===e.toLowerCase();else if("number"===r[3])o=parseInt(e);else if("array"===r[3]){const r=e.indexOf(";")>0?";":",";o=e.split(new RegExp(`[${r}\\s]+`)).map(r=>r.trim()).filter(r=>r.length>0)}else if("json"===r[3])try{o=JSON.parse(e)}catch(e){throw new Error(`Error parsing JSON from environment variable ${r[1]}: ${e}`)}else o=e}if(s.has(r[0])){const e=s.get(r[0]);if("boolean"===r[3])o=!0;else if("number"===r[3])o=parseInt(e);else if("array"===r[3])o=Array.isArray(o)?o.concat(e):e;else if("json"===r[3])try{o=JSON.parse(e)}catch(e){throw new Error(`Error parsing JSON from command line argument ${r[0]}: ${e}`)}else o=e;s.delete(r[0])}if("array"===r[3]&&r[4]&&s.has(r[4])){const e=s.get(r[4])||"";o=Array.isArray(o)?o.concat(e.split(/[,\s]+/).filter(r=>r.length>0)):e.split(/[,\s]+/).filter(r=>r.length>0),s.delete(r[4])}e[t]=o}return s.forEach((r,s)=>{if(e[s])throw new Error(`Error in command line: ${s} redefined`);e[s.replace(/^-+/,"").replace(/-/g,"_")]=r}),e}parseCliArgs(r){const e=new Map;for(let s=2;s<r.length;s++){const t=r[s];if(t.startsWith("--")||t.startsWith("-")){const o=this.config.find(r=>r[0]===t);if(o&&"boolean"===o[3])e.set(t,!0);else if(o&&"number"===o[3])e.set(t,parseInt(r[s+1]));else if(o&&"array"===o[3]){const n=e.get(t)||[],i=r[s+1];n.push(i),e.set(t,n);const a=r[s+2];if(void 0!==a&&!a.startsWith("-"))throw new Error(`Error in command line: ${t} takes one value, got "${i}" followed by "${a}". Repeat ${t} for each value`+(o[4]?`, or give them all to ${o[4]}`:""))}else{const o=r[s+1];e.set(t,o),s++}}}return e}getKeyFromCliArg(r){return r.startsWith("--")?r.substring(2).replace(/-/g,"_"):r.startsWith("-")?r.substring(1).replace(/-/g,"_"):r}}function parseConfig(r,e=process.argv){return new ConfigParser(r).parse(e)}export{ConfigParser,parseConfig};
@@ -0,0 +1 @@
1
+ class Store{constructor(e){this.logger=e}static scoped(e,t){return`${e}\0${t}`}static readable(e){return e.replace("\0",":")}async get(e,t){const s=Store.scoped(e,t),r=await this.load(s);return null===r?null:r.deadline<=Date.now()?(this.drop(s).catch(()=>{}),null):r.value}async set(e,t,s,r){await this.save(Store.scoped(e,t),{value:s,deadline:r})}async delete(e,t){await this.drop(Store.scoped(e,t))}startSweeping(e){if(this.sweeper||e<=0)return;const run=()=>{this.sweep().then(e=>{e>0&&this.logger.debug(`${this.name}: swept ${e} record(s)`)}).catch(e=>{this.logger.warn(`${this.name}: sweep failed: ${e instanceof Error?e.message:JSON.stringify(e)}`)})};this.sweeper=setInterval(run,1e3*e),this.sweeper.unref?.(),run()}stopSweeping(){this.sweeper&&clearInterval(this.sweeper),this.sweeper=void 0}}export{Store as default};
package/dist/lib/utils.js CHANGED
@@ -1 +1 @@
1
- import{BadRequestError as e}from"./errors.js";import{getLogger as t}from"./expressFormatedResponses.js";const n=t(),r=new Map;function getCompiledRegex(e,t){const n=t?`${e}:${t}`:e;let i=r.get(n);return i||(i=new RegExp(e,t),r.set(n,i)),i}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const launchHooks=async(e,...t)=>{if(e)for(const r of e)if(r)try{await r(...t)}catch(e){n.error("Hook error",e)}},launchHooksChained=async(e,t)=>{if(e)for(const n of e)n&&(t=await n(t));return t},transformSchemas=(e,t)=>e.toString().replace(/__(\S+)__/g,(e,n)=>{if(!n||"string"!=typeof n)return e;const r=n.trim().toLowerCase();return t[r]?"object"!=typeof t[r]?t[r]:JSON.stringify(t[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(t,n){const r=/^[a-zA-Z][a-zA-Z0-9-]*$/,i=n.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(t),a=i.map(e=>`(${e}=*${o}*)`);return 1===a.length?a[0]:`(|${a.join("")})`}function escapeDnValue(e){return e.replace(/\\/g,"\\\\").replace(/,/g,"\\,").replace(/\+/g,"\\+").replace(/"/g,'\\"').replace(/</g,"\\<").replace(/>/g,"\\>").replace(/;/g,"\\;").replace(/=/g,"\\=").replace(/\0/g,"\\00").replace(/^\s/,"\\ ").replace(/\s$/,"\\ ").replace(/^#/,"\\#")}function unescapeDnValue(t){if("string"!=typeof t)throw new e("DN value must be a string");let n="",r=0;for(;r<t.length;)if("\\"===t[r]&&r+1<t.length){const e=t[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<t.length){const e=t.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){n+=String.fromCharCode(parseInt(e,16)),r+=3;continue}}n+=e,r+=2}else n+=t[r],r++;return n}function validateDnValue(e,t){if(null==e||"string"!=typeof e)throw new Error(`${t} must be a string`);if(0===e.trim().length)throw new Error(`${t} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${t} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${t} contains invalid invisible characters`)}function parseDn(t){if("string"!=typeof t)throw new e("DN must be a string");const n=[];let r="",i=!1;for(let e=0;e<t.length;e++){const o=t[e];i?(r+=o,i=!1):"\\"===o?(r+=o,i=!0):","===o?(n.push(r.trim()),r=""):r+=o}return r&&n.push(r.trim()),n}function getParentDn(e){const t=parseDn(e);return t.length<=1?e:t.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function rdnValue(e){const t=/^[^=]+=((?:\\.|[^,])*)/.exec(e);return t?t[1].replace(/\\(.)/g,"$1"):""}function isChildOf(e,t){const n=e.toLowerCase(),r=t.toLowerCase();if(!n.endsWith(r))return!1;if(n.length===r.length)return!1;return n.substring(0,n.length-r.length).endsWith(",")}function normalizeDn(e){return parseDn(e).map(e=>e.split("+").map(e=>{const t=e.indexOf("=");return(-1===t?e.trim():`${e.slice(0,t).trim()}=${e.slice(t+1).trim()}`).toLowerCase()}).sort().join("+")).filter(e=>e.length>0).join(",")}function isDummyMemberDn(e,t){if(!t||"string"!=typeof e)return!1;if(e===t)return!0;try{return normalizeDn(e)===normalizeDn(t)}catch{return!1}}function isDnInBranch(e,t){const n=normalizeDn(t).split(",").filter(Boolean);if(0===n.length)return!1;const r=normalizeDn(e).split(",").filter(Boolean);if(r.length<n.length)return!1;return r.slice(r.length-n.length).every((e,t)=>e===n[t])}const asyncHandler=e=>(t,n,r)=>{Promise.resolve(e(t,n,r)).catch(r)};export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,isDummyMemberDn,launchHooks,launchHooksChained,normalizeDn,parseDn,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue};
1
+ import{BadRequestError as e}from"./errors.js";import{getLogger as n}from"./expressFormatedResponses.js";const t=new Map;function getCompiledRegex(e,n){const r=n?`${e}:${n}`:e;let i=t.get(r);return i||(i=new RegExp(e,n),t.set(r,i)),i}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const describeThrown=e=>{try{return JSON.stringify(e)??String(e)}catch{return String(e)}},hookLabel=e=>{const n=hookOwner(e);return n?` in ${n.plugin} (${n.hook})`:e.name?` in ${e.name}`:""},launchHooks=async(e,...t)=>{if(e)for(const r of e)if(r)try{await r(...t)}catch(e){const t=hookLabel(r);e instanceof Error?n()?.error(`Hook error${t}`,e):n()?.error(`Hook error${t}: ${describeThrown(e)}`)}},r=new WeakMap,recordHookOwner=(e,n,t)=>{r.set(e,{plugin:n,hook:t})},hookOwner=e=>r.get(e),isRefusal=e=>403===e?.statusCode||/\[authz-forbidden\]/.test(String(e?.message??"")),launchHooksChained=async(e,t)=>{if(e)for(const r of e)if(r)try{t=await r(t)}catch(e){if(isRefusal(e)){const t=hookOwner(r);n()?.warn(`${t?`${t.plugin} refused ${t.hook}`:`a hook no plugin registered (${r.name||"anonymous"}) refused`}: ${e.message}`)}throw e}return t},transformSchemas=(e,n)=>e.toString().replace(/__(\S+)__/g,(e,t)=>{if(!t||"string"!=typeof t)return e;const r=t.trim().toLowerCase();return n[r]?"object"!=typeof n[r]?n[r]:JSON.stringify(n[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 escapeDnValue(e){return e.replace(/\\/g,"\\\\").replace(/,/g,"\\,").replace(/\+/g,"\\+").replace(/"/g,'\\"').replace(/</g,"\\<").replace(/>/g,"\\>").replace(/;/g,"\\;").replace(/=/g,"\\=").replace(/\0/g,"\\00").replace(/^\s/,"\\ ").replace(/\s$/,"\\ ").replace(/^#/,"\\#")}function unescapeDnValue(n){if("string"!=typeof n)throw new e("DN value must be a string");let t="",r=0;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)){t+=String.fromCharCode(parseInt(e,16)),r+=3;continue}}t+=e,r+=2}else t+=n[r],r++;return 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 parseDn(n){if("string"!=typeof n)throw new e("DN must be a string");const t=[];let r="",i=!1;for(let e=0;e<n.length;e++){const o=n[e];i?(r+=o,i=!1):"\\"===o?(r+=o,i=!0):","===o?(t.push(r.trim()),r=""):r+=o}return r&&t.push(r.trim()),t}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=/^[^=]+=((?:\\.|[^,])*)/.exec(e);return n?n[1].replace(/\\(.)/g,"$1"):""}function isChildOf(e,n){const t=e.toLowerCase(),r=n.toLowerCase();if(!t.endsWith(r))return!1;if(t.length===r.length)return!1;return t.substring(0,t.length-r.length).endsWith(",")}function normalizeDn(e){return parseDn(e).map(e=>e.split("+").map(e=>{const n=e.indexOf("=");return(-1===n?e.trim():`${e.slice(0,n).trim()}=${e.slice(n+1).trim()}`).toLowerCase()}).sort().join("+")).filter(e=>e.length>0).join(",")}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 isDnInBranch(e,n){const t=normalizeDn(n).split(",").filter(Boolean);if(0===t.length)return!1;const r=normalizeDn(e).split(",").filter(Boolean);if(r.length<t.length)return!1;return r.slice(r.length-t.length).every((e,n)=>e===t[n])}const asyncHandler=e=>(n,t,r)=>{Promise.resolve(e(n,t,r)).catch(r)};export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,hookOwner,isChildOf,isDnInBranch,isDummyMemberDn,launchHooks,launchHooksChained,normalizeDn,parseDn,rdnValue,recordHookOwner,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue};
@@ -1 +1 @@
1
- import{AsyncLocalStorage as t}from"async_hooks";import e from"../../lib/auth/base.js";import{ForbiddenError as i}from"../../lib/errors.js";import{unauthorized as n,ok as r}from"../../lib/expressFormatedResponses.js";import{getParentDn as s,asyncHandler as a}from"../../lib/utils.js";import{verifyLdapPassword as o}from"./authzDynamicHash.js";const h=new t;function asString(t){if(null!=t){if(Array.isArray(t)){const e=t[0];return null==e?void 0:Buffer.isBuffer(e)?e.toString():String(e)}return Buffer.isBuffer(t)?t.toString():String(t)}}function isAtOrUnder(t,e){const i=t.toLowerCase().trim(),n=e.toLowerCase().trim();return!(!i||!n)&&(i===n||i.endsWith(","+n))}class AuthzDynamic extends e{constructor(t){if(super(t),this.name="authzDynamic",this.roles=["auth","authz","configurable"],this.tokens=[],this.lastLoad=0,this.lastFailure=0,this.hooks={ldapsearchrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,t,"read"),[t,e,i]):[t,e,i]},ldapaddrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,s(t),"write"),[t,e,i]):[t,e,i]},ldapmodifyrequest:([t,e,i,n])=>{const r=this.activeToken(n);return r?(this.requireBranchPermission(r,s(t),"write"),[t,e,i,n]):[t,e,i,n]},ldapdeleterequest:([t,e])=>{const i=this.activeToken(e);if(!i)return[t,e];const n=Array.isArray(t)?t:[t];for(const t of n)this.requireBranchPermission(i,s(t),"delete");return[t,e]},ldaprenamerequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,s(t),"read"),this.requireBranchPermission(n,s(e),"write"),[t,e,i]):[t,e,i]}},this.base=this.config.authz_dynamic_base||"",this.cacheTtlMs=1e3*(this.config.authz_dynamic_cache_ttl||60),this.failureBackoffMs=Math.max(Math.floor(this.cacheTtlMs/4),5e3),this.tokenAttr=this.config.authz_dynamic_token_attribute||"userPassword",this.configAttr=this.config.authz_dynamic_config_attribute||"description",this.tenantAttr=this.config.authz_dynamic_tenant_attribute||"cn",this.reloadEndpoint=Boolean(this.config.authz_dynamic_reload_endpoint),!this.base)throw new Error("authzDynamic requires --authz-dynamic-base (the LDAP branch holding token entries)")}api(t){if(super.api(t),this.reloadEndpoint){const e=this.config.api_prefix||"/api";t.post(`${e}/v1/authz-dynamic/reload`,a(async(t,e)=>{t.authzToken?(await this.reload(),r(e,{success:!0,tokens:this.tokens.length})):n(e)})),this.logger.info(`authzDynamic reload endpoint registered at ${e}/v1/authz-dynamic/reload`)}}async reload(){try{const t=Array.from(new Set(["dn","cn",this.tokenAttr,this.configAttr,this.tenantAttr])),e=await this.server.ldap.search({filter:"(objectClass=*)",scope:"sub",paged:!1,attributes:t},this.base),i=[];for(const t of e.searchEntries||[]){const e="string"==typeof t.dn?t.dn:String(t.dn),n=asString(t.cn)||"",r=asString(t[this.tokenAttr])||"";if(!r)continue;let s,a=[];const o=asString(t[this.configAttr]);if(o)try{const t=JSON.parse(o);t&&"string"==typeof t.tenant&&t.tenant&&(s=t.tenant),t&&Array.isArray(t.bases)&&(a=t.bases.filter(t=>t&&"string"==typeof t.dn&&t.dn.length>0).map(t=>({dn:t.dn,read:Boolean(t.read),write:Boolean(t.write),delete:Boolean(t.delete)})))}catch(t){this.logger.warn(`authzDynamic: invalid JSON in ${this.configAttr} of ${e}: ${String(t)}`)}const h="cn"===this.tenantAttr?n:asString(t[this.tenantAttr])||n,c=s||h;i.push({dn:e,cn:n,tenant:c,hash:r,bases:a})}this.tokens=i,this.lastLoad=Date.now(),this.lastFailure=0,this.logger.info(`authzDynamic: loaded ${i.length} token(s) from ${this.base}`)}catch(t){this.lastFailure=Date.now(),this.logger.error(`authzDynamic: failed to load tokens from ${this.base}: ${String(t)}`)}}async ensureFresh(){const t=Date.now();if(!(this.lastLoad>0&&t-this.lastLoad<this.cacheTtlMs||this.lastFailure>0&&t-this.lastFailure<this.failureBackoffMs))return this.loading||(this.loading=this.reload().finally(()=>{this.loading=void 0})),this.loading}authMethod(t,e,i){t.user?i():this.ensureFresh().then(()=>{const r=t.headers.authorization;if(!r||!/^Bearer\s+/.test(r))return this.logger.warn("authzDynamic: missing or invalid Authorization header"),void n(e);const s=r.slice(r.indexOf(" ")+1).trim();if(!s)return void n(e);let a;for(const t of this.tokens)if(o(s,t.hash)){a=t;break}if(!a){const t=s.length>8?`${s.substring(0,8)}...`:"***";return this.logger.warn(`authzDynamic: unauthorized token ${t}`),void n(e)}t.user=a.tenant,t.authzToken=a,h.run(a,i)}).catch(t=>{this.logger.error(`authzDynamic: authMethod error: ${String(t)}`),n(e)})}activeToken(t){return h.getStore()||t?.authzToken}requireBranchPermission(t,e,n){for(const i of t.bases)if(isAtOrUnder(e,i.dn)&&i[n])return;throw this.logger.debug(`authzDynamic: token "${t.tenant}" denied ${n} on ${e}`),new i(`[authz-forbidden] Token does not have ${n} permission on this branch`)}getConfigApiData(){return{enabled:!0,base:this.base,cacheTtlSeconds:this.cacheTtlMs/1e3,tokenCount:this.tokens.length,reloadEndpoint:this.reloadEndpoint?`${this.config.api_prefix||"/api"}/v1/authz-dynamic/reload`:void 0,tokenAttribute:this.tokenAttr,configAttribute:this.configAttr,tenantAttribute:this.tenantAttr}}_tokens(){return this.tokens}}export{h as authzContext,AuthzDynamic as default};
1
+ import{AsyncLocalStorage as t}from"async_hooks";import e from"../../lib/auth/base.js";import{runTogether as i}from"../../lib/authz/composition.js";import{ForbiddenError as n}from"../../lib/errors.js";import{unauthorized as s,ok as r}from"../../lib/expressFormatedResponses.js";import{getParentDn as a,asyncHandler as o}from"../../lib/utils.js";import{verifyLdapPassword as h}from"./authzDynamicHash.js";const u=new t;function asString(t){if(null!=t){if(Array.isArray(t)){const e=t[0];return null==e?void 0:Buffer.isBuffer(e)?e.toString():String(e)}return Buffer.isBuffer(t)?t.toString():String(t)}}function isAtOrUnder(t,e){const i=t.toLowerCase().trim(),n=e.toLowerCase().trim();return!(!i||!n)&&(i===n||i.endsWith(","+n))}class AuthzDynamic extends e{constructor(t){if(super(t),this.name="authzDynamic",this.roles=["auth","authz","configurable"],this.tokens=[],this.lastLoad=0,this.lastFailure=0,this.hooks={ldapsearchrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,t,"read"),[t,e,i]):[t,e,i]},ldapaddrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,a(t),"write"),[t,e,i]):[t,e,i]},ldapmodifyrequest:([t,e,i,n])=>{const s=this.activeToken(n);return s?(this.requireBranchPermission(s,a(t),"write"),[t,e,i,n]):[t,e,i,n]},ldapdeleterequest:([t,e])=>{const i=this.activeToken(e);if(!i)return[t,e];const n=Array.isArray(t)?t:[t];for(const t of n)this.requireBranchPermission(i,a(t),"delete");return[t,e]},ldaprenamerequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,a(t),"read"),this.requireBranchPermission(n,a(e),"write"),[t,e,i]):[t,e,i]}},this.base=this.config.authz_dynamic_base||"",this.cacheTtlMs=1e3*(this.config.authz_dynamic_cache_ttl||60),this.failureBackoffMs=Math.max(Math.floor(this.cacheTtlMs/4),5e3),this.tokenAttr=this.config.authz_dynamic_token_attribute||"userPassword",this.configAttr=this.config.authz_dynamic_config_attribute||"description",this.tenantAttr=this.config.authz_dynamic_tenant_attribute||"cn",this.reloadEndpoint=Boolean(this.config.authz_dynamic_reload_endpoint),!this.base)throw new Error("authzDynamic requires --authz-dynamic-base (the LDAP branch holding token entries)")}api(t){if(super.api(t),this.reloadEndpoint){const e=this.config.api_prefix||"/api";t.post(`${e}/v1/authz-dynamic/reload`,o(async(t,e)=>{t.authzToken?(await this.reload(),r(e,{success:!0,tokens:this.tokens.length})):s(e)})),this.logger.info(`authzDynamic reload endpoint registered at ${e}/v1/authz-dynamic/reload`)}}async reload(){try{const t=Array.from(new Set(["dn","cn",this.tokenAttr,this.configAttr,this.tenantAttr])),e=await this.server.ldap.search({filter:"(objectClass=*)",scope:"sub",paged:!1,attributes:t},this.base),i=[];for(const t of e.searchEntries||[]){const e="string"==typeof t.dn?t.dn:String(t.dn),n=asString(t.cn)||"",s=asString(t[this.tokenAttr])||"";if(!s)continue;let r,a=[];const o=asString(t[this.configAttr]);if(o)try{const t=JSON.parse(o);t&&"string"==typeof t.tenant&&t.tenant&&(r=t.tenant),t&&Array.isArray(t.bases)&&(a=t.bases.filter(t=>t&&"string"==typeof t.dn&&t.dn.length>0).map(t=>({dn:t.dn,read:Boolean(t.read),write:Boolean(t.write),delete:Boolean(t.delete)})))}catch(t){this.logger.warn(`authzDynamic: invalid JSON in ${this.configAttr} of ${e}: ${String(t)}`)}const h="cn"===this.tenantAttr?n:asString(t[this.tenantAttr])||n,u=r||h;i.push({dn:e,cn:n,tenant:u,hash:s,bases:a})}this.tokens=i,this.lastLoad=Date.now(),this.lastFailure=0,this.logger.info(`authzDynamic: loaded ${i.length} token(s) from ${this.base}`)}catch(t){this.lastFailure=Date.now(),this.logger.error(`authzDynamic: failed to load tokens from ${this.base}: ${String(t)}`)}}async ensureFresh(){const t=Date.now();if(!(this.lastLoad>0&&t-this.lastLoad<this.cacheTtlMs||this.lastFailure>0&&t-this.lastFailure<this.failureBackoffMs))return this.loading||(this.loading=this.reload().finally(()=>{this.loading=void 0})),this.loading}bypassReason(t){const e=this.config.authz_dynamic_bypass||[];if(0===e.length)return;const i=t;return e.includes("trusted-proxy")&&!0===i.trustedProxy&&i.proxyAuthUser?`trusted-proxy vouching for ${i.proxyAuthUser}`:t.user?e.includes("any-authenticated")?"any-authenticated":e.includes(t.user)?`identity ${t.user}`:void 0:void 0}identitySource(){return"req.user and req.userName: the token's tenant"}afterLoad(){super.afterLoad();const t=Object.values(this.server.loadedPlugins).filter(t=>t!==this&&t.roles?.includes("auth")&&i(this,{name:t.name,pathPrefixes:t.pathPrefixes||[]})).map(t=>t.name);if(0===t.length)return;const e=this.config.authz_dynamic_bypass||[];this.logger.warn(`authzDynamic: ${t.join(", ")} authenticate the same paths as this plugin, so a request can arrive already identified. It is `+(0===e.length?"checked against the ACLs of the token it carries, and refused without one — set --authz-dynamic-bypass to let an identity through without a token.":`let through without a token when it matches ${e.join(", ")} (--authz-dynamic-bypass), and checked against its token ACLs otherwise.`))}afterChain(t,e,i){const n=t;if(!n.authzDynamicPending)return i();n.authzDynamicPending=!1;const r=this.bypassReason(n);if(r)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${r})`),i();this.logger.warn(n.authzDynamicPendingReason||"authzDynamic: no token on a request no bypass covers"),s(e)}refuseOrDefer(t,e,i,n){const r=this.bypassReason(t);if(r)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${r})`),void this.passThrough(t,i);if(!t.user&&(this.config.authz_dynamic_bypass||[]).length>0){const e=t;return e.authzDynamicPending=!0,e.authzDynamicPendingReason=n,void this.passThrough(t,i)}this.logger.warn(n),s(e)}authMethod(t,e,i){const n=this.bypassReason(t);if(n)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${n})`),void this.passThrough(t,i);this.ensureFresh().then(()=>{const n=t.headers.authorization;if(!n||!/^Bearer\s+/.test(n))return void this.refuseOrDefer(t,e,i,"authzDynamic: missing or invalid Authorization header");const s=n.slice(n.indexOf(" ")+1).trim();if(!s)return void this.refuseOrDefer(t,e,i,"authzDynamic: empty bearer token");let r;for(const t of this.tokens)if(h(s,t.hash)){r=t;break}if(!r){const n=s.length>8?`${s.substring(0,8)}...`:"***";return void this.refuseOrDefer(t,e,i,`authzDynamic: unauthorized token ${n}`)}t.user||this.publishIdentity(t,r.tenant),t.authzToken=r,u.run(r,i)}).catch(t=>{this.logger.error(`authzDynamic: authMethod error: ${String(t)}`),s(e)})}activeToken(t){return u.getStore()||t?.authzToken}requireBranchPermission(t,e,i){for(const n of t.bases)if(isAtOrUnder(e,n.dn)&&n[i])return;throw this.logger.debug(`authzDynamic: token "${t.tenant}" denied ${i} on ${e}`),new n(`[authz-forbidden] Token does not have ${i} permission on this branch`)}getConfigApiData(){return{enabled:!0,base:this.base,cacheTtlSeconds:this.cacheTtlMs/1e3,tokenCount:this.tokens.length,reloadEndpoint:this.reloadEndpoint?`${this.config.api_prefix||"/api"}/v1/authz-dynamic/reload`:void 0,tokenAttribute:this.tokenAttr,configAttribute:this.configAttr,tenantAttribute:this.tenantAttr}}_tokens(){return this.tokens}}export{u as authzContext,AuthzDynamic as default};
@@ -1 +1 @@
1
- import e from"../../lib/authz/base.js";class AuthzPerBranch extends e{constructor(e){super(e),this.name="authzPerBranch",this.groupCache=new Map,this.cacheTTL=1e3*(this.config.authz_per_branch_cache_ttl||60),this.authConfig=this.config.authz_per_branch_config,this.authConfig&&this.logger.info("Authorization config loaded")}resolveUser(e){return Promise.resolve(e)}shouldSkipAuthorization(e){return!e?.user||!this.authConfig}async getUserPermissions(e,t){if(!this.authConfig)return{read:!0,write:!0,delete:!0};let r={read:this.authConfig.default?.read??!1,write:this.authConfig.default?.write??!1,delete:this.authConfig.default?.delete??!1};if(this.authConfig.users?.[e]){const s=this.findBranchPermissions(this.authConfig.users[e],t);s&&(r=this.mergePermissions(r,s))}const s=await this.getUserGroups(e);for(const e of s)if(this.authConfig.groups?.[e]){const s=this.findBranchPermissions(this.authConfig.groups[e],t);s&&(r=this.mergePermissions(r,s))}return r}async getAuthorizedBranches(e){return this.getAuthorizedBranchesForPermission(e,"read")}async getAuthorizedBranchesForPermission(e,t){if(!this.authConfig)return[];const r=[];if(this.authConfig.users?.[e])for(const[s,i]of Object.entries(this.authConfig.users[e]))i[t]&&r.push(s);const s=await this.getUserGroups(e);for(const e of s)if(this.authConfig.groups?.[e])for(const[s,i]of Object.entries(this.authConfig.groups[e]))i[t]&&!r.includes(s)&&r.push(s);return r}findBranchPermissions(e,t){if(e[t])return e[t];for(const[r,s]of Object.entries(e))if(t.toLowerCase().endsWith(`,${r.toLowerCase()}`))return s;return null}mergePermissions(e,t){return{read:e.read||t.read||!1,write:e.write||t.write||!1,delete:e.delete||t.delete||!1}}buildBranchFilter(e,t){if(t.some(t=>e.toLowerCase()===t.toLowerCase()||e.toLowerCase().endsWith(`,${t.toLowerCase()}`)))return null;if(1===t.length)return`(entryDN=*,${t[0]})`;if(t.length>1){return`(|${t.map(e=>`(entryDN=*,${e})`).join("")})`}return null}async getUserGroups(e){const t=Date.now(),r=this.groupCache.get(e);if(r&&t-r.timestamp<this.cacheTTL)return r.groups;const s=[];try{const t=`(${this.config.ldap_group_member_attribute||"member"}=${this.config.ldap_user_main_attribute}=${e},*)`,r=await this.server.ldap.search({paged:!1,filter:t,attributes:["dn"]},this.server.ldap.base);if(r.searchEntries)for(const e of r.searchEntries)e.dn&&s.push("string"==typeof e.dn?e.dn:String(e.dn))}catch(t){this.logger.error(`Failed to resolve groups for user ${e}: ${t}`)}return this.groupCache.set(e,{groups:s,timestamp:t}),s}}export{AuthzPerBranch as default};
1
+ import e from"../../lib/authz/base.js";import{warnUnmatchedRuleKeys as t}from"../../lib/auth/base.js";class AuthzPerBranch extends e{constructor(e){super(e),this.name="authzPerBranch",this.groupCache=new Map,this.cacheTTL=1e3*(this.config.authz_per_branch_cache_ttl||60),this.authConfig=this.config.authz_per_branch_config,this.authConfig&&this.logger.info("Authorization config loaded")}resolveUser(e){return Promise.resolve(e)}afterLoad(){t(Object.keys(this.authConfig?.users??{}),this.server.loadedPlugins,this.name,this.logger)}shouldSkipAuthorization(e){return super.shouldSkipAuthorization(e)||!this.authConfig}async getUserPermissions(e,t){if(!this.authConfig)return{read:!0,write:!0,delete:!0};let r={read:this.authConfig.default?.read??!1,write:this.authConfig.default?.write??!1,delete:this.authConfig.default?.delete??!1};if(this.authConfig.users?.[e]){const s=this.findBranchPermissions(this.authConfig.users[e],t);s&&(r=this.mergePermissions(r,s))}const s=await this.getUserGroups(e);for(const e of s)if(this.authConfig.groups?.[e]){const s=this.findBranchPermissions(this.authConfig.groups[e],t);s&&(r=this.mergePermissions(r,s))}return r}async getAuthorizedBranches(e){return this.getAuthorizedBranchesForPermission(e,"read")}async getAuthorizedBranchesForPermission(e,t){if(!this.authConfig)return[];const r=[];if(this.authConfig.users?.[e])for(const[s,i]of Object.entries(this.authConfig.users[e]))i[t]&&r.push(s);const s=await this.getUserGroups(e);for(const e of s)if(this.authConfig.groups?.[e])for(const[s,i]of Object.entries(this.authConfig.groups[e]))i[t]&&!r.includes(s)&&r.push(s);return r}findBranchPermissions(e,t){if(e[t])return e[t];for(const[r,s]of Object.entries(e))if(t.toLowerCase().endsWith(`,${r.toLowerCase()}`))return s;return null}mergePermissions(e,t){return{read:e.read||t.read||!1,write:e.write||t.write||!1,delete:e.delete||t.delete||!1}}buildBranchFilter(e,t){if(t.some(t=>e.toLowerCase()===t.toLowerCase()||e.toLowerCase().endsWith(`,${t.toLowerCase()}`)))return null;if(1===t.length)return`(entryDN=*,${t[0]})`;if(t.length>1){return`(|${t.map(e=>`(entryDN=*,${e})`).join("")})`}return null}async getUserGroups(e){const t=Date.now(),r=this.groupCache.get(e);if(r&&t-r.timestamp<this.cacheTTL)return r.groups;const s=[];try{const t=`(${this.config.ldap_group_member_attribute||"member"}=${this.config.ldap_user_main_attribute}=${e},*)`,r=await this.server.ldap.search({paged:!1,filter:t,attributes:["dn"]},this.server.ldap.base);if(r.searchEntries)for(const e of r.searchEntries)e.dn&&s.push("string"==typeof e.dn?e.dn:String(e.dn))}catch(t){this.logger.error(`Failed to resolve groups for user ${e}: ${t}`)}return this.groupCache.set(e,{groups:s,timestamp:t}),s}}export{AuthzPerBranch as default};
@@ -1 +1 @@
1
- import e from"../../abstract/plugin.js";import{forbidden as t}from"../../lib/expressFormatedResponses.js";const r=/^[\w/.\-+*]*$/;function globToRegex(e){if(!r.test(e))throw new Error(`Invalid glob pattern: "${e}" — only [a-zA-Z0-9_/.+*-] are allowed`);const t=e.replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/\\\*\\\*/g,".*").replace(/\\\*/g,"[^/]*");return new RegExp(`^${t}$`)}class AuthzPerRoute extends e{constructor(...e){super(...e),this.name="authzPerRoute",this.roles=["authz"],this.rules=new Map;const t=this.config.authz_per_route??[];for(const e of t)this.parseEntry(e);const r=[...this.rules.entries()].map(([e,t])=>`${e}: ${t.some(e=>"wildcard"===e.kind)?"full access":`${t.length} rule${1!==t.length?"s":""}`}`).join(", ");this.logger.info(`authzPerRoute: ${this.rules.size} user${1!==this.rules.size?"s":""} configured${this.rules.size>0?` (${r})`:""}`)}parseEntry(e){const t=e.split(":").map(e=>e.trim());if(t.length<2)return void this.logger.warn(`authzPerRoute: ignoring invalid rule entry: ${e}`);const r=t[0];if(r)if(2!==t.length||"*"!==t[1]){if(t.length>=3){const i=t[1].toUpperCase(),o=new Set(["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS","*"]);if(!i||!o.has(i))return void this.logger.warn(`authzPerRoute: ignoring rule with invalid method "${t[1]}" in entry: ${e}`);const n=t.slice(2).join(":").trim();if(!n)return void this.logger.warn(`authzPerRoute: ignoring rule with empty path pattern in entry: ${e}`);let s;try{s=globToRegex(n)}catch{return void this.logger.warn(`authzPerRoute: ignoring entry with invalid glob "${n}" in rule: ${e}`)}return void this.addRule(r,{kind:"method-path",method:i,pathRe:s})}this.logger.warn(`authzPerRoute: ignoring invalid rule entry: ${e}`)}else this.addRule(r,{kind:"wildcard"});else this.logger.warn(`authzPerRoute: ignoring rule with empty user: ${e}`)}addRule(e,t){const r=this.rules.get(e);r?r.push(t):this.rules.set(e,[t])}matches(e,t,r){for(const i of e){if("wildcard"===i.kind)return!0;if(("*"===i.method||i.method===t.toUpperCase())&&i.pathRe.test(r))return!0}return!1}api(e){e.use((e,r,i)=>{const o=e.user;if(!o)return i();const n=this.rules.get(o);return n?this.matches(n,e.method,e.path)?i():(this.logger.warn(`authzPerRoute: user '${o}' denied for ${e.method} ${e.path}`),t(r)):(this.logger.warn(`authzPerRoute: user '${o}' denied for ${e.method} ${e.path} (no rules configured)`),t(r))})}}export{AuthzPerRoute as default,globToRegex};
1
+ import e from"../../abstract/plugin.js";import{forbidden as t}from"../../lib/expressFormatedResponses.js";import{assertIdentityMode as i,warnUnmatchedRuleKeys as r,identityFor as s}from"../../lib/auth/base.js";import{authzFor as n,servesRequest as o}from"../../lib/authz/composition.js";const a=/^[\w/.\-+*]*$/;function globToRegex(e){if(!a.test(e))throw new Error(`Invalid glob pattern: "${e}" — only [a-zA-Z0-9_/.+*-] are allowed`);const t=e.replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/\\\*\\\*/g,".*").replace(/\\\*/g,"[^/]*");return new RegExp(`^${t}$`)}class AuthzPerRoute extends e{constructor(...e){super(...e),this.name="authzPerRoute",this.roles=["authz"],this.rules=new Map,this.fallbackWarned=!1,this.unidentifiedRoutes=new Set,this.unidentifiedWarnings=0,i(this.config,this.constructor.name),n(this.config,this.constructor.name);const t=this.config.authz_per_route??[];for(const e of t)this.parseEntry(e);const r=[...this.rules.entries()].map(([e,t])=>`${e}: ${t.some(e=>"wildcard"===e.kind)?"full access":`${t.length} rule${1!==t.length?"s":""}`}`).join(", ");this.logger.info(`authzPerRoute: ${this.rules.size} user${1!==this.rules.size?"s":""} configured${this.rules.size>0?` (${r})`:""}`)}parseEntry(e){const t=e.split(":").map(e=>e.trim());if(t.length<2)return void this.logger.warn(`authzPerRoute: ignoring invalid rule entry: ${e}`);const i=t[0];if(i)if(2!==t.length||"*"!==t[1]){if(t.length>=3){const r=t[1].toUpperCase(),s=new Set(["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS","*"]);if(!r||!s.has(r))return void this.logger.warn(`authzPerRoute: ignoring rule with invalid method "${t[1]}" in entry: ${e}`);const n=t.slice(2).join(":").trim();if(!n)return void this.logger.warn(`authzPerRoute: ignoring rule with empty path pattern in entry: ${e}`);let o;try{o=globToRegex(n)}catch{return void this.logger.warn(`authzPerRoute: ignoring entry with invalid glob "${n}" in rule: ${e}`)}return void this.addRule(i,{kind:"method-path",method:r,pathRe:o})}this.logger.warn(`authzPerRoute: ignoring invalid rule entry: ${e}`)}else this.addRule(i,{kind:"wildcard"});else this.logger.warn(`authzPerRoute: ignoring rule with empty user: ${e}`)}addRule(e,t){const i=this.rules.get(e);i?i.push(t):this.rules.set(e,[t])}matches(e,t,i){for(const r of e){if("wildcard"===r.kind)return!0;if(("*"===r.method||r.method===t.toUpperCase())&&r.pathRe.test(i))return!0}return!1}afterLoad(){r([...this.rules.keys()],this.server.loadedPlugins,this.name,this.logger)}api(e){e.use((e,i,r)=>{const{value:n,fellBack:a}=s(e,this.config);if(a&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so rules are matched against req.user instead`)),!n){const t=`${e.method} ${e.path}`,i=`${this.name}: ${t} carries no identity, so no route rule applies to it`;return!this.unidentifiedRoutes.has(t)&&this.unidentifiedWarnings<AuthzPerRoute.UNIDENTIFIED_MAX?(this.unidentifiedWarnings++,this.unidentifiedRoutes.add(t),this.logger.warn(i)):this.logger.debug(i),r()}if(!o(e,this.config))return this.logger.debug(`${this.name}: ${e.method} ${e.path} authenticated by ${(e.authenticators??[]).join(", ")}, which this plugin does not serve`),r();const h=this.rules.get(n);return h?this.matches(h,e.method,e.path)?r():(this.logger.warn(`authzPerRoute: user '${n}' denied for ${e.method} ${e.path}`),t(i)):(this.logger.warn(`authzPerRoute: user '${n}' denied for ${e.method} ${e.path} (no rules configured)`),t(i))})}}AuthzPerRoute.UNIDENTIFIED_MAX=1e3;export{AuthzPerRoute as default,globToRegex};
@@ -1 +1 @@
1
- import e from"../../abstract/plugin.js";import{asyncHandler as t}from"../../lib/utils.js";import{UnauthorizedError as s}from"../../lib/errors.js";import{roleAttribute as a}from"../../config/schema.js";class AuthzScope extends e{constructor(){super(...arguments),this.name="authzScope",this.roles=["api"]}api(e){e.get(`${this.config.api_prefix}/v1/authz/scope`,t(async(e,t)=>this.scope(e,t)))}authz(){return Object.values(this.server.loadedPlugins).find(e=>{if(!e.roles?.includes("authz"))return!1;const t=e;return"function"==typeof t.resolveUser&&"function"==typeof t.getAuthorizedBranches&&"function"==typeof t.getUserPermissions})}entities(){const e=(this.config.ldap_organization_link_attribute||"").toLowerCase(),linkable=t=>Boolean(e)&&(!t?.attributes||Object.keys(t.attributes).some(t=>t.toLowerCase()===e)),t=[],s=this.server.loadedPlugins.ldapFlatGeneric;for(const e of s?.instances||[])t.push({name:e.pluralName,base:e.base,linkable:linkable(e.schema)});const a=this.server.loadedPlugins.ldapGroups;return a?.base&&t.push({name:"groups",base:a.base,linkable:linkable(a.schema)}),this.server.loadedPlugins.ldapOrganizations&&this.config.ldap_top_organization&&t.push({name:"organizations",base:this.config.ldap_top_organization,linkable:!1}),t}async scope(e,t){const a=this.authz();if(!a)return void t.json({user:e.user??null,unrestricted:!0,branches:[],entities:this.entities().map(({name:e,base:t})=>({name:e,base:t,create:!0}))});if(!e.user)throw new s("No authenticated user");const i=await a.resolveUser(e.user);if(!i)throw new s(`Unknown user ${e.user}`);const r=await a.getAuthorizedBranches(i),n=[],o=new Map,permissionsOn=async e=>{let t=o.get(e);return t||(t=await a.getUserPermissions(i,e),o.set(e,t)),t};for(const e of r){const t=await permissionsOn(e);n.push({dn:e,...t,...await this.branchLabel(e)})}const c=r.some(e=>!0===o.get(e)?.write),u=this.config.ldap_top_organization,h=[];for(const{name:e,base:t,linkable:s}of this.entities()){const a=u&&t===u?!0===(await permissionsOn(u)).write:s?c:!0===(await permissionsOn(t)).write;h.push({name:e,base:t,create:a})}t.json({user:i,unrestricted:!1,branches:n,entities:h})}async branchLabel(e){const t=this.server.loadedPlugins.ldapOrganizations,s=a(t?.schema,"organizationPath")||this.config.ldap_organization_path_attribute;try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:["ou","o","cn",...s?[s]:[]]},e),a=t?.searchEntries?.[0];if(!a)return{};const first=e=>{const t=Array.isArray(e)?e[0]:e;return"string"==typeof t?t||void 0:Buffer.isBuffer(t)&&t.toString("utf8")||void 0};return{name:first(a.ou)||first(a.o)||first(a.cn),path:s?first(a[s]):void 0}}catch(e){return{}}}}export{AuthzScope as default};
1
+ import e from"../../abstract/plugin.js";import{identityFor as s}from"../../lib/auth/base.js";import{servesRequest as t,overlap as r,EVERYONE as i,authzFor as o}from"../../lib/authz/composition.js";import{asyncHandler as a}from"../../lib/utils.js";import{UnauthorizedError as n}from"../../lib/errors.js";import{roleAttribute as c}from"../../config/schema.js";class AuthzScope extends e{constructor(){super(...arguments),this.name="authzScope",this.roles=["api"]}api(e){e.get(`${this.config.api_prefix}/v1/authz/scope`,a(async(e,s)=>this.scope(e,s)))}static canDescribe(e){if(!e.roles?.includes("authz"))return!1;const s=e;return"function"==typeof s.resolveUser&&"function"==typeof s.getAuthorizedBranches&&"function"==typeof s.getUserPermissions}describers(){return Object.values(this.server.loadedPlugins).filter(e=>AuthzScope.canDescribe(e))}static judges(e,s){return e.roles?.includes("auth")?Boolean(s.authenticators?.includes(e.name)):Boolean(s.user)&&t(s,e.config)}assertComposition(){const e=this.config.authz_scope_source;if(!e)return;const s=this.describers().map(e=>e.name);if(!s.includes(e))throw new Error(`${this.name}: --authz-scope-source names ${e}, which is not a loaded plugin able to describe a scope`+(s.length>0?` (loaded: ${s.join(", ")})`:""))}afterLoad(){if(this.config.authz_scope_source)return;const e=this.describers(),s=this.server.authenticators,population=e=>o(e.config,e.name)??i;for(let t=0;t<e.length;t++)for(let i=t+1;i<e.length;i++){const o=e[t],a=e[i];if(r(population(o),population(a),s))return void this.logger.warn(`${this.name}: ${o.name} and ${a.name} can both describe the scope of one caller, and ${o.name} answers because it loaded first. Set --authz-scope-source to choose; the answer lists both under \`sources\``)}}entities(){const e=(this.config.ldap_organization_link_attribute||"").toLowerCase(),linkable=s=>Boolean(e)&&(!s?.attributes||Object.keys(s.attributes).some(s=>s.toLowerCase()===e)),s=[],t=this.server.loadedPlugins.ldapFlatGeneric;for(const e of t?.instances||[])s.push({name:e.pluralName,base:e.base,linkable:linkable(e.schema)});const r=this.server.loadedPlugins.ldapGroups;return r?.base&&s.push({name:"groups",base:r.base,linkable:linkable(r.schema)}),this.server.loadedPlugins.ldapOrganizations&&this.config.ldap_top_organization&&s.push({name:"organizations",base:this.config.ldap_top_organization,linkable:!1}),s}async scope(e,t){const r=this.describers();if(Object.values(this.server.loadedPlugins).some(e=>e.roles?.includes("authz"))&&!e.user)throw new n("No authenticated user");const i=Object.values(this.server.loadedPlugins).filter(s=>s.roles?.includes("authz")&&AuthzScope.judges(s,e)),o=r.filter(e=>i.includes(e)),a=this.config.authz_scope_source,c=o.find(e=>e.name===a)??o[0],u=i.map(e=>e.name);if(!c)return 0===i.length?void t.json({user:e.user??null,unrestricted:!0,described:!0,source:null,sources:[],branches:[],entities:this.entities().map(({name:e,base:s})=>({name:e,base:s,create:!0}))}):void t.json({user:e.user??null,unrestricted:!1,described:!1,source:null,sources:u,branches:[],entities:[]});const l=s(e,c.config).value,h=await c.resolveUser(l);if(!h)throw new n(`Unknown user ${l}`);const d=await c.getAuthorizedBranches(h),p=[],f=new Map,permissionsOn=async e=>{let s=f.get(e);return s||(s=await c.getUserPermissions(h,e),f.set(e,s)),s};for(const e of d){const s=await permissionsOn(e);p.push({dn:e,...s,...await this.branchLabel(e)})}const b=d.some(e=>!0===f.get(e)?.write),g=this.config.ldap_top_organization,m=[];for(const{name:e,base:s,linkable:t}of this.entities()){const r=g&&s===g?!0===(await permissionsOn(g)).write:t?b:!0===(await permissionsOn(s)).write;m.push({name:e,base:s,create:r})}t.json({user:h,unrestricted:!1,described:!0,source:c.name,sources:u,branches:p,entities:m})}async branchLabel(e){const s=this.server.loadedPlugins.ldapOrganizations,t=c(s?.schema,"organizationPath")||this.config.ldap_organization_path_attribute;try{const s=await this.server.ldap.search({paged:!1,scope:"base",attributes:["ou","o","cn",...t?[t]:[]]},e),r=s?.searchEntries?.[0];if(!r)return{};const first=e=>{const s=Array.isArray(e)?e[0]:e;return"string"==typeof s?s||void 0:Buffer.isBuffer(s)&&s.toString("utf8")||void 0};return{name:first(r.ou)||first(r.o)||first(r.cn),path:t?first(r[t]):void 0}}catch(e){return{}}}}export{AuthzScope as default};
@@ -1 +1 @@
1
- import{createHmac as e,createHash as t,timingSafeEqual as i}from"crypto";import{unauthorized as r}from"../../lib/expressFormatedResponses.js";import s from"../../lib/auth/base.js";class AuthHmac extends s{constructor(...e){super(...e),this.name="authHmac",this.roles=["auth"],this.services=new Map;const t=this.config.auth_hmac;this.timeWindow=this.config.auth_hmac_window??12e4,t&&Array.isArray(t)&&t.forEach((e,t)=>{const i=e.split(":");if(i.length>=3){const e=i[0].trim(),r=i[1].trim(),s=i.slice(2).join(":").trim();if(!e||!r||!s)return void this.logger.warn(`Invalid HMAC config at index ${t}: missing id, secret, or name`);r.length<32&&this.logger.warn(`HMAC secret for service "${e}" is too short (minimum 32 characters recommended)`),this.services.set(e,{id:e,secret:r,name:s})}else this.logger.warn(`Invalid HMAC config format at index ${t}: expected "service-id:secret:name"`)}),0===this.services.size?this.logger.warn("No valid HMAC services configured"):(this.logger.info(`HMAC authentication initialized with ${this.services.size} service(s): ${Array.from(this.services.keys()).join(", ")}`),this.logger.info(`Time window for replay protection: ${this.timeWindow}ms`))}authMethod(e,t,i){const s=e.headers.authorization;if(!s||!s.startsWith("HMAC-SHA256 "))return this.logger.warn("Missing or invalid Authorization header (expected HMAC-SHA256)"),r(t);const n=s.substring(12).split(":");if(3!==n.length)return this.logger.warn("Invalid HMAC authorization format (expected service-id:timestamp:signature)"),r(t);const[o,a,c]=n,h=this.services.get(o);if(!h)return this.logger.warn(`Unknown service ID: ${o}`),r(t);const g=parseInt(a,10);if(isNaN(g)||g<=0)return this.logger.warn(`Invalid timestamp: ${a}`),r(t);const u=Date.now(),m=Math.abs(u-g);if(m>this.timeWindow)return this.logger.warn(`Timestamp outside allowed window: ${m}ms (max: ${this.timeWindow}ms) for service ${o}`),r(t);const d=this.calculateBodyHash(e),l=`${e.method.toUpperCase()}|${e.originalUrl||e.url}|${g}|${d}`,f=this.calculateHmac(h.secret,l);if(!this.secureCompare(c,f))return this.logger.warn(`Invalid signature for service ${o} (${h.name}). Expected: ${f.substring(0,8)}..., Got: ${c.substring(0,8)}...`),this.logger.debug(`Signing string: ${l}`),r(t);this.logger.debug(`HMAC authentication successful for service: ${o} (${h.name})`),e.user=h.name,i()}calculateHmac(t,i){const r=e("sha256",t);return r.update(i),r.digest("hex")}calculateBodyHash(e){const i=e.method.toUpperCase();if("GET"===i||"DELETE"===i||"HEAD"===i)return"";if(e.body){const i="string"==typeof e.body?e.body:JSON.stringify(e.body),r=t("sha256");return r.update(i),r.digest("hex")}return""}secureCompare(e,t){if(e.length!==t.length)return!1;try{const r=Buffer.from(e,"utf8"),s=Buffer.from(t,"utf8");return i(r,s)}catch{return!1}}}export{AuthHmac as default};
1
+ import{createHmac as e,createHash as t,timingSafeEqual as i}from"crypto";import{unauthorized as r}from"../../lib/expressFormatedResponses.js";import s from"../../lib/auth/base.js";class AuthHmac extends s{knownIdentities(){return[...this.services.values()].map(e=>e.name)}identitySource(){return"req.user and req.userName: the service name given in --auth-hmac"}constructor(...e){super(...e),this.name="authHmac",this.roles=["auth"],this.services=new Map;const t=this.config.auth_hmac;this.timeWindow=this.config.auth_hmac_window??12e4,t&&Array.isArray(t)&&t.forEach((e,t)=>{const i=e.split(":");if(i.length>=3){const e=i[0].trim(),r=i[1].trim(),s=i.slice(2).join(":").trim();if(!e||!r||!s)return void this.logger.warn(`Invalid HMAC config at index ${t}: missing id, secret, or name`);r.length<32&&this.logger.warn(`HMAC secret for service "${e}" is too short (minimum 32 characters recommended)`),this.services.set(e,{id:e,secret:r,name:s})}else this.logger.warn(`Invalid HMAC config format at index ${t}: expected "service-id:secret:name"`)}),0===this.services.size?this.logger.warn("No valid HMAC services configured"):(this.logger.info(`HMAC authentication initialized with ${this.services.size} service(s): ${Array.from(this.services.keys()).join(", ")}`),this.logger.info(`Time window for replay protection: ${this.timeWindow}ms`))}authMethod(e,t,i){const s=e.headers.authorization;if(!s||!s.startsWith("HMAC-SHA256 "))return this.logger.warn("Missing or invalid Authorization header (expected HMAC-SHA256)"),r(t);const n=s.substring(12).split(":");if(3!==n.length)return this.logger.warn("Invalid HMAC authorization format (expected service-id:timestamp:signature)"),r(t);const[a,o,c]=n,h=this.services.get(a);if(!h)return this.logger.warn(`Unknown service ID: ${a}`),r(t);const u=parseInt(o,10);if(isNaN(u)||u<=0)return this.logger.warn(`Invalid timestamp: ${o}`),r(t);const m=Date.now(),g=Math.abs(m-u);if(g>this.timeWindow)return this.logger.warn(`Timestamp outside allowed window: ${g}ms (max: ${this.timeWindow}ms) for service ${a}`),r(t);const d=this.calculateBodyHash(e),l=`${e.method.toUpperCase()}|${e.originalUrl||e.url}|${u}|${d}`,f=this.calculateHmac(h.secret,l);if(!this.secureCompare(c,f))return this.logger.warn(`Invalid signature for service ${a} (${h.name}). Expected: ${f.substring(0,8)}..., Got: ${c.substring(0,8)}...`),this.logger.debug(`Signing string: ${l}`),r(t);this.logger.debug(`HMAC authentication successful for service: ${a} (${h.name})`),this.publishIdentity(e,h.name),i()}calculateHmac(t,i){const r=e("sha256",t);return r.update(i),r.digest("hex")}calculateBodyHash(e){const i=e.method.toUpperCase();if("GET"===i||"DELETE"===i||"HEAD"===i)return"";if(e.body){const i="string"==typeof e.body?e.body:JSON.stringify(e.body),r=t("sha256");return r.update(i),r.digest("hex")}return""}secureCompare(e,t){if(e.length!==t.length)return!1;try{const r=Buffer.from(e,"utf8"),s=Buffer.from(t,"utf8");return i(r,s)}catch{return!1}}}export{AuthHmac as default};
@@ -1 +1 @@
1
- import e from"../../lib/auth/base.js";const t=new WeakMap;class AuthLLNG extends e{constructor(){super(...arguments),this.name="authLemonldapNg",this.roles=["auth"]}async api(e){let n;try{n=await this.loadHandler()}catch(e){throw new Error(`${this.name}: requires the optional dependency "lemonldap-ng-handler", which is not installed. Install it to use this plugin, or remove it from the configuration. (${e instanceof Error?e.message:String(e)})`)}const a=this.config.llng_ini??"",i=t.get(n);if(void 0!==i&&i!==a)throw new Error(`${this.name}: lemonldap-ng-handler is already initialized from "${i}"; it keeps its state at the module level, so a second instance of this plugin cannot also use "${a}". Load one instance of this plugin, or point every instance at the same lemonldap-ng.ini.`);try{await n.init({configStorage:{confFile:a}})}catch(e){throw new Error(`${this.name}: cannot initialize the LemonLDAP::NG handler from ${a}. Check that the file exists, that its [configuration] section is reachable and that [node-handler] lists this server in nodeVhosts. (${e instanceof Error?e.message:String(e)})`)}t.set(n,a),this.handler=n,super.api(e)}async loadHandler(){return import("lemonldap-ng-handler")}authMethod(e,t,n){if(!this.handler)throw new Error(`${this.name}: lemonldap-ng-handler is not loaded`);this.handler.run(e,t,()=>{e.user=e.headers["Lm-Remote-User"],n()})}}export{AuthLLNG as default};
1
+ import e from"../../lib/auth/base.js";const t=new WeakMap;class AuthLLNG extends e{constructor(){super(...arguments),this.name="authLemonldapNg",this.roles=["auth"]}async api(e){let n;try{n=await this.loadHandler()}catch(e){throw new Error(`${this.name}: requires the optional dependency "lemonldap-ng-handler", which is not installed. Install it to use this plugin, or remove it from the configuration. (${e instanceof Error?e.message:String(e)})`)}const r=this.config.llng_ini??"",a=t.get(n);if(void 0!==a&&a!==r)throw new Error(`${this.name}: lemonldap-ng-handler is already initialized from "${a}"; it keeps its state at the module level, so a second instance of this plugin cannot also use "${r}". Load one instance of this plugin, or point every instance at the same lemonldap-ng.ini.`);try{await n.init({configStorage:{confFile:r}})}catch(e){throw new Error(`${this.name}: cannot initialize the LemonLDAP::NG handler from ${r}. Check that the file exists, that its [configuration] section is reachable and that [node-handler] lists this server in nodeVhosts. (${e instanceof Error?e.message:String(e)})`)}t.set(n,r),this.handler=n,super.api(e)}async loadHandler(){return import("lemonldap-ng-handler")}static dropForgedIdentity(e,t){const n=t??AuthLLNG.USER_HEADER;for(const t of Object.keys(e.headers))t.toLowerCase()===n&&delete e.headers[t]}static headerValue(e,t){for(const[n,r]of Object.entries(e.headers))if(n.toLowerCase()===t&&"string"==typeof r)return r}static vouchedIdentity(e){return AuthLLNG.headerValue(e,AuthLLNG.USER_HEADER)}identitySource(){const e=this.config.llng_username_header||"";return"req.user: LemonLDAP::NG's whatToTrace, via Lm-Remote-User; "+(e?`req.userName: the ${e} header LLNG exports`:"req.userName: the same value, until --llng-username-header names an exported header")}authMethod(e,t,n){if(!this.handler)throw new Error(`${this.name}: lemonldap-ng-handler is not loaded`);const r=(this.config.llng_username_header||"").toLowerCase();AuthLLNG.dropForgedIdentity(e),r&&AuthLLNG.dropForgedIdentity(e,r),this.handler.run(e,t,()=>{const a=AuthLLNG.vouchedIdentity(e);if(!a)return this.logger.warn(`${this.name}: the handler passed a request on without naming a user, which a \`skip\` rule does. Refusing rather than serving it with no identity`),void t.status(401).json({error:"Unauthorized"});const i=r?AuthLLNG.headerValue(e,r):void 0;this.publishIdentity(e,a,i),n()})}}AuthLLNG.USER_HEADER="lm-remote-user";export{AuthLLNG as default};
@@ -1 +1 @@
1
- import{auth as e}from"express-openid-connect";import s from"../../abstract/plugin.js";import{launchHooksChained as o}from"../../lib/utils.js";import{serverError as t}from"../../lib/expressFormatedResponses.js";class OpenIDConnect extends s{constructor(e){super(e),this.name="openidconnect",this.roles=["auth"];for(const e of["oidc_server","oidc_client_id","oidc_client_secret","base_url"])if(!this.config[e])throw new Error(`Missing config parameter ${e}`)}api(s){const i={authRequired:!0,issuerBaseURL:this.config.oidc_server,clientID:this.config.oidc_client_id,secret:this.config.oidc_client_secret,baseURL:this.config.base_url,clientSecret:this.config.oidc_client_secret,authorizationParams:{response_type:"code",scope:"openid profile email"}};s.use(async(e,s,i)=>{try{[e,s]=await o(this.server.hooks.beforeAuth,[e,s]),i()}catch(e){return t(s,e)}}),s.use(e(i)),s.use(async(e,s,i)=>{try{this.hooks?.onAuth&&([e,s]=await o(this.server.hooks.afterAuth,[e,s])),e.user=e.oidc.user.sub,i()}catch(e){t(s,e)}})}authMethod(s,o,t){e({issuerBaseURL:process.env.ISSUER_BASE_URL,clientID:process.env.CLIENT_ID,clientSecret:process.env.CLIENT_SECRET,baseURL:process.env.BASE_URL,authorizationParams:{response_type:"code",scope:"openid email profile"}})(s,o,t)}}export{OpenIDConnect as default};
1
+ import e from"express-openid-connect";import o from"../../lib/auth/base.js";import{launchHooksChained as i}from"../../lib/utils.js";import{serverError as t}from"../../lib/expressFormatedResponses.js";const{auth:s,requiresAuth:r}=e,n=["/login","/logout","/callback","/backchannel-logout"],walkSubscribers=async(e,o)=>{let i;for(const t of e??[])if(t)try{await t(o)}catch(e){i??(i=e instanceof Error?e:new Error(String(e)))}if(i)throw i};class OpenIDConnect extends o{constructor(e){super(e),this.name="openidconnect",this.roles=["auth"];for(const e of["oidc_server","oidc_client_id","oidc_client_secret","base_url"])if(!this.config[e])throw new Error(`Missing config parameter ${e}`)}buildConfig(){return{authRequired:!1,issuerBaseURL:this.config.oidc_server,clientID:this.config.oidc_client_id,secret:this.config.oidc_client_secret,baseURL:this.config.base_url,clientSecret:this.config.oidc_client_secret,authorizationParams:{response_type:"code",scope:"openid profile email"},backchannelLogout:{onLogoutToken:async e=>{this.server.hooks.oidclogouttoken?await walkSubscribers(this.server.hooks.oidclogouttoken,e):this.logger.warn("openidconnect: a logout token arrived but no Back-Channel Logout plugin is loaded, so nothing records it")},onLogin:async e=>{if(!this.server.hooks.oidclogin)return;const o=e.oidc?.idTokenClaims;if(o)try{await walkSubscribers(this.server.hooks.oidclogin,o)}catch(o){const i=e.appSession;if(i)for(const e of Object.keys(i))delete i[e];throw o}},isLoggedOut:async e=>{if(!this.server.hooks.oidcsessionvalid)return!1;const o=e.oidc.idTokenClaims,[,t]=await i(this.server.hooks.oidcsessionvalid,[o,!0]);return!t}}}}identitySource(){const e=this.config.oidc_username_claim||"sub";return`req.user: the OIDC sub, an opaque provider identifier; req.userName: the "${e}" claim`+("sub"===e?" — the same value, until --oidc-username-claim names another":"")}get pathPrefixes(){const e=super.pathPrefixes;return 0===e.length?e:[...e,...n]}api(e){this.router=s(this.buildConfig()),super.api(e)}authMethod(e,o,i){if(!this.router)return t(o,new Error(`${this.name}: api() has not built the router`));this.router(e,o,s=>{if(s)return t(o,s);r()(e,o,()=>{const o=e.oidc.user,t=this.config.oidc_username_claim||"sub",s=o[t];"sub"!==t&&"string"!=typeof s&&this.logger.warn(`${this.name}: no "${t}" claim on this session, so req.userName falls back to the sub. Check --oidc-username-claim against the scopes the provider is asked for`),this.publishIdentity(e,String(o.sub),"string"==typeof s?s:String(o.sub)),i()})})}}export{OpenIDConnect as default};
@@ -1 +1 @@
1
- import{unauthorized as t}from"../../lib/expressFormatedResponses.js";import e from"../../lib/auth/base.js";class AuthToken extends e{constructor(...t){super(...t),this.name="authToken",this.roles=["auth"],this.tokenMap=new Map;const e=this.config.auth_token;e&&Array.isArray(e)&&e.forEach((t,e)=>{if(t.includes(":")){const[e,s]=t.split(":",2);this.tokenMap.set(e.trim(),s.trim())}else this.tokenMap.set(t.trim(),`token ${e}`)})}authMethod(e,s,r){let o=e.headers.authorization;if(!o||!/^Bearer .+/.test(o))return this.logger.warn("Missing or invalid Authorization header"),t(s);o=o.split(" ")[1];const i=this.tokenMap.get(o);if(!i){const e=o.length>8?`${o.substring(0,8)}...`:"***";return this.logger.warn(`Unauthorized token: ${e}`),t(s)}e.user=i,r()}}export{AuthToken as default};
1
+ import{unauthorized as t}from"../../lib/expressFormatedResponses.js";import e from"../../lib/auth/base.js";class AuthToken extends e{knownIdentities(){return[...new Set(this.tokenMap.values())]}identitySource(){return"req.user and req.userName: the name given in --auth-token"}constructor(...t){super(...t),this.name="authToken",this.roles=["auth"],this.tokenMap=new Map;const e=this.config.auth_token;e&&Array.isArray(e)&&e.forEach((t,e)=>{if(t.includes(":")){const[e,s]=t.split(":",2);this.tokenMap.set(e.trim(),s.trim())}else this.tokenMap.set(t.trim(),`token ${e}`)})}authMethod(e,s,n){let r=e.headers.authorization;if(!r||!/^Bearer .+/.test(r))return this.logger.warn("Missing or invalid Authorization header"),t(s);r=r.split(" ")[1];const i=this.tokenMap.get(r);if(!i){const e=r.length>8?`${r.substring(0,8)}...`:"***";return this.logger.warn(`Unauthorized token: ${e}`),t(s)}this.publishIdentity(e,i),n()}}export{AuthToken as default};