diff --git a/packages/@n8n/permissions/src/public-api-permissions.ee.ts b/packages/@n8n/permissions/src/public-api-permissions.ee.ts index 3db671a4c5b..c0979c62a18 100644 --- a/packages/@n8n/permissions/src/public-api-permissions.ee.ts +++ b/packages/@n8n/permissions/src/public-api-permissions.ee.ts @@ -212,6 +212,11 @@ export const API_KEY_SCOPES_FOR_IMPLICIT_PERSONAL_PROJECT: ApiKeyScope[] = [ 'dataTableRow:update', 'dataTableRow:delete', 'dataTableRow:upsert', + 'folder:create', + 'folder:delete', + 'folder:read', + 'folder:update', + 'folder:list', 'dataTableColumn:create', 'dataTableColumn:read', 'dataTableColumn:update', diff --git a/packages/cli/src/services/__tests__/public-api-key.service.integration.test.ts b/packages/cli/src/services/__tests__/public-api-key.service.integration.test.ts index 92d14a8fd67..585db50b123 100644 --- a/packages/cli/src/services/__tests__/public-api-key.service.integration.test.ts +++ b/packages/cli/src/services/__tests__/public-api-key.service.integration.test.ts @@ -144,5 +144,27 @@ describe('PublicApiKeyService', () => { expect(result).toBe(false); }); + + it('should let a member grant folder scopes, which apply to their own projects', async () => { + // Arrange + const folderScopes: ApiKeyScope[] = [ + 'folder:create', + 'folder:read', + 'folder:update', + 'folder:delete', + 'folder:list', + ]; + + // Act + const result = publicApiKeyService.apiKeyHasValidScopesForRole( + { + role: GLOBAL_MEMBER_ROLE, + }, + folderScopes, + ); + + // Assert + expect(result).toBe(true); + }); }); }); diff --git a/packages/cli/test/integration/api-keys.api.test.ts b/packages/cli/test/integration/api-keys.api.test.ts index 41cf74519b9..94076bc7104 100644 --- a/packages/cli/test/integration/api-keys.api.test.ts +++ b/packages/cli/test/integration/api-keys.api.test.ts @@ -507,6 +507,24 @@ describe('Member', () => { expect(newApiKeyResponse.statusCode).toBe(400); }); + test('POST /api-keys should create an api key with folder scopes', async () => { + const folderScopes: ApiKeyScope[] = [ + 'folder:create', + 'folder:read', + 'folder:update', + 'folder:delete', + 'folder:list', + ]; + + const newApiKeyResponse = await testServer + .authAgentFor(member) + .post('/api-keys') + .send({ label: 'My API Key', expiresAt: null, scopes: folderScopes }) + .expect(200); + + expect(newApiKeyResponse.body.data.scopes).toEqual(folderScopes); + }); + test('GET /api-keys should fetch the api key redacted', async () => { const expirationDateInTheFuture = Date.now() + 1000; diff --git a/packages/cli/test/integration/public-api/folders.test.ts b/packages/cli/test/integration/public-api/folders.test.ts index 8dea5e453dd..70ee12ae3e8 100644 --- a/packages/cli/test/integration/public-api/folders.test.ts +++ b/packages/cli/test/integration/public-api/folders.test.ts @@ -15,6 +15,7 @@ import * as utils from '../shared/utils/'; let owner: User; let member: User; let ownerPersonalProject: Project; +let memberPersonalProject: Project; let authOwnerAgent: SuperAgentTest; let authMemberAgent: SuperAgentTest; @@ -57,7 +58,7 @@ beforeEach(async () => { }; ownerPersonalProject = await createPersonalProject(owner); - await createPersonalProject(member); + memberPersonalProject = await createPersonalProject(member); authOwnerAgent = testServer.publicApiAgentFor(owner); authMemberAgent = testServer.publicApiAgentFor(member); @@ -295,6 +296,18 @@ describe('GET /projects/:projectId/folders', () => { expect(response.body.data).toHaveLength(2); }); + test('should list folders in their own personal project when user is a member', async () => { + testServer.license.enable('feat:folders'); + + await createFolder(memberPersonalProject, { name: 'Member Folder' }); + + const response = await authMemberAgent.get(`/projects/${memberPersonalProject.id}/folders`); + + expect(response.statusCode).toBe(200); + expect(response.body.count).toBe(1); + expect(response.body.data).toHaveLength(1); + }); + test('should list folders in a team project when user is a member', async () => { testServer.license.enable('feat:folders'); testServer.license.setQuota('quota:maxTeamProjects', -1);