ACL draft integration

Signed-off-by: dzdidi <deniszalessky@gmail.com>
This commit is contained in:
dzdidi
2024-01-28 15:33:57 +00:00
parent f466308755
commit 5c743fabc2
4 changed files with 220 additions and 28 deletions

View File

@@ -1,6 +1,7 @@
const home = require('./home')
const fs = require('fs')
const roles = {
const ROLES = {
admin: {
description: 'Read and write to all branches',
},
@@ -12,9 +13,100 @@ const roles = {
},
}
const DEFAULT_ACL = {
visibibility: 'public', // public|private
visibility: 'public', // public|private
protectedBranches: ['master'],
ACL: {}
}
function getUserRole (repoName, user) {
const acl = getACL(repoName)
return acl.ACL[user]
}
function getAdmins (repoName) {
const acl = getACL(repoName)
return Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'admin')
}
function getContributors (repoName) {
const acl = getACL(repoName)
const contributors = Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'contributor')
const admins = getAdmins(repoName)
return [...contributors, ...admins].filter((user, i, arr) => arr.indexOf(user) === i)
}
function getViewers (repoName) {
const acl = getACL(repoName)
const viewers = Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'viewer')
const contributors = getContributors(repoName)
const admins = getAdmins(repoName)
return [...viewers, ...contributors, ...admins].filter((user, i, arr) => arr.indexOf(user) === i)
}
function grantAccessToUser (repoName, user, role) {
if (!ROLES[role]) throw new Error(`Role ${role} does not exist`)
if (!Object.keys(ROLES).includes(role)) throw new Error(`Role ${role} is not allowed`)
const acl = getACL(repoName)
acl.ACL[user] = role
setACL(repoName, acl)
}
function addProtectedBranch (repoName, branch) {
const acl = getACL(repoName)
acl.protectedBranches.push(branch)
setACL(repoName, acl)
}
function removeProtectedBranch (repoName, branch) {
const acl = getACL(repoName)
acl.protectedBranches = acl.protectedBranches.filter(b => b !== branch)
setACL(repoName, acl)
}
function makeRepoPublic (repoName) {
const acl = getACL(repoName)
acl.visibility = 'public'
setACL(repoName, acl)
}
function getRepoVisibility (repoName) {
const acl = getACL(repoName)
return acl.visibility
}
function makeRepoPrivate (repoName) {
const acl = getACL(repoName)
acl.visibility = 'private'
setACL(repoName, acl)
}
function setACL (repoName, acl = DEFAULT_ACL) {
acl = { ...DEFAULT_ACL, ...acl }
if (['public', 'private'].indexOf(acl.visibility) === -1) throw new Error('Visibility must be public or private')
const content = JSON.stringify(acl, null, 2)
fs.writeFileSync(home.getACLFilePath(repoName), content)
return acl
}
function getACL (repoName) {
return JSON.parse(fs.readFileSync(home.getACLFilePath(repoName), 'utf8') || JSON.stringify(DEFAULT_ACL))
}
module.exports = {
getUserRole,
grantAccessToUser,
makeRepoPublic,
makeRepoPrivate,
getRepoVisibility,
setACL,
getACL,
addProtectedBranch,
removeProtectedBranch,
getAdmins,
getContributors,
getViewers,
}

View File

