mirror of
https://github.com/aljazceru/gitpear.git
synced 2025-12-17 14:14:22 +01:00
96
src/acl.js
96
src/acl.js
@@ -1,6 +1,7 @@
|
|||||||
const home = require('./home')
|
const home = require('./home')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
const roles = {
|
const ROLES = {
|
||||||
admin: {
|
admin: {
|
||||||
description: 'Read and write to all branches',
|
description: 'Read and write to all branches',
|
||||||
},
|
},
|
||||||
@@ -12,9 +13,100 @@ const roles = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const DEFAULT_ACL = {
|
const DEFAULT_ACL = {
|
||||||
visibibility: 'public', // public|private
|
visibility: 'public', // public|private
|
||||||
protectedBranches: ['master'],
|
protectedBranches: ['master'],
|
||||||
ACL: {}
|
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,
|
||||||
|
}
|
||||||
|
|||||||
10
src/home.js
10
src/home.js
@@ -14,6 +14,10 @@ function shareAppFolder (name) {
|
|||||||
fs.openSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'w')
|
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) {
|
function unshareAppFolder (name) {
|
||||||
fs.unlinkSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)
|
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`)
|
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) {
|
function list (sharedOnly) {
|
||||||
const repos = fs.readdirSync(APP_HOME)
|
const repos = fs.readdirSync(APP_HOME)
|
||||||
if (!sharedOnly) return repos.filter(r => !r.startsWith('.') && isInitialized(r))
|
if (!sharedOnly) return repos.filter(r => !r.startsWith('.') && isInitialized(r))
|
||||||
@@ -125,5 +125,5 @@ module.exports = {
|
|||||||
getDaemonPid,
|
getDaemonPid,
|
||||||
isDaemonRunning,
|
isDaemonRunning,
|
||||||
removeDaemonPid,
|
removeDaemonPid,
|
||||||
getACL,
|
getACLFilePath
|
||||||
}
|
}
|
||||||
|
|||||||
79
src/rpc.js
79
src/rpc.js
@@ -2,6 +2,7 @@ const ProtomuxRPC = require('protomux-rpc')
|
|||||||
const { spawn } = require('child_process')
|
const { spawn } = require('child_process')
|
||||||
const home = require('./home')
|
const home = require('./home')
|
||||||
const auth = require('./auth')
|
const auth = require('./auth')
|
||||||
|
const acl = require('./acl')
|
||||||
|
|
||||||
module.exports = class RPC {
|
module.exports = class RPC {
|
||||||
constructor (announcedRefs, repositories, drives) {
|
constructor (announcedRefs, repositories, drives) {
|
||||||
@@ -32,26 +33,47 @@ module.exports = class RPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getReposHandler (publicKey, req) {
|
async getReposHandler (publicKey, req) {
|
||||||
const { branch, url } = await this.parseReq(publicKey, req, 'r')
|
const { branch, url, userId } = await this.parseReq(publicKey, req)
|
||||||
|
|
||||||
const res = {}
|
const res = {}
|
||||||
for (const repoName in this.repositories) {
|
for (const repoName in this.repositories) {
|
||||||
// TODO: add only public repos and those which are shared with the peer
|
// TODO: add only public repos and those which are shared with the peer
|
||||||
// Alternatively return only requested repo
|
// 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')
|
res[repoName] = this.drives[repoName].key.toString('hex')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return Buffer.from(JSON.stringify(res))
|
return Buffer.from(JSON.stringify(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRefsHandler (publicKey, req) {
|
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 res = this.repositories[repoName]
|
||||||
|
|
||||||
|
const isPublic = (acl.getACL(repoName).visibility === 'public')
|
||||||
|
if (isPublic || acl.getViewers(repoName).includes(userId)) {
|
||||||
return Buffer.from(JSON.stringify(res))
|
return Buffer.from(JSON.stringify(res))
|
||||||
|
} else {
|
||||||
|
throw new Error('You are not allowed to access this repo')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async pushHandler (publicKey, req) {
|
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) => {
|
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 })
|
||||||
@@ -67,7 +89,20 @@ module.exports = class RPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async forcePushHandler (publicKey, req) {
|
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) => {
|
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 })
|
||||||
@@ -83,7 +118,19 @@ module.exports = class RPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteBranchHandler (publicKey, req) {
|
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) => {
|
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 })
|
||||||
@@ -104,25 +151,19 @@ module.exports = class RPC {
|
|||||||
const parsed = {
|
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,
|
||||||
|
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) {
|
if (process.env.GIT_PEAR_AUTH !== 'naitive' && !request.header) {
|
||||||
throw new Error('You are not allowed to access this repo')
|
throw new Error('You are not allowed to access this repo')
|
||||||
}
|
}
|
||||||
|
|
||||||
let userId
|
return (await auth.getId({ ...request.body, payload: request.header })).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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
test/acl.test.js
Normal file
59
test/acl.test.js
Normal 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'])
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user