From cb4162091bb7c56661a15c545bb74d163ee4b300 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Tue, 12 May 2026 11:49:07 +1000 Subject: [PATCH] PM-5012: allow Work managers to load challenge resources What was broken TM/PM users opening Work challenge drafts could not load saved copilot and reviewer assignments because resource reads only returned submitters and the caller's own resources unless the caller had admin, copilot, or challenge-level full-access resource permissions. Root cause resource-api-v6 route access allowed manager users, but the service-level resource access check did not treat Work project/talent manager roles as resource managers. What was changed Added Work manager role recognition to the resource access check so project manager, topcoder project manager, talent manager, topcoder talent manager, and Connect Manager users can read the challenge resource rows the Work app uses to hydrate saved copilot and reviewer fields. Any added/updated tests Added getResources unit coverage for project manager and talent manager roles returning the full challenge resource list. --- src/services/ResourceService.js | 27 ++++++++++++++++++++++++--- test/common/testData.js | 20 ++++++++++++++++++++ test/unit/getResources.test.js | 18 ++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/services/ResourceService.js b/src/services/ResourceService.js index 8ddadc1..04e17a3 100644 --- a/src/services/ResourceService.js +++ b/src/services/ResourceService.js @@ -27,6 +27,13 @@ const RESTRICTED_ROLE_NAMES = [ 'checkpoint reviewer', 'approver' ] +const RESOURCE_MANAGER_ROLE_NAMES = new Set([ + _.toLower(constants.UserRoles.Manager), + 'project manager', + 'topcoder project manager', + 'talent manager', + 'topcoder talent manager' +]) let copilotResourceRoleIdsCache let restrictedRoleIdsCache @@ -72,11 +79,25 @@ async function getRestrictedRoleIds () { return restrictedRoleIdsCache } +/** + * Check whether the current user has a Work manager role that can manage challenge resources. + * @param {Object} currentUser the current user + * @returns {Boolean} true when the user has a resource manager role + */ +function hasResourceManagerRole (currentUser) { + return _.some(_.get(currentUser, 'roles', []), role => RESOURCE_MANAGER_ROLE_NAMES.has(_.toLower(role))) +} + /** * Check whether the user can access resources + * @param {Object} currentUser the current user * @param {Array} resources resources of current user for specified challenge id */ -async function checkAccess (currentUserResources) { +async function checkAccess (currentUser, currentUserResources) { + if (hasResourceManagerRole(currentUser)) { + return + } + const copilotRoleIds = await getCopilotResourceRoleIds() const hasCopilotRole = _.some(currentUserResources, r => copilotRoleIds.includes(r.roleId)) if (hasCopilotRole) { @@ -166,7 +187,7 @@ async function getResources (currentUser, challengeId, roleId, memberId, memberH } }) try { - await checkAccess(resources) + await checkAccess(currentUser, resources) hasFullAccess = true } catch (e) { hasFullAccess = false @@ -453,7 +474,7 @@ async function init (currentUser, challengeId, resource, isCreated) { if (!resourceRole.selfObtainable || _.toString(memberId) !== _.toString(currentUser.userId)) { // if user is not creating/deleting a self obtainable resource for itself // we need to perform check access first - await checkAccess(currentUserResources) + await checkAccess(currentUser, currentUserResources) } } } diff --git a/test/common/testData.js b/test/common/testData.js index e21d6d7..c37aeb6 100644 --- a/test/common/testData.js +++ b/test/common/testData.js @@ -55,6 +55,26 @@ const user = { email: 'email@domain.com.z', jti: 'f1e613be-d5b9-4231-baae-ee9f2d227234' }, + projectManager: { + roles: [ 'Topcoder User', 'project manager' ], + iss: 'https://api.topcoder-dev.com', + handle: 'projectmanager', + exp: 1561792370, + userId: '8123456', + iat: 1549791770, + email: 'email@domain.com.z', + jti: 'f1e613be-d5b9-4231-baae-ee9f2d227234' + }, + talentManager: { + roles: [ 'Topcoder User', 'talent manager' ], + iss: 'https://api.topcoder-dev.com', + handle: 'talentmanager', + exp: 1561792370, + userId: '8123457', + iat: 1549791770, + email: 'email@domain.com.z', + jti: 'f1e613be-d5b9-4231-baae-ee9f2d227234' + }, lunarkid: { roles: [ 'Topcoder User' ], iss: 'https://api.topcoder-dev.com', diff --git a/test/unit/getResources.test.js b/test/unit/getResources.test.js index a80183b..ee6b8d2 100644 --- a/test/unit/getResources.test.js +++ b/test/unit/getResources.test.js @@ -79,6 +79,24 @@ module.exports = describe('Get resources', () => { should.equal(hasReviewerRole, true) }) + it('get resources by project manager role', async () => { + const result = await service.getResources(user.projectManager, challengeId) + should.equal(result.total, 5) + for (const record of result.data) { + await assertResource(record.id, record) + should.exist(record.roleName) + } + }) + + it('get resources by talent manager role', async () => { + const result = await service.getResources(user.talentManager, challengeId) + should.equal(result.total, 5) + for (const record of result.data) { + await assertResource(record.id, record) + should.exist(record.roleName) + } + }) + it('enforces challenge user whitelist for interactive resource reads', async () => { await helper.prismaChallenge.challengeUserWhitelist.deleteMany({ where: { challengeId }