From 90172e2088df1bab1ac402c88a387c50cf71d441 Mon Sep 17 00:00:00 2001 From: doubleface Date: Mon, 9 Mar 2026 09:30:33 +0100 Subject: [PATCH] feat: Allow PermissionsCollection to handle shared drives files Since we now want to be able to share shared drives files even when we are shared drive recipients, we add the possibility to pass a `driveId` option to the `PermissionCollection` constructor. When this option is set, all requests will be made to the `/shared/{driveId}/permissions` endpoint instead of the regular `/permissions` endpoint. --- docs/api/cozy-stack-client.md | 26 ++++ .../src/PermissionCollection.js | 39 +++-- .../src/PermissionCollection.spec.js | 136 ++++++++++++++++++ 3 files changed, 191 insertions(+), 10 deletions(-) diff --git a/docs/api/cozy-stack-client.md b/docs/api/cozy-stack-client.md index 981d2105f8..0c560e42df 100644 --- a/docs/api/cozy-stack-client.md +++ b/docs/api/cozy-stack-client.md @@ -282,6 +282,9 @@ not.

TwoFactorNeededRes
+
PermissionCollectionOptions : object
+

Options that can be passed to PermissionCollection's constructor

+
PermissionPermission

async getOwnPermissions - deprecated: please use fetchOwnPermissions instead

