diff --git a/packages/@n8n/permissions/src/constants.ee.ts b/packages/@n8n/permissions/src/constants.ee.ts index 9799a2b8e5..ed2270fb97 100644 --- a/packages/@n8n/permissions/src/constants.ee.ts +++ b/packages/@n8n/permissions/src/constants.ee.ts @@ -30,7 +30,7 @@ export const RESOURCES = { export const API_KEY_RESOURCES = { tag: [...DEFAULT_OPERATIONS] as const, workflow: [...DEFAULT_OPERATIONS, 'move', 'activate', 'deactivate'] as const, - variable: ['create', 'delete', 'list'] as const, + variable: DEFAULT_OPERATIONS, securityAudit: ['generate'] as const, project: ['create', 'update', 'delete', 'list'] as const, user: ['read', 'list', 'create', 'changeRole', 'delete'] as const, diff --git a/packages/cli/src/public-api/v1/handlers/variables/spec/paths/variables.id.yml b/packages/cli/src/public-api/v1/handlers/variables/spec/paths/variables.id.yml index 79c65416b2..5b082cdc6c 100644 --- a/packages/cli/src/public-api/v1/handlers/variables/spec/paths/variables.id.yml +++ b/packages/cli/src/public-api/v1/handlers/variables/spec/paths/variables.id.yml @@ -8,9 +8,35 @@ delete: parameters: - $ref: '../schemas/parameters/variableId.yml' responses: - '204': + '201': description: Operation successful. '401': $ref: '../../../../shared/spec/responses/unauthorized.yml' '404': $ref: '../../../../shared/spec/responses/notFound.yml' + +put: + x-eov-operation-id: updateVariable + x-eov-operation-handler: v1/handlers/variables/variables.handler + tags: + - Variables + summary: Update a variable + description: Update a variable from your instance. + requestBody: + description: Payload for variable to update. + content: + application/json: + schema: + $ref: '../schemas/variable.yml' + required: true + responses: + '204': + description: Operation successful. + '400': + $ref: '../../../../shared/spec/responses/badRequest.yml' + '401': + $ref: '../../../../shared/spec/responses/unauthorized.yml' + '403': + $ref: '../../../../shared/spec/responses/forbidden.yml' + '404': + $ref: '../../../../shared/spec/responses/notFound.yml' diff --git a/packages/cli/src/public-api/v1/handlers/variables/variables.handler.ts b/packages/cli/src/public-api/v1/handlers/variables/variables.handler.ts index 3e08bc02ec..c39ec318dd 100644 --- a/packages/cli/src/public-api/v1/handlers/variables/variables.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/variables/variables.handler.ts @@ -27,6 +27,15 @@ export = { res.status(201).send(); }, ], + updateVariable: [ + isLicensed('feat:variables'), + apiKeyHasScopeWithGlobalScopeFallback({ scope: 'variable:update' }), + async (req: VariablesRequest.Update, res: Response) => { + await Container.get(VariablesController).updateVariable(req); + + res.status(204).send(); + }, + ], deleteVariable: [ isLicensed('feat:variables'), apiKeyHasScopeWithGlobalScopeFallback({ scope: 'variable:delete' }), diff --git a/packages/cli/test/integration/public-api/variables.test.ts b/packages/cli/test/integration/public-api/variables.test.ts index 61f75d4641..98befd8745 100644 --- a/packages/cli/test/integration/public-api/variables.test.ts +++ b/packages/cli/test/integration/public-api/variables.test.ts @@ -1,3 +1,5 @@ +import type { User, Variables } from '@n8n/db'; + import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; import { createOwnerWithApiKey } from '@test-integration/db/users'; import { createVariable, getVariableOrFail } from '@test-integration/db/variables'; @@ -6,7 +8,9 @@ import { setupTestServer } from '@test-integration/utils'; import * as testDb from '../shared/test-db'; describe('Variables in Public API', () => { + let owner: User; const testServer = setupTestServer({ endpointGroups: ['publicApi'] }); + const licenseErrorMessage = new FeatureNotLicensedError('feat:variables').message; beforeAll(async () => { await testDb.init(); @@ -14,6 +18,8 @@ describe('Variables in Public API', () => { beforeEach(async () => { await testDb.truncate(['Variables', 'User']); + + owner = await createOwnerWithApiKey(); }); describe('GET /variables', () => { @@ -22,7 +28,6 @@ describe('Variables in Public API', () => { * Arrange */ testServer.license.enable('feat:variables'); - const owner = await createOwnerWithApiKey(); const variables = await Promise.all([createVariable(), createVariable(), createVariable()]); /** @@ -45,12 +50,6 @@ describe('Variables in Public API', () => { }); it('if not licensed, should reject', async () => { - /** - * Arrange - */ - - const owner = await createOwnerWithApiKey(); - /** * Act */ @@ -60,10 +59,7 @@ describe('Variables in Public API', () => { * Assert */ expect(response.status).toBe(403); - expect(response.body).toHaveProperty( - 'message', - new FeatureNotLicensedError('feat:variables').message, - ); + expect(response.body).toHaveProperty('message', licenseErrorMessage); }); }); @@ -73,7 +69,6 @@ describe('Variables in Public API', () => { * Arrange */ testServer.license.enable('feat:variables'); - const owner = await createOwnerWithApiKey(); const variablePayload = { key: 'key', value: 'value' }; /** @@ -97,7 +92,6 @@ describe('Variables in Public API', () => { /** * Arrange */ - const owner = await createOwnerWithApiKey(); const variablePayload = { key: 'key', value: 'value' }; /** @@ -112,21 +106,52 @@ describe('Variables in Public API', () => { * Assert */ expect(response.status).toBe(403); - expect(response.body).toHaveProperty( - 'message', - new FeatureNotLicensedError('feat:variables').message, - ); + expect(response.body).toHaveProperty('message', licenseErrorMessage); + }); + }); + + describe('PUT /variables/:id', () => { + const variablePayload = { key: 'updatedKey', value: 'updatedValue' }; + let variable: Variables; + beforeEach(async () => { + variable = await createVariable(); + }); + + it('if licensed, should update a variable', async () => { + testServer.license.enable('feat:variables'); + + const response = await testServer + .publicApiAgentFor(owner) + .put(`/variables/${variable.id}`) + .send(variablePayload); + + expect(response.status).toBe(204); + const updatedVariable = await getVariableOrFail(variable.id); + expect(updatedVariable).toEqual(expect.objectContaining(variablePayload)); + }); + + it('if not licensed, should reject', async () => { + const response = await testServer + .publicApiAgentFor(owner) + .put(`/variables/${variable.id}`) + .send(variablePayload); + + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message', licenseErrorMessage); }); }); describe('DELETE /variables/:id', () => { + let variable: Variables; + beforeEach(async () => { + variable = await createVariable(); + }); + it('if licensed, should delete a variable', async () => { /** * Arrange */ testServer.license.enable('feat:variables'); - const owner = await createOwnerWithApiKey(); - const variable = await createVariable(); /** * Act @@ -143,12 +168,6 @@ describe('Variables in Public API', () => { }); it('if not licensed, should reject', async () => { - /** - * Arrange - */ - const owner = await createOwnerWithApiKey(); - const variable = await createVariable(); - /** * Act */ @@ -160,10 +179,7 @@ describe('Variables in Public API', () => { * Assert */ expect(response.status).toBe(403); - expect(response.body).toHaveProperty( - 'message', - new FeatureNotLicensedError('feat:variables').message, - ); + expect(response.body).toHaveProperty('message', licenseErrorMessage); }); }); });