@@ -14,6 +14,10 @@ function shareAppFolder (name) {
fs.openSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'w')
}
function getACLFilePath (name) {
if (!fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)) throw new Error('Repo is not shared')
return `${APP_HOME}/${name}/.git-daemon-export-ok`
}
function unshareAppFolder (name) {
fs.unlinkSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)
@@ -27,10 +31,6 @@ function isShared (name) {
return fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)
}
function getACL (name) {
if (!fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)) throw new Error('Repo is not shared')
}
function list (sharedOnly) {
const repos = fs.readdirSync(APP_HOME)
if (!sharedOnly) return repos.filter(r => !r.startsWith('.') && isInitialized(r))
@@ -125,5 +125,5 @@ module.exports = {
getDaemonPid,
isDaemonRunning,
removeDaemonPid,
getACL,
getACLFilePath
}

View File

@@ -2,6 +2,7 @@ const ProtomuxRPC = require('protomux-rpc')
const { spawn } = require('child_process')
const home = require('./home')
const auth = require('./auth')
const acl = require('./acl')
module.exports = class RPC {
constructor (announcedRefs, repositories, drives) {
@@ -32,26 +33,47 @@ module.exports = class RPC {
}
async getReposHandler (publicKey, req) {
const { branch, url } = await this.parseReq(publicKey, req, 'r')
const { branch, url, userId } = await this.parseReq(publicKey, req)
const res = {}
for (const repoName in this.repositories) {
// TODO: add only public repos and those which are shared with the peer
// Alternatively return only requested repo
const isPublic = (acl.getACL(repoName).visibility === 'public')
if (isPublic || acl.getViewers(repoName).includes(userId)) {
res[repoName] = this.drives[repoName].key.toString('hex')
}
}
return Buffer.from(JSON.stringify(res))
}
async getRefsHandler (publicKey, req) {
const { repoName, branch, url } = await this.parseReq(publicKey, req, 'r')
const { repoName, branch, url, userId } = await this.parseReq(publicKey, req)
const res = this.repositories[repoName]
const isPublic = (acl.getACL(repoName).visibility === 'public')
if (isPublic || acl.getViewers(repoName).includes(userId)) {
return Buffer.from(JSON.stringify(res))
} else {
throw new Error('You are not allowed to access this repo')
}
}
async pushHandler (publicKey, req) {
const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w')
const { url, repoName, branch, userId } = await this.parseReq(publicKey, req)
const isContributor = acl.getContributors(repoName).includes(userId)
if (!isContributor) {
throw new Error('You are not allowed to push to this repo')
}
const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch)
const isAdmin = acl.getAdmins(repoName).includes(userId)
if (isProtectedBranch && !isAdmin) {
throw new Error('You are not allowed to push to this branch')
}
return await new Promise((resolve, reject) => {
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['fetch', url, `${branch}:${branch}`], { env })
@@ -67,7 +89,20 @@ module.exports = class RPC {
}
async forcePushHandler (publicKey, req) {
const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w')
const { url, repoName, branch, userId } = await this.parseReq(publicKey, req)
const isContributor = acl.getContributors(repoName).includes(userId)
if (!isContributor) {
throw new Error('You are not allowed to push to this repo')
}
const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch)
const isAdmin = acl.getAdmins(repoName).includes(userId)
if (isProtectedBranch && !isAdmin) {
throw new Error('You are not allowed to push to this branch')
}
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 })
@@ -83,7 +118,19 @@ module.exports = class RPC {
}
async deleteBranchHandler (publicKey, req) {
const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w')
const { url, repoName, branch, userId } = await this.parseReq(publicKey, req)
const isContributor = acl.getContributors(repoName).includes(userId)
if (!isContributor) {
throw new Error('You are not allowed to push to this repo')
}
const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch)
const isAdmin = acl.getAdmins(repoName).includes(userId)
if (isProtectedBranch && !isAdmin) {
throw new Error('You are not allowed to push to this branch')
}
return await new Promise((resolve, reject) => {
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['branch', '-D', branch], { env })
@@ -104,25 +151,19 @@ module.exports = class RPC {
const parsed = {
repoName: request.body.url?.split('/')?.pop(),
branch: request.body.data?.split('#')[0],
url: request.body.url
url: request.body.url,
userId: await this.authenticate(publicKey, request),
}
return parsed
}
if (!process.env.GIT_PEAR_AUTH) return parsed
async authenticate (publicKey, req) {
if (!process.env.GIT_PEAR_AUTH) return publicKey
if (process.env.GIT_PEAR_AUTH === 'naitive') return publicKey
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 auth.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')
return parsed
return (await auth.getId({ ...request.body, payload: request.header })).userId
}
}

59
test/acl.test.js Normal file
View File

@@ -0,0 +1,59 @@
const test = require('brittle')
const acl = require('../src/acl')
test('acl', async t => {
const repoName = 'foo'
t.test('setACL', async t => {
const aclObj = acl.setACL(repoName)
t.is(aclObj.visibility, 'public')
t.is(aclObj.protectedBranches.length, 1)
t.is(aclObj.protectedBranches[0], 'master')
t.is(Object.keys(aclObj.ACL).length, 0)
})
test('getACL', async t => {
const aclObj = acl.setACL(repoName)
t.alike(acl.getACL(repoName), aclObj)
})
test('makeRepoPublic', async t => {
acl.makeRepoPublic(repoName)
t.is(acl.getRepoVisibility(repoName), 'public')
})
test('makeRepoPrivate', async t => {
acl.makeRepoPrivate(repoName)
t.is(acl.getRepoVisibility(repoName), 'private')
})
test('grantAccessToUser', async t => {
acl.grantAccessToUser(repoName, 'user1', 'admin')
t.alike(acl.getUserRole(repoName, 'user1'), 'admin')
})
test('addProtectedBranch', async t => {
acl.addProtectedBranch(repoName, 'branch1')
t.is(acl.getACL(repoName).protectedBranches.length, 2)
})
test('removeProtectedBranch', async t => {
acl.removeProtectedBranch(repoName, 'branch1')
t.is(acl.getACL(repoName).protectedBranches.length, 1)
})
test('getAdmins', async t => {
acl.grantAccessToUser(repoName, 'user2', 'admin')
t.alike(acl.getAdmins(repoName), ['user1', 'user2'])
})
test('getContributors', async t => {
acl.grantAccessToUser(repoName, 'user3', 'contributor')
t.alike(acl.getContributors(repoName), ['user3', 'user1', 'user2'])
})
test('getViewers', async t => {
acl.grantAccessToUser(repoName, 'user4', 'viewer')
t.alike(acl.getViewers(repoName), ['user4', 'user3', 'user1', 'user2'])
})
})