ldap-rest 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +38 -0
  2. package/bin/sync-app-accounts.mjs +276 -0
  3. package/dist/abstract/ldapFlat.js +1 -1
  4. package/dist/abstract/plugin.js +1 -1
  5. package/dist/bin/index.js +1 -1
  6. package/dist/config/args.js +1 -1
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/lib/utils.js +1 -1
  9. package/dist/logger/winston.js +1 -1
  10. package/dist/plugins/auth/hmac.js +1 -0
  11. package/dist/plugins/auth/totp.js +1 -0
  12. package/dist/plugins/demo/helloworld.js +1 -1
  13. package/dist/plugins/ldap/groups.js +1 -1
  14. package/dist/plugins/ldap/onChange.js +1 -1
  15. package/dist/plugins/static.js +1 -1
  16. package/dist/plugins/twake/appAccountsApi.js +1 -0
  17. package/dist/plugins/twake/appAccountsConsistency.js +1 -0
  18. package/dist/plugins/twake/james.js +1 -1
  19. package/dist/plugins/weblogs.js +1 -1
  20. package/dist/src/abstract/ldapFlat.d.ts +0 -5
  21. package/dist/src/abstract/plugin.d.ts +1 -0
  22. package/dist/src/bin/index.d.ts +1 -0
  23. package/dist/src/browser/shared/index.d.ts +1 -0
  24. package/dist/src/browser/shared/utils/hmac.d.ts +73 -0
  25. package/dist/src/browser/shared/utils/totp.d.ts +64 -0
  26. package/dist/src/config/args.d.ts +9 -1
  27. package/dist/src/hooks.d.ts +2 -2
  28. package/dist/src/lib/ldapActions.d.ts +3 -7
  29. package/dist/src/lib/utils.d.ts +17 -0
  30. package/dist/src/logger/winston.d.ts +5 -0
  31. package/dist/src/plugins/auth/hmac.d.ts +24 -0
  32. package/dist/src/plugins/auth/totp.d.ts +28 -0
  33. package/dist/src/plugins/ldap/onChange.d.ts +1 -0
  34. package/dist/src/plugins/static.d.ts +4 -0
  35. package/dist/src/plugins/twake/appAccountsApi.d.ts +52 -0
  36. package/dist/src/plugins/twake/appAccountsConsistency.d.ts +58 -0
  37. package/docs/DEVELOPER_GUIDE.md +1 -0
  38. package/docs/PLUGIN_DEPENDENCIES.md +19 -0
  39. package/docs/README.md +76 -30
  40. package/docs/authentication.md +604 -12
  41. package/docs/plugins/DEVELOPMENT.md +162 -76
  42. package/docs/testing-with-embedded-ldap.md +347 -0
  43. package/docs/twakeAppAccountsApi.md +485 -0
  44. package/docs/weblogs.md +37 -8
  45. package/examples/web/README.md +175 -0
  46. package/examples/web/ldap-tree-viewer.html +16 -2
  47. package/examples/web/totp-client.html +541 -0
  48. package/package.json +132 -87
  49. package/static/browser/shared/index.js +1 -0
  50. package/static/browser/shared/index.js.map +1 -1
  51. package/static/browser/shared/utils/hmac.js +202 -0
  52. package/static/browser/shared/utils/hmac.js.map +1 -0
  53. package/static/browser/shared/utils/totp.js +182 -0
  54. package/static/browser/shared/utils/totp.js.map +1 -0
  55. package/static/browser/shared.esm.js +181 -1
  56. package/static/browser/shared.esm.js.map +1 -1
  57. package/static/browser/shared.js +184 -0
  58. package/static/browser/shared.js.map +1 -1
  59. package/static/browser/shared.min.js +1 -1
  60. package/static/browser/shared.min.js.map +1 -1
  61. package/static/schemas/obm/organizations.json +24 -0
  62. package/static/schemas/obm/users.json +192 -0
