Skip to content

Commit

Permalink
Merge pull request #4782 from activepieces/fix/new-operator-role
Browse files Browse the repository at this point in the history
feat(rbac): add operator role
  • Loading branch information
abuaboud committed May 27, 2024
2 parents a1d49be + 263cdd4 commit 73872ea
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 52 deletions.
1 change: 1 addition & 0 deletions packages/ee/ui/project-members/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ProjectMemberRole } from '@activepieces/shared';
export const RolesDisplayNames: { [k: string]: string } = {
[ProjectMemberRole.ADMIN]: $localize`Admin`,
[ProjectMemberRole.EDITOR]: $localize`Editor`,
[ProjectMemberRole.OPERATOR]: $localize`Operator`,
[ProjectMemberRole.VIEWER]: $localize`Viewer`,
[ProjectMemberRole.EXTERNAL_CUSTOMER]: $localize`External Customer`,
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const rolePermissions: Record<ProjectMemberRole, Permission[]> = {
Permission.WRITE_APP_CONNECTION,
Permission.READ_FLOW,
Permission.WRITE_FLOW,
Permission.UPDATE_FLOW_STATUS,
Permission.READ_PROJECT_MEMBER,
Permission.WRITE_PROJECT_MEMBER,
Permission.WRITE_RPOJECT,
Expand All @@ -17,10 +18,19 @@ export const rolePermissions: Record<ProjectMemberRole, Permission[]> = {
Permission.WRITE_APP_CONNECTION,
Permission.READ_FLOW,
Permission.WRITE_FLOW,
Permission.UPDATE_FLOW_STATUS,
Permission.READ_PROJECT_MEMBER,
Permission.WRITE_GIT_REPO,
Permission.READ_GIT_REPO,
],
[ProjectMemberRole.OPERATOR]: [
Permission.READ_APP_CONNECTION,
Permission.WRITE_APP_CONNECTION,
Permission.READ_FLOW,
Permission.UPDATE_FLOW_STATUS,
Permission.READ_PROJECT_MEMBER,
Permission.READ_GIT_REPO,
],
[ProjectMemberRole.VIEWER]: [
Permission.READ_APP_CONNECTION,
Permission.READ_FLOW,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ActivepiecesError,
ApEdition,
ErrorCode,
FlowOperationType,
isNil,
Permission,
Principal,
Expand All @@ -19,19 +20,53 @@ export const rbacMiddleware = async (req: FastifyRequest): Promise<void> => {
if (ignoreRequest(req)) {
return
}
await assertRoleHasPermission(req.principal, req.routeConfig.permission)
}

export async function assertUserHasPermissionToFlow(
principal: Principal,
operationType: FlowOperationType,
): Promise<void> {
const edition = getEdition()
if (![ApEdition.CLOUD, ApEdition.ENTERPRISE].includes(edition)) {
return
}

const principalRole = await getPrincipalRoleOrThrow(req.principal)
switch (operationType) {
case FlowOperationType.LOCK_AND_PUBLISH:
case FlowOperationType.CHANGE_STATUS: {
await assertRoleHasPermission(principal, Permission.UPDATE_FLOW_STATUS)
break
}
case FlowOperationType.ADD_ACTION:
case FlowOperationType.UPDATE_ACTION:
case FlowOperationType.DELETE_ACTION:
case FlowOperationType.LOCK_FLOW:
case FlowOperationType.CHANGE_FOLDER:
case FlowOperationType.CHANGE_NAME:
case FlowOperationType.MOVE_ACTION:
case FlowOperationType.IMPORT_FLOW:
case FlowOperationType.UPDATE_TRIGGER:
case FlowOperationType.DUPLICATE_ACTION:
case FlowOperationType.USE_AS_DRAFT: {
await assertRoleHasPermission(principal, Permission.WRITE_FLOW)
break
}
}
}

const assertRoleHasPermission = async (principal: Principal, permission: Permission | undefined): Promise<void> => {
const principalRole = await getPrincipalRoleOrThrow(principal)
const access = grantAccess({
principalRole,
routePermission: req.routeConfig.permission,
routePermission: permission,
})

if (!access) {
throwPermissionDenied(req.principal)
throwPermissionDenied(principalRole, principal, permission)
}
}


const ignoreRequest = (req: FastifyRequest): boolean => {
if (EDITION_IS_COMMUNITY) {
return true
Expand Down Expand Up @@ -81,12 +116,14 @@ const grantAccess = ({ principalRole, routePermission }: GrantAccessArgs): boole
return principalPermissions.includes(routePermission)
}

const throwPermissionDenied = (principal: Principal): never => {
const throwPermissionDenied = (role: ProjectMemberRole, principal: Principal, permission: Permission | undefined): never => {
throw new ActivepiecesError({
code: ErrorCode.PERMISSION_DENIED,
params: {
userId: principal.id,
projectId: principal.projectId,
role,
permission,
},
})
}
Expand Down
11 changes: 7 additions & 4 deletions packages/server/api/src/app/flows/flow/flow.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import dayjs from 'dayjs'
import { StatusCodes } from 'http-status-codes'
import { isNil } from 'lodash'
import { entitiesMustBeOwnedByCurrentProject } from '../../authentication/authorization'
import { assertUserHasPermissionToFlow } from '../../ee/authentication/rbac/rbac-middleware'
import { eventsHooks } from '../../helper/application-events'
import { projectService } from '../../project/project-service'
import { flowService } from './flow.service'
import { ApplicationEventName } from '@activepieces/ee-shared'
import { ActivepiecesError,
import {
ActivepiecesError,
ApId,
CountFlowsRequest,
CreateFlowRequest,
Expand Down Expand Up @@ -48,12 +50,13 @@ export const flowController: FastifyPluginAsyncTypebox = async (app) => {
})

app.post('/:id', UpdateFlowRequestOptions, async (request) => {
const userId = await extractUserIdFromPrincipal(request.principal)
await assertUserHasPermissionToFlow(request.principal, request.body.type)

const flow = await flowService.getOnePopulatedOrThrow({
id: request.params.id,
projectId: request.principal.projectId,
})

const userId = await extractUserIdFromPrincipal(request.principal)
await assertThatFlowIsNotBeingUsed(flow, userId)
eventsHooks.get().send(request, {
action: ApplicationEventName.UPDATED_FLOW,
Expand Down Expand Up @@ -172,7 +175,7 @@ const CreateFlowRequestOptions = {

const UpdateFlowRequestOptions = {
config: {
permission: Permission.WRITE_FLOW,
permission: Permission.UPDATE_FLOW_STATUS,
},
schema: {
tags: ['flows'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const UpdatePlatformRequest = {
const GetPlatformRequest = {
config: {
allowedPrincipals: [PrincipalType.USER, PrincipalType.SERVICE],
EndpointScope: EndpointScope.PLATFORM,
scope: EndpointScope.PLATFORM,
},
schema: {
tags: ['platforms'],
Expand Down
85 changes: 63 additions & 22 deletions packages/server/api/test/integration/cloud/flow/flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ describe('Flow API', () => {

it.each([
ProjectMemberRole.VIEWER,
ProjectMemberRole.OPERATOR,
ProjectMemberRole.EXTERNAL_CUSTOMER,
])('Fails if user role is %s', async (testRole) => {
// arrange
Expand Down Expand Up @@ -153,9 +154,34 @@ describe('Flow API', () => {

describe('Update flow endpoint', () => {
it.each([
ProjectMemberRole.ADMIN,
ProjectMemberRole.EDITOR,
])('Succeeds if user role is %s', async (testRole) => {
{
role: ProjectMemberRole.ADMIN,
request: {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
},
},
{
role: ProjectMemberRole.EDITOR,
request: {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
},
},
{
role: ProjectMemberRole.OPERATOR,
request: {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
},
},
])('Succeeds if user role is %s', async ({ role, request }) => {
// arrange
const mockPlatformId = apId()
const mockOwner = createMockUser({ platformId: mockPlatformId, platformRole: PlatformRole.ADMIN })
Expand All @@ -175,7 +201,7 @@ describe('Flow API', () => {
email: mockUser.email,
platformId: mockPlatform.id,
projectId: mockProject.id,
role: testRole,
role,
})
await databaseConnection.getRepository('project_member').save([mockProjectMember])

Expand Down Expand Up @@ -206,12 +232,7 @@ describe('Flow API', () => {
},
})

const mockUpdateFlowStatusRequest = {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
}


// act
const response = await app?.inject({
Expand All @@ -220,17 +241,42 @@ describe('Flow API', () => {
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpdateFlowStatusRequest,
body: request,
})

// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
})

it.each([
ProjectMemberRole.VIEWER,
ProjectMemberRole.EXTERNAL_CUSTOMER,
])('Fails if user role is %s', async (testRole) => {
{
role: ProjectMemberRole.VIEWER,
request: {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
},
},
{
role: ProjectMemberRole.EXTERNAL_CUSTOMER,
request: {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
},
},
{
role: ProjectMemberRole.OPERATOR,
request: {
type: FlowOperationType.CHANGE_NAME,
request: {
displayName: 'hello',
},
},
},
])('Fails if user role is %s', async ({ role, request }) => {
// arrange
const mockPlatformId = apId()
const mockOwner = createMockUser({ platformId: mockPlatformId, platformRole: PlatformRole.ADMIN })
Expand All @@ -250,7 +296,7 @@ describe('Flow API', () => {
email: mockUser.email,
platformId: mockPlatform.id,
projectId: mockProject.id,
role: testRole,
role,
})
await databaseConnection.getRepository('project_member').save([mockProjectMember])

Expand Down Expand Up @@ -281,12 +327,6 @@ describe('Flow API', () => {
},
})

const mockUpdateFlowStatusRequest = {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
}

// act
const response = await app?.inject({
Expand All @@ -295,7 +335,7 @@ describe('Flow API', () => {
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpdateFlowStatusRequest,
body: request,
})

// assert
Expand All @@ -312,6 +352,7 @@ describe('Flow API', () => {
it.each([
ProjectMemberRole.ADMIN,
ProjectMemberRole.EDITOR,
ProjectMemberRole.OPERATOR,
ProjectMemberRole.VIEWER,
])('Succeeds if user role is %s', async (testRole) => {
// arrange
Expand Down
5 changes: 4 additions & 1 deletion packages/shared/src/lib/common/activepieces-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { FileId } from '../file/file'
import { FlowRunId } from '../flow-run/flow-run'
import { FlowId } from '../flows/flow'
import { FlowVersionId } from '../flows/flow-version'
import { ProjectId } from '../project'
import { ProjectId, ProjectMemberRole } from '../project'
import { UserId } from '../user'
import { ApId } from './id-generator'
import { Permission } from './security'

export class ActivepiecesError extends Error {
constructor(public error: ErrorParams, message?: string) {
Expand Down Expand Up @@ -93,6 +94,8 @@ ErrorCode.PERMISSION_DENIED,
{
userId: UserId
projectId: ProjectId
role: ProjectMemberRole
permission: Permission | undefined
}
>

Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/lib/common/security/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export enum Permission {
WRITE_APP_CONNECTION = 'WRITE_APP_CONNECTION',
READ_FLOW = 'READ_FLOW',
WRITE_FLOW = 'WRITE_FLOW',
UPDATE_FLOW_STATUS = 'UPDATE_FLOW_STATUS',
WRITE_RPOJECT = 'WRITE_RPOJECT',
READ_PROJECT_MEMBER = 'READ_PROJECT_MEMBER',
WRITE_PROJECT_MEMBER = 'WRITE_PROJECT_MEMBER',
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/lib/project/project-member.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum ProjectMemberRole {
ADMIN = 'ADMIN',
EDITOR = 'EDITOR',
OPERATOR = 'OPERATOR',
VIEWER = 'VIEWER',
/**
* Members who are customers for our customers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export class SidenavRoutesListComponent implements OnInit {
return {
...route,
showInSideNav$: forkJoin({
roleCondition: this.isRouteAllowedForRole(role, route.route),
roleCondition: of(true),
flagCondition: route.showInSideNav$,
}).pipe(
map(
Expand All @@ -217,22 +217,4 @@ export class SidenavRoutesListComponent implements OnInit {
};
});
}

private isRouteAllowedForRole(
role: ProjectMemberRole | null | undefined,
route?: string
) {
if (role === undefined || role === null || route === undefined) {
return of(true);
}

switch (role) {
case ProjectMemberRole.ADMIN:
case ProjectMemberRole.EDITOR:
case ProjectMemberRole.VIEWER:
return of(true);
case ProjectMemberRole.EXTERNAL_CUSTOMER:
return of(route === 'connections');
}
}
}

0 comments on commit 73872ea

Please sign in to comment.