@@ -1762,6 +1765,7 @@ Implements `DocumentCollection` API along with specific methods for `io.cozy.per **Kind**: global class * [PermissionCollection](#PermissionCollection) + * [new PermissionCollection(doctype, stackClient, [options])](#new_PermissionCollection_new) * [.create(permission)](#PermissionCollection+create) * [.add(document, permission, options)](#PermissionCollection+add) ⇒ Promise * ~~[.findApps()](#PermissionCollection+findApps)~~ @@ -1770,6 +1774,16 @@ Implements `DocumentCollection` API along with specific methods for `io.cozy.per * [.fetchAllLinks(document)](#PermissionCollection+fetchAllLinks) ⇒ object * [.revokeSharingLink(document)](#PermissionCollection+revokeSharingLink) + + +### new PermissionCollection(doctype, stackClient, [options]) + +| Param | Type | Description | +| --- | --- | --- | +| doctype | string | Doctype of the collection (should be `io.cozy.permissions`) | +| stackClient | [CozyStackClient](#CozyStackClient) | The client used to make requests to the server | +| [options] | [PermissionCollectionOptions](#PermissionCollectionOptions) | The collection options | + ### permissionCollection.create(permission) @@ -3473,6 +3487,18 @@ Options that can be passed to NotesCollection's constructor | --- | --- | --- | | two_factor_token | string | The 2FA token | + + +## PermissionCollectionOptions : object +Options that can be passed to PermissionCollection's constructor + +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| [driveId] | string | ID of the shared drive targeted by the collection | + ## Permission ⇒ [Permission](#Permission) diff --git a/packages/cozy-stack-client/src/PermissionCollection.js b/packages/cozy-stack-client/src/PermissionCollection.js index 73a4c5b091..263a49e780 100644 --- a/packages/cozy-stack-client/src/PermissionCollection.js +++ b/packages/cozy-stack-client/src/PermissionCollection.js @@ -1,19 +1,35 @@ import DocumentCollection from './DocumentCollection' import { normalizeDoc } from './normalize' import { isFile } from './FileCollection' -import { uri } from './utils' +import { uri, sharedDriveApiPrefix } from './utils' import logger from './logger' const normalizePermission = perm => normalizeDoc(perm, 'io.cozy.permissions') +/** + * Options that can be passed to PermissionCollection's constructor + * + * @typedef {object} PermissionCollectionOptions + * @property {string} [driveId] - ID of the shared drive targeted by the collection + */ + /** * Implements `DocumentCollection` API along with specific methods for `io.cozy.permissions`. */ class PermissionCollection extends DocumentCollection { + /** + * @param {string} doctype - Doctype of the collection (should be `io.cozy.permissions`) + * @param {CozyStackClient} stackClient -The client used to make requests to the server + * @param {PermissionCollectionOptions} [options] - The collection options + */ + constructor(doctype, stackClient, options = {}) { + super(doctype, stackClient, options) + this.prefix = options.driveId ? sharedDriveApiPrefix(options.driveId) : '' + } async get(id) { const resp = await this.stackClient.fetchJSON( 'GET', - uri`/permissions/${id}` + this.prefix + uri`/permissions/${id}` ) return { data: normalizePermission(resp.data) @@ -42,7 +58,7 @@ class PermissionCollection extends DocumentCollection { if (tiny) searchParams.append('tiny', true) const resp = await this.stackClient.fetchJSON( 'POST', - `/permissions?${searchParams}`, + `${this.prefix}/permissions?${searchParams}`, { data: { type: 'io.cozy.permissions', @@ -85,13 +101,13 @@ class PermissionCollection extends DocumentCollection { let endpoint switch (document._type) { case 'io.cozy.apps': - endpoint = `/permissions/apps/${document.slug}` + endpoint = `${this.prefix}/permissions/apps/${document.slug}` break case 'io.cozy.konnectors': - endpoint = `/permissions/konnectors/${document.slug}` + endpoint = `${this.prefix}/permissions/konnectors/${document.slug}` break case 'io.cozy.permissions': - endpoint = `/permissions/${document._id}` + endpoint = `${this.prefix}/permissions/${document._id}` break default: throw new Error( @@ -122,14 +138,14 @@ class PermissionCollection extends DocumentCollection { destroy(permission) { return this.stackClient.fetchJSON( 'DELETE', - uri`/permissions/${permission.id}` + this.prefix + uri`/permissions/${permission.id}` ) } async findLinksByDoctype(doctype) { const resp = await this.stackClient.fetchJSON( 'GET', - uri`/permissions/doctype/${doctype}/shared-by-link` + this.prefix + uri`/permissions/doctype/${doctype}/shared-by-link` ) return { ...resp, @@ -174,7 +190,7 @@ class PermissionCollection extends DocumentCollection { const resp = await this.stackClient.fetchJSON( 'POST', - `/permissions?${searchParams}`, + `${this.prefix}/permissions?${searchParams}`, { data: { type: 'io.cozy.permissions', @@ -257,7 +273,10 @@ class PermissionCollection extends DocumentCollection { * @returns {Permission} permission */ async fetchOwnPermissions() { - const resp = await this.stackClient.fetchJSON('GET', '/permissions/self') + const resp = await this.stackClient.fetchJSON( + 'GET', + `${this.prefix}/permissions/self` + ) return { data: normalizePermission(resp.data), included: resp.included ? resp.included.map(normalizePermission) : [] diff --git a/packages/cozy-stack-client/src/PermissionCollection.spec.js b/packages/cozy-stack-client/src/PermissionCollection.spec.js index 8c773ca681..a5b1dac813 100644 --- a/packages/cozy-stack-client/src/PermissionCollection.spec.js +++ b/packages/cozy-stack-client/src/PermissionCollection.spec.js @@ -429,3 +429,139 @@ describe('getPermissionsFor', () => { expect(perms2.files.verbs).toEqual(expect.arrayContaining(verbs)) }) }) + +describe('PermissionCollection with driveId', () => { + const client = new CozyStackClient() + const driveId = 'abc123drive' + const collection = new PermissionCollection('io.cozy.permissions', client, { + driveId + }) + + beforeEach(() => { + client.fetchJSON.mockReset() + client.fetchJSON.mockResolvedValue({ data: [] }) + }) + + it('should set the correct API prefix for shared drives', () => { + expect(collection.prefix).toEqual('/sharings/drives/abc123drive') + }) + + describe('get', () => { + it('calls the API with the shared drive prefix', async () => { + client.fetchJSON.mockResolvedValue({ + data: { type: 'io.cozy.permissions', id: 'test-perm-id' } + }) + await collection.get('test-perm-id') + expect(client.fetchJSON).toHaveBeenCalledWith( + 'GET', + '/sharings/drives/abc123drive/permissions/test-perm-id' + ) + }) + }) + + describe('create', () => { + it('calls the API with the shared drive prefix', async () => { + client.fetchJSON.mockResolvedValue({ + data: { type: 'io.cozy.permissions', id: 'new-perm-id' } + }) + await collection.create({ _type: 'io.cozy.permissions', codes: 'code' }) + expect(client.fetchJSON).toHaveBeenCalledWith( + 'POST', + '/sharings/drives/abc123drive/permissions?codes=code', + { data: { attributes: {}, type: 'io.cozy.permissions' } } + ) + }) + + it('includes ttl and tiny options', async () => { + client.fetchJSON.mockResolvedValue({ + data: { type: 'io.cozy.permissions', id: 'new-perm-id' } + }) + await collection.create({ + _type: 'io.cozy.permissions', + codes: 'a,b', + ttl: '1D', + tiny: true + }) + expect(client.fetchJSON).toHaveBeenCalledWith( + 'POST', + '/sharings/drives/abc123drive/permissions?codes=a%2Cb&ttl=1D&tiny=true', + { data: { attributes: {}, type: 'io.cozy.permissions' } } + ) + }) + }) + + describe('add', () => { + it('uses shared drive prefix for permissions document', async () => { + await collection.add( + { + _type: 'io.cozy.permissions', + _id: 'a340d5e0d64711e6b66c5fc9ce1e17c6' + }, + fixtures.permission + ) + expect(client.fetchJSON).toHaveBeenCalledWith( + 'PATCH', + '/sharings/drives/abc123drive/permissions/a340d5e0d64711e6b66c5fc9ce1e17c6', + { + data: { + type: 'io.cozy.permissions', + attributes: { + permissions: fixtures.permission + } + } + } + ) + }) + }) + + describe('destroy', () => { + it('calls the API with the shared drive prefix', async () => { + await collection.destroy({ id: 'perm-to-delete' }) + expect(client.fetchJSON).toHaveBeenCalledWith( + 'DELETE', + '/sharings/drives/abc123drive/permissions/perm-to-delete' + ) + }) + }) + + describe('createSharingLink', () => { + it('calls the API with the shared drive prefix', async () => { + client.fetchJSON.mockResolvedValue({ + data: { type: 'io.cozy.permissions', id: 'new-perm-id' } + }) + const document = { _type: 'io.cozy.files', _id: '1234' } + await collection.createSharingLink(document) + expect(client.fetchJSON).toHaveBeenCalledWith( + 'POST', + '/sharings/drives/abc123drive/permissions?codes=code', + { + data: { + type: 'io.cozy.permissions', + attributes: { + permissions: { + files: { + type: 'io.cozy.files', + verbs: ['GET'], + values: ['1234'] + } + } + } + } + } + ) + }) + }) + + describe('fetchOwnPermissions', () => { + it('calls the API with the shared drive prefix', async () => { + client.fetchJSON.mockResolvedValue({ + data: { type: 'io.cozy.permissions', id: 'self-perm' } + }) + await collection.fetchOwnPermissions() + expect(client.fetchJSON).toHaveBeenCalledWith( + 'GET', + '/sharings/drives/abc123drive/permissions/self' + ) + }) + }) +})