diff --git a/src/services/ResourceService.js b/src/services/ResourceService.js index f9c5374..e226deb 100644 --- a/src/services/ResourceService.js +++ b/src/services/ResourceService.js @@ -16,7 +16,20 @@ const prisma = require('../common/prisma').getClient() const payloadFields = ['id', 'challengeId', 'memberId', 'memberHandle', 'roleId', 'phaseChangeNotifications', 'created', 'createdBy', 'updated', 'updatedBy'] +// Restricted roles that cannot be combined with submitter role +const RESTRICTED_ROLE_NAMES = [ + 'manager', + 'copilot', + 'reviewer', + 'iterative reviewer', + 'screener', + 'checkpoint screener', + 'checkpoint reviewer', + 'approver' +] + let copilotResourceRoleIdsCache +let restrictedRoleIdsCache async function getCopilotResourceRoleIds () { if (copilotResourceRoleIdsCache) { @@ -34,11 +47,42 @@ async function getCopilotResourceRoleIds () { return copilotResourceRoleIdsCache } +/** + * Get resource role IDs that are restricted from being combined with submitter role. + * These include: Manager, Copilot, Reviewer, Iterative Reviewer, Screener, + * Checkpoint Screener, Checkpoint Reviewer, and Approver. + * @returns {Promise>} Array of restricted role IDs + */ +async function getRestrictedRoleIds () { + if (restrictedRoleIdsCache) { + return restrictedRoleIdsCache + } + const roles = await prisma.resourceRole.findMany({ + where: { + nameLower: { + in: RESTRICTED_ROLE_NAMES + } + }, + select: { + id: true, + nameLower: true + } + }) + restrictedRoleIdsCache = roles.map(role => role.id) + return restrictedRoleIdsCache +} + /** * Check whether the user can access resources * @param {Array} resources resources of current user for specified challenge id */ async function checkAccess (currentUserResources) { + const copilotRoleIds = await getCopilotResourceRoleIds() + const hasCopilotRole = _.some(currentUserResources, r => copilotRoleIds.includes(r.roleId)) + if (hasCopilotRole) { + return + } + const list = await prisma.resourceRole.findMany({}) const fullAccessRoles = [] _.each(list, e => { @@ -291,6 +335,12 @@ async function init (currentUser, challengeId, resource, isCreated) { const { memberId, email } = memberInfoFromDb handle = memberInfoFromDb.handle const userResources = allResources.filter((r) => _.toLower(r.memberHandle) === _.toLower(handle)) + + let restrictedRoleIds + if (isCreated) { + restrictedRoleIds = await getRestrictedRoleIds() + } + // Retrieve the constraint - Allowed Registrants if (isCreated && resource.roleId === config.SUBMITTER_RESOURCE_ROLE_ID) { const allowedRegistrants = _.get(challenge, 'constraints.allowedRegistrants') @@ -307,12 +357,20 @@ async function init (currentUser, challengeId, resource, isCreated) { `User ${handle} is not allowed to register.` ) } - if (!_.get(challenge, 'task.isTask', false) && (_.toLower(challenge.createdBy) === _.toLower(handle) || - _.some(userResources, r => r.roleId === config.REVIEWER_RESOURCE_ROLE_ID || r.roleId === config.ITERATIVE_REVIEWER_RESOURCE_ROLE_ID))) { + // Prevent challenge creator from registering as submitter (for non-tasks) + if (!_.get(challenge, 'task.isTask', false) && _.toLower(challenge.createdBy) === _.toLower(memberId)) { throw new errors.BadRequestError( `User ${handle} is not allowed to register.` ) } + // Check if user already has a restricted role (Manager, Copilot, Reviewer, etc.) + const existingRestrictedRole = _.find(userResources, r => restrictedRoleIds.includes(r.roleId)) + if (existingRestrictedRole) { + const roleNamesList = RESTRICTED_ROLE_NAMES.slice(0, -1).join(', ') + ', or ' + RESTRICTED_ROLE_NAMES.slice(-1) + throw new errors.BadRequestError( + `User ${handle} is already assigned a restricted role (${roleNamesList}) and cannot be registered as a submitter.` + ) + } } // Prevent from creating more than 1 submitter resources on tasks @@ -344,6 +402,18 @@ async function init (currentUser, challengeId, resource, isCreated) { // ensure resource role existed const resourceRole = await getResourceRole(resource.roleId, isCreated) + // Check if user is trying to assign a restricted role and already has submitter role + if (isCreated) { + if (restrictedRoleIds.includes(resource.roleId)) { + const existingSubmitterRole = _.find(userResources, r => r.roleId === config.SUBMITTER_RESOURCE_ROLE_ID) + if (existingSubmitterRole) { + throw new errors.BadRequestError( + `User ${handle} is already registered as a submitter and cannot be assigned a ${resourceRole.name} role.` + ) + } + } + } + // Verify the member has agreed to the challenge terms if (isCreated) { await helper.checkAgreedTerms(memberId, _.filter(_.get(challenge, 'terms', []), t => t.roleId === resourceRole.id)) diff --git a/test/unit/createResource.test.js b/test/unit/createResource.test.js index 4b41ef5..68416e2 100644 --- a/test/unit/createResource.test.js +++ b/test/unit/createResource.test.js @@ -9,6 +9,7 @@ const { v4: uuid } = require('uuid') const service = require('../../src/services/ResourceService') const ResourceRolePhaseDependencyService = require('../../src/services/ResourceRolePhaseDependencyService') const prisma = require('../../src/common/prisma').getClient() +const helper = require('../../src/common/helper') const ResourceRoleService = require('../../src/services/ResourceRoleService') const { requestBody, user } = require('../common/testData') const { assertValidationError, assertError, assertResource, getRoleIds, clearDependencies } = require('../common/testHelper') @@ -264,6 +265,39 @@ module.exports = describe('Create resource', () => { await assertResource(ret.id, ret) }) + it('copilot can manage resources without full access flags', async () => { + const originalRole = await helper.getById('ResourceRole', copilotRoleId) + await ResourceRoleService.updateResourceRole(user.admin, copilotRoleId, { + name: originalRole.name, + fullReadAccess: false, + fullWriteAccess: false, + isActive: originalRole.isActive, + selfObtainable: originalRole.selfObtainable + }) + + const entity = resources.createBody('diazz', reviewerRoleId, challengeId2) + let createdResource + try { + createdResource = await service.createResource(user.phead, entity) + should.equal(createdResource.roleId, entity.roleId) + should.equal(createdResource.memberHandle.toLowerCase(), entity.memberHandle.toLowerCase()) + await assertResource(createdResource.id, createdResource) + } finally { + if (createdResource && createdResource.id) { + await prisma.resource.deleteMany({ + where: { id: createdResource.id } + }) + } + await ResourceRoleService.updateResourceRole(user.admin, copilotRoleId, { + name: originalRole.name, + fullReadAccess: originalRole.fullReadAccess, + fullWriteAccess: originalRole.fullWriteAccess, + isActive: originalRole.isActive, + selfObtainable: originalRole.selfObtainable + }) + } + }) + it('create resource for user ghostar 1', async () => { const entity = resources.createBody('ghostar', reviewerRoleId, challengeId2) const ret = await service.createResource(user.m2m, entity)