diff --git a/src/cli.js b/src/cli.js index d6ad6b5..4cfb7e3 100755 --- a/src/cli.js +++ b/src/cli.js @@ -68,6 +68,7 @@ program process.exit(1) }) + program .command('unshare') .description('unshare a gitpear repo') diff --git a/src/home.js b/src/home.js index 24615d2..9d2d1aa 100644 --- a/src/home.js +++ b/src/home.js @@ -10,31 +10,37 @@ function createAppFolder (name) { fs.mkdirSync(`${APP_HOME}/${name}/code`, { recursive: true }) } -function shareAppFolder (name) { - fs.openSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'w') +function shareAppFolder (name, entry) { + const p = `${APP_HOME}/${name}/.git-daemon-export-ok` + fs.openSync(p, 'a') + const aclFile = fs.readFileSync(p, 'utf8') + const aclJson = JSON.parse(aclFile || '{ "protectedBranches": ["master"], "ACL": {}}') + + let [userId = '*', permissions = 'r', branch = '*'] = entry?.split(':') || [] + + if (!aclJson.ACL[userId]) aclJson[userId] = { [branch]: permissions } + fs.writeFileSync(p, JSON.stringify(aclJson)) } -function shareWith (userId, branch = '*', permissions = 'rw') { - if (!fs.existsSync(`${APP_HOME}/.git-daemon-export-ok`)) { - fs.writeFileSync(`${APP_HOME}/.git-daemon-export-ok`, '') - } - if (permissions.split('').some(p => !['r', 'w'].includes(p))) { - throw new Error('Permissions must be r, w or rw') - } - // TODO: read file - // generate new conent - // merge with old file - // store file - // - // EXAMPLE: - // { - // protectedBranches: ['master'], - // ACL: { - // '': { '': 'r|w|rw' }, - // '*': { '*': 'r' } - // } - // } - fs.appendFileSync(`${APP_HOME}/.git-daemon-export-ok`, `${userId}\t${branch}\t${permissions}\n`) +function addProtectedBranch (name, branch) { + const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'utf8') + const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') + if (!aclJson.protectedBranches.includes(branch)) aclJson.protectedBranches.push(branch) + fs.writeFileSync(aclFile, JSON.stringify(aclJson)) +} + +function removeProtectedBranch (name, branch) { + const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'a') + const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') + aclJson.protectedBranches = aclJson.protectedBranches.filter(b => b !== branch) + fs.writeFileSync(aclFile, JSON.stringify(aclJson)) +} + +function removeUserFromACL (name, userId) { + const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'a') + const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') + delete aclJson.ACL[userId] + fs.writeFileSync(aclFile, JSON.stringify(aclJson)) } function unshareAppFolder (name) { @@ -50,15 +56,11 @@ function isShared (name) { } function getACL (name) { - const entries = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`).toString().split('\n').filter(Boolean) - const res = {} - for (const entry of entries) { - const [userId, branch, permissions] = entry.split('\t') - if (!res[userId]) res[userId] = [] - res[userId].push({ branch, permissions }) - } - return res - // TODO: have protected branch setting - the ACL must be assigned explicitly + if (!fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)) throw new Error('Repo is not shared') + + const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'r') + aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') + return aclJson } function list (sharedOnly) { @@ -155,6 +157,5 @@ module.exports = { getDaemonPid, isDaemonRunning, removeDaemonPid, - shareWith, getACL, } diff --git a/src/rpc.js b/src/rpc.js index 253c644..9ba949b 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -20,19 +20,19 @@ module.exports = class RPC { // which can in turn be stored in a .git-daemon-export-ok file /* -- PULL HANDLERS -- */ - rpc.respond('get-repos', async req => await this.getReposHandler(req)) - rpc.respond('get-refs', async req => await this.getRefsHandler(req)) + rpc.respond('get-repos', async req => await this.getReposHandler(peerInfo.publicKey, req)) + rpc.respond('get-refs', async req => await this.getRefsHandler(peerInfo.publicKey, req)) /* -- PUSH HANDLERS -- */ - rpc.respond('push', async req => await this.pushHandler(req)) - rpc.respond('f-push', async req => await this.forcePushHandler(req)) - rpc.respond('d-branch', async req => await this.deleteBranchHandler(req)) + rpc.respond('push', async req => await this.pushHandler(peerInfo.publicKey, req)) + rpc.respond('f-push', async req => await this.forcePushHandler(peerInfo.publicKey, req)) + rpc.respond('d-branch', async req => await this.deleteBranchHandler(peerInfo.publicKey, req)) this.connections[peerInfo.publicKey] = rpc } - async getReposHandler (req) { - const { branch, url } = await this.parseReq(req) + async getReposHandler (publicKey, req) { + const { branch, url } = await this.parseReq(publicKey, req, 'r') const res = {} for (const repoName in this.repositories) { @@ -43,15 +43,15 @@ module.exports = class RPC { return Buffer.from(JSON.stringify(res)) } - async getRefsHandler (req) { - const { repoName, branch, url } = await this.parseReq(req) + async getRefsHandler (publicKey, req) { + const { repoName, branch, url } = await this.parseReq(publicKey, req, 'r') const res = this.repositories[repoName] return Buffer.from(JSON.stringify(res)) } - async pushHandler (req) { - const { url, repoName, branch } = await this.parseReq(req) + async pushHandler (publicKey, req) { + const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w') return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const child = spawn('git', ['fetch', url, `${branch}:${branch}`], { env }) @@ -66,8 +66,8 @@ module.exports = class RPC { }) } - async forcePushHandler (req) { - const { url, repoName, branch } = await this.parseReq(req) + async forcePushHandler (publicKey, req) { + const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w') return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const child = spawn('git', ['fetch', url, `${branch}:${branch}`, '--force'], { env }) @@ -82,8 +82,8 @@ module.exports = class RPC { }) } - async deleteBranchHandler (req) { - const { url, repoName, branch } = await this.parseReq(req) + async deleteBranchHandler (publicKey, req) { + const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w') return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const child = spawn('git', ['branch', '-D', branch], { env }) @@ -98,20 +98,41 @@ module.exports = class RPC { }) } - async parseReq(req, access, branch = '*') { - let payload + async parseReq(publicKey, req, access, branch = '*') { + if (!req) throw new Error('Request is empty') let request = JSON.parse(req.toString()) - const result = { + const parsed = { repoName: request.body.url?.split('/')?.pop(), branch: request.body.data?.split('#')[0], url: request.body.url } - if (!process.env.GIT_PEAR_AUTH) return result - if (!request.header) throw new Error('You are not allowed to access this repo') + if (!process.env.GIT_PEAR_AUTH) return parsed - payload = await acl.getId({ ...request.body, payload: request.header }) - const aclList = home.getACL(result.repoName) - const userACL = aclList[payload.userId] + if (process.env.GIT_PEAR_AUTH !== 'naitive' && !request.header) { + throw new Error('You are not allowed to access this repo') + } + + let userId + if (process.env.GIT_PEAR_AUTH === 'naitive') { + userId = publicKey + } else { + userId = (await acl.getId({ ...request.body, payload: request.header })).userId + } + const aclObj = home.getACL(parsed.repoName) + const userACL = aclObj[userId] || aclObj['*'] if (!userACL) throw new Error('You are not allowed to access this repo') - if (result.branch !== 'master' + if (aclObj.protectecBranches.includes(branch)) { + // protected branch must have exaplicit access grant + if (access === 'w') { + + } else { + // + } + } else { + + } + + return parsed + } +} diff --git a/test/rpc.test.js b/test/rpc.test.js index 9b9a7d2..37ee5e3 100644 --- a/test/rpc.test.js +++ b/test/rpc.test.js @@ -40,7 +40,8 @@ test('e2e', async t => { clientStore.replicate(socket) const rpc = new ProtomuxRPC(socket) - const reposRes = await rpc.request('get-repos') + let payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-repos' } })) + const reposRes = await rpc.request('get-repos', payload) const reposJSON = JSON.parse(reposRes.toString()) const driveKey = Buffer.from(reposJSON.foo, 'hex') @@ -53,7 +54,8 @@ test('e2e', async t => { await drive.core.update({ wait: true }) - const refsRes = await rpc.request('get-refs', Buffer.from('foo')) + payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-refs', data: 'foo' }})) + const refsRes = await rpc.request('get-refs', payload) t.ok(refsRes) const want = Object.values(JSON.parse(refsRes.toString()))[0]