diff --git a/packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snap b/packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snap index fb2761af0fb..703941fcff6 100644 --- a/packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snap +++ b/packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snap @@ -47,6 +47,7 @@ exports[`Scope Information > ensure scopes are defined correctly 1`] = ` "credential:update", "credential:delete", "credential:list", + "credentialTypePolicy:manage", "externalSecretsProvider:sync", "externalSecretsProvider:create", "externalSecretsProvider:read", diff --git a/packages/@n8n/permissions/src/constants.ee.ts b/packages/@n8n/permissions/src/constants.ee.ts index 2e937486db0..7d37c0b3744 100644 --- a/packages/@n8n/permissions/src/constants.ee.ts +++ b/packages/@n8n/permissions/src/constants.ee.ts @@ -22,6 +22,7 @@ export const RESOURCES = { 'manageInstance', ...DEFAULT_OPERATIONS, ] as const, + credentialTypePolicy: ['manage'] as const, externalSecretsProvider: ['sync', ...DEFAULT_OPERATIONS] as const, externalSecret: ['list'] as const, eventBusDestination: ['test', ...DEFAULT_OPERATIONS] as const, @@ -125,6 +126,7 @@ export const API_KEY_RESOURCES = { role: ['manage', 'manageProject', 'list', 'read'] as const, roleMappingRule: ['create', 'delete', 'list', 'update'] as const, nodeTypePolicy: ['manage'] as const, + // TODO: add credentialTypePolicy:manage once a Public API endpoint consumes it. } as const; export const GLOBAL_OWNER_ROLE_SLUG = 'global:owner'; diff --git a/packages/@n8n/permissions/src/roles/__tests__/credential-type-policy-scope.test.ts b/packages/@n8n/permissions/src/roles/__tests__/credential-type-policy-scope.test.ts new file mode 100644 index 00000000000..d0097cba522 --- /dev/null +++ b/packages/@n8n/permissions/src/roles/__tests__/credential-type-policy-scope.test.ts @@ -0,0 +1,58 @@ +import { API_KEY_RESOURCES } from '@/constants.ee'; +import { GLOBAL_ADMIN_SCOPES, GLOBAL_OWNER_SCOPES } from '@/roles/scopes/global-scopes.ee'; +import { + PERSONAL_PROJECT_OWNER_SCOPES, + PROJECT_EDITOR_SCOPES, + PROJECT_VIEWER_SCOPES, + REGULAR_PROJECT_ADMIN_SCOPES, +} from '@/roles/scopes/project-scopes.ee'; +import type { AuthPrincipal } from '@/types.ee'; +import { hasGlobalScope } from '@/utilities/has-global-scope.ee'; + +const principal = (slug: string, scopes: string[]): AuthPrincipal => + ({ + role: { slug, scopes: scopes.map((scope) => ({ slug: scope })) }, + }) as unknown as AuthPrincipal; + +describe('credentialTypePolicy:manage and nodeTypePolicy:manage are independent scopes', () => { + it('a user granted only nodeTypePolicy:manage does not also gain credentialTypePolicy:manage', () => { + expect( + hasGlobalScope( + principal('custom:node-policy-manager-only', ['nodeTypePolicy:manage']), + 'credentialTypePolicy:manage', + ), + ).toBe(false); + }); + + it('a user granted only credentialTypePolicy:manage does not also gain nodeTypePolicy:manage', () => { + expect( + hasGlobalScope( + principal('custom:credential-policy-manager-only', ['credentialTypePolicy:manage']), + 'nodeTypePolicy:manage', + ), + ).toBe(false); + }); +}); + +describe('credentialTypePolicy:manage default grants', () => { + it('is granted to instance owners and admins', () => { + expect(GLOBAL_OWNER_SCOPES).toContain('credentialTypePolicy:manage'); + expect(GLOBAL_ADMIN_SCOPES).toContain('credentialTypePolicy:manage'); + }); + + it('is granted to project admins, same as nodeTypePolicy:manage', () => { + expect(REGULAR_PROJECT_ADMIN_SCOPES).toContain('credentialTypePolicy:manage'); + }); + + it('is not granted to personal project owners, editors, or viewers', () => { + expect(PERSONAL_PROJECT_OWNER_SCOPES).not.toContain('credentialTypePolicy:manage'); + expect(PROJECT_EDITOR_SCOPES).not.toContain('credentialTypePolicy:manage'); + expect(PROJECT_VIEWER_SCOPES).not.toContain('credentialTypePolicy:manage'); + }); +}); + +describe('credentialTypePolicy:manage as an API key scope', () => { + it('is not declared in API_KEY_RESOURCES yet, since no Public API endpoint consumes it', () => { + expect(API_KEY_RESOURCES).not.toHaveProperty('credentialTypePolicy'); + }); +}); diff --git a/packages/@n8n/permissions/src/roles/scopes/global-scopes.ee.ts b/packages/@n8n/permissions/src/roles/scopes/global-scopes.ee.ts index 979621a55b5..959e2625880 100644 --- a/packages/@n8n/permissions/src/roles/scopes/global-scopes.ee.ts +++ b/packages/@n8n/permissions/src/roles/scopes/global-scopes.ee.ts @@ -30,6 +30,7 @@ export const GLOBAL_OWNER_SCOPES: Scope[] = [ 'credential:connect', 'credential:createEndUser', 'credential:manageInstance', + 'credentialTypePolicy:manage', 'community:register', 'communityPackage:install', 'communityPackage:uninstall', diff --git a/packages/@n8n/permissions/src/roles/scopes/project-scopes.ee.ts b/packages/@n8n/permissions/src/roles/scopes/project-scopes.ee.ts index 6b38bac3d59..3c1e7a84997 100644 --- a/packages/@n8n/permissions/src/roles/scopes/project-scopes.ee.ts +++ b/packages/@n8n/permissions/src/roles/scopes/project-scopes.ee.ts @@ -54,6 +54,7 @@ export const REGULAR_PROJECT_ADMIN_SCOPES: Scope[] = [ 'folder:move', 'sourceControl:push', 'nodeTypePolicy:manage', + 'credentialTypePolicy:manage', 'dataTable:create', 'dataTable:delete', 'dataTable:read', diff --git a/packages/@n8n/permissions/src/scope-information.ts b/packages/@n8n/permissions/src/scope-information.ts index f5fac2ec645..59fee8fbc4a 100644 --- a/packages/@n8n/permissions/src/scope-information.ts +++ b/packages/@n8n/permissions/src/scope-information.ts @@ -67,6 +67,11 @@ export const scopeInformation: Partial> = { displayName: 'Manage Node Type Policies', description: 'Allows managing which node types are available, on the instance or in a project.', }, + 'credentialTypePolicy:manage': { + displayName: 'Manage Credential Type Policies', + description: + 'Allows managing which credential types are available, on the instance or in a project.', + }, 'encryptionKey:manage': { displayName: 'Manage Encryption Keys', description: 'Allows listing and rotating instance encryption keys.', diff --git a/packages/@n8n/permissions/src/utilities/__tests__/get-resource-permissions.test.ts b/packages/@n8n/permissions/src/utilities/__tests__/get-resource-permissions.test.ts index 5f6ecf62a48..71658da113d 100644 --- a/packages/@n8n/permissions/src/utilities/__tests__/get-resource-permissions.test.ts +++ b/packages/@n8n/permissions/src/utilities/__tests__/get-resource-permissions.test.ts @@ -16,6 +16,7 @@ describe('permissions', () => { community: {}, communityPackage: {}, credential: {}, + credentialTypePolicy: {}, externalSecretsProvider: {}, externalSecret: {}, eventBusDestination: {}, @@ -113,6 +114,7 @@ describe('permissions', () => { share: true, update: true, }, + credentialTypePolicy: {}, eventBusDestination: { list: true, test: true, diff --git a/packages/frontend/@n8n/stores/src/rbac.store.ts b/packages/frontend/@n8n/stores/src/rbac.store.ts index 09756395cc2..300a530c747 100644 --- a/packages/frontend/@n8n/stores/src/rbac.store.ts +++ b/packages/frontend/@n8n/stores/src/rbac.store.ts @@ -22,6 +22,7 @@ export const useRBACStore = defineStore(STORES.RBAC, () => { annotationTag: {}, user: {}, credential: {}, + credentialTypePolicy: {}, variable: {}, projectVariable: {}, sourceControl: {},