Skip to content

Commit

Permalink
fix(backend-gitlab): check for shared group permissions (#3122)
Browse files Browse the repository at this point in the history
  • Loading branch information
erezrokah committed Jan 21, 2020
1 parent a48c02d commit f1739e9
Show file tree
Hide file tree
Showing 2 changed files with 218 additions and 28 deletions.
61 changes: 53 additions & 8 deletions packages/netlify-cms-backend-gitlab/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,28 @@ type GitLabMergeRequest = {
sha: string;
};

type GitLabRepo = {
shared_with_groups: { group_access_level: number }[] | null;
permissions: {
project_access: { access_level: number } | null;
group_access: { access_level: number } | null;
};
};

type GitLabBranch = {
developers_can_push: boolean;
developers_can_merge: boolean;
};

export const getMaxAccess = (groups: { group_access_level: number }[]) => {
return groups.reduce((previous, current) => {
if (current.group_access_level > previous.group_access_level) {
return current;
}
return previous;
}, groups[0]);
};

export default class API {
apiRoot: string;
token: string | boolean;
Expand Down Expand Up @@ -173,17 +195,40 @@ export default class API {
user = () => this.requestJSON('/user');

WRITE_ACCESS = 30;
hasWriteAccess = () =>
this.requestJSON(this.repoURL).then(({ permissions }) => {
const { project_access: projectAccess, group_access: groupAccess } = permissions;
if (projectAccess && projectAccess.access_level >= this.WRITE_ACCESS) {
MAINTAINER_ACCESS = 40;

hasWriteAccess = async () => {
const {
shared_with_groups: sharedWithGroups,
permissions,
}: GitLabRepo = await this.requestJSON(this.repoURL);
const { project_access: projectAccess, group_access: groupAccess } = permissions;
if (projectAccess && projectAccess.access_level >= this.WRITE_ACCESS) {
return true;
}
if (groupAccess && groupAccess.access_level >= this.WRITE_ACCESS) {
return true;
}
// check for group write permissions
if (sharedWithGroups && sharedWithGroups.length > 0) {
const maxAccess = getMaxAccess(sharedWithGroups);
// maintainer access
if (maxAccess.group_access_level >= this.MAINTAINER_ACCESS) {
return true;
}
if (groupAccess && groupAccess.access_level >= this.WRITE_ACCESS) {
return true;
// developer access
if (maxAccess.group_access_level >= this.WRITE_ACCESS) {
// check permissions to merge and push
const branch: GitLabBranch = await this.requestJSON(
`${this.repoURL}/repository/branches/${this.branch}`,
).catch(() => ({}));
if (branch.developers_can_merge && branch.developers_can_push) {
return true;
}
}
return false;
});
}
return false;
};

readFile = async (
path: string,
Expand Down
185 changes: 165 additions & 20 deletions packages/netlify-cms-backend-gitlab/src/__tests__/API.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import API from '../API';
import API, { getMaxAccess } from '../API';

global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));

Expand All @@ -7,29 +7,174 @@ describe('GitLab API', () => {
jest.resetAllMocks();
});

test('should get preview statuses', async () => {
const api = new API({ repo: 'repo' });
describe('hasWriteAccess', () => {
test('should return true on project access_level >= 30', async () => {
const api = new API({ repo: 'repo' });

const mr = { sha: 'sha' };
const statuses = [
{ name: 'deploy', status: 'success', target_url: 'deploy-url' },
{ name: 'build', status: 'pending' },
];
api.requestJSON = jest
.fn()
.mockResolvedValueOnce({ permissions: { project_access: { access_level: 30 } } });

api.getBranchMergeRequest = jest.fn(() => Promise.resolve(mr));
api.getMergeRequestStatues = jest.fn(() => Promise.resolve(statuses));
await expect(api.hasWriteAccess()).resolves.toBe(true);
});

const collectionName = 'posts';
const slug = 'title';
await expect(api.getStatuses(collectionName, slug)).resolves.toEqual([
{ context: 'deploy', state: 'success', target_url: 'deploy-url' },
{ context: 'build', state: 'other' },
]);
test('should return false on project access_level < 30', async () => {
const api = new API({ repo: 'repo' });

expect(api.getBranchMergeRequest).toHaveBeenCalledTimes(1);
expect(api.getBranchMergeRequest).toHaveBeenCalledWith('cms/posts/title');
api.requestJSON = jest
.fn()
.mockResolvedValueOnce({ permissions: { project_access: { access_level: 10 } } });

expect(api.getMergeRequestStatues).toHaveBeenCalledTimes(1);
expect(api.getMergeRequestStatues).toHaveBeenCalledWith(mr, 'cms/posts/title');
await expect(api.hasWriteAccess()).resolves.toBe(false);
});

test('should return true on group access_level >= 30', async () => {
const api = new API({ repo: 'repo' });

api.requestJSON = jest
.fn()
.mockResolvedValueOnce({ permissions: { group_access: { access_level: 30 } } });

await expect(api.hasWriteAccess()).resolves.toBe(true);
});

test('should return false on group access_level < 30', async () => {
const api = new API({ repo: 'repo' });

api.requestJSON = jest
.fn()
.mockResolvedValueOnce({ permissions: { group_access: { access_level: 10 } } });

await expect(api.hasWriteAccess()).resolves.toBe(false);
});

test('should return true on shared group access_level >= 40', async () => {
const api = new API({ repo: 'repo' });
api.requestJSON = jest.fn().mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 40 }],
});

await expect(api.hasWriteAccess()).resolves.toBe(true);

expect(api.requestJSON).toHaveBeenCalledTimes(1);
});

test('should return true on shared group access_level >= 30, developers can merge and push', async () => {
const api = new API({ repo: 'repo' });

api.requestJSON = jest.fn();
api.requestJSON.mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
});
api.requestJSON.mockResolvedValueOnce({
developers_can_merge: true,
developers_can_push: true,
});

await expect(api.hasWriteAccess()).resolves.toBe(true);
});

test('should return false on shared group access_level < 30,', async () => {
const api = new API({ repo: 'repo' });

api.requestJSON = jest.fn();
api.requestJSON.mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 20 }],
});
api.requestJSON.mockResolvedValueOnce({
developers_can_merge: true,
developers_can_push: true,
});

