ldap-rest 0.2.1 → 0.2.2

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"}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};
1
+ import t from"node-fetch";import{TwakePlugin as e}from"../../abstract/twakePlugin.js";class Drive extends e{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,e,i)=>{const n=e?Array.isArray(e)?String(e[0]):String(e):null,a=i?Array.isArray(i)?String(i[0]):String(i):null;if(!n)return void this.logger.debug(`Skipping mail change for ${t}: oldmail is empty (mail attribute was added, not changed)`);if(!a)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:a}):this.logger.debug(`Skipping mail change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`)},onLdapDisplayNameChange:async(t,e,i)=>{const n=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.displayNameAttr,"cn","givenName","sn"]);if(!n)return void this.logger.warn(`Cannot update Cozy display name: entry not found for ${t}`);const a=this.attributeToString(n[this.cozyDomainAttr]);if(!a)return void this.logger.debug(`Skipping display name change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`);const r=this.getDisplayNameFromAttributes(n);r?await this.updateCozyInstance("onLdapDisplayNameChange",a,t,{PublicName:r}):this.logger.warn(`Cannot update Cozy display name: no display name found for ${t}`)},onLdapDriveQuotaChange:async(t,e,i)=>{if(null===i)return void this.logger.debug(`Skipping drive quota change for ${t}: quota was deleted`);const n=await this.getCozyDomain(t);n?await this.updateCozyInstance("onLdapDriveQuotaChange",n,t,{DiskQuota:String(i)}):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 e={};if(this.webadminToken){const t=Buffer.from(`:${this.webadminToken}`).toString("base64");e.Authorization=`Basic ${t}`}return t&&(e["Content-Type"]=t),e}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 e=[this.cozyDomainAttr,...this.templateAttrs],i=await this.ldapGetAttributes(t,e),n=i?this.attributeToString(i[this.cozyDomainAttr]):null;if(n)return n;if(this.defaultDomainTemplate&&i){let t=this.defaultDomainTemplate;for(const e of this.templateAttrs){const n=this.attributeToString(i[e]);if(!n)return null;t=t.replace(`{${e}}`,n)}return t}return null}async updateCozyInstance(e,i,n,a){if(!this.isValidCozyDomain(i))return void this.logger.error({plugin:this.name,event:e,result:"error",dn:n,cozyDomain:i,error:"Invalid Cozy domain format (potential URL injection attempt)"});const r=new URL(`${this.webadminUrl}/instances/${i}`);r.searchParams.set("FromCloudery","true");for(const[t,e]of Object.entries(a))r.searchParams.set(t,e);return this.requestLimit(async()=>{const s={plugin:this.name,event:e,result:"error",dn:n,cozyDomain:i,...a};try{const e=await t(r.toString(),{method:"PATCH",headers:this.createHeaders("application/json")});e.ok?this.logger.info({...s,result:"success",http_status:e.status}):404===e.status?this.logger.debug({...s,result:"ignored",http_status:e.status,http_status_text:e.statusText,message:"Cozy instance not found (may not be provisioned yet)"}):this.logger.error({...s,http_status:e.status,http_status_text:e.statusText,url:r.toString()})}catch(t){this.logger.error({...s,error:t,url:r.toString()})}})}getDisplayNameFromAttributes(t){const e=this.attributeToString(t[this.displayNameAttr]);if(e)return e;const i=this.attributeToString(t.cn);if(i)return i;const n=this.attributeToString(t.givenName),a=this.attributeToString(t.sn);if(n||a){const t=[];return n&&t.push(n),a&&t.push(a),t.join(" ")}return null}async getDisplayNameFromDN(t){const e=[this.displayNameAttr,"cn","givenName","sn"],i=await this.ldapGetAttributes(t,e);return i?this.getDisplayNameFromAttributes(i):null}async getMailFromDN(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}async getDriveQuotaFromDN(t){const e=await this.ldapGetAttributes(t,[this.driveQuotaAttr]);if(!e)return null;const i=this.attributeToString(e[this.driveQuotaAttr]);return i?Number(i):null}async syncUserToCozy(t){const e=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.mailAttr,this.displayNameAttr,this.driveQuotaAttr,"cn","givenName","sn"]);if(!e)return this.logger.error(`Cannot sync user: entry not found for ${t}`),!1;const i=this.attributeToString(e[this.cozyDomainAttr]);if(!i)return this.logger.error(`Cannot sync user: no Twake Drive domain attribute for ${t}`),!1;const n=this.attributeToString(e[this.mailAttr]),a=this.getDisplayNameFromAttributes(e),r=this.attributeToString(e[this.driveQuotaAttr]),s={};return n&&(s.Email=n),a&&(s.PublicName=a),r&&(s.DiskQuota=r),0===Object.keys(s).length?(this.logger.warn(`No attributes to sync for ${t}`),!1):(await this.updateCozyInstance("syncUserToCozy",i,t,s),!0)}async blockInstance(t,e){const i=await this.getCozyDomain(t);if(!i)return this.logger.error(`Cannot block instance: no Cozy domain for ${t}`),!1;const n={Blocked:"true"};return e&&(n.BlockingReason=e),await this.updateCozyInstance("blockInstance",i,t,n),!0}async unblockInstance(t){const e=await this.getCozyDomain(t);return e?(await this.updateCozyInstance("unblockInstance",e,t,{Blocked:"false"}),!0):(this.logger.error(`Cannot unblock instance: no Cozy domain for ${t}`),!1)}async deleteInstance(e){const i=await this.getCozyDomain(e);if(!i)return this.logger.error(`Cannot delete instance: no Cozy domain for ${e}`),!1;if(!this.isValidCozyDomain(i))return this.logger.error({plugin:this.name,event:"deleteInstance",result:"error",dn:e,cozyDomain:i,error:"Invalid Cozy domain format (potential URL injection attempt)"}),!1;const n=`${this.webadminUrl}/instances/${i}`;return this.requestLimit(async()=>{try{const a=await t(n,{method:"DELETE",headers:this.createHeaders()});return a.ok?(this.logger.info({plugin:this.name,event:"deleteInstance",result:"success",dn:e,cozyDomain:i,http_status:a.status}),!0):404===a.status?(this.logger.info({plugin:this.name,event:"deleteInstance",result:"ignored",dn:e,cozyDomain:i,http_status:404,message:"Cozy instance not found (already deleted or never created)"}),!0):(this.logger.error({plugin:this.name,event:"deleteInstance",result:"error",dn:e,cozyDomain:i,http_status:a.status,http_status_text:a.statusText,url:n}),!1)}catch(t){return this.logger.error({plugin:this.name,event:"deleteInstance",result:"error",dn:e,cozyDomain:i,error:t,url:n}),!1}})}}export{Drive as default};
@@ -83,4 +83,11 @@ export default class Drive extends TwakePlugin {
83
83
  * @returns true if successful, false otherwise
84
84
  */
85
85
  unblockInstance(dn: string): Promise<boolean>;
86
+ /**
87
+ * Delete a Cozy instance and all its data
88
+ * Calls DELETE /instances/:domain on the Cozy Admin API
89
+ * @param dn User's LDAP DN
90
+ * @returns true if successful, false otherwise
91
+ */
92
+ deleteInstance(dn: string): Promise<boolean>;
86
93
  }
