Skip to content

Commit

Permalink
Authenticate and verify as collaborator with GitLab on init
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed Dec 12, 2019
1 parent 36cbdec commit 923a56c
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 13 deletions.
38 changes: 37 additions & 1 deletion lib/plugin/gitlab/GitLab.js
Expand Up @@ -40,6 +40,41 @@ class GitLab extends Release {
this.origin = this.options.origin || `https://${repo.host}`;
this.baseUrl = `${this.origin}/api/v4`;
this.id = encodeURIComponent(repo.repository);
if (!(await this.isAuthenticated())) {
throw new Error(`Could not authenticate with GitLab using environment variable "${this.options.tokenRef}".`);
}
if (!(await this.isCollaborator())) {
const { repository } = this.getContext('repo');
const { username } = this.getContext('gitlab');
throw new Error(`User ${username} is not a collaborator for ${repository}.`);
}
}

async isAuthenticated() {
if (this.global.isDryRun) return true;
const endpoint = `/user`;
try {
const { username } = await this.request(endpoint, { method: 'GET' });
this.setContext({ gitlab: { username } });
return true;
} catch (err) {
this.debug(err);
return false;
}
}

async isCollaborator() {
if (this.global.isDryRun) return true;
const endpoint = `/projects/${this.id}/members`;
const { username } = this.getContext('gitlab');
try {
const body = await this.request(endpoint, { method: 'GET' });
const user = _.find(body, { username });
return user && user.access_level >= 30;
} catch (err) {
this.debug(err);
return false;
}
}

