Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 8893ff9

Browse files
committed
fix: lock table for refreshing Gitlab token
1 parent a579ba3 commit 8893ff9

11 files changed

+413
-61
lines changed

constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const USER_TYPES = {
1919

2020
// The user roles
2121
const USER_ROLES = {
22-
OWNER: 'owner'
22+
OWNER: 'owner',
23+
GUEST: 'guest'
2324
};
2425

2526
// The challenge status

models/User.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ const schema = new Schema({
4949
// gitlab token data
5050
accessToken: {type: String, required: false},
5151
accessTokenExpiration: {type: Date, required: false},
52-
refreshToken: {type: String, required: false}
52+
refreshToken: {type: String, required: false},
53+
lockId: {type: String, required: false},
54+
lockExpiration: {type: Date, required: false}
5355
});
5456

5557
module.exports = schema;

package-lock.json

Lines changed: 15 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"direct-connect-migration": "node scripts/direct-connect-migration.js"
1414
},
1515
"engines": {
16-
"node": "~8.6.0"
16+
"node": ">=20",
17+
"npm": ">=9"
1718
},
1819
"repository": {
1920
"type": "git",
@@ -26,7 +27,7 @@
2627
},
2728
"homepage": "https://gitlab.com/luettich/processor#README",
2829
"dependencies": {
29-
"@gitbeaker/rest": "^39.12.0",
30+
"@gitbeaker/rest": "^39.13.0",
3031
"@octokit/rest": "^18.9.0",
3132
"axios": "^0.19.0",
3233
"circular-json": "^0.5.7",

services/GitlabService.js

Lines changed: 106 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
* @author TCSCODER
99
* @version 1.0
1010
*/
11-
1211
const config = require('config');
12+
const uuid = require('uuid').v4;
1313
const _ = require('lodash');
1414
const Joi = require('joi');
1515
const {Gitlab} = require('@gitbeaker/rest');
@@ -26,11 +26,11 @@ const request = superagentPromise(superagent, Promise);
2626
// milliseconds per second
2727
const MS_PER_SECOND = 1000;
2828

29+
const LOCK_TTL_SECONDS = 20;
30+
31+
const MAX_RETRY_COUNT = 30;
2932

30-
const USER_ROLE_TO_REDIRECT_URI_MAP = {
31-
owner: config.GITLAB_OWNER_USER_CALLBACK_URL,
32-
guest: config.GITLAB_GUEST_USER_CALLBACK_URL
33-
};
33+
const COOLDOWN_TIME = 1000;
3434

3535
/**
3636
* A schema for a Gitlab user, as stored in the TCX database.
@@ -55,7 +55,9 @@ const USER_SCHEMA = Joi.object().keys({
5555
username: Joi.string().optional(),
5656
type: Joi.string().valid('gitlab').required(),
5757
id: Joi.string().optional(),
58-
role: Joi.string().valid('owner', 'guest').required()
58+
role: Joi.string().valid('owner', 'guest').required(),
59+
lockId: Joi.string().optional(),
60+
lockExpiration: Joi.date().optional()
5961
}).required();
6062

6163
/**
@@ -107,31 +109,51 @@ class GitlabService {
107109
* Refresh the user access token if needed
108110
*/
109111
async refreshAccessToken() {
110-
const user = this.#user;
111-
if (user.accessTokenExpiration && new Date().getTime() > user.accessTokenExpiration.getTime() -
112-
(config.GITLAB_REFRESH_TOKEN_BEFORE_EXPIRATION * MS_PER_SECOND)) {
113-
const query = {
114-
client_id: config.GITLAB_CLIENT_ID,
115-
client_secret: config.GITLAB_CLIENT_SECRET,
116-
refresh_token: user.refreshToken,
117-
grant_type: 'refresh_token',
118-
redirect_uri: USER_ROLE_TO_REDIRECT_URI_MAP[user.role]
119-
};
120-
const refreshTokenResult = await request
121-
.post(`${config.GITLAB_API_BASE_URL}/oauth/token`)
122-
.query(query)
123-
.end();
124-
// save user token data
125-
const expiresIn = refreshTokenResult.body.expires_in || config.GITLAB_ACCESS_TOKEN_DEFAULT_EXPIRATION;
126-
const updates = {
127-
accessToken: refreshTokenResult.body.access_token,
128-
accessTokenExpiration: new Date(new Date().getTime() + expiresIn * MS_PER_SECOND),
129-
refreshToken: refreshTokenResult.body.refresh_token
130-
};
131-
_.assign(user, updates);
132-
await dbHelper.update(models.User, user.id, updates);
112+
const lockId = uuid().replace(/-/g, '');
113+
let lockedUser;
114+
let tries = 0;
115+
try {
116+
// eslint-disable-next-line no-constant-condition, no-restricted-syntax
117+
while ((tries < MAX_RETRY_COUNT) && !(lockedUser && lockedUser.lockId === lockId)) {
118+
logger.debug(`[Lock ID: ${lockId}][Attempt #${tries + 1}] Acquiring lock on user ${this.#user.username}.`);
119+
lockedUser = await dbHelper.acquireLockOnUser(this.#user.id, lockId, LOCK_TTL_SECONDS * MS_PER_SECOND);
120+
await new Promise((resolve) => setTimeout(resolve, COOLDOWN_TIME));
121+
tries += 1;
122+
}
123+
if (!lockedUser) {
124+
throw new Error(`Failed to acquire lock on user ${this.#user.id} after ${tries} attempts.`);
125+
}
126+
logger.debug(`[Lock ID: ${lockId}] Acquired lock on user ${this.#user.id}.`);
127+
if (lockedUser.accessTokenExpiration && new Date().getTime() > lockedUser.accessTokenExpiration.getTime() -
128+
(config.GITLAB_REFRESH_TOKEN_BEFORE_EXPIRATION * MS_PER_SECOND)) {
129+
logger.debug(`[Lock ID: ${lockId}] Refreshing access token for user ${this.#user.id}.`);
130+
const query = {
131+
client_id: config.GITLAB_CLIENT_ID,
132+
client_secret: config.GITLAB_CLIENT_SECRET,
133+
refresh_token: lockedUser.refreshToken,
134+
grant_type: 'refresh_token'
135+
};
136+
const refreshTokenResult = await request
137+
.post(`${config.GITLAB_API_BASE_URL}/oauth/token`)
138+
.query(query)
139+
.end();
140+
// save user token data
141+
const expiresIn = refreshTokenResult.body.expires_in || config.GITLAB_ACCESS_TOKEN_DEFAULT_EXPIRATION;
142+
const updates = {
143+
accessToken: refreshTokenResult.body.access_token,
144+
accessTokenExpiration: new Date(new Date().getTime() + expiresIn * MS_PER_SECOND),
145+
refreshToken: refreshTokenResult.body.refresh_token
146+
};
147+
_.assign(lockedUser, updates);
148+
await dbHelper.update(models.User, lockedUser.id, updates);
149+
}
150+
return lockedUser;
151+
} finally {
152+
if (lockedUser) {
153+
logger.debug(`[Lock ID: ${lockId}] Releasing lock on user ${this.#user.id}.`);
154+
await dbHelper.releaseLockOnUser(this.#user.id, lockId);
155+
}
133156
}
134-
return user;
135157
}
136158

137159
/**
@@ -433,6 +455,60 @@ class GitlabService {
433455
Joi.attempt({repository}, {repository: Joi.object().required()});
434456
await this.#gitlab.Projects.fork(repository.id);
435457
}
458+
459+
/**
460+
* Get the diff patch for a gitlab merge request
461+
* @param {import('@gitbeaker/rest').MergeRequestSchemaWithBasicLabels} mergeRequest The merge request
462+
* @returns {Promise<String>} The diff patch
463+
*/
464+
async getMergeRequestDiffPatches(mergeRequest) {
465+
Joi.attempt({mergeRequest}, {
466+
mergeRequest: Joi.object().keys({
467+
web_url: Joi.string().required()
468+
}).unknown(true).required()
469+
});
470+
const diff = await this.#gitlab.MergeRequests.allDiffs(mergeRequest.target_project_id, mergeRequest.iid);
471+
const patchFile = diff.reduce((acc, file) => {
472+
// Header
473+
acc += `diff --git a/${file.old_path} b/${file.new_path}\n`;
474+
// Index
475+
if (file.new_file) {
476+
acc += `new file mode ${file.b_mode}\n`;
477+
}
478+
if (file.deleted_file) {
479+
acc += `deleted file mode ${file.a_mode}\n`;
480+
}
481+
if (file.diff && file.diff !== '') {
482+
acc += `--- a/${file.new_file ? '/dev/null' : file.old_path}\n`;
483+
acc += `+++ b/${file.deleted_file ? '/dev/null' : file.new_path}\n`;
484+
acc += file.diff;
485+
} else if (file.renamed_file) {
486+
acc += 'similarity index 100%\n';
487+
acc += `rename from ${file.old_path}\n`;
488+
acc += `rename to ${file.new_path}\n`;
489+
}
490+
return acc;
491+
}, '');
492+
console.log(patchFile);
493+
return patchFile;
494+
}
495+
496+
/**
497+
* Get a list of all merge requests for a gitlab repository
498+
* @param {ProjectSchema} repository The repository
499+
* @param {Number} userId
500+
*/
501+
async getOpenMergeRequestsByUser(repository, userId) {
502+
Joi.attempt({repository, userId}, {
503+
repository: Joi.object().required(),
504+
userId: Joi.number().required()
505+
});
506+
return this.#gitlab.MergeRequests.all({
507+
projectId: repository.id,
508+
state: 'opened',
509+
authorId: userId
510+
});
511+
}
436512
}
437513

438514
module.exports = GitlabService;

services/PrivateForkService.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ const uuid = require('uuid').v4;
66
const models = require('../models');
77
const dbHelper = require('../utils/db-helper');
88
const logger = require('../utils/logger');
9+
const errors = require('../utils/errors');
910
const GitlabService = require('../services/GitlabService');
10-
const {GITLAB_ACCESS_LEVELS} = require('../constants');
11+
const {GITLAB_ACCESS_LEVELS, USER_ROLES, USER_TYPES} = require('../constants');
1112

1213
const ProjectChallengeMapping = models.ProjectChallengeMapping;
1314
const Project = models.Project;
14-
const Repository = models.Repository;
1515
const User = models.User;
1616
const GitlabUserMapping = models.GitlabUserMapping;
1717

@@ -25,7 +25,7 @@ const GitlabUserMapping = models.GitlabUserMapping;
2525
async function process(payload) {
2626
const {challengeId, memberId, memberHandle} = payload;
2727
const correlationId = uuid();
28-
const logPrefix = `[${correlationId}][PullRequestService#process]`;
28+
const logPrefix = `[Correlation ID: ${correlationId}][PullRequestService#process]`;
2929
logger.debug(`${logPrefix}: Challenge ID: ${challengeId}`);
3030
logger.debug(`${logPrefix}: Member ID: ${memberId}`);
3131
logger.debug(`${logPrefix}: Member Handle: ${memberHandle}`);
@@ -55,7 +55,7 @@ async function process(payload) {
5555
}
5656
logger.debug(`${logPrefix} Project: ${JSON.stringify(project)}`);
5757
// Get Repositories
58-
const repositories = await dbHelper.queryAllRepositoriesByProjectId(Repository, project.id);
58+
const repositories = await dbHelper.queryAllRepositoriesByProjectId(project.id);
5959
if (!repositories || repositories.length === 0) {
6060
logger.info(`${logPrefix} Repository not found for projectId: ${project.id}`);
6161
return;
@@ -69,9 +69,9 @@ async function process(payload) {
6969
}
7070
logger.debug(`${logPrefix} GitlabUserMapping[Copilot]: ${JSON.stringify(copilotGitlabUserMapping)}`);
7171
// Get Gitlab User
72-
const copilotGitlabUser = await dbHelper.queryOneUserByType(User, copilotGitlabUserMapping.gitlabUsername, 'gitlab');
72+
const copilotGitlabUser = await dbHelper.queryOneUserByTypeAndRole(User, copilotGitlabUserMapping.gitlabUsername, USER_TYPES.GITLAB, USER_ROLES.OWNER);
7373
if (!copilotGitlabUser) {
74-
logger.info(`${logPrefix} GitlabUser not found for copilot: ${project.copilot}`);
74+
logger.info(`${logPrefix} User[Type=${USER_TYPES.GITLAB}, Role=${USER_ROLES.OWNER}] not found for copilot: ${project.copilot}`);
7575
return;
7676
}
7777
logger.debug(`${logPrefix} GitlabUser[Copilot]: ${JSON.stringify(copilotGitlabUser)}`);
@@ -85,7 +85,7 @@ async function process(payload) {
8585
}
8686
logger.debug(`${logPrefix} GitlabUserMapping[Member]: ${JSON.stringify(memberGitlabUserMapping)}`);
8787
// Get Gitlab User
88-
const memberGitlabUser = await dbHelper.queryOneUserByType(User, memberGitlabUserMapping.gitlabUsername, 'gitlab');
88+
const memberGitlabUser = await dbHelper.queryOneUserByTypeAndRole(User, memberGitlabUserMapping.gitlabUsername, USER_TYPES.GITLAB, USER_ROLES.GUEST);
8989
if (!memberGitlabUser) {
9090
logger.info(`${logPrefix} GitlabUser not found for memberHandle: ${memberHandle}`);
9191
return;
@@ -102,10 +102,12 @@ async function process(payload) {
102102
}
103103
// Add user as a guest to the repo
104104
await copilotGitlabService.addUserToRepository(repository, memberGitlabUser, GITLAB_ACCESS_LEVELS.DEVELOPER);
105+
logger.debug(`${logPrefix} User (${memberGitlabUser.username}) added to repository (${repo.url})`);
105106
// Fork the repository
106107
await memberGitlabService.forkRepository(repository);
108+
logger.debug(`${logPrefix} Repository (${repo.url}) forked for user: ${memberGitlabUser.username}`);
107109
} catch (err) {
108-
logger.error(`${logPrefix} Error: ${err.message}`, err);
110+
throw errors.handleGitLabError(err, 'Error occurred while forking repository to user\'s namespace in GitLab');
109111
}
110112
}));
111113
}

0 commit comments

Comments
 (0)