ldap-rest 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/bin/sync-app-accounts.mjs +276 -0
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/plugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/logger/winston.js +1 -1
- package/dist/plugins/auth/hmac.js +1 -0
- package/dist/plugins/auth/totp.js +1 -0
- package/dist/plugins/demo/helloworld.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/static.js +1 -1
- package/dist/plugins/twake/appAccountsApi.js +1 -0
- package/dist/plugins/twake/appAccountsConsistency.js +1 -0
- package/dist/plugins/twake/james.js +1 -1
- package/dist/plugins/weblogs.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +0 -5
- package/dist/src/abstract/plugin.d.ts +1 -0
- package/dist/src/bin/index.d.ts +1 -0
- package/dist/src/browser/shared/index.d.ts +1 -0
- package/dist/src/browser/shared/utils/hmac.d.ts +73 -0
- package/dist/src/browser/shared/utils/totp.d.ts +64 -0
- package/dist/src/config/args.d.ts +9 -1
- package/dist/src/hooks.d.ts +2 -2
- package/dist/src/lib/ldapActions.d.ts +3 -7
- package/dist/src/lib/utils.d.ts +17 -0
- package/dist/src/logger/winston.d.ts +5 -0
- package/dist/src/plugins/auth/hmac.d.ts +24 -0
- package/dist/src/plugins/auth/totp.d.ts +35 -0
- package/dist/src/plugins/ldap/onChange.d.ts +1 -0
- package/dist/src/plugins/static.d.ts +4 -0
- package/dist/src/plugins/twake/appAccountsApi.d.ts +52 -0
- package/dist/src/plugins/twake/appAccountsConsistency.d.ts +58 -0
- package/docs/DEVELOPER_GUIDE.md +1 -0
- package/docs/PLUGIN_DEPENDENCIES.md +19 -0
- package/docs/README.md +74 -30
- package/docs/authentication.md +600 -12
- package/docs/plugins/DEVELOPMENT.md +148 -76
- package/docs/testing-with-embedded-ldap.md +342 -0
- package/docs/twakeAppAccountsApi.md +485 -0
- package/docs/weblogs.md +36 -8
- package/examples/web/README.md +166 -0
- package/examples/web/ldap-tree-viewer.html +16 -2
- package/examples/web/totp-client.html +543 -0
- package/package.json +46 -1
- package/static/browser/shared/index.js +1 -0
- package/static/browser/shared/index.js.map +1 -1
- package/static/browser/shared/utils/hmac.js +202 -0
- package/static/browser/shared/utils/hmac.js.map +1 -0
- package/static/browser/shared/utils/totp.js +182 -0
- package/static/browser/shared/utils/totp.js.map +1 -0
- package/static/browser/shared.esm.js +181 -1
- package/static/browser/shared.esm.js.map +1 -1
- package/static/browser/shared.js +184 -0
- package/static/browser/shared.js.map +1 -1
- package/static/browser/shared.min.js +1 -1
- package/static/browser/shared.min.js.map +1 -1
- package/static/schemas/obm/organizations.json +24 -0
- 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
|
|
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};
|
package/dist/plugins/static.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import s from"fs";import{resolve as t,join as e}from"path";import a from"express";import
|
|
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};
|
package/dist/plugins/weblogs.js
CHANGED
|
@@ -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.
|
|
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
|
/**
|
package/dist/src/bin/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -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:
|
|
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
|
*/
|
package/dist/src/lib/utils.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
4
|
*/
|
|
5
5
|
import type { Config } from '../bin';
|
|
6
|
+
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
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>;
|
|
8
9
|
export declare const transformSchemas: (schemas: string | Buffer, config: Config) => string;
|
|
@@ -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;
|
|
@@ -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,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/auth/totp
|
|
3
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
+
*
|
|
5
|
+
* TOTP-based authentication plugin
|
|
6
|
+
* @group Plugins
|
|
7
|
+
*/
|
|
8
|
+
import type { Response } from 'express';
|
|
9
|
+
import AuthBase, { type DmRequest } from '../../lib/auth/base';
|
|
10
|
+
import type { Role } from '../../abstract/plugin';
|
|
11
|
+
export default class AuthTotp extends AuthBase {
|
|
12
|
+
name: string;
|
|
13
|
+
roles: Role[];
|
|
14
|
+
private totpUsers;
|
|
15
|
+
private window;
|
|
16
|
+
private step;
|
|
17
|
+
constructor(...args: ConstructorParameters<typeof AuthBase>);
|
|
18
|
+
authMethod(req: DmRequest, res: Response, next: () => void): void;
|
|
19
|
+
/**
|
|
20
|
+
* Verify a TOTP token against a secret
|
|
21
|
+
*/
|
|
22
|
+
private verifyTotp;
|
|
23
|
+
/**
|
|
24
|
+
* Generate a TOTP token using HMAC-SHA1
|
|
25
|
+
*/
|
|
26
|
+
private generateTotp;
|
|
27
|
+
/**
|
|
28
|
+
* Decode a Base32 string to Buffer
|
|
29
|
+
*/
|
|
30
|
+
private base32Decode;
|
|
31
|
+
/**
|
|
32
|
+
* Validate if a string is valid Base32
|
|
33
|
+
*/
|
|
34
|
+
private isValidBase32;
|
|
35
|
+
}
|
|
@@ -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;
|
|
@@ -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
|
+
}
|