ldap-rest 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node-fetch";import{TwakePlugin as i}from"../../abstract/twakePlugin.js";class Drive extends i{constructor(t){super(t,"twake_drive_webadmin_url","twake_drive_webadmin_token","twake_drive_concurrency"),this.name="drive",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={onLdapMailChange:async(t,i,e)=>{const a=i?Array.isArray(i)?String(i[0]):String(i):null,n=e?Array.isArray(e)?String(e[0]):String(e):null;if(!a)return void this.logger.debug(`Skipping mail change for ${t}: oldmail is empty (mail attribute was added, not changed)`);if(!n)return void this.logger.debug(`Skipping mail change for ${t}: newmail is empty (mail attribute was deleted)`);const r=await this.getCozyDomain(t);r?await this.updateCozyInstance("onLdapMailChange",r,t,{Email:n}):this.logger.debug(`Skipping mail change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`)},onLdapDisplayNameChange:async(t,i,e)=>{const a=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.displayNameAttr,"cn","givenName","sn"]);if(!a)return void this.logger.warn(`Cannot update Cozy display name: entry not found for ${t}`);const n=this.attributeToString(a[this.cozyDomainAttr]);if(!n)return void this.logger.debug(`Skipping display name change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`);const r=this.getDisplayNameFromAttributes(a);r?await this.updateCozyInstance("onLdapDisplayNameChange",n,t,{PublicName:r}):this.logger.warn(`Cannot update Cozy display name: no display name found for ${t}`)},onLdapDriveQuotaChange:async(t,i,e)=>{if(null===e)return void this.logger.debug(`Skipping drive quota change for ${t}: quota was deleted`);const a=await this.getCozyDomain(t);a?await this.updateCozyInstance("onLdapDriveQuotaChange",a,t,{DiskQuota:String(e)}):this.logger.debug(`Skipping drive quota change for ${t}: no Twake Drive domain attribute (${this.cozyDomainAttr})`)}},this.cozyDomainAttr=this.config.twake_drive_domain_attribute||"twakeCozyDomain",this.defaultDomainTemplate=this.config.twake_drive_default_domain_template||"",this.templateAttrs=this.defaultDomainTemplate?[...this.defaultDomainTemplate.matchAll(/\{(\w+)\}/g)].map(t=>t[1]):[],this.driveQuotaAttr=this.config.drive_quota_attribute||"twakeDriveQuota"}isValidCozyDomain(t){if(!t||t.length>253)return!1;if(t.includes("/")||t.includes("\\")||t.includes("?")||t.includes("#"))return!1;return/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(t)}async getCozyDomain(t){const i=[this.cozyDomainAttr,...this.templateAttrs],e=await this.ldapGetAttributes(t,i),a=e?this.attributeToString(e[this.cozyDomainAttr]):null;if(a)return a;if(this.defaultDomainTemplate&&e){let t=this.defaultDomainTemplate;for(const i of this.templateAttrs){const a=this.attributeToString(e[i]);if(!a)return null;t=t.replace(`{${i}}`,a)}return t}return null}async updateCozyInstance(i,e,a,n){if(!this.isValidCozyDomain(e))return void this.logger.error({plugin:this.name,event:i,result:"error",dn:a,cozyDomain:e,error:"Invalid Cozy domain format (potential URL injection attempt)"});const r=new URL(`${this.webadminUrl}/instances/${e}`);r.searchParams.set("FromCloudery","true");for(const[t,i]of Object.entries(n))r.searchParams.set(t,i);return this.requestLimit(async()=>{const s={plugin:this.name,event:i,result:"error",dn:a,cozyDomain:e,...n};try{const i=await t(r.toString(),{method:"PATCH",headers:this.createHeaders("application/json")});i.ok?this.logger.info({...s,result:"success",http_status:i.status}):404===i.status?this.logger.debug({...s,result:"ignored",http_status:i.status,http_status_text:i.statusText,message:"Cozy instance not found (may not be provisioned yet)"}):this.logger.error({...s,http_status:i.status,http_status_text:i.statusText,url:r.toString()})}catch(t){this.logger.error({...s,error:t,url:r.toString()})}})}getDisplayNameFromAttributes(t){const i=this.attributeToString(t[this.displayNameAttr]);if(i)return i;const e=this.attributeToString(t.cn);if(e)return e;const a=this.attributeToString(t.givenName),n=this.attributeToString(t.sn);if(a||n){const t=[];return a&&t.push(a),n&&t.push(n),t.join(" ")}return null}async getDisplayNameFromDN(t){const i=[this.displayNameAttr,"cn","givenName","sn"],e=await this.ldapGetAttributes(t,i);return e?this.getDisplayNameFromAttributes(e):null}async getMailFromDN(t){const i=await this.ldapGetAttributes(t,[this.mailAttr]);return i?this.attributeToString(i[this.mailAttr]):null}async getDriveQuotaFromDN(t){const i=await this.ldapGetAttributes(t,[this.driveQuotaAttr]);if(!i)return null;const e=this.attributeToString(i[this.driveQuotaAttr]);return e?Number(e):null}async syncUserToCozy(t){const i=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.mailAttr,this.displayNameAttr,this.driveQuotaAttr,"cn","givenName","sn"]);if(!i)return this.logger.error(`Cannot sync user: entry not found for ${t}`),!1;const e=this.attributeToString(i[this.cozyDomainAttr]);if(!e)return this.logger.error(`Cannot sync user: no Twake Drive domain attribute for ${t}`),!1;const a=this.attributeToString(i[this.mailAttr]),n=this.getDisplayNameFromAttributes(i),r=this.attributeToString(i[this.driveQuotaAttr]),s={};return a&&(s.Email=a),n&&(s.PublicName=n),r&&(s.DiskQuota=r),0===Object.keys(s).length?(this.logger.warn(`No attributes to sync for ${t}`),!1):(await this.updateCozyInstance("syncUserToCozy",e,t,s),!0)}async blockInstance(t,i){const e=await this.getCozyDomain(t);if(!e)return this.logger.error(`Cannot block instance: no Cozy domain for ${t}`),!1;const a={Blocked:"true"};return i&&(a.BlockingReason=i),await this.updateCozyInstance("blockInstance",e,t,a),!0}async unblockInstance(t){const i=await this.getCozyDomain(t);return i?(await this.updateCozyInstance("unblockInstance",i,t,{Blocked:"false"}),!0):(this.logger.error(`Cannot unblock instance: no Cozy domain for ${t}`),!1)}}export{Drive as default};
|
|
1
|
+
import t from"node-fetch";import{TwakePlugin as i}from"../../abstract/twakePlugin.js";class Drive extends i{constructor(t){super(t,"twake_drive_webadmin_url","twake_drive_webadmin_token","twake_drive_concurrency"),this.name="drive",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={onLdapMailChange:async(t,i,e)=>{const a=i?Array.isArray(i)?String(i[0]):String(i):null,n=e?Array.isArray(e)?String(e[0]):String(e):null;if(!a)return void this.logger.debug(`Skipping mail change for ${t}: oldmail is empty (mail attribute was added, not changed)`);if(!n)return void this.logger.debug(`Skipping mail change for ${t}: newmail is empty (mail attribute was deleted)`);const r=await this.getCozyDomain(t);r?await this.updateCozyInstance("onLdapMailChange",r,t,{Email:n}):this.logger.debug(`Skipping mail change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`)},onLdapDisplayNameChange:async(t,i,e)=>{const a=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.displayNameAttr,"cn","givenName","sn"]);if(!a)return void this.logger.warn(`Cannot update Cozy display name: entry not found for ${t}`);const n=this.attributeToString(a[this.cozyDomainAttr]);if(!n)return void this.logger.debug(`Skipping display name change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`);const r=this.getDisplayNameFromAttributes(a);r?await this.updateCozyInstance("onLdapDisplayNameChange",n,t,{PublicName:r}):this.logger.warn(`Cannot update Cozy display name: no display name found for ${t}`)},onLdapDriveQuotaChange:async(t,i,e)=>{if(null===e)return void this.logger.debug(`Skipping drive quota change for ${t}: quota was deleted`);const a=await this.getCozyDomain(t);a?await this.updateCozyInstance("onLdapDriveQuotaChange",a,t,{DiskQuota:String(e)}):this.logger.debug(`Skipping drive quota change for ${t}: no Twake Drive domain attribute (${this.cozyDomainAttr})`)}},this.cozyDomainAttr=this.config.twake_drive_domain_attribute||"twakeCozyDomain",this.defaultDomainTemplate=this.config.twake_drive_default_domain_template||"",this.templateAttrs=this.defaultDomainTemplate?[...this.defaultDomainTemplate.matchAll(/\{(\w+)\}/g)].map(t=>t[1]):[],this.driveQuotaAttr=this.config.drive_quota_attribute||"twakeDriveQuota"}createHeaders(t){const i={};if(this.webadminToken){const t=Buffer.from(`:${this.webadminToken}`).toString("base64");i.Authorization=`Basic ${t}`}return t&&(i["Content-Type"]=t),i}isValidCozyDomain(t){if(!t||t.length>253)return!1;if(t.includes("/")||t.includes("\\")||t.includes("?")||t.includes("#"))return!1;return/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(t)}async getCozyDomain(t){const i=[this.cozyDomainAttr,...this.templateAttrs],e=await this.ldapGetAttributes(t,i),a=e?this.attributeToString(e[this.cozyDomainAttr]):null;if(a)return a;if(this.defaultDomainTemplate&&e){let t=this.defaultDomainTemplate;for(const i of this.templateAttrs){const a=this.attributeToString(e[i]);if(!a)return null;t=t.replace(`{${i}}`,a)}return t}return null}async updateCozyInstance(i,e,a,n){if(!this.isValidCozyDomain(e))return void this.logger.error({plugin:this.name,event:i,result:"error",dn:a,cozyDomain:e,error:"Invalid Cozy domain format (potential URL injection attempt)"});const r=new URL(`${this.webadminUrl}/instances/${e}`);r.searchParams.set("FromCloudery","true");for(const[t,i]of Object.entries(n))r.searchParams.set(t,i);return this.requestLimit(async()=>{const s={plugin:this.name,event:i,result:"error",dn:a,cozyDomain:e,...n};try{const i=await t(r.toString(),{method:"PATCH",headers:this.createHeaders("application/json")});i.ok?this.logger.info({...s,result:"success",http_status:i.status}):404===i.status?this.logger.debug({...s,result:"ignored",http_status:i.status,http_status_text:i.statusText,message:"Cozy instance not found (may not be provisioned yet)"}):this.logger.error({...s,http_status:i.status,http_status_text:i.statusText,url:r.toString()})}catch(t){this.logger.error({...s,error:t,url:r.toString()})}})}getDisplayNameFromAttributes(t){const i=this.attributeToString(t[this.displayNameAttr]);if(i)return i;const e=this.attributeToString(t.cn);if(e)return e;const a=this.attributeToString(t.givenName),n=this.attributeToString(t.sn);if(a||n){const t=[];return a&&t.push(a),n&&t.push(n),t.join(" ")}return null}async getDisplayNameFromDN(t){const i=[this.displayNameAttr,"cn","givenName","sn"],e=await this.ldapGetAttributes(t,i);return e?this.getDisplayNameFromAttributes(e):null}async getMailFromDN(t){const i=await this.ldapGetAttributes(t,[this.mailAttr]);return i?this.attributeToString(i[this.mailAttr]):null}async getDriveQuotaFromDN(t){const i=await this.ldapGetAttributes(t,[this.driveQuotaAttr]);if(!i)return null;const e=this.attributeToString(i[this.driveQuotaAttr]);return e?Number(e):null}async syncUserToCozy(t){const i=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.mailAttr,this.displayNameAttr,this.driveQuotaAttr,"cn","givenName","sn"]);if(!i)return this.logger.error(`Cannot sync user: entry not found for ${t}`),!1;const e=this.attributeToString(i[this.cozyDomainAttr]);if(!e)return this.logger.error(`Cannot sync user: no Twake Drive domain attribute for ${t}`),!1;const a=this.attributeToString(i[this.mailAttr]),n=this.getDisplayNameFromAttributes(i),r=this.attributeToString(i[this.driveQuotaAttr]),s={};return a&&(s.Email=a),n&&(s.PublicName=n),r&&(s.DiskQuota=r),0===Object.keys(s).length?(this.logger.warn(`No attributes to sync for ${t}`),!1):(await this.updateCozyInstance("syncUserToCozy",e,t,s),!0)}async blockInstance(t,i){const e=await this.getCozyDomain(t);if(!e)return this.logger.error(`Cannot block instance: no Cozy domain for ${t}`),!1;const a={Blocked:"true"};return i&&(a.BlockingReason=i),await this.updateCozyInstance("blockInstance",e,t,a),!0}async unblockInstance(t){const i=await this.getCozyDomain(t);return i?(await this.updateCozyInstance("unblockInstance",i,t,{Blocked:"false"}),!0):(this.logger.error(`Cannot unblock instance: no Cozy domain for ${t}`),!1)}}export{Drive as default};
|
|
@@ -13,6 +13,16 @@ export default class Drive extends TwakePlugin {
|
|
|
13
13
|
private templateAttrs;
|
|
14
14
|
private driveQuotaAttr;
|
|
15
15
|
constructor(server: DM);
|
|
16
|
+
/**
|
|
17
|
+
* Override createHeaders to use Basic Auth for Cozy Admin API
|
|
18
|
+
* Cozy uses Basic Auth with empty username and password as the token
|
|
19
|
+
* @param contentType Optional Content-Type header value
|
|
20
|
+
* @returns Headers object with Basic Auth
|
|
21
|
+
*/
|
|
22
|
+
protected createHeaders(contentType?: string): {
|
|
23
|
+
Authorization?: string;
|
|
24
|
+
'Content-Type'?: string;
|
|
25
|
+
};
|
|
16
26
|
/**
|
|
17
27
|
* Validate Cozy domain format to prevent URL injection attacks
|
|
18
28
|
* @param domain Cozy domain to validate
|