Skip to content

Commit

Permalink
feat(core): Implement project:viewer role (#9611)
Browse files Browse the repository at this point in the history
  • Loading branch information
despairblue authored Jun 6, 2024
1 parent e9e3b25 commit 6187cc5
Show file tree
Hide file tree
Showing 10 changed files with 880 additions and 692 deletions.
2 changes: 1 addition & 1 deletion packages/cli/src/credentials/credentials.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export class CredentialsService {

if (typeof projectId === 'string' && project === null) {
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.",
);
}

Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/databases/entities/ProjectRelation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { WithTimestamps } from './AbstractEntity';
import { Project } from './Project';

// 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()
export class ProjectRelation extends WithTimestamps {
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/permissions/project-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,12 @@ export const PROJECT_EDITOR_SCOPES: Scope[] = [
'project:list',
'project:read',
];

export const PROJECT_VIEWER_SCOPES: Scope[] = [
'credential:list',
'credential:read',
'project:list',
'project:read',
'workflow:list',
'workflow:read',
];
5 changes: 5 additions & 0 deletions packages/cli/src/services/role.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import {
PERSONAL_PROJECT_OWNER_SCOPES,
PROJECT_EDITOR_SCOPES,
PROJECT_VIEWER_SCOPES,
REGULAR_PROJECT_ADMIN_SCOPES,
} from '@/permissions/project-roles';
import {
Expand All @@ -39,6 +40,7 @@ const PROJECT_SCOPE_MAP: Record<ProjectRole, Scope[]> = {
'project:admin': REGULAR_PROJECT_ADMIN_SCOPES,
'project:personalOwner': PERSONAL_PROJECT_OWNER_SCOPES,
'project:editor': PROJECT_EDITOR_SCOPES,
'project:viewer': PROJECT_VIEWER_SCOPES,
};

const CREDENTIALS_SHARING_SCOPE_MAP: Record<CredentialSharingRole, Scope[]> = {
Expand Down Expand Up @@ -87,6 +89,7 @@ const ROLE_NAMES: Record<
'project:personalOwner': 'Project Owner',
'project:admin': 'Project Admin',
'project:editor': 'Project Editor',
'project:viewer': 'Project Viewer',
'credential:user': 'Credential User',
'credential:owner': 'Credential Owner',
'workflow:owner': 'Workflow Owner',
Expand Down Expand Up @@ -230,6 +233,8 @@ export class RoleService {
return this.license.isProjectRoleAdminLicensed();
case 'project:editor':
return this.license.isProjectRoleEditorLicensed();
case 'project:viewer':
return this.license.isProjectRoleViewerLicensed();
case 'global:admin':
return this.license.isAdvancedPermissionsLicensed();
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
} from '../shared/db/users';
import type { SuperAgentTest } from '../shared/types';
import { mockInstance } from '../../shared/mocking';

import { createTeamProject, linkUserToProject } from '../shared/db/projects';

const testServer = utils.setupTestServer({
Expand Down Expand Up @@ -82,6 +81,23 @@ afterEach(() => {
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
// ----------------------------------------
Expand Down Expand Up @@ -231,6 +247,31 @@ describe('GET /credentials', () => {
// GET /credentials/:id - fetch a certain credential
// ----------------------------------------
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 () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });

Expand Down Expand Up @@ -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
// ----------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ describe('POST /credentials', () => {
//
.expect(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.",
});
});
});
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/test/integration/executions.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as testDb from './shared/testDb';
import { setupTestServer } from './shared/utils';
import { mockInstance } from '../shared/mocking';
import { WaitTracker } from '@/WaitTracker';
import { createTeamProject, linkUserToProject } from './shared/db/projects';

const testServer = setupTestServer({ endpointGroups: ['executions'] });

Expand Down Expand Up @@ -45,6 +46,23 @@ describe('GET /executions', () => {
});

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 () => {
const workflow = await createWorkflow({}, owner);
await shareWorkflowWithUsers(workflow, [member]);
Expand Down
Loading

0 comments on commit 6187cc5

Please sign in to comment.