Skip to content

fix: checkUserPushPermission push failure on name #1110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions src/db/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const {
export const {
getRepos,
getRepo,
getRepoByUrl,
createRepo,
addUserCanPush,
addUserCanAuthorise,
Expand Down
17 changes: 16 additions & 1 deletion src/db/file/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ if (!fs.existsSync('./.data')) fs.mkdirSync('./.data');
/* istanbul ignore if */
if (!fs.existsSync('./.data/db')) fs.mkdirSync('./.data/db');

const db = new Datastore({ filename: './.data/db/repos.db', autoload: true });
// export for testing purposes
export const db = new Datastore({ filename: './.data/db/repos.db', autoload: true });
db.ensureIndex({ fieldName: 'name', unique: false });
db.setAutocompactionInterval(COMPACTION_INTERVAL);

Expand Down Expand Up @@ -49,6 +50,20 @@ export const getRepo = async (name: string) => {
});
};

export const getRepoByUrl = async (url: string) => {
return new Promise<Repo | null>((resolve, reject) => {
db.findOne({ url: url.toLowerCase() }, (err: Error | null, doc: Repo) => {
// ignore for code coverage as neDB rarely returns errors even for an invalid query
/* istanbul ignore if */
if (err) {
reject(err);
} else {
resolve(doc);
}
});
});
};

