fix(backend-gitlab): check for shared group permissions (#3122)
This commit is contained in:
@ -124,6 +124,28 @@ type GitLabMergeRequest = {
|
|||||||
sha: string;
|
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 {
|
export default class API {
|
||||||
apiRoot: string;
|
apiRoot: string;
|
||||||
token: string | boolean;
|
token: string | boolean;
|
||||||
@ -173,17 +195,40 @@ export default class API {
|
|||||||
user = () => this.requestJSON('/user');
|
user = () => this.requestJSON('/user');
|
||||||
|
|
||||||
WRITE_ACCESS = 30;
|
WRITE_ACCESS = 30;
|
||||||
hasWriteAccess = () =>
|
MAINTAINER_ACCESS = 40;
|
||||||
this.requestJSON(this.repoURL).then(({ permissions }) => {
|
|
||||||
const { project_access: projectAccess, group_access: groupAccess } = permissions;
|
hasWriteAccess = async () => {
|
||||||
if (projectAccess && projectAccess.access_level >= this.WRITE_ACCESS) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
if (groupAccess && groupAccess.access_level >= this.WRITE_ACCESS) {
|
// developer access
|
||||||
return true;
|
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 (
|
readFile = async (
|
||||||
path: string,
|
path: string,
|
||||||
|
@ -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'));
|
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
|
||||||
|
|
||||||
@ -7,29 +7,174 @@ describe('GitLab API', () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should get preview statuses', async () => {
|
describe('hasWriteAccess', () => {
|
||||||
const api = new API({ repo: 'repo' });
|
test('should return true on project access_level >= 30', async () => {
|
||||||
|
const api = new API({ repo: 'repo' });
|
||||||
|
|
||||||
const mr = { sha: 'sha' };
|
api.requestJSON = jest
|
||||||
const statuses = [
|
.fn()
|
||||||
{ name: 'deploy', status: 'success', target_url: 'deploy-url' },
|
.mockResolvedValueOnce({ permissions: { project_access: { access_level: 30 } } });
|
||||||
{ name: 'build', status: 'pending' },
|
|
||||||
];
|
|
||||||
|
|
||||||
api.getBranchMergeRequest = jest.fn(() => Promise.resolve(mr));
|
await expect(api.hasWriteAccess()).resolves.toBe(true);
|
||||||
api.getMergeRequestStatues = jest.fn(() => Promise.resolve(statuses));
|
});
|
||||||
|
|
||||||
const collectionName = 'posts';
|
test('should return false on project access_level < 30', async () => {
|
||||||
const slug = 'title';
|
const api = new API({ repo: 'repo' });
|
||||||
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);
|
api.requestJSON = jest
|
||||||
expect(api.getBranchMergeRequest).toHaveBeenCalledWith('cms/posts/title');
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ permissions: { project_access: { access_level: 10 } } });
|
||||||
|
|
||||||
expect(api.getMergeRequestStatues).toHaveBeenCalledTimes(1);
|
await expect(api.hasWriteAccess()).resolves.toBe(false);
|
||||||
expect(api.getMergeRequestStatues).toHaveBeenCalledWith(mr, 'cms/posts/title');
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user