feat(core): Implement project:viewer role (#9611)

This commit is contained in:
Danny Martini
2024-06-06 11:55:48 +02:00
committed by GitHub
parent e9e3b254fe
commit 6187cc5762
10 changed files with 933 additions and 745 deletions

View File

@@ -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.",
); );
} }

View File

@@ -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 {

View File

@@ -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',
];

View File

@@ -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:

View File

@@ -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
// ---------------------------------------- // ----------------------------------------

View File

@@ -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.",
}); });
}); });
}); });

View File

@@ -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]);

View File

@@ -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);
}); });
}); });

View File

@@ -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,
}, },
}, },