acl: more drafts

Signed-off-by: dzdidi <deniszalessky@gmail.com>
This commit is contained in:
dzdidi
2024-01-28 11:22:43 +00:00
parent 0911049fa5
commit fff32c6f3e
4 changed files with 84 additions and 59 deletions

View File

@@ -68,6 +68,7 @@ program
process.exit(1) process.exit(1)
}) })
program program
.command('unshare') .command('unshare')
.description('unshare a gitpear repo') .description('unshare a gitpear repo')

View File

@@ -10,31 +10,37 @@ function createAppFolder (name) {
fs.mkdirSync(`${APP_HOME}/${name}/code`, { recursive: true }) fs.mkdirSync(`${APP_HOME}/${name}/code`, { recursive: true })
} }
function shareAppFolder (name) { function shareAppFolder (name, entry) {
fs.openSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'w') 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') { function addProtectedBranch (name, branch) {
if (!fs.existsSync(`${APP_HOME}/.git-daemon-export-ok`)) { const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'utf8')
fs.writeFileSync(`${APP_HOME}/.git-daemon-export-ok`, '') const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}')
} if (!aclJson.protectedBranches.includes(branch)) aclJson.protectedBranches.push(branch)
if (permissions.split('').some(p => !['r', 'w'].includes(p))) { fs.writeFileSync(aclFile, JSON.stringify(aclJson))
throw new Error('Permissions must be r, w or rw') }
}
// TODO: read file function removeProtectedBranch (name, branch) {
// generate new conent const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'a')
// merge with old file const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}')
// store file aclJson.protectedBranches = aclJson.protectedBranches.filter(b => b !== branch)
// fs.writeFileSync(aclFile, JSON.stringify(aclJson))
// EXAMPLE: }
// {
// protectedBranches: ['master'], function removeUserFromACL (name, userId) {
// ACL: { const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'a')
// '<userId>': { '<branch name | *>': 'r|w|rw' }, const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}')
// '*': { '*': 'r' } delete aclJson.ACL[userId]
// } fs.writeFileSync(aclFile, JSON.stringify(aclJson))
// }
fs.appendFileSync(`${APP_HOME}/.git-daemon-export-ok`, `${userId}\t${branch}\t${permissions}\n`)
} }
function unshareAppFolder (name) { function unshareAppFolder (name) {
@@ -50,15 +56,11 @@ function isShared (name) {
} }
function getACL (name) { function getACL (name) {
const entries = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`).toString().split('\n').filter(Boolean) if (!fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)) throw new Error('Repo is not shared')
const res = {}
for (const entry of entries) { const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'r')
const [userId, branch, permissions] = entry.split('\t') aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}')
if (!res[userId]) res[userId] = [] return aclJson
res[userId].push({ branch, permissions })
}
return res
// TODO: have protected branch setting - the ACL must be assigned explicitly
} }
function list (sharedOnly) { function list (sharedOnly) {
@@ -155,6 +157,5 @@ module.exports = {
getDaemonPid, getDaemonPid,
isDaemonRunning, isDaemonRunning,
removeDaemonPid, removeDaemonPid,
shareWith,
getACL, getACL,
} }

View File

@@ -20,19 +20,19 @@ module.exports = class RPC {
// which can in turn be stored in a .git-daemon-export-ok file // which can in turn be stored in a .git-daemon-export-ok file
/* -- PULL HANDLERS -- */ /* -- PULL HANDLERS -- */
rpc.respond('get-repos', async req => await this.getReposHandler(req)) rpc.respond('get-repos', async req => await this.getReposHandler(peerInfo.publicKey, req))
rpc.respond('get-refs', async req => await this.getRefsHandler(req)) rpc.respond('get-refs', async req => await this.getRefsHandler(peerInfo.publicKey, req))
/* -- PUSH HANDLERS -- */ /* -- PUSH HANDLERS -- */
rpc.respond('push', async req => await this.pushHandler(req)) rpc.respond('push', async req => await this.pushHandler(peerInfo.publicKey, req))
rpc.respond('f-push', async req => await this.forcePushHandler(req)) rpc.respond('f-push', async req => await this.forcePushHandler(peerInfo.publicKey, req))
rpc.respond('d-branch', async req => await this.deleteBranchHandler(req)) rpc.respond('d-branch', async req => await this.deleteBranchHandler(peerInfo.publicKey, req))
this.connections[peerInfo.publicKey] = rpc this.connections[peerInfo.publicKey] = rpc
} }
async getReposHandler (req) { async getReposHandler (publicKey, req) {
const { branch, url } = await this.parseReq(req) const { branch, url } = await this.parseReq(publicKey, req, 'r')
const res = {} const res = {}
for (const repoName in this.repositories) { for (const repoName in this.repositories) {
@@ -43,15 +43,15 @@ module.exports = class RPC {
return Buffer.from(JSON.stringify(res)) return Buffer.from(JSON.stringify(res))
} }
async getRefsHandler (req) { async getRefsHandler (publicKey, req) {
const { repoName, branch, url } = await this.parseReq(req) const { repoName, branch, url } = await this.parseReq(publicKey, req, 'r')
const res = this.repositories[repoName] const res = this.repositories[repoName]
return Buffer.from(JSON.stringify(res)) return Buffer.from(JSON.stringify(res))
} }
async pushHandler (req) { async pushHandler (publicKey, req) {
const { url, repoName, branch } = await this.parseReq(req) const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w')
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['fetch', url, `${branch}:${branch}`], { env }) const child = spawn('git', ['fetch', url, `${branch}:${branch}`], { env })
@@ -66,8 +66,8 @@ module.exports = class RPC {
}) })
} }
async forcePushHandler (req) { async forcePushHandler (publicKey, req) {
const { url, repoName, branch } = await this.parseReq(req) const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w')
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['fetch', url, `${branch}:${branch}`, '--force'], { env }) const child = spawn('git', ['fetch', url, `${branch}:${branch}`, '--force'], { env })
@@ -82,8 +82,8 @@ module.exports = class RPC {
}) })
} }
async deleteBranchHandler (req) { async deleteBranchHandler (publicKey, req) {
const { url, repoName, branch } = await this.parseReq(req) const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w')
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['branch', '-D', branch], { env }) const child = spawn('git', ['branch', '-D', branch], { env })
@@ -98,20 +98,41 @@ module.exports = class RPC {
}) })
} }
async parseReq(req, access, branch = '*') { async parseReq(publicKey, req, access, branch = '*') {
let payload if (!req) throw new Error('Request is empty')
let request = JSON.parse(req.toString()) let request = JSON.parse(req.toString())
const result = { const parsed = {
repoName: request.body.url?.split('/')?.pop(), repoName: request.body.url?.split('/')?.pop(),
branch: request.body.data?.split('#')[0], branch: request.body.data?.split('#')[0],
url: request.body.url url: request.body.url
} }
if (!process.env.GIT_PEAR_AUTH) return result if (!process.env.GIT_PEAR_AUTH) return parsed
if (!request.header) throw new Error('You are not allowed to access this repo')
payload = await acl.getId({ ...request.body, payload: request.header }) if (process.env.GIT_PEAR_AUTH !== 'naitive' && !request.header) {
const aclList = home.getACL(result.repoName) throw new Error('You are not allowed to access this repo')
const userACL = aclList[payload.userId] }
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 (!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
}
}

View File

@@ -40,7 +40,8 @@ test('e2e', async t => {
clientStore.replicate(socket) clientStore.replicate(socket)
const rpc = new ProtomuxRPC(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 reposJSON = JSON.parse(reposRes.toString())
const driveKey = Buffer.from(reposJSON.foo, 'hex') const driveKey = Buffer.from(reposJSON.foo, 'hex')
@@ -53,7 +54,8 @@ test('e2e', async t => {
await drive.core.update({ wait: true }) 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) t.ok(refsRes)
const want = Object.values(JSON.parse(refsRes.toString()))[0] const want = Object.values(JSON.parse(refsRes.toString()))[0]