diff --git a/package.json b/package.json index d9efd45..9791e86 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "lint": "gulp lint", "heroku-postbuild": "gulp build", "create-tables": "CREATE_DB=true node scripts/create-update-tables.js", + "migrate-user-mapping": "node scripts/migrate-user-mapping.js", + "add-organisation": "node scripts/add-organisation.js", "log-repository-collisions": "node scripts/log-repository-collisions.js" }, "dependencies": { diff --git a/scripts/add-organisation.js b/scripts/add-organisation.js new file mode 100644 index 0000000..8524a36 --- /dev/null +++ b/scripts/add-organisation.js @@ -0,0 +1,33 @@ +const dbHelper = require('../src/common/db-helper'); +const helper = require('../src/common/helper'); +const Organisation = require('../src/models').Organisation; + +const args = process.argv; +if (args.length < 5) { + console.log('Please provide data. Example: npm run add-organisation MyOrganisation ownername PAT-Token'); + return; +} +const organisationName = args[2]; +const owner = args[3]; +const pat = args[4]; + +(async () => { + const dbOrganisation = await dbHelper.queryOneOrganisation(Organisation, organisationName); + if (dbOrganisation) { + console.log(`Updating Organisation = ${organisationName} Owner = ${owner} PAT = ${pat}.`); + await dbHelper.update(Organisation, dbOrganisation.id, { + name: organisationName, + owner, + personalAccessToken: pat + }); + } + else { + console.log(`Adding Organisation = ${organisationName} Owner = ${owner} PAT = ${pat}.`); + await dbHelper.create(Organisation, { + id: helper.generateIdentifier(), + name: organisationName, + owner, + personalAccessToken: pat + }); + } +})(); \ No newline at end of file diff --git a/scripts/migrate-user-mapping.js b/scripts/migrate-user-mapping.js new file mode 100644 index 0000000..bd93eca --- /dev/null +++ b/scripts/migrate-user-mapping.js @@ -0,0 +1,45 @@ +const AWS = require('aws-sdk'); +const helper = require('../src/common/helper'); +const dbHelper = require('../src/common/db-helper'); +const GithubUserMapping = require('../src/models').GithubUserMapping; +const GitlabUserMapping = require('../src/models').GitlabUserMapping; + +if (process.env.IS_LOCAL=="true") { + console.log("IS LOCAL") + AWS.config.update({ + endpoint: 'http://localhost:8000' + }); +} +var documentClient = new AWS.DynamoDB.DocumentClient(); + +(async () => { + console.log('Migrating...'); + const params = { + TableName: 'Topcoder_X.UserMapping' + }; + + let items; + do { + items = await documentClient.scan(params).promise(); + items.Items.forEach(async (item) => { + console.log(item); + if (item.githubUserId && item.githubUsername) { + await dbHelper.create(GithubUserMapping, { + id: helper.generateIdentifier(), + topcoderUsername: item.topcoderUsername, + githubUserId: item.githubUserId, + githubUsername: item.githubUsername, + }); + } + if (item.gitlabUsername && item.gitlabUserId) { + await dbHelper.create(GitlabUserMapping, { + id: helper.generateIdentifier(), + topcoderUsername: item.topcoderUsername, + gitlabUsername: item.gitlabUsername, + gitlabUserId: item.gitlabUserId, + }); + } + }); + params.ExclusiveStartKey = items.LastEvaluatedKey; + } while(typeof items.LastEvaluatedKey !== 'undefined'); +})(); \ No newline at end of file diff --git a/src/common/db-helper.js b/src/common/db-helper.js index 3ae537b..45b66f3 100644 --- a/src/common/db-helper.js +++ b/src/common/db-helper.js @@ -154,7 +154,7 @@ async function queryOneUserMappingByTCUsername(model, tcusername) { model.queryOne('topcoderUsername').eq(tcusername) .all() .exec((err, result) => { - if (err || !result) { + if (err) { logger.debug(`queryOneUserMappingByTCUsername. Error. ${err}`); return reject(err); } @@ -367,6 +367,25 @@ async function removeUser(Model, username, type) { }); } +/** + * Get single data by query parameters + * @param {Object} model The dynamoose model to query + * @param {String} organisation The organisation name + * @returns {Promise} + */ +async function queryOneOrganisation(model, organisation) { + return await new Promise((resolve, reject) => { + model.queryOne('name').eq(organisation) + .all() + .exec((err, result) => { + if (err) { + logger.debug(`queryOneOrganisation. Error. ${err}`); + return reject(err); + } + return resolve(result); + }); + }); +} module.exports = { getById, @@ -379,6 +398,7 @@ module.exports = { queryOneActiveCopilotPayment, queryOneActiveProject, queryOneActiveProjectWithFilter, + queryOneOrganisation, queryOneIssue, queryOneUserByType, queryOneUserByTypeAndRole, diff --git a/src/common/helper.js b/src/common/helper.js index 5dae1d2..e80dc1d 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -216,7 +216,8 @@ async function getProviderType(repoUrl) { * @returns {Object} the owner/copilot for the project */ async function getProjectCopilotOrOwner(models, project, provider, isCopilot) { - const userMapping = await dbHelper.queryOneUserMappingByTCUsername(models.UserMapping, + const userMapping = await dbHelper.queryOneUserMappingByTCUsername( + provider === 'github' ? models.GithubUserMapping : models.GitlabUserMapping, isCopilot ? project.copilot : project.owner); if (!userMapping || diff --git a/src/controllers/GithubController.js b/src/controllers/GithubController.js index d72fc9a..2794131 100644 --- a/src/controllers/GithubController.js +++ b/src/controllers/GithubController.js @@ -20,7 +20,7 @@ const GithubService = require('../services/GithubService'); const UserService = require('../services/UserService'); const OwnerUserTeam = require('../models').OwnerUserTeam; const UserTeamMapping = require('../models').UserTeamMapping; -const UserMapping = require('../models').UserMapping; +const GithubUserMapping = require('../models').GithubUserMapping; const constants = require('../common/constants'); const request = superagentPromise(superagent, Promise); @@ -127,6 +127,8 @@ async function addUserToTeam(req, res) { config.GITHUB_CLIENT_ID }&redirect_uri=${ encodeURIComponent(callbackUri) + }&scope=${ + encodeURIComponent('admin:org') }&state=${identifier}`); } @@ -156,22 +158,33 @@ async function addUserToTeamCallback(req, res) { throw new errors.UnauthorizedError('Github authorization failed.', result.body.error_description); } const token = result.body.access_token; + + // get team details + const teamDetails = await GithubService.getTeamDetails(team.ownerToken, team.teamId); + const organisation = teamDetails.organization.login; + + // Add member to organisation + const addOrganisationResult = await GithubService.addOrganisationMember(organisation, token); + console.log(`Add organisation member, state = ${addOrganisationResult.state}`); /* eslint-disable-line no-console */ + if (addOrganisationResult.state === 'pending') { + const acceptInvitation = await GithubService.acceptOrganisationInvitation(organisation, token); + console.log(`Accept organisation invitation by member, state = ${acceptInvitation.state}`); /* eslint-disable-line no-console */ + } + // 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); // associate github username with TC username - const mapping = await dbHelper.queryOneUserMappingByTCUsername(UserMapping, req.session.tcUsername); - - // get team details - const teamDetails = await GithubService.getTeamDetails(team.ownerToken, team.teamId); + const mapping = await dbHelper.queryOneUserMappingByTCUsername(GithubUserMapping, req.session.tcUsername); if (mapping) { - await dbHelper.update(UserMapping, mapping.id, { + await dbHelper.update(GithubUserMapping, mapping.id, { githubUsername: githubUser.username, githubUserId: githubUser.id, }); } else { - await dbHelper.create(UserMapping, { + console.log('User mapping not found. Create new mapping.'); /* eslint-disable-line no-console */ + await dbHelper.create(GithubUserMapping, { id: helper.generateIdentifier(), topcoderUsername: req.session.tcUsername, githubUsername: githubUser.username, diff --git a/src/controllers/GitlabController.js b/src/controllers/GitlabController.js index 7ff6477..8eba050 100644 --- a/src/controllers/GitlabController.js +++ b/src/controllers/GitlabController.js @@ -20,7 +20,7 @@ const GitlabService = require('../services/GitlabService'); const UserService = require('../services/UserService'); const User = require('../models').User; const OwnerUserGroup = require('../models').OwnerUserGroup; -const UserMapping = require('../models').UserMapping; +const GitlabUserMapping = require('../models').GitlabUserMapping; const UserGroupMapping = require('../models').UserGroupMapping; const request = superagentPromise(superagent, Promise); @@ -209,14 +209,14 @@ async function addUserToGroupCallback(req, res) { group.expiredAt); // associate gitlab username with TC username - const mapping = await dbHelper.queryOneUserMappingByTCUsername(UserMapping, req.session.tcUsername); + const mapping = await dbHelper.queryOneUserMappingByTCUsername(GitlabUserMapping, req.session.tcUsername); if (mapping) { - await dbHelper.update(UserMapping, mapping.id, { + await dbHelper.update(GitlabUserMapping, mapping.id, { gitlabUsername: gitlabUser.username, gitlabUserId: gitlabUser.id, }); } else { - await dbHelper.create(UserMapping, { + await dbHelper.create(GitlabUserMapping, { id: helper.generateIdentifier(), topcoderUsername: req.session.tcUsername, gitlabUsername: gitlabUser.username, diff --git a/src/models/GithubUserMapping.js b/src/models/GithubUserMapping.js new file mode 100644 index 0000000..6cd5d23 --- /dev/null +++ b/src/models/GithubUserMapping.js @@ -0,0 +1,46 @@ +/** + * This defines github user mapping model. + */ +'use strict'; + +const dynamoose = require('dynamoose'); + +const Schema = dynamoose.Schema; + +const schema = new Schema({ + id: { + type: String, + required: true, + hashKey: true + }, + topcoderUsername: { + type: String, + required: true, + index: { + global: true, + project: true, + rangKey: 'id', + name: 'TopcoderUsernameIndex' + } + }, + githubUsername: { + type: String, + index: { + global: true, + project: true, + rangKey: 'id', + name: 'GithubUsernameIndex' + } + }, + githubUserId: { + type: Number, + index: { + global: true, + project: true, + rangKey: 'id', + name: 'GithubUserIdIndex' + } + } +}); + +module.exports = schema; diff --git a/src/models/GitlabUserMapping.js b/src/models/GitlabUserMapping.js new file mode 100644 index 0000000..f87f1d9 --- /dev/null +++ b/src/models/GitlabUserMapping.js @@ -0,0 +1,46 @@ +/** + * This defines gitlab user mapping model. + */ +'use strict'; + +const dynamoose = require('dynamoose'); + +const Schema = dynamoose.Schema; + +const schema = new Schema({ + id: { + type: String, + required: true, + hashKey: true + }, + topcoderUsername: { + type: String, + required: true, + index: { + global: true, + project: true, + rangKey: 'id', + name: 'TopcoderUsernameIndex' + } + }, + gitlabUsername: { + type: String, + index: { + global: true, + project: true, + rangKey: 'id', + name: 'GitlabUsernameIndex' + } + }, + gitlabUserId: { + type: Number, + index: { + global: true, + project: true, + rangKey: 'id', + name: 'GitlabUserIdIndex' + } + } +}); + +module.exports = schema; diff --git a/src/models/UserMapping.js b/src/models/Organisation.js similarity index 53% rename from src/models/UserMapping.js rename to src/models/Organisation.js index 7ab8a86..dca4453 100644 --- a/src/models/UserMapping.js +++ b/src/models/Organisation.js @@ -1,6 +1,8 @@ /** - * This defines user mapping model. + * This defines organisation model. */ +'use strict'; + const dynamoose = require('dynamoose'); const Schema = dynamoose.Schema; @@ -9,22 +11,26 @@ const schema = new Schema({ id: { type: String, required: true, - hashKey: true, + hashKey: true }, - topcoderUsername: { + name: { type: String, required: true, index: { global: true, project: true, - rangeKey: 'id', - name: 'TopcoderUsernameIndex', - }, + rangKey: 'id', + name: 'NameIndex' + } + }, + owner: { + type: String, + required: true }, - githubUsername: String, - gitlabUsername: String, - githubUserId: Number, - gitlabUserId: Number + personalAccessToken: { + type: String, + required: true + } }); module.exports = schema; diff --git a/src/services/GithubService.js b/src/services/GithubService.js index dbd1d5e..fb8d2f5 100644 --- a/src/services/GithubService.js +++ b/src/services/GithubService.js @@ -17,9 +17,14 @@ const constants = require('../common/constants'); const helper = require('../common/helper'); const dbHelper = require('../common/db-helper'); const User = require('../models').User; -const UserMapping = require('../models').UserMapping; +const GithubUserMapping = require('../models').GithubUserMapping; const OwnerUserTeam = require('../models').OwnerUserTeam; +const Organisation = require('../models').Organisation; const errors = require('../common/errors'); +const superagent = require('superagent'); +const superagentPromise = require('superagent-promise'); + +const request = superagentPromise(superagent, Promise); /** * Ensure the owner user is in database. @@ -41,16 +46,16 @@ async function ensureOwnerUser(token, topcoderUsername) { constants.USER_TYPES.GITHUB, constants.USER_ROLES.OWNER); - const userMapping = await dbHelper.queryOneUserMappingByTCUsername(UserMapping, topcoderUsername); + const userMapping = await dbHelper.queryOneUserMappingByTCUsername(GithubUserMapping, topcoderUsername); if (!userMapping) { - await dbHelper.create(UserMapping, { + await dbHelper.create(GithubUserMapping, { id: helper.generateIdentifier(), topcoderUsername, githubUserId: userProfile.id, githubUsername: userProfile.login, }); } else { - await dbHelper.update(UserMapping, userMapping.id, { + await dbHelper.update(GithubUserMapping, userMapping.id, { githubUserId: userProfile.id, githubUsername: userProfile.login, }); @@ -229,6 +234,89 @@ addTeamMember.schema = Joi.object().keys({ accessLevel: Joi.string().required(), }); +/** + * Add organisation member. + * @param {String} organisation the organisation name + * @param {String} normalUserToken the normal user token + * @returns {Promise} the promise result + */ +async function addOrganisationMember(organisation, normalUserToken) { + let state; + try { + const dbOrganisation = await dbHelper.queryOneOrganisation(Organisation, organisation); + if (!dbOrganisation) { + console.log(`Personal access token not found for organisation ${organisation}.`); /* eslint-disable-line no-console */ + return {}; + } + const githubNormalUser = new GitHub({ + token: normalUserToken, + }); + const normalUser = await githubNormalUser.getUser().getProfile(); + const username = normalUser.data.login; + const base64PAT = Buffer.from(`${dbOrganisation.owner}:${dbOrganisation.personalAccessToken}`).toString('base64'); + const result = await request + .put(`https://api.github.com/orgs/${organisation}/memberships/${username}`) + .send({role: 'member'}) + .set('User-Agent', 'superagent') + .set('Accept', 'application/vnd.github.v3+json') + .set('Authorization', `Basic ${base64PAT}`) + .end(); + state = _.get(result, 'body.state'); + } catch (err) { + // if error is already exists discard + if (_.chain(err).get('body.errors').countBy({ + code: 'already_exists', + }).get('true') + .isUndefined() + .value()) { + throw helper.convertGitHubError(err, 'Failed to add organisation member'); + } + } + // return its state + return {state}; +} + +addOrganisationMember.schema = Joi.object().keys({ + organisation: Joi.string().required(), + normalUserToken: Joi.string().required() +}); + +/** + * Accept organisation invitation by member. + * @param {String} organisation the organisation name + * @param {String} normalUserToken the normal user token + * @returns {Promise} the promise result + */ +async function acceptOrganisationInvitation(organisation, normalUserToken) { + let state; + try { + const result = await request + .patch(`https://api.github.com/user/memberships/orgs/${organisation}`) + .send({state: 'active'}) + .set('User-Agent', 'superagent') + .set('Accept', 'application/vnd.github.v3+json') + .set('Authorization', `token ${normalUserToken}`) + .end(); + state = _.get(result, 'body.state'); + } catch (err) { + // if error is already exists discard + if (_.chain(err).get('body.errors').countBy({ + code: 'already_exists', + }).get('true') + .isUndefined() + .value()) { + throw helper.convertGitHubError(err, 'Failed to accept organisation invitation'); + } + } + // return its state + return {state}; +} + +acceptOrganisationInvitation.schema = Joi.object().keys({ + organisation: Joi.string().required(), + normalUserToken: Joi.string().required() +}); + /** * Gets the user id by username * @param {string} username the username @@ -320,6 +408,8 @@ module.exports = { getUserIdByUsername, getTeamDetails, deleteUserFromGithubTeam, + addOrganisationMember, + acceptOrganisationInvitation }; helper.buildService(module.exports); diff --git a/src/services/GitlabService.js b/src/services/GitlabService.js index c7d3c7d..06ede47 100644 --- a/src/services/GitlabService.js +++ b/src/services/GitlabService.js @@ -19,7 +19,7 @@ const helper = require('../common/helper'); const dbHelper = require('../common/db-helper'); const errors = require('../common/errors'); const User = require('../models').User; -const UserMapping = require('../models').UserMapping; +const GitlabUserMapping = require('../models').GitlabUserMapping; const OwnerUserGroup = require('../models').OwnerUserGroup; const request = superagentPromise(superagent, Promise); @@ -52,16 +52,16 @@ async function ensureOwnerUser(token, topcoderUsername) { constants.USER_TYPES.GITLAB, constants.USER_ROLES.OWNER); - const userMapping = await dbHelper.queryOneUserMappingByTCUsername(UserMapping, topcoderUsername); + const userMapping = await dbHelper.queryOneUserMappingByTCUsername(GitlabUserMapping, topcoderUsername); if (!userMapping) { - await dbHelper.create(UserMapping, { + await dbHelper.create(GitlabUserMapping, { id: helper.generateIdentifier(), topcoderUsername, gitlabUserId: userProfile.id, gitlabUsername: userProfile.username, }); } else { - await dbHelper.update(UserMapping, userMapping.id, { + await dbHelper.update(GitlabUserMapping, userMapping.id, { gitlabUserId: userProfile.id, gitlabUsername: userProfile.username, }); diff --git a/src/services/TCUserService.js b/src/services/TCUserService.js index 023b7a8..9dee9bf 100644 --- a/src/services/TCUserService.js +++ b/src/services/TCUserService.js @@ -12,7 +12,8 @@ const Joi = require('joi'); const decodeToken = require('tc-auth-lib').decodeToken; const errors = require('../common/errors'); const helper = require('../common/helper'); -const UserMapping = require('../models').UserMapping; +const GithubUserMapping = require('../models').GithubUserMapping; +const GitlabUserMapping = require('../models').GitlabUserMapping; /** * gets the handle of tc user. @@ -40,7 +41,12 @@ async function getUserMapping(query) { 'At least one of topcoderUsername/gitlabUsername/githubUsername should be provided.'); } - return await helper.ensureExists(UserMapping, query, 'UserMapping'); + if (query.githubUsername) { + return await helper.ensureExists(GithubUserMapping, query, 'GithubUserMapping'); + } + else { + return await helper.ensureExists(GitlabUserMapping, query, 'GitlabUserMapping'); + } } getUserMapping.schema = Joi.object().keys({ diff --git a/src/services/UserService.js b/src/services/UserService.js index 61e32f7..d04a0d0 100644 --- a/src/services/UserService.js +++ b/src/services/UserService.js @@ -15,7 +15,8 @@ const dbHelper = require('../common/db-helper'); const errors = require('../common/errors'); const constants = require('../common/constants'); const User = require('../models').User; -const UserMapping = require('../models').UserMapping; +const GithubUserMapping = require('../models').GithubUserMapping; +const GitlabUserMapping = require('../models').GitlabUserMapping; /** * gets user setting @@ -23,30 +24,32 @@ const UserMapping = require('../models').UserMapping; * @returns {Object} the user setting */ async function getUserSetting(handle) { - const mapping = await dbHelper.queryOneUserMappingByTCUsername( - UserMapping, handle.toLowerCase()); + const githubMapping = await dbHelper.queryOneUserMappingByTCUsername( + GithubUserMapping, handle.toLowerCase()); + const gitlabMapping = await dbHelper.queryOneUserMappingByTCUsername( + GitlabUserMapping, handle.toLowerCase()); const setting = { github: false, gitlab: false, expired: {} }; - if (!mapping) { + if (!githubMapping && !gitlabMapping) { return setting; } const users = []; - if (mapping.githubUsername) { + if (githubMapping && githubMapping.githubUsername) { const github = await dbHelper.queryOneUserByType( - User, mapping.githubUsername, constants.USER_TYPES.GITHUB); + User, githubMapping.githubUsername, constants.USER_TYPES.GITHUB); if (!_.isNil(github)) { users.push(github); } } - if (mapping.gitlabUsername) { + if (gitlabMapping && gitlabMapping.gitlabUsername) { const gitlab = await dbHelper.queryOneUserByType( - User, mapping.gitlabUsername, constants.USER_TYPES.GITLAB); + User, gitlabMapping.gitlabUsername, constants.USER_TYPES.GITLAB); if (!_.isNil(gitlab)) { users.push(gitlab); } @@ -76,7 +79,7 @@ getUserSetting.schema = Joi.object().keys({ */ async function revokeUserSetting(handle, provider) { const mapping = await dbHelper.queryOneUserMappingByTCUsername( - UserMapping, handle.toLowerCase()); + provider === 'github' ? GithubUserMapping : GitlabUserMapping, handle.toLowerCase()); if (!mapping) { return false; @@ -127,7 +130,7 @@ async function getUserToken(username, tokenType) { */ async function getAccessTokenByHandle(handle, provider) { const mapping = await dbHelper.queryOneUserMappingByTCUsername( - UserMapping, handle.toLowerCase()); + provider === 'github' ? GithubUserMapping : GitlabUserMapping, handle.toLowerCase()); let gitUserName; if (mapping) { gitUserName = provider === constants.USER_TYPES.GITHUB ? 'githubUsername' : //eslint-disable-line no-nested-ternary