Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/apps/work/src/lib/utils/permissions.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Project } from '../models'
import {
canCreateEngagement,
canViewAllEngagements,
checkCanEditProjectDetails,
checkCanManageProject,
checkIsUserInvitedToProject,
checkProjectAccess,
Expand Down Expand Up @@ -41,6 +42,10 @@ describe('permissions.utils project management helpers', () => {
role: 'customer',
userId: 456,
},
{
role: 'copilot',
userId: 789,
},
],
name: 'Managed project',
status: 'active',
Expand Down Expand Up @@ -77,6 +82,20 @@ describe('permissions.utils project management helpers', () => {
.toBe(false)
})

it('requires full access membership for project details edits', () => {
expect(checkCanEditProjectDetails(['Talent Manager'], '123', managedProject))
.toBe(true)
expect(checkCanEditProjectDetails(['Talent Manager'], '789', managedProject))
.toBe(false)
expect(checkCanEditProjectDetails(['Project Manager'], '456', managedProject))
.toBe(false)
})

it('allows admins to edit project details without membership', () => {
expect(checkCanEditProjectDetails(['administrator'], '999', managedProject))
.toBe(true)
})

it('limits engagement creation to admins and talent managers', () => {
expect(canCreateEngagement(['copilot']))
.toBe(false)
Expand Down
30 changes: 30 additions & 0 deletions src/apps/work/src/lib/utils/permissions.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,36 @@ export function checkCanManageProject(
|| normalizedRole === PROJECT_ROLES.MANAGER
}

/**
* Returns whether the caller can edit an existing project's core details.
*
* Admins always qualify. Non-admin callers must hold a manager-tier user role
* and the project's Full Access membership. Copilot membership is excluded so
* copilot users can keep other write access without changing project details.
*
* @param userRoles caller roles from the decoded auth token or app context.
* @param userId logged-in user identifier used for project membership checks.
* @param project project context for the edit check.
* @returns `true` when the caller can edit project details.
*/
export function checkCanEditProjectDetails(
userRoles: string[],
userId: number | string | undefined,
project: Project | undefined,
): boolean {
if (hasAdminRole(userRoles)) {
return true
}

if (!project || !hasManagerRole(userRoles)) {
return false
}

const normalizedRole = normalizeValue(getProjectMemberByUserId(project, userId)?.role)

return normalizedRole === PROJECT_ROLES.MANAGER
}

export function checkAdminOrPmOrTaskManager(
token: string,
project?: Project,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import {
ErrorMessage,
LoadingSpinner,
} from '../../../lib/components'
import { checkCanManageProject } from '../../../lib/utils'
import {
checkCanEditProjectDetails,
checkCanManageProject,
} from '../../../lib/utils'

import {
ProjectEditorForm,
Expand Down Expand Up @@ -119,7 +122,7 @@ export const ProjectEditorPage: FC = () => {

const canCreateProject = checkCanManageProject(userRoles, loginUserInfo?.userId)
const canManageProject = !!projectResult.project
&& checkCanManageProject(userRoles, loginUserInfo?.userId, projectResult.project)
&& checkCanEditProjectDetails(userRoles, loginUserInfo?.userId, projectResult.project)
const shouldRedirect = shouldRedirectToProjects(
isEdit,
canCreateProject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ jest.mock('../../../lib/constants', () => ({
PROJECTS_PAGE_SIZE: 10,
}))
jest.mock('../../../lib/utils', () => ({
checkCanEditProjectDetails:
jest.requireActual('../../../lib/utils/permissions.utils').checkCanEditProjectDetails,
checkCanManageProject: jest.requireActual('../../../lib/utils/permissions.utils').checkCanManageProject,
}))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ import {
ProjectFilters,
WorkAppContextModel,
} from '../../../lib/models'
import { checkCanManageProject } from '../../../lib/utils'
import {
checkCanEditProjectDetails,
checkCanManageProject,
} from '../../../lib/utils'
import styles from '../../../lib/components/ProjectsListPage/ProjectsListPage.module.scss'

const DEFAULT_FILTERS: ProjectFilters = {
Expand Down Expand Up @@ -127,7 +130,7 @@ export const ProjectsListPage: FC = () => {

const canCreateProject = checkCanManageProject(userRoles, loginUserInfo?.userId)
const canEditProject = useCallback(
(project: Project): boolean => checkCanManageProject(userRoles, loginUserInfo?.userId, project),
(project: Project): boolean => checkCanEditProjectDetails(userRoles, loginUserInfo?.userId, project),
[loginUserInfo?.userId, userRoles],
)

Expand Down
Loading