From c2311d9eeae4d05349f36b53428a0eda5d252c21 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 29 Apr 2025 17:44:51 +0200 Subject: [PATCH 1/7] fix: challenge task access issue for project manager --- .circleci/config.yml | 1 + app-constants.js | 1 + src/common/helper.js | 4 ++-- src/common/role-helper.js | 16 ++++++++++++++++ src/services/ChallengeService.js | 5 +++-- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 80b0f55a..e132efce 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -88,6 +88,7 @@ workflows: - dev - feature/top-262-projectid-non-mandatory - TOP-2364 + - pm-1139 - "build-qa": context: org-global diff --git a/app-constants.js b/app-constants.js index 718e2e9c..ae541bc8 100644 --- a/app-constants.js +++ b/app-constants.js @@ -9,6 +9,7 @@ const UserRoles = { Manager: "Connect Manager", User: "Topcoder User", SelfServiceCustomer: "Self-Service Customer", + ProjectManager: "Project Manager", }; const prizeSetTypes = { diff --git a/src/common/helper.js b/src/common/helper.js index eaaa0579..b376390e 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -22,7 +22,7 @@ const elasticsearch = require("elasticsearch"); const projectHelper = require("./project-helper"); const m2mHelper = require("./m2m-helper"); -const { hasAdminRole } = require("./role-helper"); +const { hasAdminRole, hasProjectManagerRole } = require("./role-helper"); // Bus API Client let busApiClient; @@ -960,7 +960,7 @@ async function _ensureAccessibleForTaskChallenge(currentUser, challenge) { } const canAccesChallenge = _.isUndefined(currentUser) ? false - : currentUser.isMachine || hasAdminRole(currentUser) || !_.isEmpty(memberResources); + : currentUser.isMachine || hasAdminRole(currentUser) || hasProjectManagerRole(currentUser) || !_.isEmpty(memberResources); if (!canAccesChallenge) { throw new errors.ForbiddenError(`You don't have access to view this challenge`); } diff --git a/src/common/role-helper.js b/src/common/role-helper.js index f30720f9..a47817fe 100644 --- a/src/common/role-helper.js +++ b/src/common/role-helper.js @@ -15,6 +15,22 @@ function hasAdminRole(authUser) { return false; } +/** + * Check if the user has admin role + * @param {Object} authUser the user + */ +function hasProjectManagerRole(authUser) { + if (authUser && authUser.roles) { + for (const role of authUser.roles) { + if (role.toLowerCase() === constants.UserRoles.ProjectManager.toLowerCase()) { + return true; + } + } + } + return false; +} + module.exports = { hasAdminRole, + hasProjectManagerRole, }; diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 8bd5e33e..79d9ea22 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -35,7 +35,7 @@ const PhaseAdvancer = require("../phase-management/PhaseAdvancer"); const { ChallengeDomain } = require("@topcoder-framework/domain-challenge"); const { QueryDomain } = require("@topcoder-framework/domain-acl"); -const { hasAdminRole } = require("../common/role-helper"); +const { hasAdminRole, hasProjectManagerRole } = require("../common/role-helper"); const { enrichChallengeForResponse, sanitizeRepeatedFieldsInUpdateRequest, @@ -152,6 +152,7 @@ async function searchChallenges(currentUser, criteria) { ]; const _hasAdminRole = hasAdminRole(currentUser); + const _hasProjectManagerRole = hasProjectManagerRole(currentUser); const includedTrackIds = _.isArray(criteria.trackIds) ? criteria.trackIds : []; const includedTypeIds = _.isArray(criteria.typeIds) ? criteria.typeIds : []; @@ -588,7 +589,7 @@ async function searchChallenges(currentUser, criteria) { // FIXME: Tech Debt let excludeTasks = true; // if you're an admin or m2m, security rules wont be applied - if (currentUser && (_hasAdminRole || _.get(currentUser, "isMachine", false))) { + if (currentUser && (_hasAdminRole || _hasProjectManagerRole || _.get(currentUser, "isMachine", false))) { excludeTasks = false; } From b1202f4e42c5427aebcbc7ba424047893e7f9464 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Apr 2025 07:52:36 +0200 Subject: [PATCH 2/7] removed circle config changes --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e132efce..80b0f55a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -88,7 +88,6 @@ workflows: - dev - feature/top-262-projectid-non-mandatory - TOP-2364 - - pm-1139 - "build-qa": context: org-global From 26c4ad29e09ad4003fe088806054821d2c3337c3 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 30 Apr 2025 07:53:48 +0200 Subject: [PATCH 3/7] comment --- src/common/role-helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/role-helper.js b/src/common/role-helper.js index a47817fe..dd98efbb 100644 --- a/src/common/role-helper.js +++ b/src/common/role-helper.js @@ -16,7 +16,7 @@ function hasAdminRole(authUser) { } /** - * Check if the user has admin role + * Check if the user has project manager role * @param {Object} authUser the user */ function hasProjectManagerRole(authUser) { From 644226a4a33066879cabfc4c1f01a18f5a78e21c Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 30 Apr 2025 14:48:54 +0300 Subject: [PATCH 4/7] adds AI Review Buddy --- .github/workflows/code_reviewer.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/code_reviewer.yml diff --git a/.github/workflows/code_reviewer.yml b/.github/workflows/code_reviewer.yml new file mode 100644 index 00000000..1d313051 --- /dev/null +++ b/.github/workflows/code_reviewer.yml @@ -0,0 +1,22 @@ +name: AI PR Reviewer + +on: + pull_request: + types: + - opened + - synchronize +permissions: + pull-requests: write +jobs: + tc-ai-pr-review: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: TC AI PR Reviewer + uses: topcoder-platform/tc-ai-pr-reviewer@master + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) + LAB45_API_KEY: ${{ secrets.LAB45_API_KEY }} + exclude: "**/*.json, **/*.md, **/*.jpg, **/*.png, **/*.jpeg, **/*.bmp, **/*.webp" # Optional: exclude patterns separated by commas From 0a4cdf9d62b71e8248fecee0c5c7f446df7aad9e Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 28 May 2025 15:21:58 +1000 Subject: [PATCH 5/7] Better debug logging --- src/services/ChallengeService.js | 300 ++++++++++++++++--------------- 1 file changed, 152 insertions(+), 148 deletions(-) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 8bd5e33e..947907a1 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -858,190 +858,194 @@ searchChallenges.schema = { * @returns {Object} the created challenge */ async function createChallenge(currentUser, challenge, userToken) { - await challengeHelper.validateCreateChallengeRequest(currentUser, challenge); - let prizeTypeTmp = challengeHelper.validatePrizeSetsAndGetPrizeType(challenge.prizeSets); - - console.log("TYPE", prizeTypeTmp); - if (challenge.legacy.selfService) { - // if self-service, create a new project (what about if projectId is provided in the payload? confirm with business!) - if (!challenge.projectId && challengeHelper.isProjectIdRequired(challenge.timelineTemplateId)) { - const selfServiceProjectName = `Self service - ${currentUser.handle} - ${challenge.name}`; - challenge.projectId = await helper.createSelfServiceProject( - selfServiceProjectName, - "N/A", - config.NEW_SELF_SERVICE_PROJECT_TYPE, - userToken - ); - } + var ret + try { + await challengeHelper.validateCreateChallengeRequest(currentUser, challenge); + let prizeTypeTmp = challengeHelper.validatePrizeSetsAndGetPrizeType(challenge.prizeSets); + + console.log("TYPE", prizeTypeTmp); + if (challenge.legacy.selfService) { + // if self-service, create a new project (what about if projectId is provided in the payload? confirm with business!) + if (!challenge.projectId && challengeHelper.isProjectIdRequired(challenge.timelineTemplateId)) { + const selfServiceProjectName = `Self service - ${currentUser.handle} - ${challenge.name}`; + challenge.projectId = await helper.createSelfServiceProject( + selfServiceProjectName, + "N/A", + config.NEW_SELF_SERVICE_PROJECT_TYPE, + userToken + ); + } - if (challenge.metadata && challenge.metadata.length > 0) { - for (const entry of challenge.metadata) { - if (challenge.description.includes(`{{${entry.name}}}`)) { - challenge.description = challenge.description - .split(`{{${entry.name}}}`) - .join(entry.value); + if (challenge.metadata && challenge.metadata.length > 0) { + for (const entry of challenge.metadata) { + if (challenge.description.includes(`{{${entry.name}}}`)) { + challenge.description = challenge.description + .split(`{{${entry.name}}}`) + .join(entry.value); + } } } } - } - /** Ensure project exists, and set direct project id, billing account id & markup */ - if (challengeHelper.isProjectIdRequired(challenge.timelineTemplateId) || challenge.projectId) { - const { projectId } = challenge; + /** Ensure project exists, and set direct project id, billing account id & markup */ + if (challengeHelper.isProjectIdRequired(challenge.timelineTemplateId) || challenge.projectId) { + const { projectId } = challenge; - const { directProjectId } = await projectHelper.getProject(projectId, currentUser); - const { billingAccountId, markup } = await projectHelper.getProjectBillingInformation( - projectId - ); + const { directProjectId } = await projectHelper.getProject(projectId, currentUser); + const { billingAccountId, markup } = await projectHelper.getProjectBillingInformation( + projectId + ); - _.set(challenge, "legacy.directProjectId", directProjectId); - _.set(challenge, "billing.billingAccountId", billingAccountId); - _.set(challenge, "billing.markup", markup || 0); - } + _.set(challenge, "legacy.directProjectId", directProjectId); + _.set(challenge, "billing.billingAccountId", billingAccountId); + _.set(challenge, "billing.markup", markup || 0); + } - if (!_.isUndefined(_.get(challenge, "legacy.reviewType"))) { - _.set(challenge, "legacy.reviewType", _.toUpper(_.get(challenge, "legacy.reviewType"))); - } + if (!_.isUndefined(_.get(challenge, "legacy.reviewType"))) { + _.set(challenge, "legacy.reviewType", _.toUpper(_.get(challenge, "legacy.reviewType"))); + } - if (!challenge.status) { - challenge.status = constants.challengeStatuses.New; - } + if (!challenge.status) { + challenge.status = constants.challengeStatuses.New; + } - if (!challenge.startDate) { - challenge.startDate = new Date().toISOString(); - } else { - challenge.startDate = convertToISOString(challenge.startDate); - } + if (!challenge.startDate) { + challenge.startDate = new Date().toISOString(); + } else { + challenge.startDate = convertToISOString(challenge.startDate); + } - const { track, type } = await challengeHelper.validateAndGetChallengeTypeAndTrack(challenge); + const { track, type } = await challengeHelper.validateAndGetChallengeTypeAndTrack(challenge); - if (_.get(type, "isTask")) { - _.set(challenge, "task.isTask", true); - // this is only applicable for WorkType: Gig, i.e., Tasks created from Salesforce - if (challenge.billing != null && challenge.billing.clientBillingRate != null) { - _.set(challenge, "billing.clientBillingRate", challenge.billing.clientBillingRate); - } + if (_.get(type, "isTask")) { + _.set(challenge, "task.isTask", true); + // this is only applicable for WorkType: Gig, i.e., Tasks created from Salesforce + if (challenge.billing != null && challenge.billing.clientBillingRate != null) { + _.set(challenge, "billing.clientBillingRate", challenge.billing.clientBillingRate); + } - if (_.isUndefined(_.get(challenge, "task.isAssigned"))) { - _.set(challenge, "task.isAssigned", false); - } - if (_.isUndefined(_.get(challenge, "task.memberId"))) { - _.set(challenge, "task.memberId", null); - } else { - throw new errors.BadRequestError(`Cannot assign a member before the challenge gets created.`); + if (_.isUndefined(_.get(challenge, "task.isAssigned"))) { + _.set(challenge, "task.isAssigned", false); + } + if (_.isUndefined(_.get(challenge, "task.memberId"))) { + _.set(challenge, "task.memberId", null); + } else { + throw new errors.BadRequestError(`Cannot assign a member before the challenge gets created.`); + } } - } - if (challenge.phases && challenge.phases.length > 0) { - await phaseHelper.validatePhases(challenge.phases); - } + if (challenge.phases && challenge.phases.length > 0) { + await phaseHelper.validatePhases(challenge.phases); + } - // populate phases - if (!challenge.timelineTemplateId) { - if (challenge.typeId && challenge.trackId) { - const supportedTemplates = - await ChallengeTimelineTemplateService.searchChallengeTimelineTemplates({ - typeId: challenge.typeId, - trackId: challenge.trackId, - isDefault: true, - }); - const challengeTimelineTemplate = supportedTemplates.result[0]; - if (!challengeTimelineTemplate) { - throw new errors.BadRequestError( - `The selected trackId ${challenge.trackId} and typeId: ${challenge.typeId} does not have a default timeline template. Please provide a timelineTemplateId` - ); + // populate phases + if (!challenge.timelineTemplateId) { + if (challenge.typeId && challenge.trackId) { + const supportedTemplates = + await ChallengeTimelineTemplateService.searchChallengeTimelineTemplates({ + typeId: challenge.typeId, + trackId: challenge.trackId, + isDefault: true, + }); + const challengeTimelineTemplate = supportedTemplates.result[0]; + if (!challengeTimelineTemplate) { + throw new errors.BadRequestError( + `The selected trackId ${challenge.trackId} and typeId: ${challenge.typeId} does not have a default timeline template. Please provide a timelineTemplateId` + ); + } + challenge.timelineTemplateId = challengeTimelineTemplate.timelineTemplateId; + } else { + throw new errors.BadRequestError(`trackId and typeId are required to create a challenge`); } - challenge.timelineTemplateId = challengeTimelineTemplate.timelineTemplateId; - } else { - throw new errors.BadRequestError(`trackId and typeId are required to create a challenge`); } - } - challenge.phases = await phaseHelper.populatePhasesForChallengeCreation( - challenge.phases, - challenge.startDate, - challenge.timelineTemplateId - ); + challenge.phases = await phaseHelper.populatePhasesForChallengeCreation( + challenge.phases, + challenge.startDate, + challenge.timelineTemplateId + ); - // populate challenge terms - // const projectTerms = await helper.getProjectDefaultTerms(challenge.projectId) - // challenge.terms = await helper.validateChallengeTerms(_.union(projectTerms, challenge.terms)) - // TODO - challenge terms returned from projects api don't have a role associated - // this will need to be updated to associate project terms with a roleId - challenge.terms = await helper.validateChallengeTerms(challenge.terms || []); + // populate challenge terms + // const projectTerms = await helper.getProjectDefaultTerms(challenge.projectId) + // challenge.terms = await helper.validateChallengeTerms(_.union(projectTerms, challenge.terms)) + // TODO - challenge terms returned from projects api don't have a role associated + // this will need to be updated to associate project terms with a roleId + challenge.terms = await helper.validateChallengeTerms(challenge.terms || []); - // default the descriptionFormat - if (!challenge.descriptionFormat) { - challenge.descriptionFormat = "markdown"; - } + // default the descriptionFormat + if (!challenge.descriptionFormat) { + challenge.descriptionFormat = "markdown"; + } - if (challenge.phases && challenge.phases.length > 0) { - challenge.endDate = helper.calculateChallengeEndDate(challenge); - } + if (challenge.phases && challenge.phases.length > 0) { + challenge.endDate = helper.calculateChallengeEndDate(challenge); + } - if (challenge.events == null) challenge.events = []; - if (challenge.attachments == null) challenge.attachments = []; - if (challenge.prizeSets == null) challenge.prizeSets = []; - if (challenge.metadata == null) challenge.metadata = []; - if (challenge.groups == null) challenge.groups = []; - if (challenge.tags == null) challenge.tags = []; - if (challenge.startDate != null) challenge.startDate = challenge.startDate; - if (challenge.endDate != null) challenge.endDate = challenge.endDate; - if (challenge.discussions == null) challenge.discussions = []; - if (challenge.skills == null) challenge.skills = []; + if (challenge.events == null) challenge.events = []; + if (challenge.attachments == null) challenge.attachments = []; + if (challenge.prizeSets == null) challenge.prizeSets = []; + if (challenge.metadata == null) challenge.metadata = []; + if (challenge.groups == null) challenge.groups = []; + if (challenge.tags == null) challenge.tags = []; + if (challenge.startDate != null) challenge.startDate = challenge.startDate; + if (challenge.endDate != null) challenge.endDate = challenge.endDate; + if (challenge.discussions == null) challenge.discussions = []; + if (challenge.skills == null) challenge.skills = []; - challenge.metadata = challenge.metadata.map((m) => ({ - name: m.name, - value: typeof m.value === "string" ? m.value : JSON.stringify(m.value), - })); + challenge.metadata = challenge.metadata.map((m) => ({ + name: m.name, + value: typeof m.value === "string" ? m.value : JSON.stringify(m.value), + })); - const grpcMetadata = new GrpcMetadata(); + const grpcMetadata = new GrpcMetadata(); - grpcMetadata.set("handle", currentUser.handle); - grpcMetadata.set("userId", currentUser.userId); - grpcMetadata.set("token", await getM2MToken()); + grpcMetadata.set("handle", currentUser.handle); + grpcMetadata.set("userId", currentUser.userId); + grpcMetadata.set("token", await getM2MToken()); - const prizeType = challengeHelper.validatePrizeSetsAndGetPrizeType(challenge.prizeSets); + const prizeType = challengeHelper.validatePrizeSetsAndGetPrizeType(challenge.prizeSets); - if (prizeType === constants.prizeTypes.USD) { - convertPrizeSetValuesToCents(challenge.prizeSets); - } + if (prizeType === constants.prizeTypes.USD) { + convertPrizeSetValuesToCents(challenge.prizeSets); + } - const ret = await challengeDomain.create(challenge, grpcMetadata); + ret = await challengeDomain.create(challenge, grpcMetadata); - if (prizeType === constants.prizeTypes.USD) { - convertPrizeSetValuesToDollars(ret.prizeSets, ret.overview); - } + if (prizeType === constants.prizeTypes.USD) { + convertPrizeSetValuesToDollars(ret.prizeSets, ret.overview); + } - ret.numOfSubmissions = 0; - ret.numOfRegistrants = 0; + ret.numOfSubmissions = 0; + ret.numOfRegistrants = 0; - enrichChallengeForResponse(ret, track, type); + enrichChallengeForResponse(ret, track, type); - // Create in ES - await esClient.create({ - index: config.get("ES.ES_INDEX"), - type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, - refresh: config.get("ES.ES_REFRESH"), - id: ret.id, - body: ret, - }); + // Create in ES + await esClient.create({ + index: config.get("ES.ES_INDEX"), + type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, + refresh: config.get("ES.ES_REFRESH"), + id: ret.id, + body: ret, + }); - // If the challenge is self-service, add the creating user as the "client manager", *not* the manager - // This is necessary for proper handling of the vanilla embed on the self-service work item dashboard + // If the challenge is self-service, add the creating user as the "client manager", *not* the manager + // This is necessary for proper handling of the vanilla embed on the self-service work item dashboard - if (challenge.legacy.selfService) { - if (currentUser.handle) { - await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); - } - } else { - if (currentUser.handle) { - await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); + if (challenge.legacy.selfService) { + if (currentUser.handle) { + await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); + } + } else { + if (currentUser.handle) { + await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); + } } - } - - // post bus event - await helper.postBusEvent(constants.Topics.ChallengeCreated, ret); + // post bus event + await helper.postBusEvent(constants.Topics.ChallengeCreated, ret); + } catch (ex){ + logger.logFullError(err); + } return ret; } createChallenge.schema = { From a0384620dadd274ca5ea5f88315d45062dc6d986 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 28 May 2025 15:45:10 +1000 Subject: [PATCH 6/7] Typo --- src/services/ChallengeService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 9f99567f..0c18772e 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1044,7 +1044,7 @@ async function createChallenge(currentUser, challenge, userToken) { // post bus event await helper.postBusEvent(constants.Topics.ChallengeCreated, ret); - } catch (ex){ + } catch (err){ logger.logFullError(err); } return ret; From 940ef7b4699062aaf22ca9a82fd1b4e9895a1ebf Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 28 May 2025 16:06:13 +1000 Subject: [PATCH 7/7] Typo --- src/services/ChallengeService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 0c18772e..0d68eba0 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1045,6 +1045,7 @@ async function createChallenge(currentUser, challenge, userToken) { // post bus event await helper.postBusEvent(constants.Topics.ChallengeCreated, ret); } catch (err){ + console.error(err); logger.logFullError(err); } return ret;