await expect(api.hasWriteAccess()).resolves.toBe(false);
});

test("should return false on shared group access_level >= 30, developers can't merge", async () => {
const api = new API({ repo: 'repo' });

api.requestJSON = jest.fn();
api.requestJSON.mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
});
api.requestJSON.mockResolvedValueOnce({
developers_can_merge: false,
developers_can_push: true,
});

await expect(api.hasWriteAccess()).resolves.toBe(false);
});

test("should return false on shared group access_level >= 30, developers can't push", async () => {
const api = new API({ repo: 'repo' });

api.requestJSON = jest.fn();
api.requestJSON.mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
});
api.requestJSON.mockResolvedValueOnce({
developers_can_merge: true,
developers_can_push: false,
});

await expect(api.hasWriteAccess()).resolves.toBe(false);
});

test('should return false on shared group access_level >= 30, error getting branch', async () => {
const api = new API({ repo: 'repo' });

api.requestJSON = jest.fn();
api.requestJSON.mockResolvedValueOnce({
permissions: { project_access: null, group_access: null },
shared_with_groups: [{ group_access_level: 10 }, { group_access_level: 30 }],
});
api.requestJSON.mockRejectedValue(new Error('Not Found'));

await expect(api.hasWriteAccess()).resolves.toBe(false);
});
});

describe('getStatuses', () => {
test('should get preview statuses', async () => {
const api = new API({ repo: 'repo' });

const mr = { sha: 'sha' };
const statuses = [
{ name: 'deploy', status: 'success', target_url: 'deploy-url' },
{ name: 'build', status: 'pending' },
];

api.getBranchMergeRequest = jest.fn(() => Promise.resolve(mr));
api.getMergeRequestStatues = jest.fn(() => Promise.resolve(statuses));

const collectionName = 'posts';
const slug = 'title';
await expect(api.getStatuses(collectionName, slug)).resolves.toEqual([
{ context: 'deploy', state: 'success', target_url: 'deploy-url' },
{ context: 'build', state: 'other' },
]);

expect(api.getBranchMergeRequest).toHaveBeenCalledTimes(1);
expect(api.getBranchMergeRequest).toHaveBeenCalledWith('cms/posts/title');

expect(api.getMergeRequestStatues).toHaveBeenCalledTimes(1);
expect(api.getMergeRequestStatues).toHaveBeenCalledWith(mr, 'cms/posts/title');
});
});

describe('getMaxAccess', () => {
it('should return group with max access level', () => {
const groups = [
{ group_access_level: 10 },
{ group_access_level: 5 },
{ group_access_level: 100 },
{ group_access_level: 1 },
];
expect(getMaxAccess(groups)).toBe(groups[2]);
});
});
});

0 comments on commit f1739e9

Please sign in to comment.