async release() {
Expand All @@ -57,7 +92,8 @@ class GitLab extends Release {

async request(endpoint, options) {
this.debug(Object.assign({ url: this.baseUrl + endpoint }, options));
const response = await this.client.post(endpoint, options);
const method = (options.method || 'POST').toLowerCase();
const response = await this.client[method](endpoint, options);
const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body || {};
this.debug(body);
return body;
Expand Down
82 changes: 71 additions & 11 deletions test/gitlab.js
Expand Up @@ -2,23 +2,27 @@ const test = require('ava');
const sinon = require('sinon');
const nock = require('nock');
const GitLab = require('../lib/plugin/gitlab/GitLab');
const { interceptPublish, interceptAsset } = require('./stub/gitlab');
const { interceptUser, interceptMembers, interceptPublish, interceptAsset } = require('./stub/gitlab');
const { factory, runTasks } = require('./util');

const tokenRef = 'GITLAB_TOKEN';

test('should validate token', async t => {
test.serial('should validate token', async t => {
const tokenRef = 'MY_GITLAB_TOKEN';
const options = { gitlab: { release: true, tokenRef } };
const remoteUrl = 'https://gitlab.com/user/repo';
const options = { gitlab: { release: true, tokenRef, remoteUrl } };
const gitlab = factory(GitLab, { options });
delete process.env[tokenRef];

await t.throwsAsync(gitlab.init(), /Environment variable "MY_GITLAB_TOKEN" is required for GitLab releases/);
process.env[tokenRef] = '123'; // eslint-disable-line require-atomic-updates

interceptUser();
interceptMembers();
await t.notThrowsAsync(gitlab.init());
});

test('should upload assets and release', async t => {
test.serial('should upload assets and release', async t => {
const remoteUrl = 'https://gitlab.com/user/repo';
const asset = 'file1';
const options = {
Expand All @@ -34,6 +38,8 @@ test('should upload assets and release', async t => {
const gitlab = factory(GitLab, { options });
sinon.stub(gitlab, 'getLatestVersion').resolves('2.0.0');

interceptUser();
interceptMembers();
interceptAsset();
interceptPublish({
body: {
Expand All @@ -58,35 +64,86 @@ test('should upload assets and release', async t => {
t.is(gitlab.isReleased, true);
});

test('should release to self-managed host', async t => {
const scope = nock('https://gitlab.example.org');
test.serial('should release to self-managed host', async t => {
const host = 'https://gitlab.example.org';
const scope = nock(host);
scope.post('/api/v4/projects/user%2Frepo/releases').reply(200, {});
const options = {
git: { remoteUrl: `https://gitlab.example.org/user/repo`, tagName: '${version}' },
git: { remoteUrl: `${host}/user/repo`, tagName: '${version}' },
gitlab: { releaseName: 'Release ${version}', releaseNotes: 'echo readme', tokenRef }
};
const gitlab = factory(GitLab, { options });
sinon.stub(gitlab, 'getLatestVersion').resolves('1.0.0');

interceptUser({ host });
interceptMembers({ host });

await runTasks(gitlab);

t.is(gitlab.origin, 'https://gitlab.example.org');
t.is(gitlab.baseUrl, 'https://gitlab.example.org/api/v4');
t.is(gitlab.origin, host);
t.is(gitlab.baseUrl, `${host}/api/v4`);
});

test('should release to sub-grouped repo', async t => {
test.serial('should release to sub-grouped repo', async t => {
const scope = nock('https://gitlab.com');
scope.post('/api/v4/projects/group%2Fsub-group%2Frepo/releases').reply(200, {});
const options = { gitlab: { tokenRef }, git: { remoteUrl: 'git@gitlab.com:group/sub-group/repo.git' } };
const gitlab = factory(GitLab, { options });

interceptUser({ owner: 'sub-group' });
interceptMembers({ owner: 'sub-group', group: 'group' });

await runTasks(gitlab);

t.is(gitlab.getReleaseUrl(), `https://gitlab.com/group/sub-group/repo/releases`);
t.is(gitlab.isReleased, true);
});

test('should handle (http) error and use fallback tag release', async t => {
test.serial('should throw for unauthenticated user', async t => {
const host = 'https://gitlab.com';
const remoteUrl = `${host}/user/repo`;
const options = { gitlab: { tokenRef, remoteUrl, host } };
const gitlab = factory(GitLab, { options });
const scope = nock(host);
scope.get(`/api/v4/user`).reply(401);

await t.throwsAsync(runTasks(gitlab), {
instanceOf: Error,
message: 'Could not authenticate with GitLab using environment variable "GITLAB_TOKEN".'
});
});

test.serial('should throw for non-collaborator', async t => {
const host = 'https://gitlab.com';
const remoteUrl = `${host}/john/repo`;
const options = { gitlab: { tokenRef, remoteUrl, host } };
const gitlab = factory(GitLab, { options });
const scope = nock(host);
scope.get(`/api/v4/projects/john%2Frepo/members`).reply(200, [{ username: 'emma' }]);
interceptUser({ owner: 'john' });

await t.throwsAsync(runTasks(gitlab), {
instanceOf: Error,
message: 'User john is not a collaborator for john/repo.'
});
});

test.serial('should throw for insufficient access level', async t => {
const host = 'https://gitlab.com';
const remoteUrl = `${host}/john/repo`;
const options = { gitlab: { tokenRef, remoteUrl, host } };
const gitlab = factory(GitLab, { options });
const scope = nock(host);
scope.get(`/api/v4/projects/john%2Frepo/members`).reply(200, [{ username: 'john', access_level: 10 }]);
interceptUser({ owner: 'john' });

await t.throwsAsync(runTasks(gitlab), {
instanceOf: Error,
message: 'User john is not a collaborator for john/repo.'
});
});

test.serial('should handle (http) error and use fallback tag release', async t => {
const [host, owner, repo] = ['https://gitlab.example.org', 'legacy', 'repo'];
const remoteUrl = `${host}/${owner}/${repo}`;
const scope = nock(host);
Expand All @@ -96,6 +153,9 @@ test('should handle (http) error and use fallback tag release', async t => {
const gitlab = factory(GitLab, { options });
sinon.stub(gitlab, 'getLatestVersion').resolves('1.0.0');

interceptUser({ host, owner });
interceptMembers({ host, owner, project: repo });

await runTasks(gitlab);

t.is(gitlab.getReleaseUrl(), `${remoteUrl}/tags/1.0.1`);
Expand Down
10 changes: 10 additions & 0 deletions test/stub/gitlab.js
@@ -1,5 +1,15 @@
const nock = require('nock');

module.exports.interceptUser = ({ host = 'https://gitlab.com', owner = 'user' } = {}) =>
nock(host)
.get('/api/v4/user')
.reply(200, { username: owner });

module.exports.interceptMembers = ({ host = 'https://gitlab.com', owner = 'user', project = 'repo', group } = {}) =>
nock(host)
.get(`/api/v4/projects/${group ? `${group}%2F` : ''}${owner}%2F${project}/members`)
.reply(200, [{ username: owner, access_level: 30 }]);

module.exports.interceptPublish = ({ host = 'https://gitlab.com', owner = 'user', project = 'repo', body } = {}) =>
nock(host)
.post(`/api/v4/projects/${owner}%2F${project}/releases`, body)
Expand Down
9 changes: 8 additions & 1 deletion test/tasks.js
Expand Up @@ -11,7 +11,12 @@ const runTasks = require('../lib/tasks');
const Plugin = require('../lib/plugin/Plugin');
const { mkTmpDir, gitAdd } = require('./util/helpers');
const ShellStub = require('./stub/shell');
const { interceptPublish: interceptGitLabPublish, interceptAsset: interceptGitLabAsset } = require('./stub/gitlab');
const {
interceptUser: interceptGitLabUser,
interceptMembers: interceptGitLabMembers,
interceptPublish: interceptGitLabPublish,
interceptAsset: interceptGitLabAsset
} = require('./stub/gitlab');
const {
interceptAuthentication: interceptGitHubAuthentication,
interceptCollaborator: interceptGitHubCollaborator,
Expand Down Expand Up @@ -200,6 +205,8 @@ test.serial('should release all the things (pre-release, github, gitlab)', async
interceptGitHubAsset({ owner, project, body: 'lineline' });
interceptGitHubPublish({ owner, project, body: { draft: false, tag_name: 'v1.1.0-alpha.0' } });

interceptGitLabUser({ owner });
interceptGitLabMembers({ owner, project });
interceptGitLabAsset({ owner, project });
interceptGitLabPublish({
owner,
Expand Down

0 comments on commit 923a56c

Please sign in to comment.