export const createRepo = async (repo: Repo) => {
if (isBlank(repo.project)) {
throw new Error('Project name cannot be empty');
Expand Down
1 change: 1 addition & 0 deletions src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const {
updateUser,
getRepos,
getRepo,
getRepoByUrl,
createRepo,
addUserCanPush,
addUserCanAuthorise,
Expand Down
1 change: 1 addition & 0 deletions src/db/mongo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const {
export const {
getRepos,
getRepo,
getRepoByUrl,
createRepo,
addUserCanPush,
addUserCanAuthorise,
Expand Down
5 changes: 5 additions & 0 deletions src/db/mongo/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export const getRepo = async (name: string) => {
return collection.findOne({ name: { $eq: name } });
};

export const getRepoByUrl = async (url: string) => {
const collection = await connect(collectionName);
return collection.findOne({ url: { $eq: url.toLowerCase() } });
};

export const createRepo = async (repo: Repo) => {
repo.name = repo.name.toLowerCase();
console.log(`creating new repo ${JSON.stringify(repo)}`);
Expand Down
32 changes: 7 additions & 25 deletions src/proxy/processors/push-action/checkRepoInAuthorisedList.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,22 @@
import { Action, Step } from '../../actions';
import { getRepos } from '../../../db';
import { Repo } from '../../../db/types';
import { trimTrailingDotGit } from '../../../db/helper';
import { getRepoByUrl } from '../../../db';

// Execute if the repo is approved
const exec = async (
req: any,
action: Action,
authorisedList: () => Promise<Repo[]> = getRepos,
): Promise<Action> => {
const step = new Step('checkRepoInAuthorisedList');

const list = await authorisedList();
console.log(list);

const found = list.find((x: Repo) => {
const targetName = trimTrailingDotGit(action.repo.toLowerCase());
const allowedName = trimTrailingDotGit(`${x.project}/${x.name}`.toLowerCase());
console.log(`${targetName} = ${allowedName}`);
return targetName === allowedName;
});

console.log(found);

if (!found) {
console.log('not found');
const found = await getRepoByUrl(action.url);
if (found) {
step.log(`repo ${action.repo} is in the authorisedList`);
} else {
step.error = true;
step.log(`repo ${action.repo} is not in the authorisedList, ending`);
console.log('setting error');
step.setError(`Rejecting repo ${action.repo} not in the authorisedList`);
action.addStep(step);
return action;
step.log(`repo ${action.repo} is not in the authorised whitelist, ending`);
step.setError(`Rejecting repo ${action.repo} not in the authorised whitelist`);
}

console.log('found');
step.log(`repo ${action.repo} is in the authorisedList`);
action.addStep(step);
return action;
};
Expand Down
61 changes: 20 additions & 41 deletions src/proxy/processors/push-action/checkUserPushPermission.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,31 @@
import { Action, Step } from '../../actions';
import { getUsers, isUserPushAllowed } from '../../../db';
import { trimTrailingDotGit } from '../../../db/helper';
import { getRepoByUrl, getUsers } from '../../../db';

const getPushUser = async (action: Action): Promise<string | null> => {
const list = await getUsers({ gitAccount: action.user });
return list.length === 1 ? list[0].username : null;
};

// Execute if the repo is approved
const exec = async (req: any, action: Action): Promise<Action> => {
const step = new Step('checkUserPushPermission');

const repoSplit = trimTrailingDotGit(action.repo.toLowerCase()).split('/');
// we expect there to be exactly one / separating org/repoName
if (repoSplit.length != 2) {
step.setError('Server-side issue extracting repoName');
action.addStep(step);
return action;
}
// pull the 2nd value of the split for repoName
const repoName = repoSplit[1];
let isUserAllowed = false;
let user = action.user;

// Find the user associated with this Git Account
const list = await getUsers({ gitAccount: action.user });

console.log(`Users for this git account: ${JSON.stringify(list)}`);

if (list.length == 1) {
user = list[0].username;
isUserAllowed = await isUserPushAllowed(repoName, user);
}

console.log(`User ${user} permission on Repo ${repoName} : ${isUserAllowed}`);

if (!isUserAllowed) {
console.log('User not allowed to Push');
step.error = true;
step.log(`User ${user} is not allowed to push on repo ${action.repo}, ending`);

console.log('setting error');

step.setError(
`Rejecting push as user ${action.user} ` +
`is not allowed to push on repo ` +
`${action.repo}`,
);
action.addStep(step);
return action;
const repo = await getRepoByUrl(action.url);
if (repo) {
const user = await getPushUser(action);
const isUserAllowed =
user && (repo.users.canPush.includes(user) || repo.users.canAuthorise.includes(user));
if (isUserAllowed) {
step.log(`User ${user} is allowed to push on repo ${action.repo}`);
} else {
step.error = true;
step.log(`User ${user} is not allowed to push on repo ${action.repo}, ending`);
step.setError(
`Rejecting push as user ${action.user} is not allowed to push on repo ${action.repo}`,
);
}
}

step.log(`User ${user} is allowed to push on repo ${action.repo}`);
action.addStep(step);
return action;
};
Expand Down
94 changes: 94 additions & 0 deletions test/db/file/repo.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const { expect } = require('chai');
const sinon = require('sinon');
const repoModule = require('../../../src/db/file/repo');

describe('File DB', () => {
let sandbox;

beforeEach(() => {
sandbox = sinon.createSandbox();
});

afterEach(() => {
sandbox.restore();
});

describe('getRepo', () => {
it('should get the repo using the name', async () => {
const repoData = {
name: 'sample',
users: { canPush: [] },
url: 'http://example.com/sample-repo.git',
};

sandbox.stub(repoModule.db, 'findOne').callsFake((query, cb) => cb(null, repoData));

const result = await repoModule.getRepo('Sample');
expect(result).to.equal(repoData);
});
});

describe('getRepoByUrl', () => {
it('should get the repo using the url', async () => {
const repoData = {
name: 'sample',
users: { canPush: [] },
url: 'https://github.com/finos/git-proxy.git',
};

sandbox.stub(repoModule.db, 'findOne').callsFake((query, cb) => cb(null, repoData));

const result = await repoModule.getRepoByUrl('https://github.com/finos/git-proxy.git');
expect(result).to.equal(repoData);
});

it('should get the repo using the url, stripping off the .git', async () => {
const repoData = {
name: 'sample',
users: { canPush: [] },
url: 'https://github.com/finos/git-proxy.git',
};

sandbox.stub(repoModule.db, 'findOne').callsFake((query, cb) => cb(null, repoData));

const result = await repoModule.getRepoByUrl('https://github.com/finos/git-proxy.git');

expect(repoModule.db.findOne.calledWith(sinon.match({ url: 'https://github.com/finos/git-proxy.git'}))).to.be.true;
expect(result).to.equal(repoData);
});

it('should get the repo using the url, ignoring the case', async () => {
const repoData = {
name: 'sample',
users: { canPush: [] },
url: 'https://github.com/finos/git-proxy.git',
};

sandbox.stub(repoModule.db, 'findOne').callsFake((query, cb) => cb(null, repoData));

const result = await repoModule.getRepoByUrl('https://github.com/Finos/Git-Proxy.git');
expect(result).to.equal(repoData);
expect(repoModule.db.findOne.calledWith(sinon.match({ url: 'https://github.com/finos/git-proxy.git' }))).to.be.true;
});

it('should return null if the repo is not found', async () => {
sandbox.stub(repoModule.db, 'findOne').callsFake((query, cb) => cb(null, null));

const result = await repoModule.getRepoByUrl('https://github.com/finos/missing-repo.git');
expect(result).to.be.null;
expect(repoModule.db.findOne.calledWith(sinon.match({ url: 'https://github.com/finos/missing-repo.git' })),
).to.be.true;
});

it('should reject if the database returns an error', async () => {
sandbox.stub(repoModule.db, 'findOne').callsFake((query, cb) => cb(new Error('DB error')));

try {
await repoModule.getRepoByUrl('https://github.com/finos/git-proxy.git');
expect.fail('Expected promise to be rejected');
} catch (err) {
expect(err.message).to.equal('DB error');
}
});
});
});
69 changes: 69 additions & 0 deletions test/db/mongo/repo.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const { expect } = require('chai');
const sinon = require('sinon');
const proxyqquire = require('proxyquire');

const repoCollection = {
findOne: sinon.stub(),
};

const connectionStub = sinon.stub().returns(repoCollection);

const { getRepo, getRepoByUrl } = proxyqquire('../../../src/db/mongo/repo', {
'./helper': { connect: connectionStub },
});

describe('MongoDB', () => {
afterEach(function () {
sinon.restore();
});

describe('getRepo', () => {
it('should get the repo using the name', async () => {
const repoData = {
name: 'sample',
users: { canPush: [] },
url: 'http://example.com/sample-repo.git',
};
repoCollection.findOne.resolves(repoData);

const result = await getRepo('Sample');
expect(result).to.equal(repoData);
expect(connectionStub.calledWith('repos')).to.be.true;
expect(repoCollection.findOne.calledWith({ name: { $eq: 'sample' } })).to.be.true;
});
});

describe('getRepoByUrl', () => {
it('should get the repo using the url', async () => {
const repoData = {
name: 'sample',
users: { canPush: [] },
url: 'https://github.com/finos/git-proxy.git',
};
repoCollection.findOne.resolves(repoData);

const result = await getRepoByUrl('https://github.com/finos/git-proxy.git');
expect(result).to.equal(repoData);
expect(connectionStub.calledWith('repos')).to.be.true;
expect(
repoCollection.findOne.calledWith({ url: { $eq: 'https://github.com/finos/git-proxy.git' } }),
).to.be.true;
});

it('should get the repo using the url, ignoring the case', async () => {
const repoData = {
name: 'sample',
users: { canPush: [] },
url: 'https://github.com/finos/git-proxy.git',
};
repoCollection.findOne.resolves(repoData);

const result = await getRepoByUrl('https://github.com/Finos/Git-Proxy.git');
expect(result).to.equal(repoData);
expect(connectionStub.calledWith('repos')).to.be.true;
expect(
repoCollection.findOne.calledWith({ url: { $eq: 'https://github.com/finos/git-proxy.git' } }),
).to.be.true;
});
});
});
Loading
Loading