@@ -357,6 +357,26 @@ async unblockInstance(dn: string): Promise<boolean>
357
357
  await drive.unblockInstance('uid=jdoe,ou=users,dc=example,dc=com');
358
358
  ```
359
359
 
360
+ ---
361
+
362
+ ### deleteInstance(dn)
363
+
364
+ Delete a Cozy instance and all its data.
365
+
366
+ **Signature:**
367
+
368
+ ```typescript
369
+ async deleteInstance(dn: string): Promise<boolean>
370
+ ```
371
+
372
+ Calls `DELETE /instances/:domain` on the Cozy Admin API. Returns `true` if the instance was successfully deleted or if the instance was not found (404). Returns `false` on error.
373
+
374
+ **Example:**
375
+
376
+ ```typescript
377
+ await drive.deleteInstance('uid=jdoe,ou=users,dc=example,dc=com');
378
+ ```
379
+
360
380
  ## Cozy Admin API
361
381
 
362
382
  ### Endpoints Used
@@ -367,6 +387,7 @@ await drive.unblockInstance('uid=jdoe,ou=users,dc=example,dc=com');
367
387
  | Update name | `/instances/{domain}?PublicName={name}&FromCloudery=true` | PATCH |
368
388
  | Update quota | `/instances/{domain}?DiskQuota={bytes}&FromCloudery=true` | PATCH |
369
389
  | Combined update | `/instances/{domain}?Email={email}&PublicName={name}&DiskQuota={bytes}&FromCloudery=true` | PATCH |
390
+ | Delete instance | `/instances/{domain}` | DELETE |
370
391
 
371
392
  ### Available Cozy Parameters
372
393
 
@@ -567,8 +588,7 @@ await drive.syncUserToCozy('uid=jdoe,ou=users,dc=example,dc=com');
567
588
 
568
589
  1. **One-way sync**: LDAP → Cozy only. Cozy changes don't sync back to LDAP.
569
590
  2. **No instance creation**: Plugin doesn't create Cozy instances, only updates existing ones.
570
- 3. **No user deletion**: Plugin doesn't delete Cozy instances when LDAP users are deleted.
571
- 4. **Domain required**: Users must have the `twakeCozyDomain` attribute to be synced.
591
+ 3. **Domain required**: Users must have the `twakeCozyDomain` attribute to be synced.
572
592
 
573
593
  ## Troubleshooting
574
594
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"