diff --git a/src/common/db-helper.js b/src/common/db-helper.js index fdca6f3..8d2cdc8 100644 --- a/src/common/db-helper.js +++ b/src/common/db-helper.js @@ -285,6 +285,27 @@ async function queryOneUserMappingByTCUsername(model, tcusername) { }); } +/** + * Get single data by query parameters + * @param {Object} model The dynamoose model to query + * @param {String} provider The git provider + * @param {String} gitUsername The git username + * @returns {Promise} + */ +async function queryTCUsernameByGitUsername(model, provider, gitUsername) { + return await new Promise((resolve, reject) => { + model.queryOne(`${provider}Username`).eq(gitUsername) + .all() + .exec((err, result) => { + if (err) { + logger.debug(`queryTCUsernameByGitUsername. Error. ${err}`); + return reject(err); + } + return resolve(result.topcoderUsername); + }); + }); +} + /** * Get single data by query parameters * @param {Object} model The dynamoose model to query @@ -673,6 +694,7 @@ module.exports = { queryOneUserGroupMapping, queryOneUserTeamMapping, queryOneUserMappingByTCUsername, + queryTCUsernameByGitUsername, queryRepositoriesByProjectId, queryRepositoryByProjectIdFilterUrl }; diff --git a/src/common/helper.js b/src/common/helper.js index e80dc1d..ff28756 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -19,6 +19,8 @@ const bcrypt = require('bcryptjs'); const moment = require('moment'); const parseDomain = require('parse-domain'); const config = require('../config'); +const kafka = require('../utils/kafka'); +const models = require('../models'); const logger = require('./logger'); const errors = require('./errors'); const constants = require('./constants'); @@ -120,6 +122,52 @@ function buildController(controller) { }); } +/** + * Convert github api error. + * @param {String} copilotHandle the copilot handle + * @param {String} provider the git provider + */ +async function sendTokenExpiredEvent(copilotHandle, provider) { + const notificationTokenExpiredEvent = { + event: 'notification.tokenExpired', + data: { + copilotHandle, + provider, + }, + }; + await kafka.send(JSON.stringify(notificationTokenExpiredEvent)); +} + +/** + * Convert github api error. + * @param {Error} err the github api error + * @param {String} message the error message + * @param {String} gitUsername the git username + * @returns {Error} converted error + */ +async function convertGitHubErrorAsync(err, message, gitUsername) { + if (err.statusCode === 401 && gitUsername) { // eslint-disable-line no-magic-numbers + const copilotHandle = await dbHelper.queryTCUsernameByGitUsername(models.GithubUserMapping, 'github', gitUsername); + await sendTokenExpiredEvent(copilotHandle, 'Github'); + } + return convertGitHubError(err, message); +} + +/** + * Convert gitlab api error. + * @param {Error} err the gitlab api error + * @param {String} message the error message + * @param {String} gitUsername the git username + * @returns {Error} converted error + */ +async function convertGitLabErrorAsync(err, message, gitUsername) { + if (err.statusCode === 401 && gitUsername) { // eslint-disable-line no-magic-numbers + const copilotHandle = await dbHelper.queryTCUsernameByGitUsername(models.GitlabUserMapping, 'gitlab', gitUsername); + await sendTokenExpiredEvent(copilotHandle, 'Gitlab'); + } + return convertGitLabError(err, message); +} + /** * Convert github api error. * @param {Error} err the github api error @@ -209,24 +257,23 @@ async function getProviderType(repoUrl) { /** * gets the git username of copilot/owner for a project - * @param {Object} models the db models * @param {Object} project the db project detail * @param {String} provider the git provider * @param {Boolean} isCopilot if true, then get copilot, otherwise get owner * @returns {Object} the owner/copilot for the project */ -async function getProjectCopilotOrOwner(models, project, provider, isCopilot) { +async function getProjectCopilotOrOwner(project, provider, isCopilot) { const userMapping = await dbHelper.queryOneUserMappingByTCUsername( - provider === 'github' ? models.GithubUserMapping : models.GitlabUserMapping, + provider === 'github' ? models.GithubUserMapping : models.GitlabUserMapping, isCopilot ? project.copilot : project.owner); - if (!userMapping || - (provider === 'github' && !userMapping.githubUserId) + if (!userMapping || + (provider === 'github' && !userMapping.githubUserId) || (provider === 'gitlab' && !userMapping.gitlabUserId)) { throw new Error(`Couldn't find ${isCopilot ? 'copilot' : 'owner'} username for '${provider}' for this repository.`); } - let user = await dbHelper.queryOneUserByType(models.User, + let user = await dbHelper.queryOneUserByType(models.User, provider === 'github' ? userMapping.githubUsername : // eslint-disable-line no-nested-ternary userMapping.gitlabUsername, provider); @@ -270,6 +317,8 @@ module.exports = { buildController, convertGitHubError, convertGitLabError, + convertGitHubErrorAsync, + convertGitLabErrorAsync, ensureExists, ensureExistsWithKey, generateIdentifier, diff --git a/src/controllers/GithubController.js b/src/controllers/GithubController.js index 2794131..ea573f6 100644 --- a/src/controllers/GithubController.js +++ b/src/controllers/GithubController.js @@ -160,7 +160,7 @@ async function addUserToTeamCallback(req, res) { const token = result.body.access_token; // get team details - const teamDetails = await GithubService.getTeamDetails(team.ownerToken, team.teamId); + const teamDetails = await GithubService.getTeamDetails(team.ownerUsername, team.ownerToken, team.teamId); const organisation = teamDetails.organization.login; // Add member to organisation @@ -173,7 +173,8 @@ async function addUserToTeamCallback(req, res) { // add user to team console.log(`adding ${token} to ${team.teamId} with ${team.ownerToken}`); /* eslint-disable-line no-console */ - const githubUser = await GithubService.addTeamMember(team.teamId, team.ownerToken, token, team.accessLevel); + const githubUser = await GithubService.addTeamMember( + team.ownerUsername, team.teamId, team.ownerToken, token, team.accessLevel); // associate github username with TC username const mapping = await dbHelper.queryOneUserMappingByTCUsername(GithubUserMapping, req.session.tcUsername); @@ -247,7 +248,8 @@ async function deleteUsersFromTeam(req, res) { }); // eslint-disable-next-line no-restricted-syntax for (const userTeamMapItem of userTeamMappings) { - await GithubService.deleteUserFromGithubTeam(token, teamId, githubOrgId, userTeamMapItem.githubUserName); + await GithubService.deleteUserFromGithubTeam( + teamInDB.ownerUsername, token, teamId, githubOrgId, userTeamMapItem.githubUserName); await dbHelper.removeById(UserTeamMapping, userTeamMapItem.id); } } catch (err) { diff --git a/src/controllers/GitlabController.js b/src/controllers/GitlabController.js index 8c60f23..09ea8be 100644 --- a/src/controllers/GitlabController.js +++ b/src/controllers/GitlabController.js @@ -99,11 +99,14 @@ async function ownerUserLoginCallback(req, res) { */ async function listOwnerUserGroups(req) { const user = await UserService.getAccessTokenByHandle(req.currentUser.handle, constants.USER_TYPES.GITLAB); + // NOTE: Only user with topcoder-x account can pass this condition. + // Only them will be inserted into `User` table, + // normal user will not be in the `User` table. if (!user || !user.accessToken) { throw new errors.UnauthorizedError('You have not setup for Gitlab.'); } const refreshedUser = await GitlabService.refreshGitlabUserAccessToken(user); - return await GitlabService.listOwnerUserGroups(refreshedUser.accessToken, req.query.page, + return await GitlabService.listOwnerUserGroups(refreshedUser.username, refreshedUser.accessToken, req.query.page, req.query.perPage, req.query.getAll); } @@ -197,14 +200,15 @@ async function addUserToGroupCallback(req, res) { const token = result.body.access_token; // get group name - const groupsResult = await GitlabService.listOwnerUserGroups(refreshedOwnerUser.accessToken, 1, - constants.MAX_PER_PAGE, true); + const groupsResult = await GitlabService.listOwnerUserGroups(refreshedOwnerUser.username, + refreshedOwnerUser.accessToken, 1, constants.MAX_PER_PAGE, true); const currentGroup = _.find(groupsResult.groups, (item) => { // eslint-disable-line arrow-body-style return item.id.toString() === group.groupId.toString(); }); // add user to group const gitlabUser = await GitlabService.addGroupMember( + refreshedOwnerUser.username, group.groupId, refreshedOwnerUser.accessToken, token, @@ -272,8 +276,8 @@ async function deleteUsersFromTeam(req, res) { const userGroupMappings = await dbHelper.scan(UserGroupMapping, {groupId}); // eslint-disable-next-line no-restricted-syntax for (const userGroupMapItem of userGroupMappings) { - await GitlabService.deleteUserFromGitlabGroup(refreshedOwnerUser.accessToken, groupId, - userGroupMapItem.gitlabUserId); + await GitlabService.deleteUserFromGitlabGroup(refreshedOwnerUser.username, + refreshedOwnerUser.accessToken, groupId, userGroupMapItem.gitlabUserId); await dbHelper.removeById(UserGroupMapping, userGroupMapItem.id); } } catch (err) { diff --git a/src/services/GithubService.js b/src/services/GithubService.js index df559c8..3439a91 100644 --- a/src/services/GithubService.js +++ b/src/services/GithubService.js @@ -187,13 +187,14 @@ getTeamRegistrationUrl.schema = Joi.object().keys({ /** * Add team member. + * @param {String} gitUsername the git username * @param {String} teamId the team id * @param {String} ownerUserToken the owner user token * @param {String} normalUserToken the normal user token * @param {String} accessLevel the team's access level * @returns {Promise} the promise result */ -async function addTeamMember(teamId, ownerUserToken, normalUserToken, accessLevel) { +async function addTeamMember(gitUsername, teamId, ownerUserToken, normalUserToken, accessLevel) { let username; let id; let state; @@ -220,7 +221,7 @@ async function addTeamMember(teamId, ownerUserToken, normalUserToken, accessLeve }).get('true') .isUndefined() .value()) { - throw helper.convertGitHubError(err, 'Failed to add team member'); + throw await helper.convertGitHubErrorAsync(err, 'Failed to add team member', gitUsername); } } // return github username and its state @@ -228,6 +229,7 @@ async function addTeamMember(teamId, ownerUserToken, normalUserToken, accessLeve } addTeamMember.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), teamId: Joi.string().required(), ownerUserToken: Joi.string().required(), normalUserToken: Joi.string().required(), @@ -342,12 +344,13 @@ getUserIdByUsername.schema = Joi.object().keys({ /** * Get team detailed data * + * @param {String} gitUsername git username * @param {String} token user owner token * @param {String|Number} teamId team id * * @returns {Object} team object, see https://developer.github.com/v3/teams/#get-team */ -async function getTeamDetails(token, teamId) { +async function getTeamDetails(gitUsername, token, teamId) { const teamIdAsNumber = !_.isNumber(teamId) ? parseInt(teamId, 10) : teamId; let team; @@ -357,13 +360,14 @@ async function getTeamDetails(token, teamId) { team = teamResponse.data; } catch (err) { - throw helper.convertGitHubError(err, `Failed to get team with id '${teamId}'.`); + throw await helper.convertGitHubErrorAsync(err, `Failed to get team with id '${teamId}'.`, gitUsername); } return team; } getTeamDetails.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), token: Joi.string().required(), teamId: Joi.alternatives().try(Joi.string(), Joi.number()).required(), }); @@ -372,6 +376,7 @@ getTeamDetails.schema = Joi.object().keys({ /** * Get team detailed data * + * @param {String} gitUsername git username * @param {String} token user owner token * @param {String|Number} teamId team id * @param {String|Number} orgId team id @@ -379,7 +384,7 @@ getTeamDetails.schema = Joi.object().keys({ * * @returns {Object} status object, see https://developer.github.com/v3/teams/members/#remove-team-membership */ -async function deleteUserFromGithubTeam(token, teamId, orgId, githubUserName) { +async function deleteUserFromGithubTeam(gitUsername, token, teamId, orgId, githubUserName) { const teamIdAsNumber = !_.isNumber(teamId) ? parseInt(teamId, 10) : teamId; let deleteResult; try { @@ -388,12 +393,15 @@ async function deleteUserFromGithubTeam(token, teamId, orgId, githubUserName) { const deleteGithubUserEndpoint = `/organizations/${orgId}/team/${teamIdAsNumber}/memberships/${githubUserName}`; deleteResult = await team._request('DELETE', deleteGithubUserEndpoint); } catch (err) { - throw helper.convertGitHubError(err, `Failed to delete user '${githubUserName}' from org with orgId '${orgId}' and team id '${teamId}'.`); + throw await helper.convertGitHubErrorAsync( + err, `Failed to delete user '${githubUserName}' from org with orgId '${orgId}' and team id '${teamId}'.`, + githubUserName); } return deleteResult; } deleteUserFromGithubTeam.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), token: Joi.string().required(), teamId: Joi.alternatives().try(Joi.string(), Joi.number()).required(), orgId: Joi.string().required(), diff --git a/src/services/GitlabService.js b/src/services/GitlabService.js index eec3086..a6c0314 100644 --- a/src/services/GitlabService.js +++ b/src/services/GitlabService.js @@ -92,6 +92,7 @@ ensureOwnerUser.schema = Joi.object().keys({ /** * List groups of owner user. + * @param {String} gitUsername the git username * @param {String} token the token * @param {Number} page the page number (default to be 1). Must be >= 1 * @param {Number} perPage the page size (default to be constants.GITLAB_DEFAULT_PER_PAGE). @@ -99,7 +100,8 @@ ensureOwnerUser.schema = Joi.object().keys({ * @param {Boolean} getAll get all groups * @returns {Promise} the promise result */ -async function listOwnerUserGroups(token, page = 1, perPage = constants.GITLAB_DEFAULT_PER_PAGE, getAll = false) { +async function listOwnerUserGroups(gitUsername, token, page = 1, perPage = constants.GITLAB_DEFAULT_PER_PAGE, + getAll = false) { try { const response = await request .get(`${config.GITLAB_API_BASE_URL}/api/v4/groups`) @@ -127,11 +129,12 @@ async function listOwnerUserGroups(token, page = 1, perPage = constants.GITLAB_D } return result; } catch (err) { - throw helper.convertGitLabError(err, 'Failed to list user groups'); + throw await helper.convertGitLabErrorAsync(err, 'Failed to list user groups', gitUsername); } } listOwnerUserGroups.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), token: Joi.string().required(), page: Joi.number().integer().min(1).optional(), perPage: Joi.number().integer().min(1).max(constants.GITLAB_MAX_PER_PAGE) @@ -176,6 +179,7 @@ getGroupRegistrationUrl.schema = Joi.object().keys({ /** * Add group member. + * @param {String} gitUsername the git username * @param {String} groupId the group id * @param {String} ownerUserToken the owner user token * @param {String} normalUserToken the normal user token @@ -183,7 +187,7 @@ getGroupRegistrationUrl.schema = Joi.object().keys({ * @param {String} expiredAt the expired at params to define how long user joined teams. can be null * @returns {Promise} the promise result */ -async function addGroupMember(groupId, ownerUserToken, normalUserToken, accessLevel, expiredAt) { +async function addGroupMember(gitUsername, groupId, ownerUserToken, normalUserToken, accessLevel, expiredAt) { // eslint-disable-line max-params let username; let userId; try { @@ -219,14 +223,16 @@ async function addGroupMember(groupId, ownerUserToken, normalUserToken, accessLe if (err instanceof errors.ApiError) { throw err; } - throw helper.convertGitLabError( - err, `Failed to add group member userId=${userId} accessLevel=${accessLevel} expiredAt=${expiredAt}`); + throw await helper.convertGitLabErrorAsync( + err, `Failed to add group member userId=${userId} accessLevel=${accessLevel} expiredAt=${expiredAt}`, + gitUsername); } return {username, id: userId}; } } addGroupMember.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), groupId: Joi.string().required(), ownerUserToken: Joi.string().required(), normalUserToken: Joi.string().required(), @@ -303,11 +309,12 @@ refreshGitlabUserAccessToken.schema = Joi.object().keys({ /** * delete user fromgroup + * @param {String} gitUsername the git username * @param {String} ownerUserToken the gitlab owner token * @param {String} groupId the gitlab group Id * @param {String} userId the normal user id */ -async function deleteUserFromGitlabGroup(ownerUserToken, groupId, userId) { +async function deleteUserFromGitlabGroup(gitUsername, ownerUserToken, groupId, userId) { try { await request .del(`${config.GITLAB_API_BASE_URL}/api/v4/groups/${groupId}/members/${userId}`) @@ -318,12 +325,14 @@ async function deleteUserFromGitlabGroup(ownerUserToken, groupId, userId) { // If a user is not found from gitlab, then ignore the error // eslint-disable-next-line no-magic-numbers if (err.status !== 404) { - throw helper.convertGitLabError(err, `Failed to delete user from group, userId is ${userId}, groupId is ${groupId}.`); + throw await helper.convertGitLabErrorAsync( + err, `Failed to delete user from group, userId is ${userId}, groupId is ${groupId}.`, gitUsername); } } } deleteUserFromGitlabGroup.schema = Joi.object().keys({ + gitUsername: Joi.string().required(), ownerUserToken: Joi.string().required(), groupId: Joi.string().required(), userId: Joi.string().required(), diff --git a/src/services/IssueService.js b/src/services/IssueService.js index a9c3fb2..00fabd4 100644 --- a/src/services/IssueService.js +++ b/src/services/IssueService.js @@ -132,7 +132,7 @@ async function _ensureEditPermissionAndGetInfo(projectId, currentUser) { async function recreate(issue, currentUser) { const dbProject = await _ensureEditPermissionAndGetInfo(issue.projectId, currentUser); const provider = await helper.getProviderType(issue.url); - const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); + const userRole = await helper.getProjectCopilotOrOwner(dbProject, provider, false); const results = issue.url.split('/'); const index = 1; const repoName = results[results.length - index]; diff --git a/src/services/ProjectService.js b/src/services/ProjectService.js index d8b57f4..2899ac7 100644 --- a/src/services/ProjectService.js +++ b/src/services/ProjectService.js @@ -501,7 +501,7 @@ search.schema = Joi.object().keys({ async function createLabel(body, currentUser, repoUrl) { const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); const provider = await helper.getProviderType(repoUrl); - const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); + const userRole = await helper.getProjectCopilotOrOwner(dbProject, provider, false); const results = repoUrl.split('/'); const index = 1; const repoName = results[results.length - index]; @@ -576,7 +576,7 @@ async function createHook(body, currentUser, repoUrl) { const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); const dbRepo = await dbHelper.queryRepositoryByProjectIdFilterUrl(dbProject.id, repoUrl); const provider = await helper.getProviderType(repoUrl); - const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, false); + const userRole = await helper.getProjectCopilotOrOwner(dbProject, provider, false); const results = repoUrl.split('/'); const index = 1; const repoName = results[results.length - index]; @@ -702,7 +702,7 @@ createHook.schema = createLabel.schema; async function addWikiRules(body, currentUser, repoUrl) { const dbProject = await _ensureEditPermissionAndGetInfo(body.projectId, currentUser); const provider = await helper.getProviderType(repoUrl); - const userRole = await helper.getProjectCopilotOrOwner(models, dbProject, provider, dbProject.copilot !== undefined); + const userRole = await helper.getProjectCopilotOrOwner(dbProject, provider, dbProject.copilot !== undefined); const results = repoUrl.split('/'); const index = 1; const repoName = results[results.length - index];