mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Implement project:viewer role (#9611)
This commit is contained in:
@@ -275,7 +275,7 @@ export class CredentialsService {
|
|||||||
|
|
||||||
if (typeof projectId === 'string' && project === null) {
|
if (typeof projectId === 'string' && project === null) {
|
||||||
throw new BadRequestError(
|
throw new BadRequestError(
|
||||||
"You don't have the permissions to save the workflow in this project.",
|
"You don't have the permissions to save the credential in this project.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { WithTimestamps } from './AbstractEntity';
|
|||||||
import { Project } from './Project';
|
import { Project } from './Project';
|
||||||
|
|
||||||
// personalOwner is only used for personal projects
|
// personalOwner is only used for personal projects
|
||||||
export type ProjectRole = 'project:personalOwner' | 'project:admin' | 'project:editor';
|
export type ProjectRole =
|
||||||
|
| 'project:personalOwner'
|
||||||
|
| 'project:admin'
|
||||||
|
| 'project:editor'
|
||||||
|
| 'project:viewer';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class ProjectRelation extends WithTimestamps {
|
export class ProjectRelation extends WithTimestamps {
|
||||||
|
|||||||
@@ -61,3 +61,12 @@ export const PROJECT_EDITOR_SCOPES: Scope[] = [
|
|||||||
'project:list',
|
'project:list',
|
||||||
'project:read',
|
'project:read',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const PROJECT_VIEWER_SCOPES: Scope[] = [
|
||||||
|
'credential:list',
|
||||||
|
'credential:read',
|
||||||
|
'project:list',
|
||||||
|
'project:read',
|
||||||
|
'workflow:list',
|
||||||
|
'workflow:read',
|
||||||
|
];
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
PERSONAL_PROJECT_OWNER_SCOPES,
|
PERSONAL_PROJECT_OWNER_SCOPES,
|
||||||
PROJECT_EDITOR_SCOPES,
|
PROJECT_EDITOR_SCOPES,
|
||||||
|
PROJECT_VIEWER_SCOPES,
|
||||||
REGULAR_PROJECT_ADMIN_SCOPES,
|
REGULAR_PROJECT_ADMIN_SCOPES,
|
||||||
} from '@/permissions/project-roles';
|
} from '@/permissions/project-roles';
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +40,7 @@ const PROJECT_SCOPE_MAP: Record<ProjectRole, Scope[]> = {
|
|||||||
'project:admin': REGULAR_PROJECT_ADMIN_SCOPES,
|
'project:admin': REGULAR_PROJECT_ADMIN_SCOPES,
|
||||||
'project:personalOwner': PERSONAL_PROJECT_OWNER_SCOPES,
|
'project:personalOwner': PERSONAL_PROJECT_OWNER_SCOPES,
|
||||||
'project:editor': PROJECT_EDITOR_SCOPES,
|
'project:editor': PROJECT_EDITOR_SCOPES,
|
||||||
|
'project:viewer': PROJECT_VIEWER_SCOPES,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CREDENTIALS_SHARING_SCOPE_MAP: Record<CredentialSharingRole, Scope[]> = {
|
const CREDENTIALS_SHARING_SCOPE_MAP: Record<CredentialSharingRole, Scope[]> = {
|
||||||
@@ -87,6 +89,7 @@ const ROLE_NAMES: Record<
|
|||||||
'project:personalOwner': 'Project Owner',
|
'project:personalOwner': 'Project Owner',
|
||||||
'project:admin': 'Project Admin',
|
'project:admin': 'Project Admin',
|
||||||
'project:editor': 'Project Editor',
|
'project:editor': 'Project Editor',
|
||||||
|
'project:viewer': 'Project Viewer',
|
||||||
'credential:user': 'Credential User',
|
'credential:user': 'Credential User',
|
||||||
'credential:owner': 'Credential Owner',
|
'credential:owner': 'Credential Owner',
|
||||||
'workflow:owner': 'Workflow Owner',
|
'workflow:owner': 'Workflow Owner',
|
||||||
@@ -230,6 +233,8 @@ export class RoleService {
|
|||||||
return this.license.isProjectRoleAdminLicensed();
|
return this.license.isProjectRoleAdminLicensed();
|
||||||
case 'project:editor':
|
case 'project:editor':
|
||||||
return this.license.isProjectRoleEditorLicensed();
|
return this.license.isProjectRoleEditorLicensed();
|
||||||
|
case 'project:viewer':
|
||||||
|
return this.license.isProjectRoleViewerLicensed();
|
||||||
case 'global:admin':
|
case 'global:admin':
|
||||||
return this.license.isAdvancedPermissionsLicensed();
|
return this.license.isAdvancedPermissionsLicensed();
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
} from '../shared/db/users';
|
} from '../shared/db/users';
|
||||||
import type { SuperAgentTest } from '../shared/types';
|
import type { SuperAgentTest } from '../shared/types';
|
||||||
import { mockInstance } from '../../shared/mocking';
|
import { mockInstance } from '../../shared/mocking';
|
||||||
|
|
||||||
import { createTeamProject, linkUserToProject } from '../shared/db/projects';
|
import { createTeamProject, linkUserToProject } from '../shared/db/projects';
|
||||||
|
|
||||||
const testServer = utils.setupTestServer({
|
const testServer = utils.setupTestServer({
|
||||||
@@ -82,6 +81,23 @@ afterEach(() => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('POST /credentials', () => {
|
||||||
|
test('project viewers cannot create credentials', async () => {
|
||||||
|
const teamProject = await createTeamProject();
|
||||||
|
await linkUserToProject(member, teamProject, 'project:viewer');
|
||||||
|
|
||||||
|
const response = await testServer
|
||||||
|
.authAgentFor(member)
|
||||||
|
.post('/credentials')
|
||||||
|
.send({ ...randomCredentialPayload(), projectId: teamProject.id });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(400);
|
||||||
|
expect(response.body.message).toBe(
|
||||||
|
"You don't have the permissions to save the credential in this project.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// GET /credentials - fetch all credentials
|
// GET /credentials - fetch all credentials
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
@@ -231,6 +247,31 @@ describe('GET /credentials', () => {
|
|||||||
// GET /credentials/:id - fetch a certain credential
|
// GET /credentials/:id - fetch a certain credential
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
describe('GET /credentials/:id', () => {
|
describe('GET /credentials/:id', () => {
|
||||||
|
test('project viewers can view credentials', async () => {
|
||||||
|
const teamProject = await createTeamProject();
|
||||||
|
await linkUserToProject(member, teamProject, 'project:viewer');
|
||||||
|
|
||||||
|
const savedCredential = await saveCredential(randomCredentialPayload(), {
|
||||||
|
project: teamProject,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await testServer
|
||||||
|
.authAgentFor(member)
|
||||||
|
.get(`/credentials/${savedCredential.id}`);
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data).toMatchObject({
|
||||||
|
id: savedCredential.id,
|
||||||
|
shared: [{ projectId: teamProject.id, role: 'credential:owner' }],
|
||||||
|
homeProject: {
|
||||||
|
id: teamProject.id,
|
||||||
|
},
|
||||||
|
sharedWithProjects: [],
|
||||||
|
scopes: ['credential:read'],
|
||||||
|
});
|
||||||
|
expect(response.body.data.data).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
test('should retrieve owned cred for owner', async () => {
|
test('should retrieve owned cred for owner', async () => {
|
||||||
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
||||||
|
|
||||||
@@ -387,6 +428,35 @@ describe('GET /credentials/:id', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('PATCH /credentials/:id', () => {
|
||||||
|
test('project viewer cannot update credentials', async () => {
|
||||||
|
//
|
||||||
|
// ARRANGE
|
||||||
|
//
|
||||||
|
const teamProject = await createTeamProject('', member);
|
||||||
|
await linkUserToProject(member, teamProject, 'project:viewer');
|
||||||
|
|
||||||
|
const savedCredential = await saveCredential(randomCredentialPayload(), {
|
||||||
|
project: teamProject,
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// ACT
|
||||||
|
//
|
||||||
|
const response = await testServer
|
||||||
|
.authAgentFor(member)
|
||||||
|
.patch(`/credentials/${savedCredential.id}`)
|
||||||
|
.send({ ...randomCredentialPayload() });
|
||||||
|
|
||||||
|
//
|
||||||
|
// ASSERT
|
||||||
|
//
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(403);
|
||||||
|
expect(response.body.message).toBe('User is missing a scope required to perform this action');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// idempotent share/unshare
|
// idempotent share/unshare
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|||||||
@@ -685,7 +685,7 @@ describe('POST /credentials', () => {
|
|||||||
//
|
//
|
||||||
.expect(400, {
|
.expect(400, {
|
||||||
code: 400,
|
code: 400,
|
||||||
message: "You don't have the permissions to save the workflow in this project.",
|
message: "You don't have the permissions to save the credential in this project.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as testDb from './shared/testDb';
|
|||||||
import { setupTestServer } from './shared/utils';
|
import { setupTestServer } from './shared/utils';
|
||||||
import { mockInstance } from '../shared/mocking';
|
import { mockInstance } from '../shared/mocking';
|
||||||
import { WaitTracker } from '@/WaitTracker';
|
import { WaitTracker } from '@/WaitTracker';
|
||||||
|
import { createTeamProject, linkUserToProject } from './shared/db/projects';
|
||||||
|
|
||||||
const testServer = setupTestServer({ endpointGroups: ['executions'] });
|
const testServer = setupTestServer({ endpointGroups: ['executions'] });
|
||||||
|
|
||||||
@@ -45,6 +46,23 @@ describe('GET /executions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /executions/:id', () => {
|
describe('GET /executions/:id', () => {
|
||||||
|
test('project viewers can view executions for workflows in the project', async () => {
|
||||||
|
// if sharing is not enabled, we're only returning the executions of
|
||||||
|
// personal workflows
|
||||||
|
testServer.license.enable('feat:sharing');
|
||||||
|
|
||||||
|
const teamProject = await createTeamProject();
|
||||||
|
await linkUserToProject(member, teamProject, 'project:viewer');
|
||||||
|
|
||||||
|
const workflow = await createWorkflow({}, teamProject);
|
||||||
|
const execution = await createSuccessfulExecution(workflow);
|
||||||
|
|
||||||
|
const response = await testServer.authAgentFor(member).get(`/executions/${execution.id}`);
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
test('only returns executions of shared workflows if sharing is enabled', async () => {
|
test('only returns executions of shared workflows if sharing is enabled', async () => {
|
||||||
const workflow = await createWorkflow({}, owner);
|
const workflow = await createWorkflow({}, owner);
|
||||||
await shareWorkflowWithUsers(workflow, [member]);
|
await shareWorkflowWithUsers(workflow, [member]);
|
||||||
|
|||||||
@@ -479,261 +479,265 @@ describe('PATCH /projects/:projectId', () => {
|
|||||||
const updatedProject = await findProject(personalProject.id);
|
const updatedProject = await findProject(personalProject.id);
|
||||||
expect(updatedProject.name).not.toEqual('New Name');
|
expect(updatedProject.name).not.toEqual('New Name');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('PATCH /projects/:projectId', () => {
|
describe('member management', () => {
|
||||||
test('should add or remove users from a project', async () => {
|
test('should add or remove users from a project', async () => {
|
||||||
const [ownerUser, testUser1, testUser2, testUser3] = await Promise.all([
|
const [ownerUser, testUser1, testUser2, testUser3] = await Promise.all([
|
||||||
createOwner(),
|
createOwner(),
|
||||||
createUser(),
|
createUser(),
|
||||||
createUser(),
|
createUser(),
|
||||||
createUser(),
|
createUser(),
|
||||||
]);
|
]);
|
||||||
const [teamProject1, teamProject2] = await Promise.all([
|
const [teamProject1, teamProject2] = await Promise.all([
|
||||||
createTeamProject(undefined, testUser1),
|
createTeamProject(undefined, testUser1),
|
||||||
createTeamProject(undefined, testUser2),
|
createTeamProject(undefined, testUser2),
|
||||||
]);
|
]);
|
||||||
const [credential1, credential2] = await Promise.all([
|
const [credential1, credential2] = await Promise.all([
|
||||||
saveCredential(randomCredentialPayload(), {
|
saveCredential(randomCredentialPayload(), {
|
||||||
role: 'credential:owner',
|
role: 'credential:owner',
|
||||||
project: teamProject1,
|
project: teamProject1,
|
||||||
}),
|
}),
|
||||||
saveCredential(randomCredentialPayload(), {
|
saveCredential(randomCredentialPayload(), {
|
||||||
role: 'credential:owner',
|
role: 'credential:owner',
|
||||||
project: teamProject2,
|
project: teamProject2,
|
||||||
}),
|
}),
|
||||||
saveCredential(randomCredentialPayload(), {
|
saveCredential(randomCredentialPayload(), {
|
||||||
role: 'credential:owner',
|
role: 'credential:owner',
|
||||||
project: teamProject2,
|
project: teamProject2,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
await shareCredentialWithProjects(credential2, [teamProject1]);
|
await shareCredentialWithProjects(credential2, [teamProject1]);
|
||||||
|
|
||||||
await linkUserToProject(ownerUser, teamProject2, 'project:editor');
|
await linkUserToProject(ownerUser, teamProject2, 'project:editor');
|
||||||
await linkUserToProject(testUser2, teamProject2, 'project:editor');
|
await linkUserToProject(testUser2, teamProject2, 'project:editor');
|
||||||
|
|
||||||
const memberAgent = testServer.authAgentFor(testUser1);
|
const memberAgent = testServer.authAgentFor(testUser1);
|
||||||
|
|
||||||
const deleteSpy = jest.spyOn(Container.get(CacheService), 'deleteMany');
|
const deleteSpy = jest.spyOn(Container.get(CacheService), 'deleteMany');
|
||||||
const resp = await memberAgent.patch(`/projects/${teamProject1.id}`).send({
|
const resp = await memberAgent.patch(`/projects/${teamProject1.id}`).send({
|
||||||
name: teamProject1.name,
|
name: teamProject1.name,
|
||||||
relations: [
|
relations: [
|
||||||
{ userId: testUser1.id, role: 'project:admin' },
|
{ userId: testUser1.id, role: 'project:admin' },
|
||||||
{ userId: testUser3.id, role: 'project:editor' },
|
{ userId: testUser3.id, role: 'project:editor' },
|
||||||
{ userId: ownerUser.id, role: 'project:viewer' },
|
{ userId: ownerUser.id, role: 'project:viewer' },
|
||||||
] as Array<{
|
] as Array<{
|
||||||
userId: string;
|
userId: string;
|
||||||
role: ProjectRole;
|
role: ProjectRole;
|
||||||
}>,
|
}>,
|
||||||
|
});
|
||||||
|
expect(resp.status).toBe(200);
|
||||||
|
|
||||||
|
expect(deleteSpy).toBeCalledWith([`credential-can-use-secrets:${credential1.id}`]);
|
||||||
|
deleteSpy.mockClear();
|
||||||
|
|
||||||
|
const [tp1Relations, tp2Relations] = await Promise.all([
|
||||||
|
getProjectRelations({ projectId: teamProject1.id }),
|
||||||
|
getProjectRelations({ projectId: teamProject2.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(tp1Relations.length).toBe(3);
|
||||||
|
expect(tp2Relations.length).toBe(2);
|
||||||
|
|
||||||
|
expect(tp1Relations.find((p) => p.userId === testUser1.id)).not.toBeUndefined();
|
||||||
|
expect(tp1Relations.find((p) => p.userId === testUser2.id)).toBeUndefined();
|
||||||
|
expect(tp1Relations.find((p) => p.userId === testUser1.id)?.role).toBe('project:admin');
|
||||||
|
expect(tp1Relations.find((p) => p.userId === testUser3.id)?.role).toBe('project:editor');
|
||||||
|
expect(tp1Relations.find((p) => p.userId === ownerUser.id)?.role).toBe('project:viewer');
|
||||||
|
|
||||||
|
// Check we haven't modified the other team project
|
||||||
|
expect(tp2Relations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
||||||
|
expect(tp2Relations.find((p) => p.userId === testUser1.id)).toBeUndefined();
|
||||||
|
expect(tp2Relations.find((p) => p.userId === testUser2.id)?.role).toBe('project:editor');
|
||||||
|
expect(tp2Relations.find((p) => p.userId === ownerUser.id)?.role).toBe('project:editor');
|
||||||
});
|
});
|
||||||
expect(resp.status).toBe(200);
|
|
||||||
|
|
||||||
expect(deleteSpy).toBeCalledWith([`credential-can-use-secrets:${credential1.id}`]);
|
test.each([['project:viewer'], ['project:editor']] as const)(
|
||||||
deleteSpy.mockClear();
|
'`%s`s should not be able to add, update or remove users from a project',
|
||||||
|
async (role) => {
|
||||||
|
//
|
||||||
|
// ARRANGE
|
||||||
|
//
|
||||||
|
const [actor, projectEditor, userToBeInvited] = await Promise.all([
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
]);
|
||||||
|
const teamProject1 = await createTeamProject();
|
||||||
|
|
||||||
const [tp1Relations, tp2Relations] = await Promise.all([
|
await linkUserToProject(actor, teamProject1, role);
|
||||||
getProjectRelations({ projectId: teamProject1.id }),
|
await linkUserToProject(projectEditor, teamProject1, 'project:editor');
|
||||||
getProjectRelations({ projectId: teamProject2.id }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(tp1Relations.length).toBe(3);
|
//
|
||||||
expect(tp2Relations.length).toBe(2);
|
// ACT
|
||||||
|
//
|
||||||
|
const response = await testServer
|
||||||
|
.authAgentFor(actor)
|
||||||
|
.patch(`/projects/${teamProject1.id}`)
|
||||||
|
.send({
|
||||||
|
name: teamProject1.name,
|
||||||
|
relations: [
|
||||||
|
// update the viewer to be the project admin
|
||||||
|
{ userId: actor.id, role: 'project:admin' },
|
||||||
|
// add a user to the project
|
||||||
|
{ userId: userToBeInvited.id, role: 'project:editor' },
|
||||||
|
// implicitly remove the project editor
|
||||||
|
] as Array<{
|
||||||
|
userId: string;
|
||||||
|
role: ProjectRole;
|
||||||
|
}>,
|
||||||
|
});
|
||||||
|
//.expect(403);
|
||||||
|
|
||||||
expect(tp1Relations.find((p) => p.userId === testUser1.id)).not.toBeUndefined();
|
//
|
||||||
expect(tp1Relations.find((p) => p.userId === testUser2.id)).toBeUndefined();
|
// ASSERT
|
||||||
expect(tp1Relations.find((p) => p.userId === testUser1.id)?.role).toBe('project:admin');
|
//
|
||||||
expect(tp1Relations.find((p) => p.userId === testUser3.id)?.role).toBe('project:editor');
|
expect(response.status).toBe(403);
|
||||||
expect(tp1Relations.find((p) => p.userId === ownerUser.id)?.role).toBe('project:viewer');
|
expect(response.body).toMatchObject({
|
||||||
|
message: 'User is missing a scope required to perform this action',
|
||||||
|
});
|
||||||
|
const tp1Relations = await getProjectRelations({ projectId: teamProject1.id });
|
||||||
|
|
||||||
// Check we haven't modified the other team project
|
expect(tp1Relations.length).toBe(2);
|
||||||
expect(tp2Relations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
expect(tp1Relations).toMatchObject(
|
||||||
expect(tp2Relations.find((p) => p.userId === testUser1.id)).toBeUndefined();
|
expect.arrayContaining([
|
||||||
expect(tp2Relations.find((p) => p.userId === testUser2.id)?.role).toBe('project:editor');
|
expect.objectContaining({ userId: actor.id, role }),
|
||||||
expect(tp2Relations.find((p) => p.userId === ownerUser.id)?.role).toBe('project:editor');
|
expect.objectContaining({ userId: projectEditor.id, role: 'project:editor' }),
|
||||||
});
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should not add or remove users from a project if lacking permissions', async () => {
|
test.each([
|
||||||
const [ownerUser, testUser1, testUser2, testUser3] = await Promise.all([
|
['project:viewer', 'feat:projectRole:viewer'],
|
||||||
createOwner(),
|
['project:editor', 'feat:projectRole:editor'],
|
||||||
createUser(),
|
] as const)(
|
||||||
createUser(),
|
"should not be able to add a user with the role %s if it's not licensed",
|
||||||
createUser(),
|
async (role, feature) => {
|
||||||
]);
|
testServer.license.disable(feature);
|
||||||
const [teamProject1, teamProject2] = await Promise.all([
|
const [projectAdmin, userToBeInvited] = await Promise.all([createUser(), createUser()]);
|
||||||
createTeamProject(undefined, testUser2),
|
const teamProject = await createTeamProject('Team Project', projectAdmin);
|
||||||
createTeamProject(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await linkUserToProject(testUser1, teamProject1, 'project:viewer');
|
await testServer
|
||||||
await linkUserToProject(ownerUser, teamProject2, 'project:editor');
|
.authAgentFor(projectAdmin)
|
||||||
await linkUserToProject(testUser2, teamProject2, 'project:editor');
|
.patch(`/projects/${teamProject.id}`)
|
||||||
|
.send({
|
||||||
|
name: teamProject.name,
|
||||||
|
relations: [
|
||||||
|
{ userId: projectAdmin.id, role: 'project:admin' },
|
||||||
|
{ userId: userToBeInvited.id, role },
|
||||||
|
] as Array<{
|
||||||
|
userId: string;
|
||||||
|
role: ProjectRole;
|
||||||
|
}>,
|
||||||
|
})
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
const memberAgent = testServer.authAgentFor(testUser1);
|
const tpRelations = await getProjectRelations({ projectId: teamProject.id });
|
||||||
|
expect(tpRelations.length).toBe(1);
|
||||||
|
expect(tpRelations).toMatchObject(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ userId: projectAdmin.id, role: 'project:admin' }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const resp = await memberAgent.patch(`/projects/${teamProject1.id}`).send({
|
test("should not edit a relation of a project when changing a user's role to an unlicensed role", async () => {
|
||||||
name: teamProject1.name,
|
testServer.license.disable('feat:projectRole:editor');
|
||||||
relations: [
|
const [testUser1, testUser2, testUser3] = await Promise.all([
|
||||||
{ userId: testUser1.id, role: 'project:admin' },
|
createUser(),
|
||||||
{ userId: testUser3.id, role: 'project:editor' },
|
createUser(),
|
||||||
{ userId: ownerUser.id, role: 'project:viewer' },
|
createUser(),
|
||||||
] as Array<{
|
]);
|
||||||
userId: string;
|
const teamProject = await createTeamProject(undefined, testUser2);
|
||||||
role: ProjectRole;
|
|
||||||
}>,
|
await linkUserToProject(testUser1, teamProject, 'project:admin');
|
||||||
|
await linkUserToProject(testUser3, teamProject, 'project:admin');
|
||||||
|
|
||||||
|
const memberAgent = testServer.authAgentFor(testUser2);
|
||||||
|
|
||||||
|
const resp = await memberAgent.patch(`/projects/${teamProject.id}`).send({
|
||||||
|
name: teamProject.name,
|
||||||
|
relations: [
|
||||||
|
{ userId: testUser2.id, role: 'project:admin' },
|
||||||
|
{ userId: testUser1.id, role: 'project:editor' },
|
||||||
|
{ userId: testUser3.id, role: 'project:editor' },
|
||||||
|
] as Array<{
|
||||||
|
userId: string;
|
||||||
|
role: ProjectRole;
|
||||||
|
}>,
|
||||||
|
});
|
||||||
|
expect(resp.status).toBe(400);
|
||||||
|
|
||||||
|
const tpRelations = await getProjectRelations({ projectId: teamProject.id });
|
||||||
|
expect(tpRelations.length).toBe(3);
|
||||||
|
|
||||||
|
expect(tpRelations.find((p) => p.userId === testUser1.id)).not.toBeUndefined();
|
||||||
|
expect(tpRelations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
||||||
|
expect(tpRelations.find((p) => p.userId === testUser1.id)?.role).toBe('project:admin');
|
||||||
|
expect(tpRelations.find((p) => p.userId === testUser2.id)?.role).toBe('project:admin');
|
||||||
|
expect(tpRelations.find((p) => p.userId === testUser3.id)?.role).toBe('project:admin');
|
||||||
});
|
});
|
||||||
expect(resp.status).toBe(403);
|
|
||||||
|
|
||||||
const [tp1Relations, tp2Relations] = await Promise.all([
|
test("should edit a relation of a project when changing a user's role to an licensed role but unlicensed roles are present", async () => {
|
||||||
getProjectRelations({ projectId: teamProject1.id }),
|
testServer.license.disable('feat:projectRole:viewer');
|
||||||
getProjectRelations({ projectId: teamProject2.id }),
|
const [testUser1, testUser2, testUser3] = await Promise.all([
|
||||||
]);
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
]);
|
||||||
|
const teamProject = await createTeamProject(undefined, testUser2);
|
||||||
|
|
||||||
expect(tp1Relations.length).toBe(2);
|
await linkUserToProject(testUser1, teamProject, 'project:viewer');
|
||||||
expect(tp2Relations.length).toBe(2);
|
await linkUserToProject(testUser3, teamProject, 'project:editor');
|
||||||
|
|
||||||
expect(tp1Relations.find((p) => p.userId === testUser1.id)).not.toBeUndefined();
|
const memberAgent = testServer.authAgentFor(testUser2);
|
||||||
expect(tp1Relations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
|
||||||
expect(tp1Relations.find((p) => p.userId === testUser1.id)?.role).toBe('project:viewer');
|
|
||||||
expect(tp1Relations.find((p) => p.userId === testUser2.id)?.role).toBe('project:admin');
|
|
||||||
expect(tp1Relations.find((p) => p.userId === testUser3.id)).toBeUndefined();
|
|
||||||
|
|
||||||
// Check we haven't modified the other team project
|
const resp = await memberAgent.patch(`/projects/${teamProject.id}`).send({
|
||||||
expect(tp2Relations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
name: teamProject.name,
|
||||||
expect(tp2Relations.find((p) => p.userId === testUser1.id)).toBeUndefined();
|
relations: [
|
||||||
expect(tp2Relations.find((p) => p.userId === testUser2.id)?.role).toBe('project:editor');
|
{ userId: testUser1.id, role: 'project:viewer' },
|
||||||
expect(tp2Relations.find((p) => p.userId === ownerUser.id)?.role).toBe('project:editor');
|
{ userId: testUser2.id, role: 'project:admin' },
|
||||||
});
|
{ userId: testUser3.id, role: 'project:admin' },
|
||||||
|
] as Array<{
|
||||||
|
userId: string;
|
||||||
|
role: ProjectRole;
|
||||||
|
}>,
|
||||||
|
});
|
||||||
|
expect(resp.status).toBe(200);
|
||||||
|
|
||||||
test('should not add from a project adding user with an unlicensed role', async () => {
|
const tpRelations = await getProjectRelations({ projectId: teamProject.id });
|
||||||
testServer.license.disable('feat:projectRole:editor');
|
expect(tpRelations.length).toBe(3);
|
||||||
const [testUser1, testUser2, testUser3] = await Promise.all([
|
|
||||||
createUser(),
|
|
||||||
createUser(),
|
|
||||||
createUser(),
|
|
||||||
]);
|
|
||||||
const teamProject = await createTeamProject(undefined, testUser2);
|
|
||||||
|
|
||||||
await linkUserToProject(testUser1, teamProject, 'project:admin');
|
expect(tpRelations.find((p) => p.userId === testUser1.id)).not.toBeUndefined();
|
||||||
|
expect(tpRelations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
||||||
const memberAgent = testServer.authAgentFor(testUser2);
|
expect(tpRelations.find((p) => p.userId === testUser3.id)).not.toBeUndefined();
|
||||||
|
expect(tpRelations.find((p) => p.userId === testUser1.id)?.role).toBe('project:viewer');
|
||||||
const resp = await memberAgent.patch(`/projects/${teamProject.id}`).send({
|
expect(tpRelations.find((p) => p.userId === testUser2.id)?.role).toBe('project:admin');
|
||||||
name: teamProject.name,
|
expect(tpRelations.find((p) => p.userId === testUser3.id)?.role).toBe('project:admin');
|
||||||
relations: [
|
|
||||||
{ userId: testUser2.id, role: 'project:admin' },
|
|
||||||
{ userId: testUser1.id, role: 'project:editor' },
|
|
||||||
] as Array<{
|
|
||||||
userId: string;
|
|
||||||
role: ProjectRole;
|
|
||||||
}>,
|
|
||||||
});
|
});
|
||||||
expect(resp.status).toBe(400);
|
|
||||||
|
|
||||||
const tpRelations = await getProjectRelations({ projectId: teamProject.id });
|
test('should not add or remove users from a personal project', async () => {
|
||||||
expect(tpRelations.length).toBe(2);
|
const [testUser1, testUser2] = await Promise.all([createUser(), createUser()]);
|
||||||
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser1.id)).not.toBeUndefined();
|
const personalProject = await getPersonalProject(testUser1);
|
||||||
expect(tpRelations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser1.id)?.role).toBe('project:admin');
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser2.id)?.role).toBe('project:admin');
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser3.id)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should not edit a relation of a project when changing a user's role to an unlicensed role", async () => {
|
const memberAgent = testServer.authAgentFor(testUser1);
|
||||||
testServer.license.disable('feat:projectRole:editor');
|
|
||||||
const [testUser1, testUser2, testUser3] = await Promise.all([
|
|
||||||
createUser(),
|
|
||||||
createUser(),
|
|
||||||
createUser(),
|
|
||||||
]);
|
|
||||||
const teamProject = await createTeamProject(undefined, testUser2);
|
|
||||||
|
|
||||||
await linkUserToProject(testUser1, teamProject, 'project:admin');
|
const resp = await memberAgent.patch(`/projects/${personalProject.id}`).send({
|
||||||
await linkUserToProject(testUser3, teamProject, 'project:admin');
|
relations: [
|
||||||
|
{ userId: testUser1.id, role: 'project:personalOwner' },
|
||||||
|
{ userId: testUser2.id, role: 'project:admin' },
|
||||||
|
] as Array<{
|
||||||
|
userId: string;
|
||||||
|
role: ProjectRole;
|
||||||
|
}>,
|
||||||
|
});
|
||||||
|
expect(resp.status).toBe(403);
|
||||||
|
|
||||||
const memberAgent = testServer.authAgentFor(testUser2);
|
const p1Relations = await getProjectRelations({ projectId: personalProject.id });
|
||||||
|
expect(p1Relations.length).toBe(1);
|
||||||
const resp = await memberAgent.patch(`/projects/${teamProject.id}`).send({
|
|
||||||
name: teamProject.name,
|
|
||||||
relations: [
|
|
||||||
{ userId: testUser2.id, role: 'project:admin' },
|
|
||||||
{ userId: testUser1.id, role: 'project:editor' },
|
|
||||||
{ userId: testUser3.id, role: 'project:editor' },
|
|
||||||
] as Array<{
|
|
||||||
userId: string;
|
|
||||||
role: ProjectRole;
|
|
||||||
}>,
|
|
||||||
});
|
});
|
||||||
expect(resp.status).toBe(400);
|
|
||||||
|
|
||||||
const tpRelations = await getProjectRelations({ projectId: teamProject.id });
|
|
||||||
expect(tpRelations.length).toBe(3);
|
|
||||||
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser1.id)).not.toBeUndefined();
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser1.id)?.role).toBe('project:admin');
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser2.id)?.role).toBe('project:admin');
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser3.id)?.role).toBe('project:admin');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should edit a relation of a project when changing a user's role to an licensed role but unlicensed roles are present", async () => {
|
|
||||||
testServer.license.disable('feat:projectRole:viewer');
|
|
||||||
const [testUser1, testUser2, testUser3] = await Promise.all([
|
|
||||||
createUser(),
|
|
||||||
createUser(),
|
|
||||||
createUser(),
|
|
||||||
]);
|
|
||||||
const teamProject = await createTeamProject(undefined, testUser2);
|
|
||||||
|
|
||||||
await linkUserToProject(testUser1, teamProject, 'project:viewer');
|
|
||||||
await linkUserToProject(testUser3, teamProject, 'project:editor');
|
|
||||||
|
|
||||||
const memberAgent = testServer.authAgentFor(testUser2);
|
|
||||||
|
|
||||||
const resp = await memberAgent.patch(`/projects/${teamProject.id}`).send({
|
|
||||||
name: teamProject.name,
|
|
||||||
relations: [
|
|
||||||
{ userId: testUser1.id, role: 'project:viewer' },
|
|
||||||
{ userId: testUser2.id, role: 'project:admin' },
|
|
||||||
{ userId: testUser3.id, role: 'project:admin' },
|
|
||||||
] as Array<{
|
|
||||||
userId: string;
|
|
||||||
role: ProjectRole;
|
|
||||||
}>,
|
|
||||||
});
|
|
||||||
expect(resp.status).toBe(200);
|
|
||||||
|
|
||||||
const tpRelations = await getProjectRelations({ projectId: teamProject.id });
|
|
||||||
expect(tpRelations.length).toBe(3);
|
|
||||||
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser1.id)).not.toBeUndefined();
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser3.id)).not.toBeUndefined();
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser1.id)?.role).toBe('project:viewer');
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser2.id)?.role).toBe('project:admin');
|
|
||||||
expect(tpRelations.find((p) => p.userId === testUser3.id)?.role).toBe('project:admin');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not add or remove users from a personal project', async () => {
|
|
||||||
const [testUser1, testUser2] = await Promise.all([createUser(), createUser()]);
|
|
||||||
|
|
||||||
const personalProject = await getPersonalProject(testUser1);
|
|
||||||
|
|
||||||
const memberAgent = testServer.authAgentFor(testUser1);
|
|
||||||
|
|
||||||
const resp = await memberAgent.patch(`/projects/${personalProject.id}`).send({
|
|
||||||
relations: [
|
|
||||||
{ userId: testUser1.id, role: 'project:personalOwner' },
|
|
||||||
{ userId: testUser2.id, role: 'project:admin' },
|
|
||||||
] as Array<{
|
|
||||||
userId: string;
|
|
||||||
role: ProjectRole;
|
|
||||||
}>,
|
|
||||||
});
|
|
||||||
expect(resp.status).toBe(403);
|
|
||||||
|
|
||||||
const p1Relations = await getProjectRelations({ projectId: personalProject.id });
|
|
||||||
expect(p1Relations.length).toBe(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,12 @@ describe('SharedCredentialsRepository', () => {
|
|||||||
role: In(['credential:owner', 'credential:user']),
|
role: In(['credential:owner', 'credential:user']),
|
||||||
project: {
|
project: {
|
||||||
projectRelations: {
|
projectRelations: {
|
||||||
role: In(['project:admin', 'project:personalOwner', 'project:editor']),
|
role: In([
|
||||||
|
'project:admin',
|
||||||
|
'project:personalOwner',
|
||||||
|
'project:editor',
|
||||||
|
'project:viewer',
|
||||||
|
]),
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -83,7 +88,12 @@ describe('SharedCredentialsRepository', () => {
|
|||||||
role: In(['credential:owner', 'credential:user']),
|
role: In(['credential:owner', 'credential:user']),
|
||||||
project: {
|
project: {
|
||||||
projectRelations: {
|
projectRelations: {
|
||||||
role: In(['project:admin', 'project:personalOwner', 'project:editor']),
|
role: In([
|
||||||
|
'project:admin',
|
||||||
|
'project:personalOwner',
|
||||||
|
'project:editor',
|
||||||
|
'project:viewer',
|
||||||
|
]),
|
||||||
userId: member.id,
|
userId: member.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user