@@ -1 +1 @@
1
- import t from"../../abstract/plugin.js";import{launchHooks as e}from"../../lib/utils.js";const r={mail_attribute:"onLdapMailChange",quota_attribute:"onLdapQuotaChange",alias_attribute:"onLdapAliasChange",forward_attribute:"onLdapForwardChange",display_name_attribute:"onLdapDisplayNameChange"};class OnLdapChange extends t{constructor(){super(...arguments),this.name="onLdapChange",this.roles=["consistency"],this.stack={},this.hooks={ldapmodifyrequest:async([t,e,r])=>{const n=await this.server.ldap.search({paged:!1},t);return 1==n.searchEntries.length?this.stack[r]=n.searchEntries[0]:this.logger.warn(`Could not find unique entry ${t} before modification, got ${n.searchEntries.length} entries`),[t,e,r]},ldapmodifydone:([t,e,r])=>{const n=this.stack[r];if(!n)return delete this.stack[r],void this.logger.warn(`Received a ldapmodifydone for an unknown operation (${r})`);const a={};if(e.add)for(const[t,r]of Object.entries(e.add))a[t]=[null,r];if(e.delete)if(Array.isArray(e.delete))for(const t of e.delete)a[t]=[n[t],null];else for(const[t,r]of Object.entries(e.delete))a[t]=[r,null];if(e.replace)for(const[t,r]of Object.entries(e.replace))a[t]=[n[t],r];this.notify(t,a)}}}notify(t,n){e(this.server.hooks.onLdapChange,t,n);for(const[e,a]of Object.entries(r))this.config[e]&&n[this.config[e]]&&("onLdapQuotaChange"===a||"onLdapForwardChange"===a||"onLdapAliasChange"===a?this.notifyAttributeChangeWithMail(this.config[e],a,t,n):"onLdapMailChange"===a&&this.notifyAttributeChange(this.config[e],a,t,n));if(n.cn||n.givenName||n.sn){const r=this.reconstructDisplayName(n,0),a=this.reconstructDisplayName(n,1);e(this.server.hooks.onLdapDisplayNameChange,t,r,a)}}notifyAttributeChange(t,r,n,a,i=!1){const[s,o]=a[t]||[];void 0===s&&void 0===o||(i&&(Array.isArray(s)||Array.isArray(o))?this.logger.error(`Attribute ${t} change detected but one of the values is an array, cannot handle that`):s!==o&&e(this.server.hooks[r],n,s,o))}async notifyAttributeChangeWithMail(t,r,n,a){const[i,s]=a[t]||[];if(void 0===i&&void 0===s)return;const o=this.config.mail_attribute||"mail",h=a[o];let l;if(h)l=Array.isArray(h[1])?String(h[1][0]):String(h[1]);else try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[o]},n);if(1!==t.searchEntries.length)return void this.logger.warn(`Could not find mail for ${n}, skipping ${r} notification`);{const e=t.searchEntries[0][o];l=Array.isArray(e)?String(e[0]):String(e)}}catch(t){return void this.logger.error(`Error fetching mail for ${n}:`,t)}if("onLdapQuotaChange"===r){const t=i?Number(i):0,a=s?Number(s):0;t!==a&&e(this.server.hooks[r],n,l,t,a)}else if("onLdapForwardChange"===r||"onLdapAliasChange"===r){const t=i?Array.isArray(i)?i:[i]:[],a=s?Array.isArray(s)?s:[s]:[];(t.length>0||a.length>0)&&e(this.server.hooks[r],n,l,t,a)}}reconstructDisplayName(t,e){const getValue=r=>{if(!t[r])return null;const n=t[r][e];return n?Array.isArray(n)?n.length>0?String(n[0]):null:String(n):null},r=getValue("cn");if(r)return r;const n=getValue("givenName"),a=getValue("sn");if(n||a){const t=[];return n&&t.push(n),a&&t.push(a),t.join(" ")}return null}}export{OnLdapChange as default};
1
+ import t from"../../abstract/plugin.js";import{launchHooks as e}from"../../lib/utils.js";const n={mail_attribute:"onLdapMailChange",quota_attribute:"onLdapQuotaChange",alias_attribute:"onLdapAliasChange",forward_attribute:"onLdapForwardChange",display_name_attribute:"onLdapDisplayNameChange"};class OnLdapChange extends t{constructor(){super(...arguments),this.name="onLdapChange",this.roles=["consistency"],this.stack={},this.pendingDeletions=new Map,this.hooks={ldapadddone:t=>{const[e,n]=t,r={};for(const[t,e]of Object.entries(n))r[t]=[null,e];this.notify(e,r)},ldapmodifyrequest:async([t,e,n])=>{const r=await this.server.ldap.search({paged:!1},t);return 1==r.searchEntries.length?this.stack[n]=r.searchEntries[0]:this.logger.warn(`Could not find unique entry ${t} before modification, got ${r.searchEntries.length} entries`),[t,e,n]},ldapmodifydone:([t,e,n])=>{const r=this.stack[n];if(!r)return delete this.stack[n],void this.logger.warn(`Received a ldapmodifydone for an unknown operation (${n})`);const i={};if(e.add)for(const[t,n]of Object.entries(e.add))i[t]=[null,n];if(e.delete)if(Array.isArray(e.delete))for(const t of e.delete)i[t]=[r[t],null];else for(const[t,n]of Object.entries(e.delete))i[t]=[n,null];if(e.replace)for(const[t,n]of Object.entries(e.replace))i[t]=[r[t],n];this.notify(t,i)},ldapdeleterequest:async t=>{const e=Array.isArray(t)?t:[t];for(const t of e)try{const e=await this.server.ldap.search({scope:"base",paged:!1},t),n=e.searchEntries?.[0];n&&this.pendingDeletions.set(t,n)}catch(t){}return t},ldapdeletedone:t=>{const e=Array.isArray(t)?t:[t];for(const t of e){const e=this.pendingDeletions.get(t);if(e){this.pendingDeletions.delete(t);const n={};for(const[t,r]of Object.entries(e))"dn"!==t&&(n[t]=[r,null]);this.notify(t,n)}}}}}notify(t,r){e(this.server.hooks.onLdapChange,t,r);for(const[e,i]of Object.entries(n))this.config[e]&&r[this.config[e]]&&("onLdapQuotaChange"===i||"onLdapForwardChange"===i||"onLdapAliasChange"===i?this.notifyAttributeChangeWithMail(this.config[e],i,t,r):"onLdapMailChange"===i&&this.notifyAttributeChange(this.config[e],i,t,r));if(r.cn||r.givenName||r.sn){const n=this.reconstructDisplayName(r,0),i=this.reconstructDisplayName(r,1);e(this.server.hooks.onLdapDisplayNameChange,t,n,i)}}notifyAttributeChange(t,n,r,i,s=!1){const[a,o]=i[t]||[];void 0===a&&void 0===o||(s&&(Array.isArray(a)||Array.isArray(o))?this.logger.error(`Attribute ${t} change detected but one of the values is an array, cannot handle that`):a!==o&&e(this.server.hooks[n],r,a,o))}async notifyAttributeChangeWithMail(t,n,r,i){const[s,a]=i[t]||[];if(void 0===s&&void 0===a)return;const o=this.config.mail_attribute||"mail",h=i[o];let c;if(h)c=Array.isArray(h[1])?String(h[1][0]):String(h[1]);else try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[o]},r);if(1!==t.searchEntries.length)return void this.logger.warn(`Could not find mail for ${r}, skipping ${n} notification`);{const e=t.searchEntries[0][o];c=Array.isArray(e)?String(e[0]):String(e)}}catch(t){return void this.logger.error(`Error fetching mail for ${r}:`,t)}if("onLdapQuotaChange"===n){const t=s?Number(s):0,i=a?Number(a):0;t!==i&&e(this.server.hooks[n],r,c,t,i)}else if("onLdapForwardChange"===n||"onLdapAliasChange"===n){const t=s?Array.isArray(s)?s:[s]:[],i=a?Array.isArray(a)?a:[a]:[];(t.length>0||i.length>0)&&e(this.server.hooks[n],r,c,t,i)}}reconstructDisplayName(t,e){const getValue=n=>{if(!t[n])return null;const r=t[n][e];return r?Array.isArray(r)?r.length>0?String(r[0]):null:String(r):null},n=getValue("cn");if(n)return n;const r=getValue("givenName"),i=getValue("sn");if(r||i){const t=[];return r&&t.push(r),i&&t.push(i),t.join(" ")}return null}}export{OnLdapChange as default};
@@ -1 +1 @@
1
- import s from"fs";import{resolve as t,join as e}from"path";import a from"express";import r from"../abstract/plugin.js";import{notFound as i}from"../lib/expressFormatedResponses.js";import{transformSchemas as n}from"../lib/utils.js";class Static extends r{constructor(){super(...arguments),this.name="static",this.roles=["api"]}api(r){const c=this.config.static_path;if(!c)throw new Error("--static-path is not defined");try{if(!s.statSync(c).isDirectory())throw new Error(`${c} isn't a directory`);s.accessSync(c,s.constants.R_OK)}catch(s){throw new Error(`Bad directory ${c}: ${s}`)}r.get(`/${this.config.static_name}/schemas/:name`,(a,r)=>{if(!/^[\w-]+\.json$/.test(a.params.name))return r.status(400).send("Invalid schema name");const o=t(e(c,"schemas",a.params.name)),m=t(e(c,"schemas"));if(!o.startsWith(m+"/"))return r.status(403).send("Access denied");s.readFile(o,(s,t)=>{if(s)return i(r,"Schema not found");const e=n(t,this.config);r.type("json").send(e)})}),r.get(`/${this.config.static_name}/schemas/:dir/:name`,(a,r)=>{if(!/^[\w-]+$/.test(a.params.dir)||!/^[\w-]+\.json$/.test(a.params.name))return r.status(400).send("Invalid schema name");const o=t(e(c,"schemas",a.params.dir,a.params.name)),m=t(e(c,"schemas"));if(!o.startsWith(m+"/"))return r.status(403).send("Access denied");s.readFile(o,(s,t)=>{if(s)return i(r,"Schema not found");const e=n(t,this.config);r.type("json").send(e)})}),r.use(`/${this.config.static_name}`,a.static(c))}}export{Static as default};
1
+ import s from"fs";import{resolve as t,join as e}from"path";import a from"express";import i from"../abstract/plugin.js";import{notFound as n}from"../lib/expressFormatedResponses.js";import{transformSchemas as r}from"../lib/utils.js";class Static extends i{constructor(){super(...arguments),this.name="static",this.roles=["api","configurable"]}api(i){const c=this.config.static_path;if(!c)throw new Error("--static-path is not defined");try{if(!s.statSync(c).isDirectory())throw new Error(`${c} isn't a directory`);s.accessSync(c,s.constants.R_OK)}catch(s){throw new Error(`Bad directory ${c}: ${s}`)}i.get(`/${this.config.static_name}/schemas/:name`,(a,i)=>{if(!/^[\w-]+\.json$/.test(a.params.name))return i.status(400).send("Invalid schema name");const o=t(e(c,"schemas",a.params.name)),m=t(e(c,"schemas"));if(!o.startsWith(m+"/"))return i.status(403).send("Access denied");s.readFile(o,(s,t)=>{if(s)return n(i,"Schema not found");const e=r(t,this.config);i.type("json").send(e)})}),i.get(`/${this.config.static_name}/schemas/:dir/:name`,(a,i)=>{if(!/^[\w-]+$/.test(a.params.dir)||!/^[\w-]+\.json$/.test(a.params.name))return i.status(400).send("Invalid schema name");const o=t(e(c,"schemas",a.params.dir,a.params.name)),m=t(e(c,"schemas"));if(!o.startsWith(m+"/"))return i.status(403).send("Access denied");s.readFile(o,(s,t)=>{if(s)return n(i,"Schema not found");const e=r(t,this.config);i.type("json").send(e)})}),i.use(`/${this.config.static_name}`,a.static(c))}getConfigApiData(){const s=`/${this.config.static_name||"static"}`;return{enabled:!0,staticPath:s,endpoints:{schema:`${s}/schemas/:name`,schemaInSubdir:`${s}/schemas/:dir/:name`,files:`${s}/*`}}}}export{Static as default};
@@ -0,0 +1 @@
1
+ import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";class AppAccountsApi extends r{constructor(t){if(super(t),this.name="appAccountsApi",this.roles=["api","configurable"],this.dependencies={authToken:"core/auth/token",appAccountsConsistency:"core/twake/appAccountsConsistency"},this.applicativeAccountBase=this.config.applicative_account_base,this.maxAppAccounts=this.config.max_app_accounts||5,this.mailAttr=this.config.mail_attribute||"mail",!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}, max_app_accounts=${this.maxAppAccounts}`)}api(t){const r=this.config.api_prefix||"/api";t.get(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.listAccounts(t,r)),t.post(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.createAccount(t,r)),t.delete(`${r}/v1/users/:user/app-accounts/:uid`,(t,r)=>this.deleteAccount(t,r)),this.logger.info(`AppAccounts API registered at ${r}/v1/users/:user/app-accounts`)}async listAccounts(t,r){if(!s(t,r))return;const e=t.params.user;try{const t=await this.server.ldap.search({scope:"sub",filter:`(uid=${e})`,paged:!1,attributes:[this.mailAttr]},this.config.ldap_base||""),s=t.searchEntries?.[0];if(!s)return void r.status(404).json({error:`User ${e} not found`});const a=s[this.mailAttr];if(!a)return void r.status(400).json({error:`User ${e} has no mail attribute`});const i=Array.isArray(a)?String(a[0]):String(a),n=await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${i})`,paged:!1},this.applicativeAccountBase),o=(n.searchEntries||[]).filter(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r);return s!==i&&s.startsWith(`${e}_`)}).map(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r),e=t.description,a=e?Array.isArray(e)?String(e[0]):String(e):void 0;return{uid:s,...a?{name:a}:{}}}).sort((t,r)=>t.uid.localeCompare(r.uid));r.json(o)}catch(t){this.logger.error(`${this.name}: Failed to list accounts for ${e}:`,t),r.status(500).json({error:"Internal server error"})}}async createAccount(t,r){if(!s(t,r))return;const e=t.params.user,{name:a}=t.body||{};try{const t=await this.server.ldap.search({scope:"sub",filter:`(uid=${e})`,paged:!1},this.config.ldap_base||""),s=t.searchEntries?.[0];if(!s)return void r.status(404).json({error:`User ${e} not found`});const i=s[this.mailAttr];if(!i)return void r.status(400).json({error:`User ${e} has no mail attribute`});const n=Array.isArray(i)?String(i[0]):String(i),o=await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${n})`,paged:!1},this.applicativeAccountBase),c=(o.searchEntries||[]).filter(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r);return s!==n&&s.startsWith(`${e}_`)});if(c.length>=this.maxAppAccounts)return void r.status(400).json({error:`Maximum number of accounts (${this.maxAppAccounts}) reached`});let u,p=0;do{if(u=this.generateAccountId(),p++,p>100)throw new Error("Failed to generate unique account ID after 100 attempts")}while(c.some(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r))===`${e}_${u}`}));const d=`${e}_${u}`,l=this.generatePassword(),h={objectClass:["inetOrgPerson"],uid:d,userPassword:l},A=["cn","sn","givenName",this.mailAttr,"displayName"];for(const t of A)if(s[t]){const r=s[t];Array.isArray(r)?h[t]=r.map(t=>Buffer.isBuffer(t)?t.toString():String(t)):h[t]=Buffer.isBuffer(r)?r.toString():String(r)}a&&(h.description=a);const g=`uid=${d},${this.applicativeAccountBase}`;await this.server.ldap.add(g,h),this.logger.info(`${this.name}: Created applicative account ${g} for user ${e}`);const f=`uid=${n},${this.applicativeAccountBase}`;try{await this.server.ldap.modify(f,{add:{userPassword:l}})}catch(t){this.logger.warn(`${this.name}: Failed to add password to principal account ${f}:`,t)}r.json({uid:d,pwd:l,mail:n})}catch(t){this.logger.error(`${this.name}: Failed to create account for ${e}:`,t),r.status(500).json({error:"Internal server error"})}}async deleteAccount(t,r){if(!s(t,r))return;const e=t.params.user,a=t.params.uid;try{if(!a.startsWith(`${e}_`))return void r.status(403).json({error:`Account ${a} does not belong to user ${e}`});const t=await this.server.ldap.search({scope:"sub",filter:`(uid=${a})`,paged:!1},this.applicativeAccountBase),s=t.searchEntries?.[0];if(!s)return this.logger.warn(`${this.name}: Account ${a} already deleted or not found`),void r.json({uid:a});const i=s.userPassword;i||this.logger.warn(`${this.name}: Account ${a} has no userPassword attribute`);const n=s[this.mailAttr],o=n?Array.isArray(n)?String(n[0]):String(n):null;if(o&&i){const t=`uid=${o},${this.applicativeAccountBase}`;try{const r=Array.isArray(i)?i[0]:i;await this.server.ldap.modify(t,{delete:{userPassword:r}})}catch(r){this.logger.warn(`${this.name}: Failed to delete password from principal account ${t}:`,r)}}const c=s.dn;await this.server.ldap.delete(c),this.logger.info(`${this.name}: Deleted applicative account ${c}`),r.json({uid:a})}catch(t){this.logger.error(`${this.name}: Failed to delete account ${a} for ${e}:`,t),r.status(500).json({error:"Internal server error"})}}generateAccountId(){return`c${t(0,1e8).toString().padStart(8,"0")}`}generatePassword(){const r="ABCDEFGHJKMNPQRSTUVWXYZ",s="abcdefghjkmnpqrstuvwxyz",e="23456789",a="@!#$%",i=[];for(let n=0;n<6;n++){const n=t(0,23),o=t(0,23),c=t(0,8),u=t(0,5),p=[r[n],s[o],e[c],a[u]];for(let r=p.length-1;r>0;r--){const s=t(0,r+1);[p[r],p[s]]=[p[s],p[r]]}i.push(p.join(""))}return i.join("-")}getConfigApiData(){const t=this.config.api_prefix||"/api";return{enabled:!0,base:this.applicativeAccountBase,maxAccounts:this.maxAppAccounts,mailAttribute:this.mailAttr,endpoints:{list:`${t}/v1/users/:user/app-accounts`,create:`${t}/v1/users/:user/app-accounts`,delete:`${t}/v1/users/:user/app-accounts/:uid`}}}}export{AppAccountsApi as default};
@@ -0,0 +1 @@
1
+ import t from"../../abstract/plugin.js";class AppAccountsConsistency extends t{constructor(t){if(super(t),this.name="appAccountsConsistency",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={onLdapMailChange:async(t,e,i)=>{try{const a=null!=e?Array.isArray(e)?String(e[0]):String(e):null,c=null!=i?Array.isArray(i)?String(i[0]):String(i):null;if(!a&&c)return void await this.createApplicativeAccount(t,c);if(a&&!c)return void await this.deleteApplicativeAccount(a);if(a&&c&&a!==c)return void await this.updateApplicativeAccount(t,a,c)}catch(e){this.logger.error(`${this.name}: Failed to handle mail change for ${t}:`,e)}}},this.mailAttr=this.config.mail_attribute||"mail",this.applicativeAccountBase=this.config.applicative_account_base,this.operationalAttributes=this.config.ldap_operational_attributes||[],!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}`)}shouldExcludeAttribute(t){return this.operationalAttributes.includes(t)}async createApplicativeAccount(t,e){const i=`uid=${e},${this.applicativeAccountBase}`;try{const a=await this.server.ldap.search({scope:"base",paged:!1},t),c=a.searchEntries?.[0];if(!c)return void this.logger.warn(`${this.name}: Could not find user ${t} to create applicative account`);const s={};for(const[t,e]of Object.entries(c))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(s[t]=e));s.uid=e,await this.server.ldap.add(i,s),this.logger.info(`${this.name}: Created applicative account ${i} for user ${t}`)}catch(e){if(68===e.code||e.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${i} already exists`);this.logger.error(`${this.name}: Failed to create applicative account for ${t}:`,e)}}async deleteApplicativeAccount(t){try{const e=`(${this.mailAttr}=${t})`,i=(await this.server.ldap.search({filter:e,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===i.length)return void this.logger.debug(`${this.name}: No applicative accounts found for mail ${t}`);for(const t of i){const e=t.dn;try{await this.server.ldap.delete(e),this.logger.info(`${this.name}: Deleted applicative account ${e}`)}catch(t){32===t.code||t.message?.includes("NoSuchObject")?this.logger.debug(`${this.name}: Applicative account ${e} already deleted`):this.logger.error(`${this.name}: Failed to delete applicative account ${e}:`,t)}}}catch(e){if(32===e.code||e.message?.includes("NoSuchObject"))return void this.logger.debug(`${this.name}: Applicative account base does not exist or no accounts found for mail ${t}`);this.logger.error(`${this.name}: Failed to delete applicative account for mail ${t}:`,e)}}async updateApplicativeAccount(t,e,i){try{const a=`(${this.mailAttr}=${e})`,c=(await this.server.ldap.search({filter:a,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===c.length)return void this.logger.debug(`${this.name}: No applicative account found for old mail ${e}`);for(const a of c){const c=a.dn,s=Array.isArray(a.uid)?String(a.uid[0]):String(a.uid),r=s===e;let o;o=r?`uid=${i},${this.applicativeAccountBase}`:`uid=${s},${this.applicativeAccountBase}`;try{const n={};for(const[t,e]of Object.entries(a))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(n[t]=e));try{await this.server.ldap.delete(c),this.logger.info(`${this.name}: Deleted old applicative account ${c}`)}catch(t){if(32!==t.code&&!t.message?.includes("NoSuchObject"))throw t;this.logger.debug(`${this.name}: Applicative account ${c} already deleted`)}const l={...n},u=await this.server.ldap.search({scope:"base",paged:!1},t),d=u.searchEntries?.[0];if(!d)return void this.logger.warn(`${this.name}: Could not find user ${t} to update applicative account`);for(const[t,e]of Object.entries(d))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(l[t]=e));l.uid=r?i:s,await this.server.ldap.add(o,l),this.logger.info(`${this.name}: Created new applicative account ${o} (mail changed from ${e} to ${i})`)}catch(t){if(68===t.code||t.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${o} already exists after mail change`);this.logger.error(`${this.name}: Failed to update applicative account for mail change:`,t)}}}catch(e){this.logger.error(`${this.name}: Failed to update applicative account for ${t}:`,e)}}}export{AppAccountsConsistency as default};
@@ -1 +1 @@
1
- import i from"node-fetch";import{TwakePlugin as t}from"../../abstract/twakePlugin.js";class James extends t{constructor(i){super(i,"james_webadmin_url","james_webadmin_token","james_concurrency"),this.name="james",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange",ldapGroups:"core/ldap/groups"},this.hooks={ldapadddone:async i=>{const[t,e]=i,a=e[this.mailAttr],s=e[this.quotaAttr],r=e[this.aliasAttr];if(!a)return;const n=Array.isArray(a)?String(a[0]):String(a);if(this.initDelay>0&&await new Promise(i=>setTimeout(i,this.initDelay)),s){const i=Array.isArray(s)?Number(s[0]):Number(s);!isNaN(i)&&i>0&&await this.callWebAdminApi("ldapadddone:quota",`${this.webadminUrl}/quota/users/${n}/size`,"PUT",t,i.toString(),{mail:n,quota:i})}if(r){const i=this.getAliases(r);await Promise.all(i.map(i=>this.callWebAdminApi("ldapadddone:alias",`${this.webadminUrl}/address/aliases/${n}/sources/${i}`,"PUT",t,null,{mail:n,alias:i})))}const l=this.getDisplayNameFromAttributes(e);l&&await this.updateJamesIdentity(t,n,l)},onLdapMailChange:async(i,t,e)=>{if(t&&"undefined"!==t){await this.callWebAdminApi("onLdapMailChange",`${this.webadminUrl}/users/${t}/rename/${e}?action=rename`,"POST",i,null,{oldmail:t,newmail:e});try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.aliasAttr]},i);if(a.searchEntries&&a.searchEntries.length>0){const s=this.getAliases(a.searchEntries[0][this.aliasAttr]);s.length>0&&await Promise.all([...s.map(e=>this.callWebAdminApi("onLdapMailChange-delete",`${this.webadminUrl}/address/aliases/${t}/sources/${e}`,"DELETE",i,null,{oldmail:t,alias:e})),...s.map(t=>this.callWebAdminApi("onLdapMailChange-create",`${this.webadminUrl}/address/aliases/${e}/sources/${t}`,"PUT",i,null,{newmail:e,alias:t}))])}}catch(i){this.logger.debug("Could not fetch aliases for mail change:",i)}}else this.logger.debug(`Skipping mail rename for ${i}: oldmail is empty (mail attribute was added, not changed)`)},onLdapAliasChange:async(i,t,e,a)=>{const s=e.map(i=>this.normalizeAlias(i)),r=a.map(i=>this.normalizeAlias(i)),n=s.filter(i=>!r.includes(i)),l=r.filter(i=>!s.includes(i));await Promise.all([...n.map(e=>this.callWebAdminApi("onLdapAliasChange-delete",`${this.webadminUrl}/address/aliases/${t}/sources/${e}`,"DELETE",i,null,{mail:t,alias:e,action:"delete"})),...l.map(e=>this.callWebAdminApi("onLdapAliasChange-add",`${this.webadminUrl}/address/aliases/${t}/sources/${e}`,"PUT",i,null,{mail:t,alias:e,action:"add"}))])},onLdapQuotaChange:(i,t,e,a)=>this.callWebAdminApi("onLdapQuotaChange",`${this.webadminUrl}/quota/users/${t}/size`,"PUT",i,a.toString(),{oldQuota:e,newQuota:a}),onLdapForwardChange:async(i,t,e,a)=>{const s=this.extractMailDomain(t);if(!s)return void this.logger.error(`Cannot extract domain from mail ${t} for forward management`);const r=e.filter(i=>!a.includes(i)),n=a.filter(i=>!e.includes(i));await Promise.all([...r.map(e=>this.callWebAdminApi("onLdapForwardChange-delete",`${this.webadminUrl}/domains/${s}/forwards/${t}/${e}`,"DELETE",i,null,{mail:t,forward:e,domain:s,action:"delete"})),...n.map(e=>this.callWebAdminApi("onLdapForwardChange-add",`${this.webadminUrl}/domains/${s}/forwards/${t}/${e}`,"PUT",i,null,{mail:t,forward:e,domain:s,action:"add"}))])},onLdapChange:async(i,t)=>{this.config.delegation_attribute&&t[this.config.delegation_attribute]&&await this._handleDelegationChange(i,t)},onLdapDisplayNameChange:async i=>{const t=[this.mailAttr,this.displayNameAttr,"cn","givenName","sn"],e=await this.ldapGetAttributes(i,t);if(!e)return void this.logger.warn(`Cannot update James identity: entry not found for ${i}`);const a=this.attributeToString(e[this.mailAttr]);if(!a)return void this.logger.warn(`Cannot update James identity: no mail found for ${i}`);const s=this.getDisplayNameFromAttributes(e);if(s)return this.updateJamesIdentity(i,a,s);this.logger.warn(`Cannot update James identity: no display name found for ${i}`)},ldapgroupadddone:async i=>{const[t,e]=i,a=e.mail;if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);const s=this.getMailboxType(e),r=Array.isArray(a)?String(a[0]):String(a);if("teamMailbox"===s){const i=this.config.james_mailing_list_branch||[];if(i.length>0&&this.isInAllowedBranches(t,i))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:t,mailboxType:"teamMailbox",error:`Team mailbox cannot be in mailing list branches: ${i.join(", ")}`});await this.createTeamMailbox(t,e,r)}else if("mailingList"===s||null===s){const i=this.config.james_mailing_list_branch||[];if(i.length>0&&!this.isInAllowedBranches(t,i))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:t,mailboxType:s||"mailingList (default)",error:`Mailing list must be in allowed branches: ${i.join(", ")}`});await this.createMailingList(t,e,r)}},ldapgroupmodifydone:async i=>{const[t,e]=i;if(e.replace?.[this.mailboxTypeAttr]||e.add?.[this.mailboxTypeAttr])return void await this.handleMailboxTypeTransition(t,e);const a=await this.getGroupMail(t);if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);const s="teamMailbox"===await this.getGroupMailboxType(t),r=s?this.extractMailDomain(a):null,n=[];if(e.add?.member){const i=e.add.member,l=Array.isArray(i)?i:[i],o=await this.getMemberEmails(l);n.push(...o.map(i=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${i}`,"PUT",t,null,{domain:r,groupMail:a,memberMail:i,action:"add"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${i}`,"PUT",t,null,{groupMail:a,memberMail:i,action:"add"})))}if(this.isAttributesList(e.delete)){const i=e.delete.member;if(i){const e=Array.isArray(i)?i:[i],l=await this.getMemberEmails(e);n.push(...l.map(i=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${i}`,"DELETE",t,null,{domain:r,groupMail:a,memberMail:i,action:"delete"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${i}`,"DELETE",t,null,{groupMail:a,memberMail:i,action:"delete"})))}}await Promise.all(n)},ldapgroupdeletedone:async i=>{const t=await this.getGroupMail(i);if(!t)return void this.logger.debug(`Group ${i} has no mail attribute, skipping James sync`);"teamMailbox"===await this.getGroupMailboxType(i)?await this.removeAllTeamMailboxMembers(i,t):await this.callWebAdminApi("ldapgroupdeletedone:mailingList",`${this.webadminUrl}/address/groups/${t}`,"DELETE",i,null,{groupMail:t})}},this.quotaAttr=this.config.quota_attribute||"mailQuotaSize",this.aliasAttr=this.config.alias_attribute||"mailAlternateAddress",this.mailboxTypeAttr=this.config.james_mailbox_type_attribute||"twakeMailboxType",this.initDelay="number"==typeof this.config.james_init_delay?this.config.james_init_delay:1e3}async handleMailboxTypeTransition(i,t){const e=await this.ldapGetAttributes(i,["mail","member",this.mailboxTypeAttr]);if(!e)return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,error:"Could not fetch group attributes"});const a=e.mail;if(!a)return void this.logger.debug(`Group ${i} has no mail attribute, skipping mailbox type transition`);const s=Array.isArray(a)?String(a[0]):String(a),r=t.replace?.[this.mailboxTypeAttr]||t.add?.[this.mailboxTypeAttr],n=Array.isArray(r)?String(r[0]):String(r),l=this.getMailboxType({[this.mailboxTypeAttr]:n}),o=this.getMailboxType(e);if(this.logger.info({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,mail:s,oldType:o||"none",newType:l||"none"}),o!==l)if("mailingList"===o?await this.deleteMailingList(i,s):"teamMailbox"===o&&await this.removeAllTeamMailboxMembers(i,s),"mailingList"===l){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&!this.isInAllowedBranches(i,t))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,newType:l,error:`Mailing list must be in allowed branches: ${t.join(", ")}`});await this.createMailingList(i,e,s)}else if("teamMailbox"===l){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&this.isInAllowedBranches(i,t))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,newType:l,error:`Team mailbox cannot be in mailing list branches: ${t.join(", ")}`});await this.createTeamMailbox(i,e,s)}}getDisplayNameFromAttributes(i){const t=this.attributeToString(i[this.displayNameAttr]);if(t)return t;const e=this.attributeToString(i.cn);if(e)return e;const a=this.attributeToString(i.givenName),s=this.attributeToString(i.sn);if(a||s){const i=[];return a&&i.push(a),s&&i.push(s),i.join(" ")}const r=this.attributeToString(i[this.mailAttr]);return r||null}async getMailFromDN(i){const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):null}async getDisplayNameFromDN(i){const t=[this.displayNameAttr,"cn","givenName","sn",this.mailAttr],e=await this.ldapGetAttributes(i,t);return e?this.getDisplayNameFromAttributes(e):null}async generateSignature(i){const t=this.config.james_signature_template;if(!t)return null;const e=await this.ldapGetAttributes(i);if(!e)return null;let a=t;return a=a.replace(/\{(\w+)\}/g,(i,t)=>"string"==typeof t&&e&&"object"==typeof e&&Object.prototype.hasOwnProperty.call(e,t)&&this.attributeToString(e[t])||""),a}async updateJamesIdentity(t,e,a){const s={plugin:this.name,event:"onLdapDisplayNameChange",result:"error",dn:t,mail:e,displayName:a};try{const r=`${this.webadminUrl}/jmap/identities/${e}`,n=await this.requestLimit(()=>i(r,{method:"GET",headers:this.createHeaders()}));if(!n.ok)return void this.logger.error({...s,step:"get_identities",http_status:n.status,http_status_text:n.statusText});const l=await n.json(),o=l.find(i=>i.email===e)||l[0];if(!o)return void this.logger.warn({...s,step:"find_identity",message:"No identity found for user"});const m=await this.generateSignature(t),d=`${this.webadminUrl}/jmap/identities/${e}/${o.id}`,h={id:o.id,email:o.email,name:a};m&&(h.htmlSignature=m);const g=await this.requestLimit(()=>i(d,{method:"PUT",headers:this.createHeaders("application/json"),body:JSON.stringify(h)}));g.ok?this.logger.info({...s,result:"success",http_status:g.status}):this.logger.error({...s,step:"update_identity",http_status:g.status,http_status_text:g.statusText})}catch(i){this.logger.error({...s,error:`${i}`})}}isAttributesList(i){return void 0!==i&&"object"==typeof i&&!Array.isArray(i)}getMailboxType(i){const t=i[this.mailboxTypeAttr];if(!t)return null;const e=(Array.isArray(t)?String(t[0]):String(t)).match(/^cn=([^,]+)/);if(!e)return null;const a=e[1];return"group"===a||"mailingList"===a||"teamMailbox"===a?a:null}isInAllowedBranches(i,t){return!t||0===t.length||t.some(t=>i===t||i.endsWith(","+t))}async createMailingList(i,t,e){const a=t.member||[],s=Array.isArray(a)?a:[a];this.logger.debug(`Creating mailing list ${e} in James with ${s.length} members`);const r=await this.getMemberEmails(s);await Promise.all(r.map(t=>this.callWebAdminApi("ldapgroupadddone:mailingList",`${this.webadminUrl}/address/groups/${e}/${t}`,"PUT",i,null,{groupMail:e,memberMail:t})))}async createTeamMailbox(i,t,e){const a=this.extractMailDomain(e);if(!a)return void this.logger.error({plugin:this.name,event:"createTeamMailbox",dn:i,mail:e,error:"Cannot extract domain from mail address"});const s=t.member||[],r=Array.isArray(s)?s:[s];this.logger.debug(`Creating team mailbox ${e} in James with ${r.length} members`);const n=await this.getMemberEmails(r),l=[];l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:create",`${this.webadminUrl}/domains/${a}/team-mailboxes/${e}`,"PUT",i,null,{domain:a,teamMailbox:e}));for(const t of n)l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:addMember",`${this.webadminUrl}/domains/${a}/team-mailboxes/${e}/members/${t}`,"PUT",i,null,{domain:a,teamMailbox:e,memberMail:t}));await Promise.all(l)}async deleteTeamMailbox(i,t){const e=this.extractMailDomain(t);e?await this.callWebAdminApi("ldapgroupdeletedone:teamMailbox",`${this.webadminUrl}/domains/${e}/team-mailboxes/${t}`,"DELETE",i,null,{domain:e,teamMailbox:t}):this.logger.error({plugin:this.name,event:"deleteTeamMailbox",dn:i,mail:t,error:"Cannot extract domain from mail address"})}async removeAllTeamMailboxMembers(i,t){const e=this.extractMailDomain(t);if(e)try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:["member"]},i);if(a.searchEntries&&a.searchEntries.length>0){const s=a.searchEntries[0].member||[],r=Array.isArray(s)?s:[s],n=await this.getMemberEmails(r);await Promise.all(n.map(a=>this.callWebAdminApi("removeAllTeamMailboxMembers",`${this.webadminUrl}/domains/${e}/team-mailboxes/${t}/members/${a}`,"DELETE",i,null,{domain:e,teamMailbox:t,memberMail:a}))),this.logger.info({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:i,teamMailbox:t,membersRemoved:n.length,message:"All members removed from team mailbox (mailbox preserved)"})}}catch(i){const e=i instanceof Error?i.message:String(i);this.logger.error(`Failed to remove members from team mailbox ${t}: ${e}`)}else this.logger.error({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:i,mail:t,error:"Cannot extract domain from mail address"})}async deleteMailingList(i,t){await this.callWebAdminApi("deleteMailingList",`${this.webadminUrl}/address/groups/${t}`,"DELETE",i,null,{groupMail:t})}async getMemberEmails(i){const t=i.filter(i=>i!==this.config.group_dummy_user).map(i=>this.server.ldap.queryLimit(async()=>{const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):null}));return(await Promise.all(t)).filter(i=>null!==i)}async getGroupMail(i){const t=await this.ldapGetAttributes(i,["mail"]);return t?this.attributeToString(t.mail):null}async getGroupMailboxType(i){try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.mailboxTypeAttr]},i);if(t.searchEntries&&t.searchEntries.length>0)return this.getMailboxType(t.searchEntries[0])}catch(t){const e=t instanceof Error?t.message:String(t);this.logger.debug(`Could not get mailbox type for group ${i}: ${e}`)}return null}async _handleDelegationChange(i,t){const e=await this.ldapGetAttributes(i,[this.mailAttr]);if(!e)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:i,message:"Could not find user entry to get mail attribute"});const a=e[this.mailAttr];if(!a||"string"!=typeof a)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:i,message:"User has no mail attribute, cannot manage delegation"});const s=this.config.delegation_attribute;if(!s)return;const[r,n]=t[s]||[],l=this._normalizeToArray(r),o=this._normalizeToArray(n),m=o.filter(i=>!l.includes(i)),d=l.filter(i=>!o.includes(i)),h=m.map(i=>this.server.ldap.queryLimit(async()=>{const t=await this._getDelegateEmail(i);return{dn:i,email:t}})),g=d.map(i=>this.server.ldap.queryLimit(async()=>{const t=await this._getDelegateEmail(i);return{dn:i,email:t}})),[u,p]=await Promise.all([Promise.all(h),Promise.all(g)]),c=[];for(const{dn:t,email:e}of u)e&&c.push(this.callWebAdminApi("onLdapChange:addDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${e}`,"PUT",i,null,{userMail:a,delegateEmail:e,delegateDN:t,action:"add"}));for(const{dn:t,email:e}of p)e&&c.push(this.callWebAdminApi("onLdapChange:removeDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${e}`,"DELETE",i,null,{userMail:a,delegateEmail:e,delegateDN:t,action:"remove"}));await Promise.all(c)}async _getDelegateEmail(i){const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):(this.logger.warn({plugin:this.name,event:"getDelegateEmail",dn:i,message:"Could not resolve delegate DN to email"}),null)}_normalizeToArray(i){return i?Array.isArray(i)?i:[i]:[]}}export{James as default};
1
+ import i from"node-fetch";import{TwakePlugin as t}from"../../abstract/twakePlugin.js";class James extends t{constructor(i){super(i,"james_webadmin_url","james_webadmin_token","james_concurrency"),this.name="james",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange",ldapGroups:"core/ldap/groups"},this.hooks={ldapadddone:async i=>{const[t,e]=i,a=e[this.mailAttr],s=e[this.quotaAttr],r=e[this.aliasAttr];if(!a)return;const n=Array.isArray(a)?String(a[0]):String(a);if(this.initDelay>0&&await new Promise(i=>setTimeout(i,this.initDelay)),s){const i=Array.isArray(s)?Number(s[0]):Number(s);!isNaN(i)&&i>0&&await this.callWebAdminApi("ldapadddone:quota",`${this.webadminUrl}/quota/users/${n}/size`,"PUT",t,i.toString(),{mail:n,quota:i})}if(r){const i=this.getAliases(r);await Promise.all(i.map(i=>this.callWebAdminApi("ldapadddone:alias",`${this.webadminUrl}/address/aliases/${n}/sources/${i}`,"PUT",t,null,{mail:n,alias:i})))}const l=this.getDisplayNameFromAttributes(e);l&&await this.updateJamesIdentity(t,n,l)},onLdapMailChange:async(i,t,e)=>{const a=t?Array.isArray(t)?String(t[0]):String(t):null,s=e?Array.isArray(e)?String(e[0]):String(e):null;if(a){await this.callWebAdminApi("onLdapMailChange",`${this.webadminUrl}/users/${a}/rename/${s}?action=rename`,"POST",i,null,{oldmail:a,newmail:s});try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.aliasAttr]},i);if(t.searchEntries&&t.searchEntries.length>0){const e=this.getAliases(t.searchEntries[0][this.aliasAttr]);e.length>0&&await Promise.all([...e.map(t=>this.callWebAdminApi("onLdapMailChange-delete",`${this.webadminUrl}/address/aliases/${a}/sources/${t}`,"DELETE",i,null,{oldmail:a,alias:t})),...e.map(t=>this.callWebAdminApi("onLdapMailChange-create",`${this.webadminUrl}/address/aliases/${s}/sources/${t}`,"PUT",i,null,{newmail:s,alias:t}))])}}catch(i){this.logger.debug("Could not fetch aliases for mail change:",i)}}else this.logger.debug(`Skipping mail rename for ${i}: oldmail is empty (mail attribute was added, not changed)`)},onLdapAliasChange:async(i,t,e,a)=>{const s=e.map(i=>this.normalizeAlias(i)),r=a.map(i=>this.normalizeAlias(i)),n=s.filter(i=>!r.includes(i)),l=r.filter(i=>!s.includes(i));await Promise.all([...n.map(e=>this.callWebAdminApi("onLdapAliasChange-delete",`${this.webadminUrl}/address/aliases/${t}/sources/${e}`,"DELETE",i,null,{mail:t,alias:e,action:"delete"})),...l.map(e=>this.callWebAdminApi("onLdapAliasChange-add",`${this.webadminUrl}/address/aliases/${t}/sources/${e}`,"PUT",i,null,{mail:t,alias:e,action:"add"}))])},onLdapQuotaChange:(i,t,e,a)=>this.callWebAdminApi("onLdapQuotaChange",`${this.webadminUrl}/quota/users/${t}/size`,"PUT",i,a.toString(),{oldQuota:e,newQuota:a}),onLdapForwardChange:async(i,t,e,a)=>{const s=this.extractMailDomain(t);if(!s)return void this.logger.error(`Cannot extract domain from mail ${t} for forward management`);const r=e.filter(i=>!a.includes(i)),n=a.filter(i=>!e.includes(i));await Promise.all([...r.map(e=>this.callWebAdminApi("onLdapForwardChange-delete",`${this.webadminUrl}/domains/${s}/forwards/${t}/${e}`,"DELETE",i,null,{mail:t,forward:e,domain:s,action:"delete"})),...n.map(e=>this.callWebAdminApi("onLdapForwardChange-add",`${this.webadminUrl}/domains/${s}/forwards/${t}/${e}`,"PUT",i,null,{mail:t,forward:e,domain:s,action:"add"}))])},onLdapChange:async(i,t)=>{this.config.delegation_attribute&&t[this.config.delegation_attribute]&&await this._handleDelegationChange(i,t)},onLdapDisplayNameChange:async i=>{const t=[this.mailAttr,this.displayNameAttr,"cn","givenName","sn"],e=await this.ldapGetAttributes(i,t);if(!e)return void this.logger.warn(`Cannot update James identity: entry not found for ${i}`);const a=this.attributeToString(e[this.mailAttr]);if(!a)return void this.logger.warn(`Cannot update James identity: no mail found for ${i}`);const s=this.getDisplayNameFromAttributes(e);if(s)return this.updateJamesIdentity(i,a,s);this.logger.warn(`Cannot update James identity: no display name found for ${i}`)},ldapgroupadddone:async i=>{const[t,e]=i,a=e.mail;if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);const s=this.getMailboxType(e),r=Array.isArray(a)?String(a[0]):String(a);if("teamMailbox"===s){const i=this.config.james_mailing_list_branch||[];if(i.length>0&&this.isInAllowedBranches(t,i))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:t,mailboxType:"teamMailbox",error:`Team mailbox cannot be in mailing list branches: ${i.join(", ")}`});await this.createTeamMailbox(t,e,r)}else if("mailingList"===s||null===s){const i=this.config.james_mailing_list_branch||[];if(i.length>0&&!this.isInAllowedBranches(t,i))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:t,mailboxType:s||"mailingList (default)",error:`Mailing list must be in allowed branches: ${i.join(", ")}`});await this.createMailingList(t,e,r)}},ldapgroupmodifydone:async i=>{const[t,e]=i;if(e.replace?.[this.mailboxTypeAttr]||e.add?.[this.mailboxTypeAttr])return void await this.handleMailboxTypeTransition(t,e);const a=await this.getGroupMail(t);if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);const s="teamMailbox"===await this.getGroupMailboxType(t),r=s?this.extractMailDomain(a):null,n=[];if(e.add?.member){const i=e.add.member,l=Array.isArray(i)?i:[i],o=await this.getMemberEmails(l);n.push(...o.map(i=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${i}`,"PUT",t,null,{domain:r,groupMail:a,memberMail:i,action:"add"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${i}`,"PUT",t,null,{groupMail:a,memberMail:i,action:"add"})))}if(this.isAttributesList(e.delete)){const i=e.delete.member;if(i){const e=Array.isArray(i)?i:[i],l=await this.getMemberEmails(e);n.push(...l.map(i=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${i}`,"DELETE",t,null,{domain:r,groupMail:a,memberMail:i,action:"delete"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${i}`,"DELETE",t,null,{groupMail:a,memberMail:i,action:"delete"})))}}await Promise.all(n)},ldapgroupdeletedone:async i=>{const t=await this.getGroupMail(i);if(!t)return void this.logger.debug(`Group ${i} has no mail attribute, skipping James sync`);"teamMailbox"===await this.getGroupMailboxType(i)?await this.removeAllTeamMailboxMembers(i,t):await this.callWebAdminApi("ldapgroupdeletedone:mailingList",`${this.webadminUrl}/address/groups/${t}`,"DELETE",i,null,{groupMail:t})}},this.quotaAttr=this.config.quota_attribute||"mailQuotaSize",this.aliasAttr=this.config.alias_attribute||"mailAlternateAddress",this.mailboxTypeAttr=this.config.james_mailbox_type_attribute||"twakeMailboxType",this.initDelay="number"==typeof this.config.james_init_delay?this.config.james_init_delay:1e3}async handleMailboxTypeTransition(i,t){const e=await this.ldapGetAttributes(i,["mail","member",this.mailboxTypeAttr]);if(!e)return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,error:"Could not fetch group attributes"});const a=e.mail;if(!a)return void this.logger.debug(`Group ${i} has no mail attribute, skipping mailbox type transition`);const s=Array.isArray(a)?String(a[0]):String(a),r=t.replace?.[this.mailboxTypeAttr]||t.add?.[this.mailboxTypeAttr],n=Array.isArray(r)?String(r[0]):String(r),l=this.getMailboxType({[this.mailboxTypeAttr]:n}),o=this.getMailboxType(e);if(this.logger.info({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,mail:s,oldType:o||"none",newType:l||"none"}),o!==l)if("mailingList"===o?await this.deleteMailingList(i,s):"teamMailbox"===o&&await this.removeAllTeamMailboxMembers(i,s),"mailingList"===l){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&!this.isInAllowedBranches(i,t))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,newType:l,error:`Mailing list must be in allowed branches: ${t.join(", ")}`});await this.createMailingList(i,e,s)}else if("teamMailbox"===l){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&this.isInAllowedBranches(i,t))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,newType:l,error:`Team mailbox cannot be in mailing list branches: ${t.join(", ")}`});await this.createTeamMailbox(i,e,s)}}getDisplayNameFromAttributes(i){const t=this.attributeToString(i[this.displayNameAttr]);if(t)return t;const e=this.attributeToString(i.cn);if(e)return e;const a=this.attributeToString(i.givenName),s=this.attributeToString(i.sn);if(a||s){const i=[];return a&&i.push(a),s&&i.push(s),i.join(" ")}const r=this.attributeToString(i[this.mailAttr]);return r||null}async getMailFromDN(i){const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):null}async getDisplayNameFromDN(i){const t=[this.displayNameAttr,"cn","givenName","sn",this.mailAttr],e=await this.ldapGetAttributes(i,t);return e?this.getDisplayNameFromAttributes(e):null}async generateSignature(i){const t=this.config.james_signature_template;if(!t)return null;const e=await this.ldapGetAttributes(i);if(!e)return null;let a=t;return a=a.replace(/\{(\w+)\}/g,(i,t)=>"string"==typeof t&&e&&"object"==typeof e&&Object.prototype.hasOwnProperty.call(e,t)&&this.attributeToString(e[t])||""),a}async updateJamesIdentity(t,e,a){const s={plugin:this.name,event:"onLdapDisplayNameChange",result:"error",dn:t,mail:e,displayName:a};try{const r=`${this.webadminUrl}/jmap/identities/${e}`,n=await this.requestLimit(()=>i(r,{method:"GET",headers:this.createHeaders()}));if(!n.ok)return void this.logger.error({...s,step:"get_identities",http_status:n.status,http_status_text:n.statusText});const l=await n.json(),o=l.find(i=>i.email===e)||l[0];if(!o)return void this.logger.warn({...s,step:"find_identity",message:"No identity found for user"});const m=await this.generateSignature(t),d=`${this.webadminUrl}/jmap/identities/${e}/${o.id}`,h={id:o.id,email:o.email,name:a};m&&(h.htmlSignature=m);const g=await this.requestLimit(()=>i(d,{method:"PUT",headers:this.createHeaders("application/json"),body:JSON.stringify(h)}));g.ok?this.logger.info({...s,result:"success",http_status:g.status}):this.logger.error({...s,step:"update_identity",http_status:g.status,http_status_text:g.statusText})}catch(i){this.logger.error({...s,error:`${i}`})}}isAttributesList(i){return void 0!==i&&"object"==typeof i&&!Array.isArray(i)}getMailboxType(i){const t=i[this.mailboxTypeAttr];if(!t)return null;const e=(Array.isArray(t)?String(t[0]):String(t)).match(/^cn=([^,]+)/);if(!e)return null;const a=e[1];return"group"===a||"mailingList"===a||"teamMailbox"===a?a:null}isInAllowedBranches(i,t){return!t||0===t.length||t.some(t=>i===t||i.endsWith(","+t))}async createMailingList(i,t,e){const a=t.member||[],s=Array.isArray(a)?a:[a];this.logger.debug(`Creating mailing list ${e} in James with ${s.length} members`);const r=await this.getMemberEmails(s);await Promise.all(r.map(t=>this.callWebAdminApi("ldapgroupadddone:mailingList",`${this.webadminUrl}/address/groups/${e}/${t}`,"PUT",i,null,{groupMail:e,memberMail:t})))}async createTeamMailbox(i,t,e){const a=this.extractMailDomain(e);if(!a)return void this.logger.error({plugin:this.name,event:"createTeamMailbox",dn:i,mail:e,error:"Cannot extract domain from mail address"});const s=t.member||[],r=Array.isArray(s)?s:[s];this.logger.debug(`Creating team mailbox ${e} in James with ${r.length} members`);const n=await this.getMemberEmails(r),l=[];l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:create",`${this.webadminUrl}/domains/${a}/team-mailboxes/${e}`,"PUT",i,null,{domain:a,teamMailbox:e}));for(const t of n)l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:addMember",`${this.webadminUrl}/domains/${a}/team-mailboxes/${e}/members/${t}`,"PUT",i,null,{domain:a,teamMailbox:e,memberMail:t}));await Promise.all(l)}async deleteTeamMailbox(i,t){const e=this.extractMailDomain(t);e?await this.callWebAdminApi("ldapgroupdeletedone:teamMailbox",`${this.webadminUrl}/domains/${e}/team-mailboxes/${t}`,"DELETE",i,null,{domain:e,teamMailbox:t}):this.logger.error({plugin:this.name,event:"deleteTeamMailbox",dn:i,mail:t,error:"Cannot extract domain from mail address"})}async removeAllTeamMailboxMembers(i,t){const e=this.extractMailDomain(t);if(e)try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:["member"]},i);if(a.searchEntries&&a.searchEntries.length>0){const s=a.searchEntries[0].member||[],r=Array.isArray(s)?s:[s],n=await this.getMemberEmails(r);await Promise.all(n.map(a=>this.callWebAdminApi("removeAllTeamMailboxMembers",`${this.webadminUrl}/domains/${e}/team-mailboxes/${t}/members/${a}`,"DELETE",i,null,{domain:e,teamMailbox:t,memberMail:a}))),this.logger.info({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:i,teamMailbox:t,membersRemoved:n.length,message:"All members removed from team mailbox (mailbox preserved)"})}}catch(i){const e=i instanceof Error?i.message:String(i);this.logger.error(`Failed to remove members from team mailbox ${t}: ${e}`)}else this.logger.error({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:i,mail:t,error:"Cannot extract domain from mail address"})}async deleteMailingList(i,t){await this.callWebAdminApi("deleteMailingList",`${this.webadminUrl}/address/groups/${t}`,"DELETE",i,null,{groupMail:t})}async getMemberEmails(i){const t=i.filter(i=>i!==this.config.group_dummy_user).map(i=>this.server.ldap.queryLimit(async()=>{const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):null}));return(await Promise.all(t)).filter(i=>null!==i)}async getGroupMail(i){const t=await this.ldapGetAttributes(i,["mail"]);return t?this.attributeToString(t.mail):null}async getGroupMailboxType(i){try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.mailboxTypeAttr]},i);if(t.searchEntries&&t.searchEntries.length>0)return this.getMailboxType(t.searchEntries[0])}catch(t){const e=t instanceof Error?t.message:String(t);this.logger.debug(`Could not get mailbox type for group ${i}: ${e}`)}return null}async _handleDelegationChange(i,t){const e=await this.ldapGetAttributes(i,[this.mailAttr]);if(!e)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:i,message:"Could not find user entry to get mail attribute"});const a=e[this.mailAttr];if(!a||"string"!=typeof a)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:i,message:"User has no mail attribute, cannot manage delegation"});const s=this.config.delegation_attribute;if(!s)return;const[r,n]=t[s]||[],l=this._normalizeToArray(r),o=this._normalizeToArray(n),m=o.filter(i=>!l.includes(i)),d=l.filter(i=>!o.includes(i)),h=m.map(i=>this.server.ldap.queryLimit(async()=>{const t=await this._getDelegateEmail(i);return{dn:i,email:t}})),g=d.map(i=>this.server.ldap.queryLimit(async()=>{const t=await this._getDelegateEmail(i);return{dn:i,email:t}})),[u,p]=await Promise.all([Promise.all(h),Promise.all(g)]),c=[];for(const{dn:t,email:e}of u)e&&c.push(this.callWebAdminApi("onLdapChange:addDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${e}`,"PUT",i,null,{userMail:a,delegateEmail:e,delegateDN:t,action:"add"}));for(const{dn:t,email:e}of p)e&&c.push(this.callWebAdminApi("onLdapChange:removeDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${e}`,"DELETE",i,null,{userMail:a,delegateEmail:e,delegateDN:t,action:"remove"}));await Promise.all(c)}async _getDelegateEmail(i){const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):(this.logger.warn({plugin:this.name,event:"getDelegateEmail",dn:i,message:"Could not resolve delegate DN to email"}),null)}_normalizeToArray(i){return i?Array.isArray(i)?i:[i]:[]}}export{James as default};
@@ -1 +1 @@
1
- import o from"../abstract/plugin.js";class WebLogs extends o{constructor(){super(...arguments),this.name="weblogs",this.roles=["logging"]}api(o){o.use((o,e,s)=>{let r=!0;this.logger.debug(`Incoming request: ${o.method} ${o.originalUrl}`);const t=Date.now();e.on("finish",()=>{const s={};o.user&&(s.user=o.user),r&&this.log(o,e,t,s),r=!1}),e.on("error",s=>{r&&this.log(o,e,t,{error:s.message}),r=!1}),e.on("close",()=>{e.writableEnded||(r&&this.log(o,e,t,{error:`Connection closed before response was sent for ${o.method} ${o.originalUrl}`}),r=!1)}),s()})}log(o,e,s,r){const t=Date.now()-s,n={method:o.method,url:o.originalUrl,status:e.statusCode,duration:t,...r};this.logger.info(n)}}export{WebLogs as default};
1
+ import o from"../abstract/plugin.js";class WebLogs extends o{constructor(){super(...arguments),this.name="weblogs",this.roles=["logging"]}api(o){o.use((o,e,s)=>{let r=!0;this.logger.debug(`Incoming request: ${o.method} ${o.originalUrl}`);const t=Date.now();e.on("finish",()=>{const s={};o.user&&(s.user=o.user),r&&this.log(o,e,t,s),r=!1}),e.on("error",s=>{r&&this.log(o,e,t,{error:s.message}),r=!1}),e.on("close",()=>{e.writableEnded||(r&&this.log(o,e,t,{error:`Connection closed before response was sent for ${o.method} ${o.originalUrl}`}),r=!1)}),s()})}log(o,e,s,r){const t=Date.now()-s,n={method:o.method,url:o.originalUrl,status:e.statusCode,duration:t,...r};this.logger.notice(n)}}export{WebLogs as default};
@@ -86,9 +86,4 @@ export default abstract class LdapFlat extends DmPlugin {
86
86
  validateNewEntry(dn: string, entry: AttributesList): Promise<boolean>;
87
87
  validateChanges(dn: string, changes: ModifyRequest): Promise<boolean>;
88
88
  _validateOneChange(field: string, value: AttributeValue | null): Promise<boolean>;
89
- /**
90
- * Escape special characters in DN values for LDAP attributes
91
- * Escape spaces and special characters with backslashes
92
- */
93
- private escapeDnValue;
94
89
  }
@@ -6,6 +6,7 @@ import type { Express } from 'express';
6
6
  import type winston from 'winston';
7
7
  import type { Config, DM } from '../bin';
8
8
  import type { Hooks, MaybePromise } from '../hooks';
9
+ export { asyncHandler } from '../lib/utils';
9
10
  export type Role = 'auth' | 'authz' | 'protect' | 'api' | 'logging' | 'demo' | 'consistency' | 'configurable';
10
11
  export default abstract class DmPlugin {
11
12
  /**
@@ -18,6 +18,7 @@ import type { Hooks } from '../hooks';
18
18
  import ldapActions from '../lib/ldapActions';
19
19
  import type DmPlugin from '../abstract/plugin';
20
20
  export type { Config };
21
+ export * from '../lib/utils';
21
22
  /**
22
23
  * @class DM
23
24
  */
@@ -5,6 +5,7 @@
5
5
  export * from './utils/dom';
6
6
  export * from './utils/form';
7
7
  export * from './utils/schema';
8
+ export * from './utils/totp';
8
9
  export { Modal, type ModalOptions } from './components/Modal';
9
10
  export { StatusMessage, showStatus, type StatusMessageOptions, } from './components/StatusMessage';
10
11
  export * from './types';
@@ -0,0 +1,73 @@
1
+ /**
2
+ * HMAC-SHA256 request signing utility for browser
3
+ * For backend services authentication
4
+ * @module browser/shared/utils/hmac
5
+ */
6
+ /**
7
+ * HMAC Authentication configuration
8
+ */
9
+ export interface HmacConfig {
10
+ serviceId: string;
11
+ secret: string;
12
+ }
13
+ /**
14
+ * Generate HMAC signature for a request
15
+ *
16
+ * Signature = HMAC-SHA256(secret, "METHOD|PATH|timestamp|body-hash")
17
+ */
18
+ export declare function generateHmacSignature(secret: string, method: string, path: string, timestamp: number, body?: unknown): Promise<string>;
19
+ /**
20
+ * HMAC Authentication Client
21
+ * Provides HTTP client with HMAC-SHA256 request signing
22
+ */
23
+ export declare class HmacAuthClient {
24
+ private config;
25
+ constructor(config: HmacConfig);
26
+ /**
27
+ * Generate Authorization header for a request
28
+ *
29
+ * Format: HMAC-SHA256 service-id:timestamp:signature
30
+ */
31
+ getAuthHeader(method: string, path: string, body?: unknown): Promise<string>;
32
+ /**
33
+ * Extract path with query string from URL
34
+ */
35
+ private extractPath;
36
+ /**
37
+ * Perform an authenticated fetch request
38
+ */
39
+ fetch(url: string, options?: RequestInit): Promise<Response>;
40
+ /**
41
+ * Prepare request body and headers for methods with body
42
+ */
43
+ private prepareRequestBody;
44
+ /**
45
+ * Perform an authenticated GET request
46
+ */
47
+ get(url: string, options?: RequestInit): Promise<Response>;
48
+ /**
49
+ * Perform an authenticated POST request
50
+ */
51
+ post(url: string, body?: unknown, options?: RequestInit): Promise<Response>;
52
+ /**
53
+ * Perform an authenticated PUT request
54
+ */
55
+ put(url: string, body?: unknown, options?: RequestInit): Promise<Response>;
56
+ /**
57
+ * Perform an authenticated DELETE request
58
+ */
59
+ delete(url: string, options?: RequestInit): Promise<Response>;
60
+ /**
61
+ * Perform an authenticated PATCH request
62
+ */
63
+ patch(url: string, body?: unknown, options?: RequestInit): Promise<Response>;
64
+ }
65
+ /**
66
+ * Verify the minimum recommended secret length
67
+ */
68
+ export declare function isSecretSecure(secret: string): boolean;
69
+ /**
70
+ * Generate a random secure secret for HMAC (for testing/development)
71
+ * In production, secrets should be generated securely on the backend
72
+ */
73
+ export declare function generateRandomSecret(length?: number): Promise<string>;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * TOTP (Time-based One-Time Password) utility for browser
3
+ * @module browser/shared/utils/totp
4
+ */
5
+ /**
6
+ * TOTP Generator configuration
7
+ */
8
+ export interface TotpConfig {
9
+ secret: string;
10
+ digits?: number;
11
+ step?: number;
12
+ }
13
+ /**
14
+ * Generate a TOTP code from a Base32 secret
15
+ */
16
+ export declare function generateTotp(config: TotpConfig): Promise<string>;
17
+ /**
18
+ * Validate if a string is valid Base32
19
+ */
20
+ export declare function isValidBase32(input: string): boolean;
21
+ /**
22
+ * Get remaining seconds until next TOTP code
23
+ */
24
+ export declare function getRemainingSeconds(step?: number): number;
25
+ /**
26
+ * TOTP Authentication Client
27
+ * Provides HTTP client with TOTP authentication header
28
+ */
29
+ export declare class TotpAuthClient {
30
+ private config;
31
+ constructor(config: TotpConfig);
32
+ /**
33
+ * Get current TOTP code
34
+ */
35
+ getCode(): Promise<string>;
36
+ /**
37
+ * Get Authorization header with Bearer token
38
+ */
39
+ getAuthHeader(): Promise<string>;
40
+ /**
41
+ * Perform an authenticated fetch request
42
+ */
43
+ fetch(url: string, options?: RequestInit): Promise<Response>;
44
+ /**
45
+ * Perform an authenticated GET request
46
+ */
47
+ get(url: string, options?: RequestInit): Promise<Response>;
48
+ /**
49
+ * Perform an authenticated POST request
50
+ */
51
+ post(url: string, body?: unknown, options?: RequestInit): Promise<Response>;
52
+ /**
53
+ * Perform an authenticated PUT request
54
+ */
55
+ put(url: string, body?: unknown, options?: RequestInit): Promise<Response>;
56
+ /**
57
+ * Perform an authenticated DELETE request
58
+ */
59
+ delete(url: string, options?: RequestInit): Promise<Response>;
60
+ /**
61
+ * Perform an authenticated PATCH request
62
+ */
63
+ patch(url: string, body?: unknown, options?: RequestInit): Promise<Response>;
64
+ }
@@ -35,7 +35,7 @@ export interface Config {
35
35
  ldap_base?: string;
36
36
  ldap_dn?: string;
37
37
  ldap_pwd?: string;
38
- ldap_url?: string;
38
+ ldap_url?: string[];
39
39
  ldap_user_main_attribute?: string;
40
40
  ldap_cache_max?: number;
41
41
  ldap_cache_ttl?: number;
@@ -67,6 +67,11 @@ export interface Config {
67
67
  static_name?: string;
68
68
  llng_ini?: string;
69
69
  auth_token?: string[];
70
+ auth_totp?: string[];
71
+ auth_totp_window?: number;
72
+ auth_totp_step?: number;
73
+ auth_hmac?: string[];
74
+ auth_hmac_window?: number;
70
75
  oidc_server?: string;
71
76
  oidc_client_id?: string;
72
77
  oidc_client_secret?: string;
@@ -100,6 +105,9 @@ export interface Config {
100
105
  calendar_resource_objectclass?: string;
101
106
  calendar_resource_creator?: string;
102
107
  calendar_resource_domain?: string;
108
+ applicative_account_base?: string;
109
+ max_app_accounts?: number;
110
+ ldap_operational_attributes?: string[];
103
111
  trash_base?: string;
104
112
  trash_watched_bases?: string;
105
113
  trash_add_metadata?: string;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { SearchOptions, SearchResult } from 'ldapts';
6
6
  import type { Request, Response } from 'express';
7
- import type { ModifyRequest, AttributesList } from './lib/ldapActions';
7
+ import type { ModifyRequest, AttributesList, AttributeValue } from './lib/ldapActions';
8
8
  import type { ChangesToNotify } from './plugins/ldap/onChange';
9
9
  import * as utils from './lib/utils';
10
10
  export type MaybePromise<T> = Promise<T> | T;
@@ -48,7 +48,7 @@ export interface Hooks {
48
48
  _ldapgrouplist?: ChainedHook<AsyncGenerator<SearchResult>>;
49
49
  /** "onLdapChange" */
50
50
  onLdapChange?: (dn: string, changes: ChangesToNotify) => MaybePromise<void>;
51
- onLdapMailChange?: (dn: string, oldMail: string, newMail: string) => MaybePromise<void>;
51
+ onLdapMailChange?: (dn: string, oldMail: AttributeValue | null, newMail: AttributeValue | null) => MaybePromise<void>;
52
52
  onLdapAliasChange?: (dn: string, mail: string, oldAliases: string[], newAliases: string[]) => MaybePromise<void>;
53
53
  onLdapQuotaChange?: (dn: string, mail: string, oldQuota: number, newQuota: number) => MaybePromise<void>;
54
54
  onLdapForwardChange?: (dn: string, mail: string, oldForwards: string[], newForwards: string[]) => MaybePromise<void>;
@@ -3,7 +3,6 @@
3
3
  * @author Xavier Guimard <xguimard@linagora.com>
4
4
  */
5
5
  import type { Request } from 'express';
6
- import { Client } from 'ldapts';
7
6
  import type { ClientOptions, SearchResult, SearchOptions } from 'ldapts';
8
7
  import type winston from 'winston';
9
8
  import pLimit from 'p-limit';
@@ -31,9 +30,11 @@ declare class ldapActions {
31
30
  private connectionPool;
32
31
  private poolSize;
33
32
  private connectionTtl;
33
+ private ldapUrls;
34
+ private currentUrlIndex;
34
35
  constructor(server: DM);
35
36
  /**
36
- * Create a new LDAP connection
37
+ * Create a new LDAP connection with failover support
37
38
  */
38
39
  private createConnection;
39
40
  /**
@@ -48,11 +49,6 @@ declare class ldapActions {
48
49
  * Release a connection back to the pool
49
50
  */
50
51
  private releaseConnection;
51
- /**
52
- * Legacy connect method for backward compatibility
53
- * @deprecated Use acquireConnection/releaseConnection instead
54
- */
55
- connect(): Promise<Client>;
56
52
  /**
57
53
  * Generate cache key for LDAP search
58
54
  */
@@ -2,6 +2,7 @@
2
2
  * Utility functions
3
3
  * @author Xavier Guimard <xguimard@linagora.com>
4
4
  */
5
+ import type { Request, Response, NextFunction, RequestHandler } from 'express';
5
6
  import type { Config } from '../bin';
6
7
  export declare const launchHooks: (hooks: Function[] | undefined, ...args: unknown[]) => Promise<void>;
7
8
  export declare const launchHooksChained: <T>(hooks: Function[] | undefined, args: T) => Promise<T>;
@@ -71,3 +72,19 @@ export declare function getRdn(dn: string): string;
71
72
  * ```
72
73
  */
73
74
  export declare function isChildOf(dn: string, parentDn: string): boolean;
75
+ /**
76
+ * Wrapper for async Express route handlers to catch errors and pass them to error middleware
77
+ * This ensures that errors in async routes are properly handled and don't crash the server
78
+ *
79
+ * @param fn - The async route handler function
80
+ * @returns A wrapped handler that catches errors
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * app.get('/api/data', asyncHandler(async (req, res) => {
85
+ * const data = await fetchData();
86
+ * res.json(data);
87
+ * }));
88
+ * ```
89
+ */
90
+ export declare const asyncHandler: (fn: (req: Request, res: Response, next: NextFunction) => Promise<void>) => RequestHandler;
@@ -1,3 +1,8 @@
1
1
  import winston from 'winston';
2
2
  import { Config } from '../bin';
3
+ declare module 'winston' {
4
+ interface Logger {
5
+ notice: winston.LeveledLogMethod;
6
+ }
7
+ }
3
8
  export declare const buildLogger: (config: Config) => winston.Logger;
@@ -0,0 +1,24 @@
1
+ import type { Response } from 'express';
2
+ import AuthBase, { type DmRequest } from '../../lib/auth/base';
3
+ import type { Role } from '../../abstract/plugin';
4
+ export default class AuthHmac extends AuthBase {
5
+ name: string;
6
+ roles: Role[];
7
+ private services;
8
+ private timeWindow;
9
+ constructor(...args: ConstructorParameters<typeof AuthBase>);
10
+ authMethod(req: DmRequest, res: Response, next: () => void): void;
11
+ /**
12
+ * Calculate HMAC-SHA256 signature
13
+ */
14
+ private calculateHmac;
15
+ /**
16
+ * Calculate SHA256 hash of request body
17
+ * Returns empty string for GET/DELETE/HEAD methods
18
+ */
19
+ private calculateBodyHash;
20
+ /**
21
+ * Constant-time string comparison to prevent timing attacks
22
+ */
23
+ private secureCompare;
24
+ }
@@ -0,0 +1,28 @@
1
+ import type { Response } from 'express';
2
+ import AuthBase, { type DmRequest } from '../../lib/auth/base';
3
+ import type { Role } from '../../abstract/plugin';
4
+ export default class AuthTotp extends AuthBase {
5
+ name: string;
6
+ roles: Role[];
7
+ private totpUsers;
8
+ private window;
9
+ private step;
10
+ constructor(...args: ConstructorParameters<typeof AuthBase>);
11
+ authMethod(req: DmRequest, res: Response, next: () => void): void;
12
+ /**
13
+ * Verify a TOTP token against a secret
14
+ */
15
+ private verifyTotp;
16
+ /**
17
+ * Generate a TOTP token using HMAC-SHA1
18
+ */
19
+ private generateTotp;
20
+ /**
21
+ * Decode a Base32 string to Buffer
22
+ */
23
+ private base32Decode;
24
+ /**
25
+ * Validate if a string is valid Base32
26
+ */
27
+ private isValidBase32;
28
+ }
@@ -17,6 +17,7 @@ declare class OnLdapChange extends DmPlugin {
17
17
  name: string;
18
18
  roles: Role[];
19
19
  stack: Record<number, Entry>;
20
+ pendingDeletions: Map<string, Entry>;
20
21
  hooks: Hooks;
21
22
  notify(dn: string, changes: ChangesToNotify): void;
22
23
  notifyAttributeChange(attribute: string, hookName: keyof Hooks, dn: string, changes: ChangesToNotify, stringOnly?: boolean): void;
@@ -4,4 +4,8 @@ export default class Static extends DmPlugin {
4
4
  name: string;
5
5
  roles: Role[];
6
6
  api(app: Express): void;
7
+ /**
8
+ * Provide configuration for config API
9
+ */
10
+ getConfigApiData(): Record<string, unknown>;
7
11
  }
@@ -0,0 +1,52 @@
1
+ import type { Express } from 'express';
2
+ import DmPlugin, { type Role } from '../../abstract/plugin';
3
+ import type { DM } from '../../bin';
4
+ export default class AppAccountsApi extends DmPlugin {
5
+ name: string;
6
+ roles: Role[];
7
+ dependencies: {
8
+ authToken: string;
9
+ appAccountsConsistency: string;
10
+ };
11
+ private applicativeAccountBase;
12
+ private maxAppAccounts;
13
+ private mailAttr;
14
+ constructor(server: DM);
15
+ /**
16
+ * Register API routes
17
+ */
18
+ api(app: Express): void;
19
+ /**
20
+ * List applicative accounts for a user
21
+ */
22
+ private listAccounts;
23
+ /**
24
+ * Create a new applicative account
25
+ */
26
+ private createAccount;
27
+ /**
28
+ * Delete an applicative account
29
+ */
30
+ private deleteAccount;
31
+ /**
32
+ * Generate a cryptographically secure random account ID
33
+ * Format: c + 8 random digits
34
+ */
35
+ private generateAccountId;
36
+ /**
37
+ * Generate a cryptographically secure random password
38
+ * Format: 6 blocks of 4 characters separated by "-"
39
+ * Example: AbC3@-2xYz!-9pQr@-St4v!-mN8#-pQ5$
40
+ * Length: 29 characters (6*4 + 5 dashes)
41
+ * Ensures: uppercase, lowercase, digit, special char in each block
42
+ *
43
+ * Note: Password is passed in cleartext to LDAP. OpenLDAP automatically hashes it
44
+ * via ppolicy overlay before storage. This is the correct approach - attempting to
45
+ * pre-hash would fail ppolicy validation.
46
+ */
47
+ private generatePassword;
48
+ /**
49
+ * Expose configuration for configApi
50
+ */
51
+ getConfigApiData(): Record<string, unknown>;
52
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * App Accounts Consistency Plugin
3
+ *
4
+ * Automatically manages applicative account entries for protocol-based authentication
5
+ * (e.g., IMAP, SMTP, CalDAV, CardDAV).
6
+ *
7
+ * ## Concept
8
+ *
9
+ * Instead of using a single primary password for all services, this system separates:
10
+ * - **Primary authentication**: May use passwordless methods (smart cards, biometrics, SSO)
11
+ * - **Applicative accounts**: Dedicated accounts per device/application
12
+ *
13
+ * This is essential for protocols requiring password authentication (IMAP, SMTP, CalDAV)
14
+ * while maintaining security and allowing easy revocation per device.
15
+ *
16
+ * ## Behavior
17
+ *
18
+ * When a user with a mail attribute is created, this plugin creates a corresponding
19
+ * principal applicative account entry in a separate branch (e.g., ou=applicative).
20
+ *
21
+ * When a user is deleted, all corresponding applicative accounts are also deleted.
22
+ *
23
+ * When a user's mail changes, all applicative accounts are updated with the new mail.
24
+ */
25
+ import DmPlugin from '../../abstract/plugin';
26
+ import type { Role } from '../../abstract/plugin';
27
+ import type { DM } from '../../bin';
28
+ import { Hooks } from '../../hooks';
29
+ export default class AppAccountsConsistency extends DmPlugin {
30
+ name: string;
31
+ roles: Role[];
32
+ dependencies: {
33
+ onLdapChange: string;
34
+ };
35
+ private mailAttr;
36
+ private applicativeAccountBase;
37
+ private operationalAttributes;
38
+ constructor(server: DM);
39
+ /**
40
+ * Check if an attribute should be excluded when copying LDAP entry attributes
41
+ * @param key - The attribute name to check
42
+ * @returns true if the attribute should be excluded
43
+ */
44
+ private shouldExcludeAttribute;
45
+ hooks: Hooks;
46
+ /**
47
+ * Create applicative account for a user
48
+ */
49
+ private createApplicativeAccount;
50
+ /**
51
+ * Delete applicative account by mail
52
+ */
53
+ private deleteApplicativeAccount;
54
+ /**
55
+ * Update applicative account when mail changes
56
+ */
57
+ private updateApplicativeAccount;
58
+ }