mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 13:39:44 +02:00
feat(core): Support disabling certain permission scopes in personal space for canvas only (no-changelog) (#38858)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
c4527242c9
commit
4faeb001ba
|
|
@ -0,0 +1,88 @@
|
|||
import { Container } from '@n8n/di';
|
||||
|
||||
import { CanvasOnlyConfig } from '../canvas-only.config';
|
||||
|
||||
describe('CanvasOnlyConfig', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
Container.reset();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('defaults', () => {
|
||||
test('enabled is false', () => {
|
||||
process.env = {};
|
||||
expect(Container.get(CanvasOnlyConfig).enabled).toBe(false);
|
||||
});
|
||||
|
||||
test('personalSpaceScopeDenyList is empty', () => {
|
||||
process.env = {};
|
||||
expect(Container.get(CanvasOnlyConfig).personalSpaceScopeDenyList).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('N8N_CANVAS_ONLY', () => {
|
||||
test('sets enabled to true', () => {
|
||||
process.env = { N8N_CANVAS_ONLY: 'true' };
|
||||
expect(Container.get(CanvasOnlyConfig).enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('N8N_CANVAS_ONLY_PERSONAL_SPACE_SCOPE_DENY_LIST', () => {
|
||||
test('parses a comma-separated list of deniable scopes', () => {
|
||||
process.env = {
|
||||
N8N_CANVAS_ONLY_PERSONAL_SPACE_SCOPE_DENY_LIST: 'credential:create,agent:create',
|
||||
};
|
||||
expect(Container.get(CanvasOnlyConfig).personalSpaceScopeDenyList).toEqual([
|
||||
'credential:create',
|
||||
'agent:create',
|
||||
]);
|
||||
});
|
||||
|
||||
test('accepts every deniable scope', () => {
|
||||
process.env = {
|
||||
N8N_CANVAS_ONLY_PERSONAL_SPACE_SCOPE_DENY_LIST:
|
||||
'credential:create,dataTable:create,agent:create',
|
||||
};
|
||||
expect(Container.get(CanvasOnlyConfig).personalSpaceScopeDenyList).toEqual([
|
||||
'credential:create',
|
||||
'dataTable:create',
|
||||
'agent:create',
|
||||
]);
|
||||
});
|
||||
|
||||
test('trims whitespace, drops empty entries and removes duplicates', () => {
|
||||
process.env = {
|
||||
N8N_CANVAS_ONLY_PERSONAL_SPACE_SCOPE_DENY_LIST:
|
||||
' credential:create , ,agent:create,credential:create,',
|
||||
};
|
||||
expect(Container.get(CanvasOnlyConfig).personalSpaceScopeDenyList).toEqual([
|
||||
'credential:create',
|
||||
'agent:create',
|
||||
]);
|
||||
});
|
||||
|
||||
test('treats a blank value as an empty list', () => {
|
||||
process.env = { N8N_CANVAS_ONLY_PERSONAL_SPACE_SCOPE_DENY_LIST: '' };
|
||||
expect(Container.get(CanvasOnlyConfig).personalSpaceScopeDenyList).toEqual([]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['a scope that is not deniable', 'credential:create,workflow:create'],
|
||||
['an unknown value', 'nonsense'],
|
||||
])('warns and keeps the default when the list contains %s', (_label, value) => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
process.env = { N8N_CANVAS_ONLY_PERSONAL_SPACE_SCOPE_DENY_LIST: value };
|
||||
|
||||
expect(Container.get(CanvasOnlyConfig).personalSpaceScopeDenyList).toEqual([]);
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid value for N8N_CANVAS_ONLY_PERSONAL_SPACE_SCOPE_DENY_LIST'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
47
packages/@n8n/config/src/configs/canvas-only.config.ts
Normal file
47
packages/@n8n/config/src/configs/canvas-only.config.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { Config, Env } from '../decorators';
|
||||
|
||||
/**
|
||||
* The scopes an operator may take away from the personal space role in
|
||||
* canvas-only mode. Every other scope of that role stays fixed.
|
||||
*/
|
||||
export const CANVAS_ONLY_PERSONAL_SPACE_DENIABLE_SCOPES = [
|
||||
'credential:create',
|
||||
'dataTable:create',
|
||||
'agent:create',
|
||||
] as const;
|
||||
|
||||
export type CanvasOnlyPersonalSpaceDeniableScope =
|
||||
(typeof CANVAS_ONLY_PERSONAL_SPACE_DENIABLE_SCOPES)[number];
|
||||
|
||||
const deniableScopeSchema = z.enum(CANVAS_ONLY_PERSONAL_SPACE_DENIABLE_SCOPES);
|
||||
|
||||
/**
|
||||
* Parses the comma-separated deny list. One entry outside the deniable scopes
|
||||
* rejects the whole value, so the `Env` decorator warns and keeps the default.
|
||||
*/
|
||||
const personalSpaceScopeDenyListSchema = z
|
||||
.string()
|
||||
.transform((raw) =>
|
||||
[...new Set(raw.split(',').map((entry) => entry.trim()))].filter((entry) => entry.length > 0),
|
||||
)
|
||||
.pipe(z.array(deniableScopeSchema));
|
||||
|
||||
@Config
|
||||
export class CanvasOnlyConfig {
|
||||
/** Whether to enable canvas-only mode, hiding the chrome UI. */
|
||||
@Env('N8N_CANVAS_ONLY')
|
||||
enabled: boolean = false;
|
||||
|
||||
/**
|
||||
* Comma-separated scopes to remove from the personal space role, which every
|
||||
* user holds in their own personal project. Only `credential:create`,
|
||||
* `dataTable:create` and `agent:create` are accepted. Has no effect unless
|
||||
* `N8N_CANVAS_ONLY` is `true`. Applied by the role sync on every start.
|
||||
*
|
||||
* @example `N8N_CANVAS_ONLY_PERSONAL_SPACE_SCOPE_DENY_LIST=credential:create,agent:create`
|
||||
*/
|
||||
@Env('N8N_CANVAS_ONLY_PERSONAL_SPACE_SCOPE_DENY_LIST', personalSpaceScopeDenyListSchema)
|
||||
personalSpaceScopeDenyList: CanvasOnlyPersonalSpaceDeniableScope[] = [];
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { AiGatewayConfig } from './configs/ai-gateway.config';
|
|||
import { AiConfig } from './configs/ai.config';
|
||||
import { AuthConfig } from './configs/auth.config';
|
||||
import { CacheConfig } from './configs/cache.config';
|
||||
import { CanvasOnlyConfig } from './configs/canvas-only.config';
|
||||
import { ChatHubConfig } from './configs/chat-hub.config';
|
||||
import { ChatTriggerConfig } from './configs/chat-trigger.config';
|
||||
import { CollaborationConfig } from './configs/collaboration.config';
|
||||
|
|
@ -92,6 +93,11 @@ export { McpClientConfig } from './configs/mcp-client.config';
|
|||
export { McpServerConfig } from './configs/mcp-server.config';
|
||||
export { MfaConfig } from './configs/mfa.config';
|
||||
export { HiringBannerConfig } from './configs/hiring-banner.config';
|
||||
export {
|
||||
CanvasOnlyConfig,
|
||||
CANVAS_ONLY_PERSONAL_SPACE_DENIABLE_SCOPES,
|
||||
type CanvasOnlyPersonalSpaceDeniableScope,
|
||||
} from './configs/canvas-only.config';
|
||||
export { HttpRequestConfig } from './configs/http-request.config';
|
||||
export { PersonalizationConfig } from './configs/personalization.config';
|
||||
export { NodesConfig } from './configs/nodes.config';
|
||||
|
|
@ -269,9 +275,8 @@ export class GlobalConfig {
|
|||
@Env('N8N_SSL_CERT')
|
||||
ssl_cert: string = '';
|
||||
|
||||
/** Whether to enable canvas-only mode, hiding the chrome UI. */
|
||||
@Env('N8N_CANVAS_ONLY')
|
||||
canvasOnly: boolean = false;
|
||||
@Nested
|
||||
canvasOnly: CanvasOnlyConfig;
|
||||
|
||||
/** Public URL where the editor is accessible. Also used for emails sent from n8n. */
|
||||
@Env('N8N_EDITOR_BASE_URL')
|
||||
|
|
|
|||
|
|
@ -78,7 +78,10 @@ describe('GlobalConfig', () => {
|
|||
proxy_hops: 0,
|
||||
ssl_key: '',
|
||||
ssl_cert: '',
|
||||
canvasOnly: false,
|
||||
canvasOnly: {
|
||||
enabled: false,
|
||||
personalSpaceScopeDenyList: [],
|
||||
},
|
||||
editorBaseUrl: '',
|
||||
webhookUrl: '',
|
||||
dataTable: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Logger } from '@n8n/backend-common';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { Service } from '@n8n/di';
|
||||
// eslint-disable-next-line import-x/order
|
||||
import {
|
||||
|
|
@ -25,6 +26,7 @@ export class AuthRolesService {
|
|||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly dbLockService: DbLockService,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
) {}
|
||||
|
||||
private async syncScopes(tx: EntityManager) {
|
||||
|
|
@ -144,6 +146,22 @@ export class AuthRolesService {
|
|||
return scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Canvas-only mode lets an operator take some scopes away from the personal
|
||||
* owner role through `N8N_CANVAS_ONLY_PERSONAL_SPACE_SCOPE_DENY_LIST`.
|
||||
* The config already limits the list to the scopes that may be removed.
|
||||
*/
|
||||
private removeCanvasOnlyPersonalOwnerScopes(scopes: string[]) {
|
||||
const { enabled, personalSpaceScopeDenyList } = this.globalConfig.canvasOnly;
|
||||
if (!enabled || personalSpaceScopeDenyList.length === 0) return scopes;
|
||||
|
||||
this.logger.debug(
|
||||
`Canvas-only mode - removing ${personalSpaceScopeDenyList.join(', ')} scopes from ${PROJECT_OWNER_ROLE_SLUG} role`,
|
||||
);
|
||||
const denied: readonly string[] = personalSpaceScopeDenyList;
|
||||
return scopes.filter((slug) => !denied.includes(slug));
|
||||
}
|
||||
|
||||
private async getExternalSecretsSystemRolesScopes(
|
||||
roleSlug: string,
|
||||
tx: EntityManager,
|
||||
|
|
@ -172,7 +190,8 @@ export class AuthRolesService {
|
|||
/**
|
||||
* Modifies the expected scopes for a role based on settings.
|
||||
* Uses a "closed first" approach: certain scopes are not in the base definition
|
||||
* and are added when the corresponding setting is enabled.
|
||||
* and are added when the corresponding setting is enabled. Canvas-only mode
|
||||
* can also remove scopes from the personal owner role through config.
|
||||
*/
|
||||
private async updateScopesBasedOnSettings(
|
||||
roleSlug: string,
|
||||
|
|
@ -183,6 +202,7 @@ export class AuthRolesService {
|
|||
// Special handling for project:personalOwner role
|
||||
if (roleSlug === PROJECT_OWNER_ROLE_SLUG) {
|
||||
scopes.push(...(await this.getPersonalOwnerSettingsScopes(tx)));
|
||||
return this.removeCanvasOnlyPersonalOwnerScopes(scopes);
|
||||
}
|
||||
|
||||
// External secrets system roles scopes
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Logger } from '@n8n/backend-common';
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import { CANVAS_ONLY_PERSONAL_SPACE_DENIABLE_SCOPES, type GlobalConfig } from '@n8n/config';
|
||||
import { AuthRolesService, Role, Scope } from '@n8n/db';
|
||||
import type { DbLock, DbLockService } from '@n8n/db';
|
||||
import {
|
||||
|
|
@ -43,7 +44,11 @@ describe('AuthRolesService', () => {
|
|||
await fn(mockEntityManager),
|
||||
);
|
||||
|
||||
const authRolesService = new AuthRolesService(logger, dbLockService);
|
||||
const globalConfig = mock<GlobalConfig>({
|
||||
canvasOnly: { enabled: false, personalSpaceScopeDenyList: [] },
|
||||
});
|
||||
|
||||
const authRolesService = new AuthRolesService(logger, dbLockService, globalConfig);
|
||||
|
||||
// Helper functions for creating test data
|
||||
function createScope(
|
||||
|
|
@ -118,6 +123,8 @@ describe('AuthRolesService', () => {
|
|||
// Default: no settings rows (backward compat: undefined values => grant scopes)
|
||||
mockEntityManager.findBy.mockResolvedValue([]);
|
||||
mockEntityManager.findOneBy.mockResolvedValue(null);
|
||||
globalConfig.canvasOnly.enabled = false;
|
||||
globalConfig.canvasOnly.personalSpaceScopeDenyList = [];
|
||||
dbLockService.withLock.mockImplementation(
|
||||
async (_lockId: DbLock, fn: (tx: EntityManager) => Promise<unknown>) =>
|
||||
await fn(mockEntityManager),
|
||||
|
|
@ -623,6 +630,95 @@ describe('AuthRolesService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('canvas-only personal space role', () => {
|
||||
const DENIABLE_SCOPES = [...CANVAS_ONLY_PERSONAL_SPACE_DENIABLE_SCOPES];
|
||||
|
||||
/** The scope slugs the sync gives the personal owner role on a fresh instance. */
|
||||
async function syncedPersonalOwnerScopes(): Promise<string[]> {
|
||||
setupDefaultMocks(createAllScopes());
|
||||
await authRolesService.init();
|
||||
|
||||
const personalOwnerCall = roleRepository.create.mock.calls.find(
|
||||
(call) => (call[0] as Role).slug === PROJECT_OWNER_ROLE_SLUG,
|
||||
);
|
||||
expect(personalOwnerCall).toBeDefined();
|
||||
return (personalOwnerCall?.[0] as Role).scopes.map((s: Scope) => s.slug);
|
||||
}
|
||||
|
||||
test('should give the personalOwner role every deniable scope when the deny list is empty', async () => {
|
||||
globalConfig.canvasOnly.enabled = true;
|
||||
globalConfig.canvasOnly.personalSpaceScopeDenyList = [];
|
||||
|
||||
const scopeSlugs = await syncedPersonalOwnerScopes();
|
||||
|
||||
expect(scopeSlugs).toEqual(expect.arrayContaining(DENIABLE_SCOPES));
|
||||
});
|
||||
|
||||
test('should remove the denied scopes when canvas-only mode is on', async () => {
|
||||
globalConfig.canvasOnly.enabled = true;
|
||||
globalConfig.canvasOnly.personalSpaceScopeDenyList = ['credential:create'];
|
||||
|
||||
const scopeSlugs = await syncedPersonalOwnerScopes();
|
||||
|
||||
expect(scopeSlugs).not.toContain('credential:create');
|
||||
expect(scopeSlugs).toContain('dataTable:create');
|
||||
expect(scopeSlugs).toContain('agent:create');
|
||||
expect(scopeSlugs).toContain('workflow:create');
|
||||
});
|
||||
|
||||
test('should remove every deniable scope when all are denied', async () => {
|
||||
globalConfig.canvasOnly.enabled = true;
|
||||
globalConfig.canvasOnly.personalSpaceScopeDenyList = DENIABLE_SCOPES;
|
||||
|
||||
const scopeSlugs = await syncedPersonalOwnerScopes();
|
||||
|
||||
for (const scope of DENIABLE_SCOPES) {
|
||||
expect(scopeSlugs).not.toContain(scope);
|
||||
}
|
||||
expect(scopeSlugs).toContain('workflow:create');
|
||||
});
|
||||
|
||||
test('should ignore the deny list when canvas-only mode is off', async () => {
|
||||
globalConfig.canvasOnly.enabled = false;
|
||||
globalConfig.canvasOnly.personalSpaceScopeDenyList = ['credential:create'];
|
||||
|
||||
const scopeSlugs = await syncedPersonalOwnerScopes();
|
||||
|
||||
expect(scopeSlugs).toContain('credential:create');
|
||||
});
|
||||
|
||||
test('should update an existing personalOwner role to remove the denied scopes', async () => {
|
||||
globalConfig.canvasOnly.enabled = true;
|
||||
globalConfig.canvasOnly.personalSpaceScopeDenyList = ['credential:create'];
|
||||
const allScopes = createAllScopes();
|
||||
const personalOwnerRoleDef = ALL_ROLES.project.find(
|
||||
(r) => r.slug === PROJECT_OWNER_ROLE_SLUG,
|
||||
)!;
|
||||
const existingRole = createRole(PROJECT_OWNER_ROLE_SLUG, {
|
||||
displayName: personalOwnerRoleDef.displayName,
|
||||
description: personalOwnerRoleDef.description ?? null,
|
||||
roleType: 'project',
|
||||
scopes: allScopes.filter((s) => personalOwnerRoleDef.scopes.includes(s.slug)),
|
||||
});
|
||||
scopeRepository.find.mockResolvedValue(allScopes);
|
||||
roleRepository.find.mockResolvedValue([existingRole]);
|
||||
roleRepository.save.mockImplementation(async (entities) => entities as never);
|
||||
|
||||
await authRolesService.init();
|
||||
|
||||
const projectRoleSaveCall = roleRepository.save.mock.calls.find((call) => {
|
||||
const roles = call[0] as Role[];
|
||||
return Array.isArray(roles) && roles.some((r) => r?.roleType === 'project');
|
||||
});
|
||||
expect(projectRoleSaveCall).toBeDefined();
|
||||
const savedRoles = projectRoleSaveCall?.[0] as Role[];
|
||||
const updatedRole = savedRoles.find((r) => r?.slug === PROJECT_OWNER_ROLE_SLUG);
|
||||
const scopeSlugs = updatedRole?.scopes.map((s) => s.slug) ?? [];
|
||||
expect(scopeSlugs).not.toContain('credential:create');
|
||||
expect(scopeSlugs).toContain('dataTable:create');
|
||||
});
|
||||
});
|
||||
|
||||
describe('personal space sharing', () => {
|
||||
test('should add sharing scopes to personalOwner role when setting is null (backward compatibility)', async () => {
|
||||
const allScopes = createAllScopes();
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ export class FrontendService {
|
|||
agentEvalsEnabled: this.globalConfig.evaluation.agentEvalsEnabled,
|
||||
},
|
||||
activeModules: this.moduleRegistry.getActiveModules(),
|
||||
canvasOnly: this.globalConfig.canvasOnly,
|
||||
canvasOnly: this.globalConfig.canvasOnly.enabled,
|
||||
collaboration: {
|
||||
crdt: this.globalConfig.collaboration.crdt,
|
||||
},
|
||||
|
|
|
|||
132
packages/cli/test/integration/auth-roles-canvas-only.test.ts
Normal file
132
packages/cli/test/integration/auth-roles-canvas-only.test.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { testDb } from '@n8n/backend-test-utils';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { AuthRolesService, RoleRepository, type User } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { PROJECT_OWNER_ROLE_SLUG } from '@n8n/permissions';
|
||||
|
||||
import { RoleCacheService } from '@/services/role-cache.service';
|
||||
|
||||
import { createMemberWithApiKey } from './shared/db/users';
|
||||
import * as utils from './shared/utils/';
|
||||
|
||||
describe('N8N_CANVAS_ONLY_PERSONAL_SPACE_SCOPE_DENY_LIST', () => {
|
||||
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
|
||||
const { canvasOnly } = Container.get(GlobalConfig);
|
||||
|
||||
let member: User;
|
||||
|
||||
const credentialPayload = () => ({
|
||||
name: 'test credential',
|
||||
type: 'githubApi',
|
||||
data: { accessToken: 'abcdefghijklmnopqrstuvwxyz', user: 'test', server: 'testServer' },
|
||||
});
|
||||
|
||||
const workflowPayload = () => ({
|
||||
name: 'test workflow',
|
||||
nodes: [],
|
||||
connections: {},
|
||||
staticData: null,
|
||||
settings: { executionOrder: 'v1' },
|
||||
});
|
||||
|
||||
const storedScopes = async () => {
|
||||
const role = await Container.get(RoleRepository).findBySlug(PROJECT_OWNER_ROLE_SLUG);
|
||||
return role?.scopes.map((s) => s.slug) ?? [];
|
||||
};
|
||||
|
||||
/** Re-runs the system role sync, as a restart does, and drops the role cache. */
|
||||
const syncRoles = async () => {
|
||||
await Container.get(AuthRolesService).init();
|
||||
await Container.get(RoleCacheService).invalidateCache();
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
await utils.initCredentialsTypes();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate([
|
||||
'SharedCredentials',
|
||||
'CredentialsEntity',
|
||||
'SharedWorkflow',
|
||||
'WorkflowEntity',
|
||||
'User',
|
||||
]);
|
||||
member = await createMemberWithApiKey();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
canvasOnly.enabled = false;
|
||||
canvasOnly.personalSpaceScopeDenyList = [];
|
||||
await syncRoles();
|
||||
});
|
||||
|
||||
it('stops credential creation in the personal project but keeps workflows working', async () => {
|
||||
const before = await testServer
|
||||
.publicApiAgentFor(member)
|
||||
.post('/credentials')
|
||||
.send(credentialPayload());
|
||||
expect(before.status).toBe(200);
|
||||
|
||||
canvasOnly.enabled = true;
|
||||
canvasOnly.personalSpaceScopeDenyList = ['credential:create'];
|
||||
await syncRoles();
|
||||
|
||||
expect(await storedScopes()).not.toContain('credential:create');
|
||||
|
||||
const after = await testServer
|
||||
.publicApiAgentFor(member)
|
||||
.post('/credentials')
|
||||
.send(credentialPayload());
|
||||
expect(after.status).toBe(403);
|
||||
|
||||
const workflow = await testServer
|
||||
.publicApiAgentFor(member)
|
||||
.post('/workflows')
|
||||
.send(workflowPayload());
|
||||
expect(workflow.status).toBe(200);
|
||||
});
|
||||
|
||||
it('removes every denied scope and keeps the rest of the role', async () => {
|
||||
canvasOnly.enabled = true;
|
||||
canvasOnly.personalSpaceScopeDenyList = [
|
||||
'credential:create',
|
||||
'dataTable:create',
|
||||
'agent:create',
|
||||
];
|
||||
await syncRoles();
|
||||
|
||||
const scopes = await storedScopes();
|
||||
expect(scopes).not.toContain('credential:create');
|
||||
expect(scopes).not.toContain('dataTable:create');
|
||||
expect(scopes).not.toContain('agent:create');
|
||||
expect(scopes).toContain('workflow:create');
|
||||
expect(scopes).toContain('credential:read');
|
||||
});
|
||||
|
||||
it('gives the scopes back once the deny list is cleared', async () => {
|
||||
canvasOnly.enabled = true;
|
||||
canvasOnly.personalSpaceScopeDenyList = ['credential:create'];
|
||||
await syncRoles();
|
||||
expect(await storedScopes()).not.toContain('credential:create');
|
||||
|
||||
canvasOnly.personalSpaceScopeDenyList = [];
|
||||
await syncRoles();
|
||||
|
||||
expect(await storedScopes()).toContain('credential:create');
|
||||
const created = await testServer
|
||||
.publicApiAgentFor(member)
|
||||
.post('/credentials')
|
||||
.send(credentialPayload());
|
||||
expect(created.status).toBe(200);
|
||||
});
|
||||
|
||||
it('ignores the deny list when canvas-only mode is off', async () => {
|
||||
canvasOnly.enabled = false;
|
||||
canvasOnly.personalSpaceScopeDenyList = ['credential:create'];
|
||||
await syncRoles();
|
||||
|
||||
expect(await storedScopes()).toContain('credential:create');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user