fix(API): Let global members grant variable scopes to API keys (#40064)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Uddish Verma 2026-10-02 09:20:55 +00:00 committed by GitHub
parent 01e5ebf726
commit c95e34f97a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 61 additions and 14 deletions

View File

@ -146,6 +146,10 @@ export const MEMBER_API_KEY_SCOPES: ApiKeyScope[] = [
'credential:update',
'credential:move',
'credential:delete',
'variable:create',
'variable:delete',
'variable:list',
'variable:update',
'dataTable:create',
'dataTable:read',
'dataTable:update',
@ -206,6 +210,11 @@ export const API_KEY_SCOPES_FOR_IMPLICIT_PERSONAL_PROJECT: ApiKeyScope[] = [
'credential:update',
'credential:move',
'credential:delete',
// Members hold these for team projects where they are editor or admin.
// The variables service checks the project role on every request.
'variable:create',
'variable:delete',
'variable:update',
'dataTable:create',
'dataTable:read',
'dataTable:update',

View File

@ -1,7 +1,11 @@
import { testDb } from '@n8n/backend-test-utils';
import { ApiKeyRepository, GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE } from '@n8n/db';
import { Container } from '@n8n/di';
import { getOwnerOnlyApiKeyScopes, type ApiKeyScope } from '@n8n/permissions';
import {
getOwnerOnlyApiKeyScopes,
MEMBER_API_KEY_SCOPES,
type ApiKeyScope,
} from '@n8n/permissions';
import type { InstanceSettings } from 'n8n-core';
import { mock } from 'vitest-mock-extended';
@ -70,6 +74,18 @@ describe('PublicApiKeyService', () => {
false,
);
});
it('should keep every scope a member may hold', async () => {
const adminUser = await createAdminWithApiKey({
scopes: [...MEMBER_API_KEY_SCOPES, 'user:create'],
});
const apiKeyId = adminUser.apiKeys[0].id;
await publicApiKeyService.removeOwnerOnlyScopesFromApiKeys(adminUser);
const apiKeyOnDb = await apiKeyRepository.findOneByOrFail({ id: apiKeyId });
expect(apiKeyOnDb.scopes).toEqual(MEMBER_API_KEY_SCOPES);
});
});
describe('apiKeyHasValidScopes', () => {
@ -145,25 +161,14 @@ 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
it('should let a member grant every member scope', async () => {
const result = publicApiKeyService.apiKeyHasValidScopesForRole(
{
role: GLOBAL_MEMBER_ROLE,
},
folderScopes,
MEMBER_API_KEY_SCOPES,
);
// Assert
expect(result).toBe(true);
});
});

View File

@ -352,6 +352,39 @@ describe('Variables in Public API', () => {
);
});
it('should let a member create a variable in a project they can edit', async () => {
testServer.license.enable('feat:variables');
const member = await createMemberWithApiKey({ scopes: ['variable:create'] });
await linkUserToProject(member, project, 'project:editor');
const response = await testServer
.publicApiAgentFor(member)
.post('/variables')
.send({ key: 'key', value: 'value', projectId: project.id });
expect(response.status).toBe(201);
const created = await getVariableByKey('key');
expect(created).not.toBeNull();
await expect(getVariableByIdOrFail(created!.id)).resolves.toEqual(
expect.objectContaining({ project: expect.objectContaining({ id: project.id }) }),
);
});
it('should not let a member create a global variable', async () => {
testServer.license.enable('feat:variables');
const member = await createMemberWithApiKey({ scopes: ['variable:create'] });
await linkUserToProject(member, project, 'project:admin');
const response = await testServer
.publicApiAgentFor(member)
.post('/variables')
.send({ key: 'key', value: 'value' });
expect(response.status).toBe(403);
expect(response.body).toHaveProperty('message', 'You are not allowed to create a variable');
await expect(getVariableByKey('key')).resolves.toBeNull();
});
it('if not licensed, should reject', async () => {
const variablePayload = { key: 'key', value: 'value' };