ldap-rest 0.2.0 → 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
|
|
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};
|
|
@@ -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
|
|
@@ -73,4 +83,11 @@ export default class Drive extends TwakePlugin {
|
|
|
73
83
|
* @returns true if successful, false otherwise
|
|
74
84
|
*/
|
|
75
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>;
|
|
76
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. **
|
|
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
|
|