ldap-rest 0.17.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/twakePlugin.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/config/schema.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/plugins/ldap/enterpriseRules.js +1 -1
- package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +9 -11
- package/dist/src/browser/ldap-group-editor/types.d.ts +2 -1
- package/dist/src/browser/ldap-resource-editor/types.d.ts +3 -2
- package/dist/src/browser/ldap-unit-editor/types.d.ts +2 -1
- package/dist/src/browser/ldap-user-editor/types.d.ts +3 -2
- package/dist/src/browser/shared/types/index.d.ts +7 -1
- package/dist/src/browser/shared/utils/schema.d.ts +20 -1
- package/dist/src/config/args.d.ts +1 -0
- package/dist/src/config/schema.d.ts +27 -4
- package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +11 -0
- package/dist/src/plugins/ldap/organizations.d.ts +0 -8
- package/dist/src/plugins/twake/james.d.ts +63 -75
- package/docs/client-development/api/reference.md +1 -1
- package/docs/client-development/api/rest-api.md +13 -1
- package/docs/client-development/schemas/README.md +18 -13
- package/docs/plugin-development/dependencies.md +1 -3
- package/docs/presentations/en.md +0 -28
- package/docs/presentations/fr.md +0 -28
- package/docs/usage/configuration.md +13 -12
- package/docs/usage/plugins/integrations/james-mail.md +92 -129
- package/docs/usage/plugins/integrations/james-mailboxes.md +70 -83
- package/docs/usage/plugins/ldap/external-users.md +23 -2
- package/docs/usage/plugins/ldap/flat-generic.md +23 -5
- package/docs/usage/plugins/utilities/weblogs.md +1 -1
- package/docs/usage/upgrading.md +48 -0
- package/package.json +1 -1
- package/static/browser/ldap-resource-editor/api/ResourceApiClient.js +2 -1
- package/static/browser/ldap-resource-editor/api/ResourceApiClient.js.map +1 -1
- package/static/browser/ldap-user-editor/api/UserApiClient.js +2 -1
- package/static/browser/ldap-user-editor/api/UserApiClient.js.map +1 -1
- package/static/browser/ldap-user-editor/components/UserEditor.js +11 -8
- package/static/browser/ldap-user-editor/components/UserEditor.js.map +1 -1
- package/static/browser/ldap-user-editor/components/UserList.js +3 -1
- package/static/browser/ldap-user-editor/components/UserList.js.map +1 -1
- package/static/browser/ldap-user-editor.esm.js +52 -10
- package/static/browser/ldap-user-editor.esm.js.map +1 -1
- package/static/browser/ldap-user-editor.js +52 -10
- package/static/browser/ldap-user-editor.js.map +1 -1
- package/static/browser/ldap-user-editor.min.js +1 -1
- package/static/browser/ldap-user-editor.min.js.map +1 -1
- package/static/browser/shared/index.js +1 -1
- package/static/browser/shared/types/index.js.map +1 -1
- package/static/browser/shared/utils/form.js +2 -1
- package/static/browser/shared/utils/form.js.map +1 -1
- package/static/browser/shared/utils/schema.js +33 -1
- package/static/browser/shared/utils/schema.js.map +1 -1
- package/static/browser/shared.esm.js +153 -121
- package/static/browser/shared.esm.js.map +1 -1
- package/static/browser/shared.js +154 -120
- 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/standard/groups.json +1 -1
- package/static/schemas/twake/groups.json +53 -18
- package/static/schemas/twake/nomenclature/twakeAccountStatus.json +24 -8
- package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +22 -7
- package/static/schemas/twake/nomenclature/twakeDomain.json +21 -8
- package/static/schemas/twake/nomenclature/twakeListType.json +22 -7
- package/static/schemas/twake/nomenclature/twakeMailboxType.json +22 -7
- package/static/schemas/twake/nomenclature/twakeTitle.json +32 -12
- package/static/schemas/twake/organizations.json +29 -13
- package/static/schemas/twake/positions.json +13 -5
- package/static/schemas/twake/users.json +103 -38
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node-fetch";import{TwakePlugin as e}from"../../abstract/twakePlugin.js";import{asyncHandler as i,escapeLdapFilter as a,isDummyMemberDn as s}from"../../lib/utils.js";import{wantJson as r}from"../../lib/expressFormatedResponses.js";const n=[400,404];class James extends e{constructor(t){super(t,"james_webadmin_url","james_webadmin_token","james_concurrency"),this.name="james",this.roles=["consistency","api"],this.dependencies={onLdapChange:"core/ldap/onChange",ldapGroups:"core/ldap/groups"},this.hooks={ldapadddone:async t=>{const[e,i]=t,a=i[this.mailAttr],s=i[this.quotaAttr],r=i[this.aliasAttr];if(!a)return;const l=Array.isArray(a)?String(a[0]):String(a);if(this.initDelay>0&&await new Promise(t=>setTimeout(t,this.initDelay)),s){const t=Array.isArray(s)?Number(s[0]):Number(s);!isNaN(t)&&t>0&&await this.callWebAdminApi("ldapadddone:quota",`${this.webadminUrl}/quota/users/${l}/size`,"PUT",e,t.toString(),{mail:l,quota:t},n)}if(r){const t=this.getAliases(r);await Promise.all(t.map(t=>this.callWebAdminApi("ldapadddone:alias",`${this.webadminUrl}/address/aliases/${l}/sources/${t}`,"PUT",e,null,{mail:l,alias:t},n)))}const o=this.getDisplayNameFromAttributes(i);o&&await this.updateJamesIdentity(e,l,o)},onLdapMailChange:async(t,e,i)=>{const a=e?Array.isArray(e)?String(e[0]):String(e):null,s=i?Array.isArray(i)?String(i[0]):String(i):null;if(a)if(s){await this.callWebAdminApi("onLdapMailChange",`${this.webadminUrl}/users/${a}/rename/${s}?action=rename&force`,"POST",t,null,{oldmail:a,newmail:s},n);try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.aliasAttr]},t);if(e.searchEntries&&e.searchEntries.length>0){const i=this.getAliases(e.searchEntries[0][this.aliasAttr]);i.length>0&&await Promise.all([...i.map(e=>this.callWebAdminApi("onLdapMailChange-delete",`${this.webadminUrl}/address/aliases/${a}/sources/${e}`,"DELETE",t,null,{oldmail:a,alias:e})),...i.map(e=>this.callWebAdminApi("onLdapMailChange-create",`${this.webadminUrl}/address/aliases/${s}/sources/${e}`,"PUT",t,null,{newmail:s,alias:e},n))])}}catch(t){this.logger.debug("Could not fetch aliases for mail change:",t)}}else this.logger.debug(`Skipping mail rename for ${t}: newmail is empty (mail attribute was deleted, not changed)`);else this.logger.debug(`Skipping mail rename for ${t}: oldmail is empty (mail attribute was added, not changed)`)},onLdapAliasChange:async(t,e,i,a)=>{const s=i.map(t=>this.normalizeAlias(t)),r=a.map(t=>this.normalizeAlias(t)),l=s.filter(t=>!r.includes(t)),o=r.filter(t=>!s.includes(t));await Promise.all([...l.map(i=>this.callWebAdminApi("onLdapAliasChange-delete",`${this.webadminUrl}/address/aliases/${e}/sources/${i}`,"DELETE",t,null,{mail:e,alias:i,action:"delete"})),...o.map(i=>this.callWebAdminApi("onLdapAliasChange-add",`${this.webadminUrl}/address/aliases/${e}/sources/${i}`,"PUT",t,null,{mail:e,alias:i,action:"add"},n))])},onLdapQuotaChange:(t,e,i,a)=>this.callWebAdminApi("onLdapQuotaChange",`${this.webadminUrl}/quota/users/${e}/size`,"PUT",t,a.toString(),{oldQuota:i,newQuota:a},n),onLdapForwardChange:async(t,e,i,a)=>{const s=this.extractMailDomain(e);if(!s)return void this.logger.error(`Cannot extract domain from mail ${e} for forward management`);const r=i.filter(t=>!a.includes(t)),l=a.filter(t=>!i.includes(t));await Promise.all([...r.map(i=>this.callWebAdminApi("onLdapForwardChange-delete",`${this.webadminUrl}/address/forwards/${e}/targets/${i}`,"DELETE",t,null,{mail:e,forward:i,domain:s,action:"delete"})),...l.map(i=>this.callWebAdminApi("onLdapForwardChange-add",`${this.webadminUrl}/address/forwards/${e}/targets/${i}`,"PUT",t,null,{mail:e,forward:i,domain:s,action:"add"},n))])},onLdapChange:async(t,e)=>{this.config.delegation_attribute&&e[this.config.delegation_attribute]&&await this._handleDelegationChange(t,e)},onLdapDisplayNameChange:async t=>{const e=[this.mailAttr,this.displayNameAttr,"cn","givenName","sn"],i=await this.ldapGetAttributes(t,e);if(!i)return void this.logger.warn(`Cannot update James identity: entry not found for ${t}`);const a=this.attributeToString(i[this.mailAttr]);if(!a)return void this.logger.warn(`Cannot update James identity: no mail found for ${t}`);const s=this.getDisplayNameFromAttributes(i);if(s)return this.updateJamesIdentity(t,a,s);this.logger.warn(`Cannot update James identity: no display name found for ${t}`)},ldapgroupadddone:async t=>{const[e,i]=t,a=i.mail;if(!a)return void this.logger.debug(`Group ${e} has no mail attribute, skipping James sync`);const s=this.getMailboxType(i),r=Array.isArray(a)?String(a[0]):String(a);if("teamMailbox"===s){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&this.isInAllowedBranches(e,t))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:e,mailboxType:"teamMailbox",error:`Team mailbox cannot be in mailing list branches: ${t.join(", ")}`});await this.createTeamMailbox(e,i,r)}else if("mailingList"===s||null===s){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&!this.isInAllowedBranches(e,t))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:e,mailboxType:s||"mailingList (default)",error:`Mailing list must be in allowed branches: ${t.join(", ")}`});await this.createMailingList(e,i,r)}},ldapgroupmodifydone:async t=>{const[e,i]=t;if(i.replace?.[this.mailboxTypeAttr]||i.add?.[this.mailboxTypeAttr])return void await this.handleMailboxTypeTransition(e,i);const a=await this.getGroupMail(e);if(!a)return void this.logger.debug(`Group ${e} has no mail attribute, skipping James sync`);const s="teamMailbox"===await this.getGroupMailboxType(e),r=s?this.extractMailDomain(a):null,n=[];if(i.add?.member){const t=i.add.member,l=Array.isArray(t)?t:[t],o=await this.getMemberEmails(l);n.push(...o.map(t=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${t}`,"PUT",e,null,{domain:r,groupMail:a,memberMail:t,action:"add"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${t}`,"PUT",e,null,{groupMail:a,memberMail:t,action:"add"})))}if(this.isAttributesList(i.delete)){const t=i.delete.member;if(t){const i=Array.isArray(t)?t:[t],l=await this.getMemberEmails(i);n.push(...l.map(t=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${t}`,"DELETE",e,null,{domain:r,groupMail:a,memberMail:t,action:"delete"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${t}`,"DELETE",e,null,{groupMail:a,memberMail:t,action:"delete"})))}}await Promise.all(n)},ldapgroupdeletedone:async t=>{const e=await this.getGroupMail(t);if(!e)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);"teamMailbox"===await this.getGroupMailboxType(t)?await this.removeAllTeamMailboxMembers(t,e):await this.callWebAdminApi("ldapgroupdeletedone:mailingList",`${this.webadminUrl}/address/groups/${e}`,"DELETE",t,null,{groupMail:e})}},this.identityQueue=new Map,this.quotaAttr=this.config.quota_attribute,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,this.retryAttempts="number"==typeof this.config.james_retry_attempts?Math.max(1,this.config.james_retry_attempts):5,this.retryDelay="number"==typeof this.config.james_retry_delay?Math.max(0,this.config.james_retry_delay):1e3}api(t){const e=this.config.api_prefix||"/api";t.get(`${e}/v1/users/:user/quota-usage`,i((t,e)=>this.getUserQuota(t,e))),this.logger.info(`James API registered at ${e}/v1/users/:user/quota-usage`)}async getUserQuota(e,i){if(!r(e,i))return;const s=e.params.user;try{const e=a(s),r=await this.server.ldap.search({scope:"sub",filter:`(uid=${e})`,paged:!1,attributes:[this.mailAttr]},this.config.ldap_base||""),n=r.searchEntries?.[0];if(!n)return void i.status(404).json({error:`User ${s} not found`});const l=n[this.mailAttr];if(!l)return void i.status(400).json({error:`User ${s} has no mail attribute`});const o=Array.isArray(l)?String(l[0]):String(l),m=`${this.webadminUrl}/quota/users/${o}`,d=await this.requestLimit(()=>t(m,{method:"GET",headers:this.createHeaders()}));if(!d.ok)return void(404===d.status?i.status(404).json({error:`Quota not found for user ${s}`}):i.status(502).json({error:`Failed to retrieve quota from James: ${d.statusText}`}));const h=await d.json();i.json(h)}catch(t){this.logger.error({plugin:this.name,event:"getUserQuota",username:s,error:t instanceof Error?t.message:String(t)}),i.status(500).json({error:"Internal server error while retrieving quota"})}}async handleMailboxTypeTransition(t,e){const i=await this.ldapGetAttributes(t,["mail","member",this.mailboxTypeAttr]);if(!i)return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,error:"Could not fetch group attributes"});const a=i.mail;if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping mailbox type transition`);const s=Array.isArray(a)?String(a[0]):String(a),r=e.replace?.[this.mailboxTypeAttr]||e.add?.[this.mailboxTypeAttr],n=Array.isArray(r)?String(r[0]):String(r),l=this.getMailboxType({[this.mailboxTypeAttr]:n}),o=this.getMailboxType(i);if(this.logger.info({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,mail:s,oldType:o||"none",newType:l||"none"}),o!==l)if("mailingList"===o?await this.deleteMailingList(t,s):"teamMailbox"===o&&await this.removeAllTeamMailboxMembers(t,s),"mailingList"===l){const e=this.config.james_mailing_list_branch||[];if(e.length>0&&!this.isInAllowedBranches(t,e))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,newType:l,error:`Mailing list must be in allowed branches: ${e.join(", ")}`});await this.createMailingList(t,i,s)}else if("teamMailbox"===l){const e=this.config.james_mailing_list_branch||[];if(e.length>0&&this.isInAllowedBranches(t,e))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,newType:l,error:`Team mailbox cannot be in mailing list branches: ${e.join(", ")}`});await this.createTeamMailbox(t,i,s)}}getDisplayNameFromAttributes(t){const e=this.attributeToString(t[this.displayNameAttr]);if(e)return e;const i=this.attributeToString(t.cn);if(i)return i;const a=this.attributeToString(t.givenName),s=this.attributeToString(t.sn);if(a||s){const t=[];return a&&t.push(a),s&&t.push(s),t.join(" ")}const r=this.attributeToString(t[this.mailAttr]);return r||null}async getMailFromDN(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}async getDisplayNameFromDN(t){const e=[this.displayNameAttr,"cn","givenName","sn",this.mailAttr],i=await this.ldapGetAttributes(t,e);return i?this.getDisplayNameFromAttributes(i):null}async generateSignature(t){const e=this.config.james_signature_template;if(!e)return null;const i=await this.ldapGetAttributes(t);if(!i)return null;let a=e;return a=a.replace(/\{(\w+)\}/g,(t,e)=>"string"==typeof e&&i&&"object"==typeof i&&Object.prototype.hasOwnProperty.call(i,e)&&this.attributeToString(i[e])||""),a}async updateJamesIdentity(t,e,i){const a=(this.identityQueue.get(e)??Promise.resolve()).then(()=>this.doUpdateJamesIdentity(t,e,i));this.identityQueue.set(e,a);try{await a}finally{this.identityQueue.get(e)===a&&this.identityQueue.delete(e)}}async doUpdateJamesIdentity(e,i,a){const s={plugin:this.name,event:"onLdapDisplayNameChange",result:"error",dn:e,mail:i,displayName:a};for(let r=1;;r++){try{const l=`${this.webadminUrl}/users/${i}/identities`,o=await this.requestLimit(()=>t(`${l}?default=true`,{method:"GET",headers:this.createHeaders()}));let m;if(o.ok){const t=await o.json();m=(Array.isArray(t)?t:[t]).filter(t=>t&&t.id&&!1!==t.mayDelete).sort((t,e)=>(t.sortOrder??0)-(e.sortOrder??0))[0]}else if(404!==o.status){const t="get_default_identity";if(this.shouldRetry(o.status,n,r)){await this.warnAndWait(s,t,r,o);continue}return void this.logger.error({...s,step:t,...r>1?{attempts:r}:{},http_status:o.status,http_status_text:o.statusText})}const d=await this.generateSignature(e),h=m?.id?{id:m.id,email:m.email,name:a}:{email:i,name:a,sortOrder:0};d&&(h.htmlSignature=d);const u=m?.id?"update_identity":"create_identity",g=await this.requestLimit(()=>t(m?.id?`${l}/${m.id}`:l,{method:m?.id?"PUT":"POST",headers:this.createHeaders("application/json"),body:JSON.stringify(h)}));if(g.ok)this.logger.info({...s,step:u,result:"success",http_status:g.status});else{if(this.shouldRetry(g.status,n,r)){await this.warnAndWait(s,u,r,g);continue}this.logger.error({...s,step:u,...r>1?{attempts:r}:{},http_status:g.status,http_status_text:g.statusText})}}catch(t){this.logger.error({...s,error:`${t}`})}return}}async warnAndWait(t,e,i,a){this.logger.warn({...t,step:e,result:"retry",attempt:i,http_status:a.status,http_status_text:a.statusText}),await this.waitBeforeRetry(i)}async deleteUserData(e,i){const a={plugin:this.name,event:"deleteUserData",mail:e,fromStep:i};try{const s=new URL(`${this.webadminUrl}/users/${e}`);s.searchParams.set("action","deleteData"),i&&s.searchParams.set("fromStep",i);const r=await this.requestLimit(()=>t(s.toString(),{method:"POST",headers:this.createHeaders()}));if(!r.ok)return this.logger.error({...a,http_status:r.status,http_status_text:r.statusText}),null;const n=await r.json();return this.logger.info({...a,http_status:r.status,taskId:n.taskId}),n}catch(t){return this.logger.error({...a,error:`${t}`}),null}}isAttributesList(t){return void 0!==t&&"object"==typeof t&&!Array.isArray(t)}getMailboxType(t){const e=t[this.mailboxTypeAttr];if(!e)return null;const i=(Array.isArray(e)?String(e[0]):String(e)).match(/^cn=([^,]+)/);if(!i)return null;const a=i[1];return"group"===a||"mailingList"===a||"teamMailbox"===a?a:null}isInAllowedBranches(t,e){return!e||0===e.length||e.some(e=>t===e||t.endsWith(","+e))}async createMailingList(t,e,i){const a=e.member||[],s=Array.isArray(a)?a:[a];this.logger.debug(`Creating mailing list ${i} in James with ${s.length} members`);const r=await this.getMemberEmails(s);await Promise.all(r.map(e=>this.callWebAdminApi("ldapgroupadddone:mailingList",`${this.webadminUrl}/address/groups/${i}/${e}`,"PUT",t,null,{groupMail:i,memberMail:e})))}async createTeamMailbox(t,e,i){const a=this.extractMailDomain(i);if(!a)return void this.logger.error({plugin:this.name,event:"createTeamMailbox",dn:t,mail:i,error:"Cannot extract domain from mail address"});const s=e.member||[],r=Array.isArray(s)?s:[s];this.logger.debug(`Creating team mailbox ${i} in James with ${r.length} members`);const n=await this.getMemberEmails(r);await this.callWebAdminApi("ldapgroupadddone:teamMailbox:create",`${this.webadminUrl}/domains/${a}/team-mailboxes/${i}`,"PUT",t,null,{domain:a,teamMailbox:i});const l=[];for(const e of n)l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:addMember",`${this.webadminUrl}/domains/${a}/team-mailboxes/${i}/members/${e}`,"PUT",t,null,{domain:a,teamMailbox:i,memberMail:e}));await Promise.all(l)}async deleteTeamMailbox(t,e){const i=this.extractMailDomain(e);i?await this.callWebAdminApi("ldapgroupdeletedone:teamMailbox",`${this.webadminUrl}/domains/${i}/team-mailboxes/${e}`,"DELETE",t,null,{domain:i,teamMailbox:e}):this.logger.error({plugin:this.name,event:"deleteTeamMailbox",dn:t,mail:e,error:"Cannot extract domain from mail address"})}async removeAllTeamMailboxMembers(t,e){const i=this.extractMailDomain(e);if(i)try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:["member"]},t);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/${i}/team-mailboxes/${e}/members/${a}`,"DELETE",t,null,{domain:i,teamMailbox:e,memberMail:a}))),this.logger.info({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:t,teamMailbox:e,membersRemoved:n.length,message:"All members removed from team mailbox (mailbox preserved)"})}}catch(t){const i=t instanceof Error?t.message:String(t);this.logger.error(`Failed to remove members from team mailbox ${e}: ${i}`)}else this.logger.error({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:t,mail:e,error:"Cannot extract domain from mail address"})}async deleteMailingList(t,e){await this.callWebAdminApi("deleteMailingList",`${this.webadminUrl}/address/groups/${e}`,"DELETE",t,null,{groupMail:e})}async getMemberEmails(t){const e=t.filter(t=>!s(t,this.config.group_dummy_user)).map(t=>this.server.ldap.queryLimit(async()=>{const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}));return(await Promise.all(e)).filter(t=>null!==t)}async getGroupMail(t){const e=await this.ldapGetAttributes(t,["mail"]);return e?this.attributeToString(e.mail):null}async getGroupMailboxType(t){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.mailboxTypeAttr]},t);if(e.searchEntries&&e.searchEntries.length>0)return this.getMailboxType(e.searchEntries[0])}catch(e){const i=e instanceof Error?e.message:String(e);this.logger.debug(`Could not get mailbox type for group ${t}: ${i}`)}return null}async _handleDelegationChange(t,e){const i=await this.ldapGetAttributes(t,[this.mailAttr]);if(!i)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"Could not find user entry to get mail attribute"});const a=i[this.mailAttr];if(!a||"string"!=typeof a)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"User has no mail attribute, cannot manage delegation"});const s=this.config.delegation_attribute;if(!s)return;const[r,l]=e[s]||[],o=this._normalizeToArray(r),m=this._normalizeToArray(l),d=m.filter(t=>!o.includes(t)),h=o.filter(t=>!m.includes(t)),u=d.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),g=h.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),[c,p]=await Promise.all([Promise.all(u),Promise.all(g)]),b=[];for(const{dn:e,email:i}of c)i&&b.push(this.callWebAdminApi("onLdapChange:addDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${i}`,"PUT",t,null,{userMail:a,delegateEmail:i,delegateDN:e,action:"add"},n));for(const{dn:e,email:i}of p)i&&b.push(this.callWebAdminApi("onLdapChange:removeDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${i}`,"DELETE",t,null,{userMail:a,delegateEmail:i,delegateDN:e,action:"remove"}));await Promise.all(b)}async _getDelegateEmail(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):(this.logger.warn({plugin:this.name,event:"getDelegateEmail",dn:t,message:"Could not resolve delegate DN to email"}),null)}_normalizeToArray(t){return t?Array.isArray(t)?t:[t]:[]}}export{James as default};
|
|
1
|
+
import t from"node-fetch";import{TwakePlugin as e}from"../../abstract/twakePlugin.js";import{asyncHandler as a,escapeLdapFilter as i,isDummyMemberDn as s,normalizeDn as r}from"../../lib/utils.js";import{wantJson as n}from"../../lib/expressFormatedResponses.js";const o=[400,404],pickDefaultIdentity=t=>(Array.isArray(t)?t:[t]).filter(t=>t&&t.id&&!1!==t.mayDelete).sort((t,e)=>(t.sortOrder??0)-(e.sortOrder??0))[0],members=t=>{const e=t?.member;return void 0===e?[]:(Array.isArray(e)?e:[e]).map(String)},memberKey=t=>{try{return r(t)}catch{return t.toLowerCase()}},l=["groupofnames","groupofuniquenames","groupofurls"],isGroup=t=>(Array.isArray(t)?t:t?[t]:[]).some(t=>l.includes(String(t).toLowerCase()));class James extends e{constructor(t){super(t,"james_webadmin_url","james_webadmin_token","james_concurrency"),this.name="james",this.roles=["consistency","api"],this.dependencies={onLdapChange:"core/ldap/onChange",ldapGroups:"core/ldap/groups"},this.hooks={ldapadddone:([t,e])=>this.provisionAccount(t,e),onLdapMailChange:async(t,e,a)=>{const i=e?Array.isArray(e)?String(e[0]):String(e):null,s=a?Array.isArray(a)?String(a[0]):String(a):null;if(!i)return void this.logger.debug(`Skipping mail rename for ${t}: oldmail is empty (mail attribute was added, not changed)`);if(!s)return void this.logger.debug(`Skipping mail rename for ${t}: newmail is empty (mail attribute was deleted, not changed)`);if(await this.isGroupDn(t))return;const r=await this.defaultSignatures(i),n=await this.ldapGetAttributes(t,[this.forwardAttr,this.displayNameAttr,"cn","givenName","sn",this.mailAttr]);await this.dropStaleForwards(t,s,this.ownForwards(n?.[this.forwardAttr])),await this.callWebAdminApi("onLdapMailChange",`${this.webadminUrl}/users/${i}/rename/${s}?action=rename&force`,"POST",t,null,{oldmail:i,newmail:s},o);try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.aliasAttr]},t);if(e.searchEntries&&e.searchEntries.length>0){const a=this.getAliases(e.searchEntries[0][this.aliasAttr]);a.length>0&&await Promise.all([...a.map(e=>this.callWebAdminApi("onLdapMailChange-delete",`${this.webadminUrl}/address/aliases/${i}/sources/${e}`,"DELETE",t,null,{oldmail:i,alias:e})),...a.map(e=>this.callWebAdminApi("onLdapMailChange-create",`${this.webadminUrl}/address/aliases/${s}/sources/${e}`,"PUT",t,null,{newmail:s,alias:e},o))])}}catch(t){this.logger.debug("Could not fetch aliases for mail change:",t)}const l=n&&this.getDisplayNameFromAttributes(n);l&&await this.updateJamesIdentity(t,s,l,r)},onLdapAliasChange:async(t,e,a,i)=>{if(await this.isGroupDn(t))return;const s=a.map(t=>this.normalizeAlias(t)),r=i.map(t=>this.normalizeAlias(t)),n=s.filter(t=>!r.includes(t)),l=r.filter(t=>!s.includes(t));await Promise.all([...n.map(a=>this.callWebAdminApi("onLdapAliasChange-delete",`${this.webadminUrl}/address/aliases/${e}/sources/${a}`,"DELETE",t,null,{mail:e,alias:a,action:"delete"})),...l.map(async a=>{await this.dropStaleForwards(t,a,[]),await this.callWebAdminApi("onLdapAliasChange-add",`${this.webadminUrl}/address/aliases/${e}/sources/${a}`,"PUT",t,null,{mail:e,alias:a,action:"add"},o)})])},onLdapQuotaChange:async(t,e,a,i)=>{if(!await this.isGroupDn(t))return this.callWebAdminApi("onLdapQuotaChange",`${this.webadminUrl}/quota/users/${e}/size`,"PUT",t,i.toString(),{oldQuota:a,newQuota:i},o)},onLdapForwardChange:async(t,e,a,i)=>{if(!this.config.james_sync_forwards)return;if(await this.isGroupDn(t))return;const s=this.extractMailDomain(e);if(!s)return void this.logger.error(`Cannot extract domain from mail ${e} for forward management`);const r=a.filter(t=>!i.includes(t)),n=i.filter(t=>!a.includes(t));await Promise.all([...r.map(a=>this.callWebAdminApi("onLdapForwardChange-delete",`${this.webadminUrl}/address/forwards/${e}/targets/${a}`,"DELETE",t,null,{mail:e,forward:a,domain:s,action:"delete"})),...n.map(a=>this.callWebAdminApi("onLdapForwardChange-add",`${this.webadminUrl}/address/forwards/${e}/targets/${a}`,"PUT",t,null,{mail:e,forward:a,domain:s,action:"add"},o))])},onLdapChange:async(t,e)=>{this.config.delegation_attribute&&e[this.config.delegation_attribute]&&!await this.isGroupDn(t)&&await this._handleDelegationChange(t,e)},onLdapDisplayNameChange:async t=>{const e=[this.mailAttr,this.displayNameAttr,"cn","givenName","sn","objectClass"],a=await this.ldapGetAttributes(t,e);if(!a)return void this.logger.warn(`Cannot update James identity: entry not found for ${t}`);if(isGroup(a.objectClass))return;const i=this.attributeToString(a[this.mailAttr]);if(!i)return void this.logger.warn(`Cannot update James identity: no mail found for ${t}`);const s=this.getDisplayNameFromAttributes(a);if(s)return this.updateJamesIdentity(t,i,s);this.logger.warn(`Cannot update James identity: no display name found for ${t}`)},onLdapEntryChange:async(t,e,a)=>{e&&a&&!e[this.mailAttr]&&a[this.mailAttr]&&await this.provisionAccount(t,a),await this.followTeamMailbox(e,a)}},this.teamMailboxQueue=new Map,this.identityQueue=new Map,this.quotaAttr=this.config.quota_attribute,this.aliasAttr=this.config.alias_attribute||"mailAlternateAddress",this.mailboxTypeAttr=this.config.james_mailbox_type_attribute||"twakeMailboxType",this.forwardAttr=this.config.forward_attribute||"mailForwardingAddress",this.initDelay="number"==typeof this.config.james_init_delay?this.config.james_init_delay:1e3,this.retryAttempts="number"==typeof this.config.james_retry_attempts?Math.max(1,this.config.james_retry_attempts):5,this.retryDelay="number"==typeof this.config.james_retry_delay?Math.max(0,this.config.james_retry_delay):1e3}api(t){const e=this.config.api_prefix||"/api";t.get(`${e}/v1/users/:user/quota-usage`,a((t,e)=>this.getUserQuota(t,e))),this.logger.info(`James API registered at ${e}/v1/users/:user/quota-usage`)}async getUserQuota(e,a){if(!n(e,a))return;const s=e.params.user;try{const e=i(s),r=await this.server.ldap.search({scope:"sub",filter:`(uid=${e})`,paged:!1,attributes:[this.mailAttr]},this.config.ldap_base||""),n=r.searchEntries?.[0];if(!n)return void a.status(404).json({error:`User ${s} not found`});const o=n[this.mailAttr];if(!o)return void a.status(400).json({error:`User ${s} has no mail attribute`});const l=Array.isArray(o)?String(o[0]):String(o),d=`${this.webadminUrl}/quota/users/${l}`,u=await this.requestLimit(()=>t(d,{method:"GET",headers:this.createHeaders()}));if(!u.ok)return void(404===u.status?a.status(404).json({error:`Quota not found for user ${s}`}):a.status(502).json({error:`Failed to retrieve quota from James: ${u.statusText}`}));const m=await u.json();a.json(m)}catch(t){this.logger.error({plugin:this.name,event:"getUserQuota",username:s,error:t instanceof Error?t.message:String(t)}),a.status(500).json({error:"Internal server error while retrieving quota"})}}teamMailboxOf(t){if(!t||"teamMailbox"!==this.getMailboxType(t))return null;const e=this.attributeToString(t.mail);if(!e)return null;const a=String(t.dn),i=this.config.james_mailing_list_branch||[];if(i.some(t=>a===t||a.endsWith(","+t)))return{mail:e,refused:`Team mailbox cannot be in mailing list branches: ${i.join(", ")}`};const s=e.lastIndexOf("@");return/^[#a-zA-Z0-9_-]{1,255}$/.test(e.slice(0,s))&&this.extractMailDomain(e)?{mail:e,refused:null}:{mail:e,refused:"A team mailbox address is a name of letters, digits, #, - and _ at a domain"}}async provisionAccount(t,e){const a=e[this.mailAttr],i=e[this.quotaAttr],s=e[this.aliasAttr];if(!a||isGroup(e.objectClass))return;const r=Array.isArray(a)?String(a[0]):String(a);if(this.initDelay>0&&await new Promise(t=>setTimeout(t,this.initDelay)),i){const e=Array.isArray(i)?Number(i[0]):Number(i);!isNaN(e)&&e>0&&await this.callWebAdminApi("ldapadddone:quota",`${this.webadminUrl}/quota/users/${r}/size`,"PUT",t,e.toString(),{mail:r,quota:e},o)}const n=this.getAliases(s);await Promise.all([this.dropStaleForwards(t,r,this.ownForwards(e[this.forwardAttr])),...n.map(e=>this.dropStaleForwards(t,e,[]))]),n.length>0&&await Promise.all(n.map(e=>this.callWebAdminApi("ldapadddone:alias",`${this.webadminUrl}/address/aliases/${r}/sources/${e}`,"PUT",t,null,{mail:r,alias:e},o)));const l=this.getDisplayNameFromAttributes(e);l&&await this.updateJamesIdentity(t,r,l)}async followTeamMailbox(t,e){const a=this.teamMailboxOf(t),i=this.teamMailboxOf(e);if(!a&&!i)return;const s=String((e??t).dn);i?.refused&&i.refused!==a?.refused&&this.logger.error({plugin:this.name,event:"teamMailbox",dn:s,mail:i.mail,error:i.refused});const r=a&&!a.refused?a.mail:null,n=i&&!i.refused?i.mail:null;if(!r&&!n)return;const o=memberKey(s),follow=async()=>{if(r&&n&&r.toLowerCase()===n.toLowerCase()){const a=new Map(members(t).map(t=>[memberKey(t),t])),i=[];for(const t of members(e))a.delete(memberKey(t))||i.push(t);return void await Promise.all([this.teamMailboxMembers(s,n,"PUT",i),this.teamMailboxMembers(s,n,"DELETE",[...a.values()])])}r&&await this.teamMailboxMembers(s,r,"DELETE",members(t)),n&&await this.createTeamMailbox(s,e,n)},l=(this.teamMailboxQueue.get(o)??Promise.resolve()).then(follow,follow);this.teamMailboxQueue.set(o,l);try{await l}finally{this.teamMailboxQueue.get(o)===l&&this.teamMailboxQueue.delete(o)}}teamMailboxUrl(t){const e=t.toLowerCase(),a=e.lastIndexOf("@");return`${this.webadminUrl}/domains/${e.slice(a+1)}/team-mailboxes/${encodeURIComponent(e.slice(0,a))}`}async teamMailboxMembers(t,e,a,i){if(!i.length)return;const s=this.teamMailboxUrl(e),r=await this.getMemberEmails(i);await Promise.all(r.map(i=>this.callWebAdminApi("teamMailbox:"+("PUT"===a?"addMember":"removeMember"),`${s}/members/${i}`,a,t,null,{teamMailbox:e,memberMail:i})))}getDisplayNameFromAttributes(t){const e=this.attributeToString(t[this.displayNameAttr]);if(e)return e;const a=this.attributeToString(t.cn);if(a)return a;const i=this.attributeToString(t.givenName),s=this.attributeToString(t.sn);if(i||s){const t=[];return i&&t.push(i),s&&t.push(s),t.join(" ")}const r=this.attributeToString(t[this.mailAttr]);return r||null}async isGroupDn(t){const e=await this.ldapGetAttributes(t,["objectClass"]);return isGroup(e?.objectClass)}async getMailFromDN(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}async getDisplayNameFromDN(t){const e=[this.displayNameAttr,"cn","givenName","sn",this.mailAttr],a=await this.ldapGetAttributes(t,e);return a?this.getDisplayNameFromAttributes(a):null}async generateSignature(t){const e=this.config.james_signature_template;if(!e)return null;const a=await this.ldapGetAttributes(t);if(!a)return null;let i=e;return i=i.replace(/\{(\w+)\}/g,(t,e)=>"string"==typeof e&&a&&"object"==typeof a&&Object.prototype.hasOwnProperty.call(a,e)&&this.attributeToString(a[e])||""),i}async updateJamesIdentity(t,e,a,i={}){const s=(this.identityQueue.get(e)??Promise.resolve()).then(()=>this.doUpdateJamesIdentity(t,e,a,i));this.identityQueue.set(e,s);try{await s}finally{this.identityQueue.get(e)===s&&this.identityQueue.delete(e)}}async doUpdateJamesIdentity(e,a,i,s){const r={plugin:this.name,event:"onLdapDisplayNameChange",result:"error",dn:e,mail:a,displayName:i};for(let n=1;;n++){try{const l=`${this.webadminUrl}/users/${a}/identities`,d=await this.requestLimit(()=>t(`${l}?default=true`,{method:"GET",headers:this.createHeaders()}));let u;if(d.ok)u=pickDefaultIdentity(await d.json());else if(404!==d.status){const t="get_default_identity";if(this.shouldRetry(d.status,o,n)){await this.warnAndWait(r,t,n,d);continue}return void this.logger.error({...r,step:t,...n>1?{attempts:n}:{},http_status:d.status,http_status_text:d.statusText})}const m=await this.generateSignature(e),h=u?.id?{id:u.id,email:u.email,name:i}:{email:a,name:i,sortOrder:0};m?h.htmlSignature=m:Object.assign(h,s);const c=u?.id?"update_identity":"create_identity",g=await this.requestLimit(()=>t(u?.id?`${l}/${u.id}`:l,{method:u?.id?"PUT":"POST",headers:this.createHeaders("application/json"),body:JSON.stringify(h)}));if(g.ok)this.logger.info({...r,step:c,result:"success",http_status:g.status});else{if(this.shouldRetry(g.status,o,n)){await this.warnAndWait(r,c,n,g);continue}this.logger.error({...r,step:c,...n>1?{attempts:n}:{},http_status:g.status,http_status_text:g.statusText})}}catch(t){this.logger.error({...r,error:`${t}`})}return}}async defaultSignatures(e){try{const a=await this.requestLimit(()=>t(`${this.webadminUrl}/users/${e}/identities?default=true`,{method:"GET",headers:this.createHeaders()}));if(!a.ok)return{};const i=pickDefaultIdentity(await a.json()),s={};return i?.htmlSignature&&(s.htmlSignature=i.htmlSignature),i?.textSignature&&(s.textSignature=i.textSignature),s}catch(t){return this.logger.debug(`Could not read the identities of ${e}: ${t}`),{}}}ownForwards(t){return this.config.james_sync_forwards?this.getAliases(t):[]}async dropStaleForwards(e,a,i){let s;try{const r=await this.requestLimit(()=>t(`${this.webadminUrl}/address/forwards/${a}`,{method:"GET",headers:this.createHeaders()}));if(!r.ok)return void(404!==r.status&&this.logger.warn({plugin:this.name,event:"dropStaleForward",result:"error",dn:e,address:a,step:"list_forwards",http_status:r.status,http_status_text:r.statusText}));const n=i.map(t=>t.toLowerCase());s=(await r.json()).map(t=>t.mailAddress).filter(t=>t&&!n.includes(t.toLowerCase()))}catch(t){return void this.logger.debug(`Could not read the forwards of ${a}: ${t}`)}await Promise.all(s.map(t=>this.callWebAdminApi("dropStaleForward",`${this.webadminUrl}/address/forwards/${a}/targets/${t}`,"DELETE",e,null,{address:a,forward:t})))}async warnAndWait(t,e,a,i){this.logger.warn({...t,step:e,result:"retry",attempt:a,http_status:i.status,http_status_text:i.statusText}),await this.waitBeforeRetry(a)}async deleteUserData(e,a){const i={plugin:this.name,event:"deleteUserData",mail:e,fromStep:a};try{const s=new URL(`${this.webadminUrl}/users/${e}`);s.searchParams.set("action","deleteData"),a&&s.searchParams.set("fromStep",a);const r=await this.requestLimit(()=>t(s.toString(),{method:"POST",headers:this.createHeaders()}));if(!r.ok)return this.logger.error({...i,http_status:r.status,http_status_text:r.statusText}),null;const n=await r.json();return this.logger.info({...i,http_status:r.status,taskId:n.taskId}),n}catch(t){return this.logger.error({...i,error:`${t}`}),null}}getMailboxType(t){const e=t[this.mailboxTypeAttr];if(!e)return null;const a=(Array.isArray(e)?String(e[0]):String(e)).match(/^cn=([^,]+)/);if(!a)return null;const i=a[1];return"group"===i||"mailingList"===i||"teamMailbox"===i?i:null}async createTeamMailbox(t,e,a){const i=this.teamMailboxUrl(a);await this.callWebAdminApi("teamMailbox:create",i,"PUT",t,null,{teamMailbox:a}),await this.teamMailboxMembers(t,a,"PUT",members(e))}async getMemberEmails(t){const e=t.filter(t=>!s(t,this.config.group_dummy_user)).map(t=>this.server.ldap.queryLimit(async()=>{const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}));return(await Promise.all(e)).filter(t=>null!==t)}async getGroupMail(t){const e=await this.ldapGetAttributes(t,["mail"]);return e?this.attributeToString(e.mail):null}async getGroupMailboxType(t){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.mailboxTypeAttr]},t);if(e.searchEntries&&e.searchEntries.length>0)return this.getMailboxType(e.searchEntries[0])}catch(e){const a=e instanceof Error?e.message:String(e);this.logger.debug(`Could not get mailbox type for group ${t}: ${a}`)}return null}async _handleDelegationChange(t,e){const a=await this.ldapGetAttributes(t,[this.mailAttr]);if(!a)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"Could not find user entry to get mail attribute"});const i=a[this.mailAttr];if(!i||"string"!=typeof i)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"User has no mail attribute, cannot manage delegation"});const s=this.config.delegation_attribute;if(!s)return;const[r,n]=e[s]||[],l=this._normalizeToArray(r),d=this._normalizeToArray(n),u=d.filter(t=>!l.includes(t)),m=l.filter(t=>!d.includes(t)),h=u.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),c=m.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),[g,p]=await Promise.all([Promise.all(h),Promise.all(c)]),f=[];for(const{dn:e,email:a}of g)a&&f.push(this.callWebAdminApi("onLdapChange:addDelegation",`${this.webadminUrl}/users/${i}/authorizedUsers/${a}`,"PUT",t,null,{userMail:i,delegateEmail:a,delegateDN:e,action:"add"},o));for(const{dn:e,email:a}of p)a&&f.push(this.callWebAdminApi("onLdapChange:removeDelegation",`${this.webadminUrl}/users/${i}/authorizedUsers/${a}`,"DELETE",t,null,{userMail:i,delegateEmail:a,delegateDN:e,action:"remove"}));await Promise.all(f)}async _getDelegateEmail(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):(this.logger.warn({plugin:this.name,event:"getDelegateEmail",dn:t,message:"Could not resolve delegate DN to email"}),null)}_normalizeToArray(t){return t?Array.isArray(t)?t:[t]:[]}}export{James as default};
|
|
@@ -239,6 +239,15 @@ export default abstract class LdapFlat extends DmPlugin {
|
|
|
239
239
|
* @returns lowercased attribute names
|
|
240
240
|
*/
|
|
241
241
|
protected hiddenAttributes(): Set<string>;
|
|
242
|
+
/**
|
|
243
|
+
* Attributes to ask for when reading one entry. `*` brings the user
|
|
244
|
+
* attributes only: an operational one the schema declares, such as
|
|
245
|
+
* `memberOf`, comes back only when named. A name the directory does not
|
|
246
|
+
* define is ignored by it.
|
|
247
|
+
*
|
|
248
|
+
* @returns attribute list
|
|
249
|
+
*/
|
|
250
|
+
protected entryAttributes(): string[];
|
|
242
251
|
/**
|
|
243
252
|
* Remove the `neverReturn` attributes from an entry about to be serialised.
|
|
244
253
|
* LDAP attribute names are case-insensitive and may carry options
|
|
@@ -294,17 +303,6 @@ export default abstract class LdapFlat extends DmPlugin {
|
|
|
294
303
|
* @returns true when the DN is taken
|
|
295
304
|
*/
|
|
296
305
|
protected rdnExists(value: string): Promise<boolean>;
|
|
297
|
-
/**
|
|
298
|
-
* Build the rejection message for a value that failed its schema `test`.
|
|
299
|
-
*
|
|
300
|
-
* When the schema carries a `hint`, the message says what a valid value
|
|
301
|
-
* looks like instead of only that this one was not. The hint lives next to
|
|
302
|
-
* the pattern so the two cannot drift apart, and so a client can show it
|
|
303
|
-
* under the field *before* the user gets it wrong.
|
|
304
|
-
*
|
|
305
|
-
* @param field attribute name
|
|
306
|
-
* @returns message for a `BadRequestError`
|
|
307
|
-
*/
|
|
308
306
|
protected invalidValueMessage(field: string): string;
|
|
309
307
|
/**
|
|
310
308
|
* API routes
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypeScript types for LDAP Group Editor
|
|
3
3
|
*/
|
|
4
|
+
import type { LocalizedText } from '../shared/types';
|
|
4
5
|
export interface GroupEditorOptions {
|
|
5
6
|
containerId: string;
|
|
6
7
|
apiBaseUrl?: string;
|
|
@@ -45,7 +46,7 @@ export interface SchemaAttribute {
|
|
|
45
46
|
type: string;
|
|
46
47
|
branch?: string[];
|
|
47
48
|
};
|
|
48
|
-
group?:
|
|
49
|
+
group?: LocalizedText;
|
|
49
50
|
}
|
|
50
51
|
export interface LdapGroup {
|
|
51
52
|
dn: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LDAP Resource Editor - Generic Type Definitions
|
|
3
3
|
*/
|
|
4
|
+
import type { LocalizedText } from '../shared/types';
|
|
4
5
|
export type ResourceType = 'users' | 'groups' | 'organizations';
|
|
5
6
|
export type NavigationType = 'flat' | 'tree';
|
|
6
7
|
export interface EditorOptions {
|
|
@@ -19,13 +20,13 @@ export interface SchemaAttribute {
|
|
|
19
20
|
type: string;
|
|
20
21
|
required?: boolean;
|
|
21
22
|
fixed?: boolean;
|
|
22
|
-
role?: string;
|
|
23
|
+
role?: string | string[];
|
|
23
24
|
test?: string;
|
|
24
25
|
branch?: string[];
|
|
25
26
|
items?: SchemaAttribute;
|
|
26
27
|
default?: unknown;
|
|
27
28
|
ui?: 'select' | 'search';
|
|
28
|
-
group?:
|
|
29
|
+
group?: LocalizedText;
|
|
29
30
|
}
|
|
30
31
|
export interface Schema {
|
|
31
32
|
entity: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypeScript types for LDAP Unit Editor
|
|
3
3
|
*/
|
|
4
|
+
import type { LocalizedText } from '../shared/types';
|
|
4
5
|
export interface UnitEditorOptions {
|
|
5
6
|
containerId: string;
|
|
6
7
|
apiBaseUrl?: string;
|
|
@@ -45,7 +46,7 @@ export interface SchemaAttribute {
|
|
|
45
46
|
type: string;
|
|
46
47
|
branch?: string[];
|
|
47
48
|
};
|
|
48
|
-
group?:
|
|
49
|
+
group?: LocalizedText;
|
|
49
50
|
}
|
|
50
51
|
export interface LdapUnit {
|
|
51
52
|
dn: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LDAP User Editor - Type definitions
|
|
3
3
|
*/
|
|
4
|
+
import type { LocalizedText } from '../shared/types';
|
|
4
5
|
export interface EditorOptions {
|
|
5
6
|
containerId: string;
|
|
6
7
|
apiBaseUrl?: string;
|
|
@@ -15,13 +16,13 @@ export interface SchemaAttribute {
|
|
|
15
16
|
type: string;
|
|
16
17
|
required?: boolean;
|
|
17
18
|
fixed?: boolean;
|
|
18
|
-
role?: string;
|
|
19
|
+
role?: string | string[];
|
|
19
20
|
test?: string;
|
|
20
21
|
branch?: string[];
|
|
21
22
|
items?: SchemaAttribute;
|
|
22
23
|
default?: unknown;
|
|
23
24
|
ui?: 'select' | 'search';
|
|
24
|
-
group?:
|
|
25
|
+
group?: LocalizedText;
|
|
25
26
|
}
|
|
26
27
|
export interface Schema {
|
|
27
28
|
entity: {
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Shared TypeScript types for browser components
|
|
3
3
|
* @module browser/shared/types
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Text of a schema in one language (a plain string) or several (a map keyed
|
|
7
|
+
* by language tag), see `localizedText`
|
|
8
|
+
*/
|
|
9
|
+
export type LocalizedText = string | Record<string, string>;
|
|
5
10
|
export interface EditorConfig {
|
|
6
11
|
ldapBase?: string;
|
|
7
12
|
features?: {
|
|
@@ -27,13 +32,14 @@ export interface SchemaAttribute {
|
|
|
27
32
|
type: 'string' | 'number' | 'integer' | 'array' | 'pointer';
|
|
28
33
|
required?: boolean;
|
|
29
34
|
fixed?: boolean;
|
|
35
|
+
role?: string | string[];
|
|
30
36
|
default?: unknown;
|
|
31
37
|
branch?: string[];
|
|
32
38
|
items?: {
|
|
33
39
|
type: string;
|
|
34
40
|
branch?: string[];
|
|
35
41
|
};
|
|
36
|
-
group?:
|
|
42
|
+
group?: LocalizedText;
|
|
37
43
|
}
|
|
38
44
|
export interface PointerOption {
|
|
39
45
|
dn: string;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Schema utility functions
|
|
3
3
|
* @module browser/shared/utils/schema
|
|
4
4
|
*/
|
|
5
|
-
import type { PointerOption } from '../types';
|
|
5
|
+
import type { LocalizedText, PointerOption, SchemaAttribute } from '../types';
|
|
6
6
|
/**
|
|
7
7
|
* Load pointer options from API
|
|
8
8
|
*/
|
|
@@ -27,3 +27,22 @@ export declare function getOrganizationPath(orgDn: string, apiBaseUrl: string):
|
|
|
27
27
|
* Load schema from URL or object
|
|
28
28
|
*/
|
|
29
29
|
export declare function loadSchema<T = unknown>(schemaSource: string | T): Promise<T>;
|
|
30
|
+
/**
|
|
31
|
+
* Tell whether an attribute carries a role: a schema names one, or several.
|
|
32
|
+
*
|
|
33
|
+
* @param attr attribute definition
|
|
34
|
+
* @param role role to look for
|
|
35
|
+
* @returns true when the attribute declares that role
|
|
36
|
+
*/
|
|
37
|
+
export declare function hasRole(attr: Pick<SchemaAttribute, 'role'> | undefined, role: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Pick the text of a {@link LocalizedText} for a language: the exact tag,
|
|
40
|
+
* then its primary language, then English, then whatever the map holds.
|
|
41
|
+
*
|
|
42
|
+
* The editors are written in English, so English is the default.
|
|
43
|
+
*
|
|
44
|
+
* @param text plain string or map keyed by language tag
|
|
45
|
+
* @param language tag to look for
|
|
46
|
+
* @returns the text, or undefined when there is none
|
|
47
|
+
*/
|
|
48
|
+
export declare function localizedText(text: LocalizedText | undefined, language?: string): string | undefined;
|
|
@@ -170,6 +170,7 @@ export interface Config {
|
|
|
170
170
|
james_retry_delay?: number;
|
|
171
171
|
james_mailing_list_branch?: string[];
|
|
172
172
|
james_mailbox_type_attribute?: string;
|
|
173
|
+
james_sync_forwards?: boolean;
|
|
173
174
|
twake_drive_webadmin_url?: string;
|
|
174
175
|
twake_drive_webadmin_token?: string;
|
|
175
176
|
twake_drive_concurrency?: number;
|
|
@@ -49,6 +49,29 @@ export type SchemaRole = string;
|
|
|
49
49
|
* rather than in a catalogue shipped with the code.
|
|
50
50
|
*/
|
|
51
51
|
export type LocalizedText = string | Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Pick the text of a {@link LocalizedText} for a language: the exact tag,
|
|
54
|
+
* then its primary language, then English, then whatever the map holds.
|
|
55
|
+
*
|
|
56
|
+
* @param text plain string or map keyed by language tag
|
|
57
|
+
* @param language tag to look for, English when omitted
|
|
58
|
+
* @returns the text, or undefined when there is none
|
|
59
|
+
*/
|
|
60
|
+
export declare function localizedText(text: LocalizedText | undefined, language?: string): string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Refusal of a value that failed its schema `test`, for a `BadRequestError`.
|
|
63
|
+
*
|
|
64
|
+
* When the schema carries a `hint`, the message says what a valid value
|
|
65
|
+
* looks like instead of only that this one was not. The hint lives next to
|
|
66
|
+
* the pattern so the two cannot drift apart, and so a client can show it
|
|
67
|
+
* under the field *before* the user gets it wrong. Its English text is
|
|
68
|
+
* quoted, the language of every other API message.
|
|
69
|
+
*
|
|
70
|
+
* @param name attribute name
|
|
71
|
+
* @param attr its schema definition
|
|
72
|
+
* @returns the message
|
|
73
|
+
*/
|
|
74
|
+
export declare function invalidValueMessage(name: string, attr: SchemaAttribute | undefined): string;
|
|
52
75
|
/** Roles the core itself looks for. See {@link SchemaRole}. */
|
|
53
76
|
export declare const CORE_ROLES: readonly ["identifier", "displayName", "primaryEmail", "emailAliases", "emailQuota", "organizationLink", "organizationPath", "members", "owners", "accountStatus", "password", "passwordReset", "accountExpiry", "domainLink", "domainName"];
|
|
54
77
|
/**
|
|
@@ -114,7 +137,7 @@ export interface SchemaAttribute {
|
|
|
114
137
|
items?: {
|
|
115
138
|
type: string;
|
|
116
139
|
test?: string | RegExp;
|
|
117
|
-
hint?:
|
|
140
|
+
hint?: LocalizedText;
|
|
118
141
|
branch?: string[];
|
|
119
142
|
};
|
|
120
143
|
default?: AttributeValue;
|
|
@@ -126,7 +149,7 @@ export interface SchemaAttribute {
|
|
|
126
149
|
* schema so the pattern and its explanation cannot drift apart, and so a
|
|
127
150
|
* client never has to hardcode either.
|
|
128
151
|
*/
|
|
129
|
-
hint?:
|
|
152
|
+
hint?: LocalizedText;
|
|
130
153
|
branch?: string[];
|
|
131
154
|
fixed?: boolean;
|
|
132
155
|
/** Semantic role(s) — see {@link SchemaRole} */
|
|
@@ -209,8 +232,8 @@ export interface SchemaAttribute {
|
|
|
209
232
|
* under `example.org`. Off by default: the domain must match exactly.
|
|
210
233
|
*/
|
|
211
234
|
allowSubdomains?: boolean;
|
|
212
|
-
/**
|
|
213
|
-
group?:
|
|
235
|
+
/** Section of a form the attribute belongs to, e.g. `Mailbox settings` */
|
|
236
|
+
group?: LocalizedText;
|
|
214
237
|
/** Name a client shows for this attribute, in one or several languages */
|
|
215
238
|
label?: LocalizedText;
|
|
216
239
|
}
|
|
@@ -18,4 +18,15 @@ export default class ExternalUsersInGroups extends DmPlugin {
|
|
|
18
18
|
ldap: ldapActions;
|
|
19
19
|
constructor(server: DM);
|
|
20
20
|
hooks: Hooks;
|
|
21
|
+
/**
|
|
22
|
+
* The address of an external member, unescaped: the value of a `mail` RDN
|
|
23
|
+
* right under the branch. Null for any other member.
|
|
24
|
+
*/
|
|
25
|
+
externalAddress(member: string): string | null;
|
|
26
|
+
/** `--mail-domain`, lower-cased: the domains no external member may use */
|
|
27
|
+
managedDomains(): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Where a client sends an external member: `mail=<escaped address>,<branch>`
|
|
30
|
+
*/
|
|
31
|
+
getConfigApiData(): Record<string, unknown>;
|
|
21
32
|
}
|
|
@@ -231,14 +231,6 @@ export default class LdapOrganizations extends DmPlugin {
|
|
|
231
231
|
addOrganization(dn: string, entry: AttributesList, req?: Request): Promise<boolean>;
|
|
232
232
|
modifyOrganization(dn: string, changes: ModifyRequest, req?: Request): Promise<boolean>;
|
|
233
233
|
validateNewOrganization(dn: string, entry: AttributesList): Promise<boolean>;
|
|
234
|
-
/**
|
|
235
|
-
* Build the rejection message for a value that failed its schema `test`,
|
|
236
|
-
* quoting the `hint` when the schema carries one so the answer says what a
|
|
237
|
-
* valid value looks like.
|
|
238
|
-
*
|
|
239
|
-
* @param field attribute name
|
|
240
|
-
* @returns message for a `BadRequestError`
|
|
241
|
-
*/
|
|
242
234
|
invalidValueMessage(field: string): string;
|
|
243
235
|
validateChanges(dn: string, changes: ModifyRequest): Promise<boolean>;
|
|
244
236
|
_validateOneChange(field: string, value: AttributeValue | null): Promise<boolean>;
|
|
@@ -5,53 +5,16 @@ import type { DM } from '../../bin';
|
|
|
5
5
|
import type { AttributeValue } from '../../lib/ldapActions';
|
|
6
6
|
import { Hooks } from '../../hooks';
|
|
7
7
|
import type { ChangesToNotify } from '../ldap/onChange';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* properties:
|
|
19
|
-
* count:
|
|
20
|
-
* type: object
|
|
21
|
-
* description: Message-count quota.
|
|
22
|
-
* properties:
|
|
23
|
-
* used:
|
|
24
|
-
* type: integer
|
|
25
|
-
* description: Number of messages currently stored.
|
|
26
|
-
* example: 142
|
|
27
|
-
* max:
|
|
28
|
-
* type: integer
|
|
29
|
-
* nullable: true
|
|
30
|
-
* description: |
|
|
31
|
-
* Maximum number of messages allowed. `null` means unlimited.
|
|
32
|
-
* example: 10000
|
|
33
|
-
* size:
|
|
34
|
-
* type: object
|
|
35
|
-
* description: Storage-size quota (bytes).
|
|
36
|
-
* properties:
|
|
37
|
-
* used:
|
|
38
|
-
* type: integer
|
|
39
|
-
* description: Bytes currently used.
|
|
40
|
-
* example: 524288000
|
|
41
|
-
* max:
|
|
42
|
-
* type: integer
|
|
43
|
-
* nullable: true
|
|
44
|
-
* description: |
|
|
45
|
-
* Maximum bytes allowed. `null` means unlimited.
|
|
46
|
-
* example: 2147483648
|
|
47
|
-
* example:
|
|
48
|
-
* count:
|
|
49
|
-
* used: 142
|
|
50
|
-
* max: 10000
|
|
51
|
-
* size:
|
|
52
|
-
* used: 524288000
|
|
53
|
-
* max: 2147483648
|
|
54
|
-
*/
|
|
8
|
+
type Identity = {
|
|
9
|
+
id: string;
|
|
10
|
+
email: string;
|
|
11
|
+
mayDelete?: boolean;
|
|
12
|
+
sortOrder?: number;
|
|
13
|
+
htmlSignature?: string;
|
|
14
|
+
textSignature?: string;
|
|
15
|
+
};
|
|
16
|
+
/** Signatures an identity carries over, see {@link Identity} */
|
|
17
|
+
export type Signatures = Pick<Identity, 'htmlSignature' | 'textSignature'>;
|
|
55
18
|
export default class James extends TwakePlugin {
|
|
56
19
|
name: string;
|
|
57
20
|
roles: Role[];
|
|
@@ -62,6 +25,7 @@ export default class James extends TwakePlugin {
|
|
|
62
25
|
private quotaAttr;
|
|
63
26
|
private aliasAttr;
|
|
64
27
|
private mailboxTypeAttr;
|
|
28
|
+
private forwardAttr;
|
|
65
29
|
private initDelay;
|
|
66
30
|
constructor(server: DM);
|
|
67
31
|
/**
|
|
@@ -74,14 +38,37 @@ export default class James extends TwakePlugin {
|
|
|
74
38
|
private getUserQuota;
|
|
75
39
|
hooks: Hooks;
|
|
76
40
|
/**
|
|
77
|
-
*
|
|
41
|
+
* The address of the team mailbox an entry stands for, and why it is
|
|
42
|
+
* refused, if it is: it lies in a mailing list branch, or TMail cannot
|
|
43
|
+
* name a team mailbox after its address.
|
|
78
44
|
*/
|
|
79
|
-
private
|
|
45
|
+
private teamMailboxOf;
|
|
46
|
+
/**
|
|
47
|
+
* Give James what an account holds: quota, aliases and identity, after
|
|
48
|
+
* dropping the forwards left on its addresses. Runs when an entry is
|
|
49
|
+
* added with a mail, or gains one.
|
|
50
|
+
*/
|
|
51
|
+
private provisionAccount;
|
|
52
|
+
/**
|
|
53
|
+
* Bring James from the team mailbox an entry was to the one it is. A
|
|
54
|
+
* mailbox the entry no longer stands for is kept, emptied of its members.
|
|
55
|
+
*/
|
|
56
|
+
private followTeamMailbox;
|
|
57
|
+
private teamMailboxQueue;
|
|
58
|
+
/**
|
|
59
|
+
* The WebAdmin path of the team mailbox at an address
|
|
60
|
+
* {@link teamMailboxOf} accepted: TMail names it by the local part, under
|
|
61
|
+
* its domain. Lower-cased, so that a change of case only, which keeps the
|
|
62
|
+
* mailbox, keeps its path.
|
|
63
|
+
*/
|
|
64
|
+
private teamMailboxUrl;
|
|
65
|
+
private teamMailboxMembers;
|
|
80
66
|
/**
|
|
81
67
|
* Extract display name from LDAP attributes
|
|
82
68
|
* Fallback logic: displayName → cn → givenName+sn → mail
|
|
83
69
|
*/
|
|
84
70
|
private getDisplayNameFromAttributes;
|
|
71
|
+
private isGroupDn;
|
|
85
72
|
getMailFromDN(dn: string): Promise<string | null>;
|
|
86
73
|
getDisplayNameFromDN(dn: string): Promise<string | null>;
|
|
87
74
|
generateSignature(dn: string): Promise<string | null>;
|
|
@@ -100,10 +87,35 @@ export default class James extends TwakePlugin {
|
|
|
100
87
|
* Calls for one address run one after the other: an add triggers this twice
|
|
101
88
|
* (ldapadddone, then the display name "change" onChange publishes), and two
|
|
102
89
|
* concurrent first calls would both find no default and both create one.
|
|
90
|
+
*
|
|
91
|
+
* `signatures` are written when `--james-signature-template` is not set:
|
|
92
|
+
* those of the identity the user had before a rename.
|
|
103
93
|
*/
|
|
104
|
-
updateJamesIdentity(dn: string, mail: string, displayName: string): Promise<void>;
|
|
94
|
+
updateJamesIdentity(dn: string, mail: string, displayName: string, signatures?: Signatures): Promise<void>;
|
|
105
95
|
private identityQueue;
|
|
106
96
|
private doUpdateJamesIdentity;
|
|
97
|
+
/**
|
|
98
|
+
* The signatures of an address's default identity, none when it has none
|
|
99
|
+
* or James cannot be read.
|
|
100
|
+
*/
|
|
101
|
+
private defaultSignatures;
|
|
102
|
+
/**
|
|
103
|
+
* The forwards the directory gives an entry, those James is to keep on its
|
|
104
|
+
* address: none unless the directory owns forwards
|
|
105
|
+
* (`--james-sync-forwards`).
|
|
106
|
+
*/
|
|
107
|
+
private ownForwards;
|
|
108
|
+
/**
|
|
109
|
+
* Remove the forwards James holds from an address an entry takes, but
|
|
110
|
+
* those in `keep`: the entry's own. A rename leaves one from the old
|
|
111
|
+
* address to the new, and its next owner would not get its mail.
|
|
112
|
+
*
|
|
113
|
+
* Runs whether forwards are synced or not: Twake Mail keeps a user's
|
|
114
|
+
* forwards under their own address, and none can have been set yet on an
|
|
115
|
+
* address the account is only now taking. The rename calls this before
|
|
116
|
+
* James moves the user's forwards to the new address.
|
|
117
|
+
*/
|
|
118
|
+
private dropStaleForwards;
|
|
107
119
|
private warnAndWait;
|
|
108
120
|
/**
|
|
109
121
|
* Delete user data via James WebAdmin API
|
|
@@ -113,40 +125,15 @@ export default class James extends TwakePlugin {
|
|
|
113
125
|
deleteUserData(mail: string, fromStep?: string): Promise<{
|
|
114
126
|
taskId: string;
|
|
115
127
|
} | null>;
|
|
116
|
-
/**
|
|
117
|
-
* Type guard to check if delete operation is an AttributesList
|
|
118
|
-
*/
|
|
119
|
-
private isAttributesList;
|
|
120
128
|
/**
|
|
121
129
|
* Extract mailbox type from group attributes
|
|
122
130
|
* Returns: 'group', 'mailingList', 'teamMailbox', or null if not set
|
|
123
131
|
*/
|
|
124
132
|
private getMailboxType;
|
|
125
|
-
/**
|
|
126
|
-
* Validate that a DN is within one of the allowed branches
|
|
127
|
-
*/
|
|
128
|
-
private isInAllowedBranches;
|
|
129
|
-
/**
|
|
130
|
-
* Create a mailing list in James
|
|
131
|
-
*/
|
|
132
|
-
private createMailingList;
|
|
133
133
|
/**
|
|
134
134
|
* Create a team mailbox in James
|
|
135
135
|
*/
|
|
136
136
|
private createTeamMailbox;
|
|
137
|
-
/**
|
|
138
|
-
* Delete a team mailbox from James
|
|
139
|
-
*/
|
|
140
|
-
private deleteTeamMailbox;
|
|
141
|
-
/**
|
|
142
|
-
* Remove all members from a team mailbox (without deleting it)
|
|
143
|
-
* This preserves the mailbox for potential future use
|
|
144
|
-
*/
|
|
145
|
-
private removeAllTeamMailboxMembers;
|
|
146
|
-
/**
|
|
147
|
-
* Delete a mailing list from James
|
|
148
|
-
*/
|
|
149
|
-
private deleteMailingList;
|
|
150
137
|
/**
|
|
151
138
|
* Get email addresses for a list of member DNs
|
|
152
139
|
* Uses p-limit to parallelize LDAP queries while limiting concurrency
|
|
@@ -164,3 +151,4 @@ export default class James extends TwakePlugin {
|
|
|
164
151
|
_getDelegateEmail(dn: string): Promise<string | null>;
|
|
165
152
|
_normalizeToArray(value: AttributeValue | null): string[];
|
|
166
153
|
}
|
|
154
|
+
export {};
|
|
@@ -875,7 +875,7 @@ Ensure all required fields are included:
|
|
|
875
875
|
|
|
876
876
|
```json
|
|
877
877
|
{
|
|
878
|
-
"error": "Invalid value for attribute \"mail\":
|
|
878
|
+
"error": "Invalid value for attribute \"mail\": Expected an email address, e.g. jane.doe@example.org"
|
|
879
879
|
}
|
|
880
880
|
```
|
|
881
881
|
|
|
@@ -245,6 +245,8 @@ curl -H "Accept: application/json" \
|
|
|
245
245
|
- `features.ldapFlatGeneric.flatResources`: Array of flat LDAP resources (users, positions, etc.)
|
|
246
246
|
- `features.ldapGroups`: Group management configuration (if enabled)
|
|
247
247
|
- `features.ldapOrganizations`: Organization tree configuration (if enabled)
|
|
248
|
+
- `features.externalUsersInGroups`: where to send an external group member
|
|
249
|
+
(if enabled), [details](../../usage/plugins/ldap/external-users.md#config-api)
|
|
248
250
|
- `features.openidconnect.endpoints.logout`: the logout route, when OpenID Connect
|
|
249
251
|
guards the server ([details](../../usage/plugins/auth/oidc.md#logout))
|
|
250
252
|
|
|
@@ -1075,6 +1077,16 @@ curl -X POST \
|
|
|
1075
1077
|
}
|
|
1076
1078
|
```
|
|
1077
1079
|
|
|
1080
|
+
A member that does not exist answers 400 naming it, and none of the members
|
|
1081
|
+
is added (unless `--group-allow-unexistent-members` is set). Group creation
|
|
1082
|
+
checks its `member` list the same way:
|
|
1083
|
+
|
|
1084
|
+
```json
|
|
1085
|
+
{
|
|
1086
|
+
"error": "Member uid=nobody,ou=users,dc=example,dc=com not found"
|
|
1087
|
+
}
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1078
1090
|
### Remove Group Member
|
|
1079
1091
|
|
|
1080
1092
|
Remove a user from a group.
|
|
@@ -1146,7 +1158,7 @@ All errors return a JSON object with an error message:
|
|
|
1146
1158
|
|
|
1147
1159
|
```json
|
|
1148
1160
|
{
|
|
1149
|
-
"error": "Invalid value for attribute \"uid\"
|
|
1161
|
+
"error": "Invalid value for attribute \"uid\""
|
|
1150
1162
|
}
|
|
1151
1163
|
```
|
|
1152
1164
|
|