mirror of
https://github.com/aljazceru/gitpear.git
synced 2025-12-17 22:24:22 +01:00
46
Readme.md
46
Readme.md
@@ -38,13 +38,13 @@ All data will be persisted in application directory (default `~/.gitpear`). To c
|
||||
|
||||
* `git pear daemon <-s, --start | -k, --stop>` - start or stop daemon
|
||||
* `git pear key` - print out public key. Share it with your peers so that they can do `git pull pear:<public key>/<repo name>`
|
||||
* `git pear init <path> [-s, --share [branch]]` - It will create [bare repository](https://git-scm.com/docs/git-init#Documentation/git-init.txt---bare) of the same name in application directory (default ~/.gitpear/<repository name>). It will add [git remote](https://git-scm.com/docs/git-remote) in current repository with name `pear`. So just like in traditional flow doing `git push orign`, here we do `git push pear`. By default repository will not be shared. To enable sharing provide `-s | --share [branch]` (default branch to share is current) or call `gitpear share <path>` later
|
||||
* `git pear share [-p, --path [path (default: ".")]> [-b, --branch [branch name (default: "_current_")] [-v, --visibility <private|public> (default: "public")]` - share repository, if branch is not specified, default branch will be shared
|
||||
* `git pear unshare <path>` - stop sharing repository
|
||||
* `git pear init [-s, --share [branch]]` - It will create [bare repository](https://git-scm.com/docs/git-init#Documentation/git-init.txt---bare) of the same name in application directory (default ~/.gitpear/<repository name>). It will add [git remote](https://git-scm.com/docs/git-remote) in current repository with name `pear`. So just like in traditional flow doing `git push orign`, here we do `git push pear`. By default repository will not be shared. To enable sharing provide `-s | --share [branch]` (default branch to share is current) or call `gitpear share` later
|
||||
* `git pear share [-b, --branch [branch name (default: "_current_")] [-v, --visibility <private|public> (default: "public")]` - share current repository, if branch is not specified, default branch will be shared
|
||||
* `git pear unshare` - stop sharing current repository
|
||||
* `git pear list [-s, --shared]` - list all or (only shared) repositories
|
||||
* `git pear list <url>` - list repositories of a peer
|
||||
|
||||
### ACL (for authenticated access to enable support of PUSH)
|
||||
### User Access and Branch Protection Rules (for authenticated access to enable support of PUSH)
|
||||
|
||||
Support of `push` capabilities only enabled for authenticated users. Currently supported authentications are based on:
|
||||
* [noise](https://github.com/libp2p/specs/blob/master/noise/README.md);
|
||||
@@ -62,22 +62,26 @@ or
|
||||
GIT_PEAR_AUTH=native git pear daemon -s
|
||||
```
|
||||
|
||||
#### User Access Control
|
||||
|
||||
To manage access to repository use one or combination of the following commands, if `path` is not provide the command will be executed in the current directory. For `userId` use [NIP19 npub](https://github.com/nostr-protocol/nips/blob/master/19.md).
|
||||
|
||||
* `git pear acl [command] <path>` - ACL managegement
|
||||
* `git pear acl list [userId] <path>` - list repository visitbility and user's role (or roles of all users if userId is not provided)
|
||||
* `git pear acl add <userId:role> <path>` - add user as a "role" to repository, available roles are `viewer`, `contributor`, `admin`. Roles exaplained:
|
||||
* `viewer` - can read all branches;
|
||||
* `contributor` - can edit all branches except protected (default master)
|
||||
* `admin` - can edit protected branches
|
||||
* `git pear acl remove <userId> <path>` - revoke use access to repository
|
||||
* `git pear acl -u [command] -p <repo path or url (default ".")>` - ACL managegement of for users access in local or remote repo (requires `owner` permission for remote repositories)
|
||||
* `git pear acl -u list [userId] -p <repo path or url (default ".")>` - list repository visitbility and user's role (or roles of all users if userId is not provided)
|
||||
* `git pear acl add -u <userId:role> -p <repo path or url (default ".")>` - add user as a "role" to repository available roles are `viewer`, `contributor`, `admin`, `owner`. Roles exaplained:
|
||||
* `viewer` - can read all branches;
|
||||
* `contributor` - can edit all branches except protected (default master)
|
||||
* `admin` - can edit protected branches
|
||||
* `owner` - can edit repo ack
|
||||
* `git pear acl remove -u| <userId> -p <repo path or url (default ".")>` - revoke use access to repository.
|
||||
|
||||
|
||||
### Branch protection rules
|
||||
It is possible to setup basic branch protection rules (master is proteted by default).
|
||||
* `git pear branch`, same as `git pear branch list .` - list protection rules
|
||||
* `git pear branch add <branch name> <repo path>` - mark branch as protected (defatul repo path is ".")
|
||||
* `git pear branch remove <branch name> <repo path>` - unmark branch as protected
|
||||
* `git pear acl -b [command] -p <repo path or url (deafult ".")`, same as `git pear branch list` - list protection rules
|
||||
* `git pear acl -b list` - git pear branch list .` - list protection rules;
|
||||
* `git pear acl -b add <branchName> <repo path or url (default ".")>` - mark branch as protected;
|
||||
* `git pear acl -b remove <branchName> <repo path or url (deafult ".")>` - unmark branch as protected;
|
||||
|
||||
# Examples of usage
|
||||
|
||||
@@ -128,7 +132,7 @@ git pull
|
||||
|
||||
## Authenticated usage example (push) - at your own risk
|
||||
|
||||
Collaboration is possible with the following flow between Carol and David in a peer-to-peer manner.
|
||||
Collaboration is possible with the following flow between Carol and Dave in a peer-to-peer manner.
|
||||
|
||||
Supported authentication methods are `native` and `nip98`. The `nip98` authentication, requires environment variable `GIT_PEAR_AUTH_NSEC` with nsec
|
||||
|
||||
@@ -138,12 +142,12 @@ Supported authentication methods are `native` and `nip98`. The `nip98` authentic
|
||||
2. Go to repository
|
||||
* `cd repo`
|
||||
3. Initialize git pear repository
|
||||
* `git pear init .`
|
||||
* `git pear init`
|
||||
4. Share repository wit hviben visibility () - (default is `public`)
|
||||
* `git pear share . public`
|
||||
5. Add Daviv as a `contirbutor`.
|
||||
6. List David's npub as a contributor
|
||||
* `git pear acl add <David pub key hex>:contributor`
|
||||
* `git pear share public`
|
||||
5. Add Dave as a `contirbutor`.
|
||||
6. List Dave's npub as a contributor
|
||||
* `git pear acl add <Dave pub key hex>:contributor`
|
||||
7. Retreive repo url and share it with Dave
|
||||
* `git pear list -s`
|
||||
|
||||
@@ -176,4 +180,4 @@ git clone https://github.com/dzdidi/gitpear.git
|
||||
cd gitpear
|
||||
docker build -t gitpear .
|
||||
docker run -it -p 80:80 -e REPO_URL=https://github.com/dzdidi/repo.git gitpear
|
||||
```
|
||||
```
|
||||
|
||||
14
src/acl.js
14
src/acl.js
@@ -3,20 +3,20 @@ const fs = require('fs')
|
||||
|
||||
const ROLES = {
|
||||
owner: {
|
||||
description: 'Read and write to all branches, and ACL management',
|
||||
description: 'Read and write to all branches, and ACL management'
|
||||
},
|
||||
admin: {
|
||||
description: 'Read and write to all branches',
|
||||
description: 'Read and write to all branches'
|
||||
},
|
||||
contributor: {
|
||||
description: 'Read and write to all branches except protected ones',
|
||||
description: 'Read and write to all branches except protected ones'
|
||||
},
|
||||
viewer: {
|
||||
description: 'Read all branches',
|
||||
},
|
||||
description: 'Read all branches'
|
||||
}
|
||||
}
|
||||
const DEFAULT_ACL = {
|
||||
visibility: 'public', // public|private
|
||||
visibility: 'public', // public|private
|
||||
protectedBranches: ['master', 'main'],
|
||||
ACL: {}
|
||||
}
|
||||
@@ -127,5 +127,5 @@ module.exports = {
|
||||
getAdmins,
|
||||
getContributors,
|
||||
getViewers,
|
||||
revokeAccessFromUser,
|
||||
revokeAccessFromUser
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
function getId(data) {
|
||||
if (!process.env.GIT_PEAR_AUTH) return payload
|
||||
function getId (data) {
|
||||
if (!process.env.GIT_PEAR_AUTH) return data
|
||||
if (process.env.GIT_PEAR_AUTH === 'nip98') {
|
||||
const nip98 = require('./nip98')
|
||||
return nip98.getId(data)
|
||||
}
|
||||
}
|
||||
|
||||
async function getToken(payload) {
|
||||
if (!process.env.GIT_PEAR_AUTH) return userId
|
||||
async function getToken (payload) {
|
||||
if (!process.env.GIT_PEAR_AUTH) return payload
|
||||
if (process.env.GIT_PEAR_AUTH === 'nip98') {
|
||||
const nip98 = require('./nip98')
|
||||
return nip98.getToken(payload)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { nip98, nip19, finalizeEvent } = require('nostr-tools')
|
||||
|
||||
async function getToken({ url, method, data }) {
|
||||
async function getToken ({ url, method, data }) {
|
||||
if (!process.env.GIT_PEAR_AUTH_NSEC) throw new Error('Missing NSEC')
|
||||
const { data: sK } = nip19.decode(process.env.GIT_PEAR_AUTH_NSEC)
|
||||
return nip98.getToken(
|
||||
@@ -13,11 +13,11 @@ async function getToken({ url, method, data }) {
|
||||
}
|
||||
|
||||
// FIXME
|
||||
async function getId({ payload, url, method, data }) {
|
||||
async function getId ({ payload, url, method, data }) {
|
||||
const event = JSON.parse(Buffer.from(payload, 'base64').toString())
|
||||
const isValid = await nip98.validateEvent(event, url, method, data)
|
||||
if (!isValid) throw new Error('Invalid event')
|
||||
return {
|
||||
return {
|
||||
...event,
|
||||
userId: nip19.npubEncode(event.pubkey)
|
||||
}
|
||||
|
||||
156
src/cli-helpers.js
Normal file
156
src/cli-helpers.js
Normal file
@@ -0,0 +1,156 @@
|
||||
const path = require('path')
|
||||
|
||||
const home = require('./home')
|
||||
const acl = require('./acl')
|
||||
const git = require('./git')
|
||||
|
||||
const { aclRemote } = require('./rpc-requests')
|
||||
const { printACL, printACLForUser, checkIfGitRepo, logBranches } = require('./utils')
|
||||
|
||||
async function remoteACL (a, b, p, options) {
|
||||
if (a === 'list') {
|
||||
await aclRemote.list(p, b)
|
||||
} else if (a === 'add') {
|
||||
if (!b) {
|
||||
console.error('User not provided')
|
||||
process.exit(1)
|
||||
}
|
||||
if (b.split(':').length !== 2) {
|
||||
console.error('Invalid role')
|
||||
process.exit(1)
|
||||
}
|
||||
await aclRemote.add(p, b)
|
||||
} else if (a === 'remove') {
|
||||
if (!b) {
|
||||
console.error('User not provided')
|
||||
process.exit(1)
|
||||
}
|
||||
await aclRemote.remove(p, b)
|
||||
} else {
|
||||
throw new Error('Invalid action')
|
||||
}
|
||||
}
|
||||
|
||||
async function share (name, branchToShare, options) {
|
||||
let aclOptions
|
||||
let message = `Shared "${name}" project, ${branchToShare} branch`
|
||||
if (options?.visibility) {
|
||||
aclOptions = { visibility: options.visibility }
|
||||
message = `${message}, as ${options.visibility} repo`
|
||||
}
|
||||
|
||||
try { home.shareAppFolder(name) } catch (e) { }
|
||||
try { acl.setACL(name, aclOptions) } catch (e) { }
|
||||
try { await git.push(branchToShare) } catch (e) { }
|
||||
console.log(message)
|
||||
}
|
||||
|
||||
async function remoteBranchProtectionRules (a, b, p, options) {
|
||||
if (a === 'list') {
|
||||
await aclRemote.list(p, b, { branch: true })
|
||||
} else if (a === 'add') {
|
||||
await aclRemote.add(p, b, { branch: true })
|
||||
if (!b) {
|
||||
console.error('branch is not provided')
|
||||
process.exit(1)
|
||||
}
|
||||
} else if (a === 'remove') {
|
||||
if (!b) {
|
||||
console.error('branch is not provided')
|
||||
process.exit(1)
|
||||
}
|
||||
await aclRemote.remove(p, b, { branch: true })
|
||||
} else {
|
||||
throw new Error('Invalid action')
|
||||
}
|
||||
}
|
||||
|
||||
function localACL (a, u, p, options) {
|
||||
const fullPath = path.resolve(p)
|
||||
checkIfGitRepo(fullPath)
|
||||
|
||||
const name = fullPath.split(path.sep).pop()
|
||||
if (!home.isInitialized(name)) {
|
||||
console.error(`${name} is not initialized`)
|
||||
process.exit(1)
|
||||
}
|
||||
const repoACL = acl.getACL(name)
|
||||
|
||||
if (a === 'list' && !u) {
|
||||
printACL(repoACL)
|
||||
return
|
||||
}
|
||||
|
||||
if (a === 'list') {
|
||||
printACLForUser(repoACL, u)
|
||||
return
|
||||
}
|
||||
|
||||
if (a === 'add') {
|
||||
if (!u) {
|
||||
console.error('User not provided')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const [userId, role] = u.split(':')
|
||||
if (repoACL.ACL[userId]) {
|
||||
console.error(`${userId} already has access to ${name} as ${repoACL.ACL[userId]}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
acl.grantAccessToUser(name, userId, role)
|
||||
console.log(`Added ${userId} to ${name} as ${role}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (a === 'remove') {
|
||||
if (!u) {
|
||||
console.error('User not provided')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!repoACL.ACL[u]) {
|
||||
console.error(`${u} does not have access to ${name}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
acl.revokeAccessFromUser(name, u)
|
||||
console.log(`Removed ${u} from ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
function localBranchProtectionRules (a, b, p, options) {
|
||||
const fullPath = path.resolve(p)
|
||||
checkIfGitRepo(fullPath)
|
||||
|
||||
const name = fullPath.split(path.sep).pop()
|
||||
if (!home.isInitialized(name)) {
|
||||
console.error(`${name} is not initialized`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (a === 'list' && !b) {
|
||||
const repoACL = acl.getACL(name)
|
||||
logBranches(repoACL)
|
||||
}
|
||||
|
||||
if (a === 'add') {
|
||||
acl.addProtectedBranch(name, b)
|
||||
const repoACL = acl.getACL(name)
|
||||
logBranches(repoACL)
|
||||
}
|
||||
if (a === 'remove') {
|
||||
acl.removeProtectedBranch(name, b)
|
||||
const repoACL = acl.getACL(name)
|
||||
logBranches(repoACL)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
remoteACL,
|
||||
share,
|
||||
remoteBranchProtectionRules,
|
||||
localACL,
|
||||
localBranchProtectionRules
|
||||
|
||||
}
|
||||
184
src/cli.js
184
src/cli.js
@@ -6,12 +6,15 @@ const commander = require('commander')
|
||||
const program = new commander.Command()
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const home = require('./home')
|
||||
const acl = require('./acl')
|
||||
const git = require('./git')
|
||||
|
||||
const { listRemote } = require('./rpc-requests')
|
||||
|
||||
const { checkIfGitRepo } = require('./utils')
|
||||
const { remoteACL, share, remoteBranchProtectionRules, localACL, localBranchProtectionRules } = require('./cli-helpers')
|
||||
|
||||
const pkg = require('../package.json')
|
||||
program
|
||||
.name('gitpear')
|
||||
@@ -21,25 +24,23 @@ program
|
||||
program
|
||||
.command('init')
|
||||
.description('initialize a gitpear repo')
|
||||
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
|
||||
.option('-s, --share [branch]', 'share the repo as public, default false, default branch is current', '')
|
||||
.action(async (p, options) => {
|
||||
const fullPath = path.resolve(p)
|
||||
if (!fs.existsSync(path.join(fullPath, '.git'))) {
|
||||
console.error('Not a git repo')
|
||||
process.exit(1)
|
||||
}
|
||||
.action(async (options) => {
|
||||
const fullPath = path.resolve('.')
|
||||
checkIfGitRepo(fullPath)
|
||||
|
||||
const name = fullPath.split(path.sep).pop()
|
||||
if ((home.isInitialized(name))) {
|
||||
console.error(`${name} is already initialized`)
|
||||
await git.addRemote(name)
|
||||
console.log(`Added git remote for "${name}" as "pear"`)
|
||||
try {
|
||||
await git.addRemote(name)
|
||||
console.log(`Added git remote for "${name}" as "pear"`)
|
||||
} catch (e) { }
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
home.createAppFolder(name)
|
||||
home.createAppFolder(name)
|
||||
console.log(`Added project "${name}" to gitpear`)
|
||||
} catch (e) { }
|
||||
try {
|
||||
@@ -56,12 +57,7 @@ program
|
||||
branchToShare = options.share
|
||||
}
|
||||
|
||||
if (options.share) {
|
||||
try { home.shareAppFolder(name) } catch (e) { }
|
||||
try { acl.setACL(name) } catch (e) { }
|
||||
try { await git.push(branchToShare) } catch (e) { }
|
||||
console.log(`Shared "${name}" project, ${branchToShare} branch`)
|
||||
}
|
||||
if (options.share) await share(name, branchToShare)
|
||||
})
|
||||
|
||||
program
|
||||
@@ -69,13 +65,9 @@ program
|
||||
.description('share a gitpear repo')
|
||||
.option('-b, --branch [b]', 'branch to share, default is current branch', '')
|
||||
.option('-v, --visibility [v]', 'visibility of the repo', 'public')
|
||||
.option('-p, --path [p]', 'path to the repo', '.')
|
||||
.action(async (options) => {
|
||||
const fullPath = path.resolve(options.path)
|
||||
if (!fs.existsSync(path.join(fullPath, '.git'))) {
|
||||
console.error('Not a git repo')
|
||||
process.exit(1)
|
||||
}
|
||||
const fullPath = path.resolve('.')
|
||||
checkIfGitRepo(fullPath)
|
||||
|
||||
const name = fullPath.split(path.sep).pop()
|
||||
if (!home.isInitialized(name)) {
|
||||
@@ -85,137 +77,49 @@ program
|
||||
|
||||
const currentBranch = await git.getCurrentBranch()
|
||||
const branchToShare = options.branch || currentBranch
|
||||
try { home.shareAppFolder(name) } catch (e) { }
|
||||
try { acl.setACL(name, { visibility: options.visibility }) } catch (e) { }
|
||||
try { await git.push(branchToShare) } catch (e) { }
|
||||
console.log(`Shared "${name}" project, ${branchToShare} branch, as ${options.visibility} repo`)
|
||||
return
|
||||
await share(name, branchToShare, options)
|
||||
})
|
||||
|
||||
program
|
||||
.command('branch')
|
||||
.description('branch protection rules')
|
||||
.addArgument(new commander.Argument('[a]', 'actiont to perform').choices(['add', 'remove', 'list']).default('list'))
|
||||
.addArgument(new commander.Argument('[b]', 'branch name').default(''))
|
||||
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
|
||||
.action(async (a, b, p, options) => {
|
||||
const fullPath = path.resolve(p)
|
||||
if (!fs.existsSync(path.join(fullPath, '.git'))) {
|
||||
console.error('Not a git repo')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const name = fullPath.split(path.sep).pop()
|
||||
if (!home.isInitialized(name)) {
|
||||
console.error(`${name} is not initialized`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (a === 'list' && !b) { logBranches(name) }
|
||||
|
||||
if (a === 'add') {
|
||||
acl.addProtectedBranch(name, b)
|
||||
logBranches(name)
|
||||
}
|
||||
|
||||
if (a === 'remove') {
|
||||
acl.removeProtectedBranch(name, b)
|
||||
logBranches(name)
|
||||
}
|
||||
|
||||
function logBranches(name) {
|
||||
const repoACL = acl.getACL(name)
|
||||
console.log('Visibility:', '\t', repoACL.visibility)
|
||||
console.log('Branch:')
|
||||
for (const branch of repoACL.protectedBranches) { console.log(branch) }
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
|
||||
program
|
||||
.command('acl')
|
||||
.description('set acl of a gitpear repo')
|
||||
.description('manage acl of a gitpear repo')
|
||||
.option('-u, --user', 'user to add/remove/list')
|
||||
.option('-b, --branch', 'branch to add/remove/list in protected branches')
|
||||
.option('-p, --path [path]', 'path to the repo', '.')
|
||||
.addArgument(new commander.Argument('[a]', 'actiont to perform').choices(['add', 'remove', 'list']).default('list'))
|
||||
.addArgument(new commander.Argument('[u]', 'user to add/remove/list').default(''))
|
||||
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
|
||||
.action(async (a, u, p, options) => {
|
||||
|
||||
// TODO: add branch protection logic
|
||||
const fullPath = path.resolve(p)
|
||||
if (!fs.existsSync(path.join(fullPath, '.git'))) {
|
||||
console.error('Not a git repo')
|
||||
process.exit(1)
|
||||
.addArgument(new commander.Argument('[n]', 'user or branch to add/remove/list').default(''))
|
||||
.action(async (a, n, options) => {
|
||||
if (options.user && options.branch) {
|
||||
throw new Error('Cannot perform both user and branch action at the same time')
|
||||
}
|
||||
|
||||
const name = fullPath.split(path.sep).pop()
|
||||
if (!home.isInitialized(name)) {
|
||||
console.error(`${name} is not initialized`)
|
||||
process.exit(1)
|
||||
}
|
||||
const repoACL = acl.getACL(name)
|
||||
|
||||
if (a === 'list' && !u) {
|
||||
console.log('Repo Visibility:', '\t', repoACL.visibility)
|
||||
console.log('User:', '\t', 'Role:')
|
||||
for (const user in repoACL.ACL) {
|
||||
console.log(user, '\t', repoACL.ACL[user])
|
||||
}
|
||||
return
|
||||
if (!options.user && !options.branch) {
|
||||
throw new Error('Either user or branch option is required')
|
||||
}
|
||||
|
||||
if (a === 'list') {
|
||||
console.log('Repo Visibility:', '\t', repoACL.visibility)
|
||||
console.log('User:', u, '\t', repoACL.ACL[u])
|
||||
return
|
||||
}
|
||||
|
||||
if (a === 'add') {
|
||||
if (!u) {
|
||||
console.error('User not provided')
|
||||
process.exit(1)
|
||||
if (options.user) {
|
||||
if (options.path.startsWith('pear://')) {
|
||||
if (n === '.') n = ''
|
||||
await remoteACL(a, n, options.path, options)
|
||||
} else {
|
||||
localACL(a, n, options.path, options)
|
||||
}
|
||||
|
||||
const [ userId, role ] = u.split(':')
|
||||
if (repoACL.ACL[userId]) {
|
||||
console.error(`${userId} already has access to ${name} as ${repoACL.ACL[userId]}`)
|
||||
process.exit(1)
|
||||
} else if (options.branch) {
|
||||
if (options.path.startsWith('pear://')) {
|
||||
await remoteBranchProtectionRules(a, n, options.path, options)
|
||||
} else {
|
||||
localBranchProtectionRules(a, n, options.path, options)
|
||||
}
|
||||
|
||||
acl.grantAccessToUser(name, userId, role)
|
||||
console.log(`Added ${userId} to ${name} as ${role}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (a === 'remove') {
|
||||
if (!u) {
|
||||
console.error('User not provided')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!repoACL.ACL[u]) {
|
||||
console.error(`${u} does not have access to ${name}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
acl.revokeAccessFromUser(name, u)
|
||||
console.log(`Removed ${u} from ${name}`)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
program
|
||||
.command('unshare')
|
||||
.description('unshare a gitpear repo')
|
||||
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
|
||||
.action((p, options) => {
|
||||
const fullPath = path.resolve(p)
|
||||
if (!fs.existsSync(path.join(fullPath, '.git'))) {
|
||||
console.error('Not a git repo')
|
||||
process.exit(1)
|
||||
}
|
||||
.option('-p, --path [path]', 'path to the repo', '.')
|
||||
.action((options) => {
|
||||
const fullPath = path.resolve(options.path)
|
||||
checkIfGitRepo(fullPath)
|
||||
|
||||
const name = fullPath.split(path.sep).pop()
|
||||
if ((home.isInitialized(name))) {
|
||||
@@ -235,7 +139,7 @@ program
|
||||
.addArgument(new commander.Argument('[u]', 'url to remote pear').default(''))
|
||||
.option('-s, --shared', 'list only shared repos')
|
||||
.action((u, options) => {
|
||||
if (u) return require('./list-remote')(u)
|
||||
if (u) return listRemote(u)
|
||||
|
||||
const k = home.readPk()
|
||||
const s = options.shared
|
||||
@@ -276,7 +180,7 @@ program
|
||||
opts.stdio = 'inherit'
|
||||
} else {
|
||||
opts.detached = true
|
||||
opts.stdio = [ 'ignore', home.getOutStream(), home.getErrStream() ]
|
||||
opts.stdio = ['ignore', home.getOutStream(), home.getErrStream()]
|
||||
}
|
||||
|
||||
const daemon = spawn('git-peard', opts)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { spawn } = require('child_process')
|
||||
const ProtomuxRPC = require('protomux-rpc')
|
||||
|
||||
const RAM = require('random-access-memory')
|
||||
@@ -14,8 +13,6 @@ const home = require('./home')
|
||||
const auth = require('./auth')
|
||||
const acl = require('./acl')
|
||||
|
||||
const fs = require('fs')
|
||||
|
||||
const url = process.argv[3]
|
||||
const matches = url.match(/pear:\/\/([a-f0-9]{64})\/(.*)/)
|
||||
|
||||
@@ -76,13 +73,13 @@ swarm.on('connection', async (socket) => {
|
||||
|
||||
await drive.core.update({ wait: true })
|
||||
|
||||
payload = { body: { url, method: 'get-refs', data: repoName }}
|
||||
payload = { body: { url, method: 'get-refs', data: repoName } }
|
||||
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
|
||||
payload.header = await auth.getToken(payload.body)
|
||||
}
|
||||
const refsRes = await rpc.request('get-refs', Buffer.from(JSON.stringify(payload)))
|
||||
|
||||
let commit
|
||||
let commit
|
||||
try {
|
||||
commit = await git.getCommit()
|
||||
} catch (e) { }
|
||||
@@ -99,7 +96,7 @@ async function talkToGit (refs, drive, repoName, rpc, commit) {
|
||||
process.stdout.write('push\n')
|
||||
process.stdout.write('fetch\n\n')
|
||||
} else if (chunk && chunk.search(/^push/) !== -1) {
|
||||
const [_command, path] = chunk.split(' ')
|
||||
const path = chunk.split(' ')[1]
|
||||
let [src, dst] = path.split(':')
|
||||
|
||||
const isDelete = !src
|
||||
@@ -107,13 +104,6 @@ async function talkToGit (refs, drive, repoName, rpc, commit) {
|
||||
|
||||
dst = dst.replace('refs/heads/', '').replace('\n\n', '')
|
||||
|
||||
try { home.createAppFolder(repoName) } catch (e) { }
|
||||
try { await git.createBareRepo(repoName) } catch (e) { }
|
||||
try { await git.addRemote(repoName) } catch (e) { }
|
||||
try { await git.push(dst) } catch (e) { }
|
||||
try { home.shareAppFolder(repoName) } catch (e) { }
|
||||
try { acl.setACL(repoName, acl.getACL(repoName)) } catch (e) { }
|
||||
|
||||
let method
|
||||
if (isDelete) {
|
||||
method = 'd-branch'
|
||||
@@ -128,16 +118,25 @@ async function talkToGit (refs, drive, repoName, rpc, commit) {
|
||||
method = 'push'
|
||||
}
|
||||
|
||||
try { home.createAppFolder(repoName) } catch (e) { }
|
||||
try { await git.createBareRepo(repoName) } catch (e) { }
|
||||
try { await git.addRemote(repoName, { quite: true }) } catch (e) { }
|
||||
try { await git.push(dst, isForce) } catch (e) { }
|
||||
try { home.shareAppFolder(repoName) } catch (e) { }
|
||||
try { acl.setACL(repoName, acl.getACL(repoName)) } catch (e) { }
|
||||
|
||||
const publicKey = home.readPk()
|
||||
let payload = { body: {
|
||||
url: `pear://${publicKey}/${repoName}`,
|
||||
data: `${dst}#${commit}`,
|
||||
method
|
||||
} }
|
||||
const payload = {
|
||||
body: {
|
||||
url: `pear://${publicKey}/${repoName}`,
|
||||
data: `${dst}#${commit}`,
|
||||
method
|
||||
}
|
||||
}
|
||||
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
|
||||
payload.header = await auth.getToken(payload.body)
|
||||
}
|
||||
const res = await rpc.request(method, Buffer.from(JSON.stringify(payload)))
|
||||
await rpc.request(method, Buffer.from(JSON.stringify(payload)))
|
||||
|
||||
process.stdout.write('\n\n')
|
||||
process.exit(0)
|
||||
|
||||
10
src/git.js
10
src/git.js
@@ -64,9 +64,9 @@ async function createBareRepo (name) {
|
||||
return await doGit(init)
|
||||
}
|
||||
|
||||
async function addRemote (name) {
|
||||
async function addRemote (name, opts = { quiet: false }) {
|
||||
const init = spawn('git', ['remote', 'add', 'pear', getCodePath(name)])
|
||||
return await doGit(init)
|
||||
return await doGit(init, opts)
|
||||
}
|
||||
|
||||
async function push (branch = 'master', force = false) {
|
||||
@@ -76,8 +76,8 @@ async function push (branch = 'master', force = false) {
|
||||
return await doGit(push)
|
||||
}
|
||||
|
||||
async function doGit (child) {
|
||||
child.stderr.pipe(process.stderr)
|
||||
async function doGit (child, opts = { quiet: false }) {
|
||||
if (!opts.quiet) child.stderr.pipe(process.stderr)
|
||||
return new Promise((resolve, reject) => {
|
||||
child.on('close', (code) => {
|
||||
if (code) {
|
||||
@@ -229,5 +229,5 @@ module.exports = {
|
||||
addRemote,
|
||||
push,
|
||||
getCommit,
|
||||
getCurrentBranch,
|
||||
getCurrentBranch
|
||||
}
|
||||
|
||||
53
src/rpc-handlers/acl.js
Normal file
53
src/rpc-handlers/acl.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const ACL = require('../acl')
|
||||
const home = require('../home')
|
||||
|
||||
async function getACLHandler (publicKey, req) {
|
||||
const { repoName } = await parseACLRequest.bind(this)(publicKey, req)
|
||||
const repoACL = ACL.getACL(repoName)
|
||||
|
||||
return Buffer.from(JSON.stringify(repoACL))
|
||||
}
|
||||
|
||||
async function addACLHandler (publicKey, req) {
|
||||
const { repoName, isBranch, name } = await parseACLRequest.bind(this)(publicKey, req)
|
||||
|
||||
isBranch ? ACL.addProtectedBranch(repoName, name) : ACL.grantAccessToUser(repoName, ...name.split(':'))
|
||||
|
||||
const repoACL = ACL.getACL(repoName)
|
||||
return Buffer.from(JSON.stringify(repoACL))
|
||||
}
|
||||
|
||||
async function delACLHandler (publicKey, req) {
|
||||
const { repoName, isBranch, name } = await parseACLRequest.bind(this)(publicKey, req)
|
||||
|
||||
isBranch ? ACL.removeProtectedBranch(repoName, name) : ACL.revokeAccessFromUser(repoName, name)
|
||||
|
||||
const repoACL = ACL.getACL(repoName)
|
||||
return Buffer.from(JSON.stringify(repoACL))
|
||||
}
|
||||
|
||||
async function parseACLRequest (publicKey, req) {
|
||||
if (!req) throw new Error('Request is empty')
|
||||
const request = JSON.parse(req.toString())
|
||||
const userId = await this.authenticate(publicKey, request)
|
||||
const repoName = request.body.url?.split('/')?.pop()
|
||||
|
||||
if (!home.isInitialized(repoName)) throw new Error('Repo does not exist')
|
||||
|
||||
const isOwner = ACL.getOwners(repoName).includes(userId)
|
||||
if (!isOwner) throw new Error('You are not allowed to access this repo')
|
||||
|
||||
return {
|
||||
repoName,
|
||||
name: request.body.name,
|
||||
userId,
|
||||
acl: request.body.acl,
|
||||
isBranch: !!request.body.branch
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getACLHandler,
|
||||
addACLHandler,
|
||||
delACLHandler
|
||||
}
|
||||
122
src/rpc-handlers/git.js
Normal file
122
src/rpc-handlers/git.js
Normal file
@@ -0,0 +1,122 @@
|
||||
const ACL = require('../acl')
|
||||
const home = require('../home')
|
||||
const { spawn } = require('child_process')
|
||||
|
||||
async function getReposHandler (publicKey, req) {
|
||||
const { userId } = await parseReq.bind(this)(publicKey, req)
|
||||
|
||||
const res = {}
|
||||
for (const repoName in this.repositories) {
|
||||
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 function getRefsHandler (publicKey, req) {
|
||||
const { repoName, userId } = await parseReq.bind(this)(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 function pushHandler (publicKey, req) {
|
||||
const { url, repoName, branch, userId } = await parseReq.bind(this)(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 })
|
||||
return doGit(child, resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
async function forcePushHandler (publicKey, req) {
|
||||
const { url, repoName, branch, userId } = await parseReq.bind(this)(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 })
|
||||
return doGit(child, resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteBranchHandler (publicKey, req) {
|
||||
const { repoName, branch, userId } = await parseReq.bind(this)(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 })
|
||||
return doGit(child, resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
async function parseReq (publicKey, req) {
|
||||
if (!req) throw new Error('Request is empty')
|
||||
const request = JSON.parse(req.toString())
|
||||
const parsed = {
|
||||
repoName: request.body.url?.split('/')?.pop(),
|
||||
branch: request.body.data?.split('#')[0],
|
||||
url: request.body.url,
|
||||
userId: await this.authenticate(publicKey, request)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
function doGit (child, resolve, reject) {
|
||||
let errBuffer = Buffer.from('')
|
||||
let outBuffer = Buffer.from('')
|
||||
|
||||
child.stdout.on('data', data => {
|
||||
outBuffer = Buffer.concat([outBuffer, data])
|
||||
})
|
||||
|
||||
child.stderr.on('data', data => {
|
||||
errBuffer = Buffer.concat([errBuffer, data])
|
||||
})
|
||||
|
||||
child.on('close', code => {
|
||||
console.error('errBuffer', errBuffer.toString())
|
||||
console.log('outBuffer', outBuffer.toString())
|
||||
|
||||
return code === 0 ? resolve(outBuffer) : reject(errBuffer)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getReposHandler,
|
||||
getRefsHandler,
|
||||
pushHandler,
|
||||
forcePushHandler,
|
||||
deleteBranchHandler
|
||||
}
|
||||
7
src/rpc-handlers/index.js
Normal file
7
src/rpc-handlers/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const git = require('./git')
|
||||
const acl = require('./acl')
|
||||
|
||||
module.exports = {
|
||||
git,
|
||||
acl
|
||||
}
|
||||
116
src/rpc-requests/acl-remote.js
Normal file
116
src/rpc-requests/acl-remote.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const ProtomuxRPC = require('protomux-rpc')
|
||||
|
||||
const Hyperswarm = require('hyperswarm')
|
||||
const crypto = require('hypercore-crypto')
|
||||
|
||||
const home = require('../home')
|
||||
const auth = require('../auth')
|
||||
|
||||
const { printACL, printACLForUser, logBranches } = require('../utils')
|
||||
|
||||
async function list (url, name, rpc, opts) {
|
||||
const payload = { body: { url, method: 'get-acl' } }
|
||||
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
|
||||
payload.header = await auth.getToken(payload.body)
|
||||
}
|
||||
const repoACLres = await rpc.request('get-acl', Buffer.from(JSON.stringify(payload)))
|
||||
const repoACL = JSON.parse(repoACLres.toString())
|
||||
|
||||
opts.branch ? listACLBranch(repoACL) : listACLUser(repoACL, name)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
function listACLUser (repoACL, u) {
|
||||
u ? printACLForUser(repoACL, u) : printACL(repoACL)
|
||||
}
|
||||
|
||||
function listACLBranch (repoACL) {
|
||||
logBranches(repoACL)
|
||||
}
|
||||
|
||||
async function add (url, name, rpc, opts) {
|
||||
const payload = { body: { url, method: 'add-acl', name } }
|
||||
if (opts.branch) payload.body.branch = true
|
||||
|
||||
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
|
||||
payload.header = await auth.getToken(payload.body)
|
||||
}
|
||||
const repoACLres = await rpc.request('add-acl', Buffer.from(JSON.stringify(payload)))
|
||||
const repoACL = JSON.parse(repoACLres.toString())
|
||||
opts.branch ? listACLBranch(repoACL) : listACLUser(repoACL, name.split(':')[0])
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
async function del (url, name, rpc, opts) {
|
||||
const payload = { body: { url, method: 'del-acl', name } }
|
||||
if (opts.branch) payload.body.branch = true
|
||||
|
||||
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
|
||||
payload.header = await auth.getToken(payload.body)
|
||||
}
|
||||
const repoACLres = await rpc.request('del-acl', Buffer.from(JSON.stringify(payload)))
|
||||
const repoACL = JSON.parse(repoACLres.toString())
|
||||
opts.branch ? listACLBranch(repoACL) : listACLUser(repoACL, name)
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
async function wrapper (url, name, opts = {}, cb) {
|
||||
if (typeof opts === 'function') {
|
||||
cb = opts
|
||||
opts = {}
|
||||
}
|
||||
|
||||
const matches = url.match(/pear:\/\/([a-f0-9]{64})\/(.*)/)
|
||||
|
||||
if (!matches || matches.length < 3) {
|
||||
console.error('Invalid URL')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const targetKey = matches[1]
|
||||
const repoName = matches[2]
|
||||
console.log('Connecting to:', targetKey)
|
||||
|
||||
const swarmOpts = {}
|
||||
if (process.env.GIT_PEAR_AUTH === 'native') {
|
||||
swarmOpts.keyPair = home.getKeyPair()
|
||||
}
|
||||
const swarm = new Hyperswarm(swarmOpts)
|
||||
|
||||
swarm.join(crypto.discoveryKey(Buffer.from(targetKey, 'hex')), { server: false })
|
||||
|
||||
swarm.on('connection', async (socket) => {
|
||||
const rpc = new ProtomuxRPC(socket)
|
||||
|
||||
const payload = { body: { url, method: 'get-repos' } }
|
||||
if (!process.env.GIT_PEAR_AUTH) {
|
||||
console.debug('Retreiving data using un-authenticated access')
|
||||
} else {
|
||||
console.debug('Retreiving data using authenticated access')
|
||||
}
|
||||
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
|
||||
payload.header = await auth.getToken(payload.body)
|
||||
}
|
||||
|
||||
const reposRes = await rpc.request('get-repos', Buffer.from(JSON.stringify(payload)))
|
||||
const repositories = JSON.parse(reposRes.toString())
|
||||
if (!repositories) {
|
||||
console.error('Failed to retrieve repositories')
|
||||
process.exit(1)
|
||||
}
|
||||
if (!repositories[repoName]) {
|
||||
console.error('Repository not found')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cb(url, name, rpc, opts)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
list: (url, name, opts) => wrapper(url, name, opts, list),
|
||||
add: (url, name, opts) => wrapper(url, name, opts, add),
|
||||
remove: (url, name, opts) => wrapper(url, name, opts, del)
|
||||
}
|
||||
7
src/rpc-requests/index.js
Normal file
7
src/rpc-requests/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const listRemote = require('./list-remote')
|
||||
const aclRemote = require('./acl-remote')
|
||||
|
||||
module.exports = {
|
||||
listRemote,
|
||||
aclRemote
|
||||
}
|
||||
@@ -3,8 +3,8 @@ const ProtomuxRPC = require('protomux-rpc')
|
||||
const Hyperswarm = require('hyperswarm')
|
||||
const crypto = require('hypercore-crypto')
|
||||
|
||||
const home = require('./home')
|
||||
const auth = require('./auth')
|
||||
const home = require('../home')
|
||||
const auth = require('../auth')
|
||||
|
||||
module.exports = async function listRemote (url) {
|
||||
const matches = url.match(/pear:\/\/([a-f0-9]{64})/)
|
||||
@@ -28,14 +28,15 @@ module.exports = async function listRemote (url) {
|
||||
swarm.on('connection', async (socket) => {
|
||||
const rpc = new ProtomuxRPC(socket)
|
||||
|
||||
let payload = { body: { url, method: 'get-repos' } }
|
||||
const payload = { body: { url, method: 'get-repos' } }
|
||||
if (!process.env.GIT_PEAR_AUTH) {
|
||||
console.debug('Retreiving data using un-authenticated access')
|
||||
} else {
|
||||
console.debug('Retreiving data using authenticated access')
|
||||
}
|
||||
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
|
||||
payload.header = await auth.getToken(payload.body)
|
||||
console.debug('Retreiving data using authenticated access')
|
||||
} else {
|
||||
console.debug('Retreiving data using un-authenticated access')
|
||||
}
|
||||
console.log()
|
||||
|
||||
const reposRes = await rpc.request('get-repos', Buffer.from(JSON.stringify(payload)))
|
||||
const repositories = JSON.parse(reposRes.toString())
|
||||
141
src/rpc.js
141
src/rpc.js
@@ -1,9 +1,6 @@
|
||||
const ProtomuxRPC = require('protomux-rpc')
|
||||
const SecretStream = require('@hyperswarm/secret-stream')
|
||||
const { spawn } = require('child_process')
|
||||
const home = require('./home')
|
||||
const auth = require('./auth')
|
||||
const acl = require('./acl')
|
||||
const { git, acl } = require('./rpc-handlers')
|
||||
|
||||
module.exports = class RPC {
|
||||
constructor (announcedRefs, repositories, drives) {
|
||||
@@ -15,8 +12,9 @@ module.exports = class RPC {
|
||||
|
||||
async setHandlers (socket, peerInfo) {
|
||||
if (this.connections[peerInfo.publicKey]) return this.connections[peerInfo.publicKey]
|
||||
|
||||
const rpc = new ProtomuxRPC(socket)
|
||||
this.connections[peerInfo.publicKey] = rpc
|
||||
|
||||
rpc.on('error', err => console.error('rpc error', err))
|
||||
rpc.on('close', () => delete this.connections[peerInfo.publicKey])
|
||||
// XXX: handshaking can be used for access and permission management
|
||||
@@ -24,131 +22,22 @@ 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(socket.remotePublicKey, req))
|
||||
rpc.respond('get-refs', async req => await this.getRefsHandler(socket.remotePublicKey, req))
|
||||
rpc.respond('get-repos', async req => await git.getReposHandler.bind(this)(socket.remotePublicKey, req))
|
||||
rpc.respond('get-refs', async req => await git.getRefsHandler.bind(this)(socket.remotePublicKey, req))
|
||||
|
||||
if (process.env.GIT_PEAR_AUTH) {
|
||||
/* -- PUSH HANDLERS -- */
|
||||
rpc.respond('push', async req => await this.pushHandler(socket.remotePublicKey, req))
|
||||
rpc.respond('f-push', async req => await this.forcePushHandler(socket.remotePublicKey, req))
|
||||
rpc.respond('d-branch', async req => await this.deleteBranchHandler(socket.remotePublicKey, req))
|
||||
}
|
||||
if (!process.env.GIT_PEAR_AUTH) return
|
||||
|
||||
this.connections[peerInfo.publicKey] = rpc
|
||||
}
|
||||
/* -- PUSH HANDLERS -- */
|
||||
rpc.respond('push', async req => await git.pushHandler.bind(this)(socket.remotePublicKey, req))
|
||||
rpc.respond('f-push', async req => await git.forcePushHandler.bind(this)(socket.remotePublicKey, req))
|
||||
rpc.respond('d-branch', async req => await git.deleteBranchHandler.bind(this)(socket.remotePublicKey, req))
|
||||
|
||||
async getReposHandler (publicKey, req) {
|
||||
const { branch, url, userId } = await this.parseReq(publicKey, req)
|
||||
/* -- REPO ADMINISTRATION HANDLERS -- */
|
||||
|
||||
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, 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, 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 })
|
||||
let errBuffer = Buffer.from('')
|
||||
child.stderr.on('data', data => {
|
||||
errBuffer = Buffer.concat([errBuffer, data])
|
||||
})
|
||||
|
||||
child.on('close', code => {
|
||||
return code === 0 ? resolve(errBuffer) : reject(errBuffer)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async forcePushHandler (publicKey, req) {
|
||||
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 })
|
||||
let errBuffer = Buffer.from('')
|
||||
child.stderr.on('data', data => {
|
||||
errBuffer = Buffer.concat([errBuffer, data])
|
||||
})
|
||||
|
||||
child.on('close', code => {
|
||||
return code === 0 ? resolve(errBuffer) : reject(errBuffer)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async deleteBranchHandler (publicKey, req) {
|
||||
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 })
|
||||
let errBuffer = Buffer.from('')
|
||||
child.stderr.on('data', data => {
|
||||
errBuffer = Buffer.concat([errBuffer, data])
|
||||
})
|
||||
|
||||
child.on('close', code => {
|
||||
return code === 0 ? resolve(errBuffer) : reject(errBuffer)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async parseReq(publicKey, req) {
|
||||
if (!req) throw new Error('Request is empty')
|
||||
const request = JSON.parse(req.toString())
|
||||
const parsed = {
|
||||
repoName: request.body.url?.split('/')?.pop(),
|
||||
branch: request.body.data?.split('#')[0],
|
||||
url: request.body.url,
|
||||
userId: await this.authenticate(publicKey, request),
|
||||
}
|
||||
return parsed
|
||||
/* -- ACL HANDLERS -- */
|
||||
rpc.respond('get-acl', async req => await acl.getACLHandler.bind(this)(socket.remotePublicKey, req))
|
||||
rpc.respond('add-acl', async req => await acl.addACLHandler.bind(this)(socket.remotePublicKey, req))
|
||||
rpc.respond('del-acl', async req => await acl.delACLHandler.bind(this)(socket.remotePublicKey, req))
|
||||
}
|
||||
|
||||
async authenticate (publicKey, request) {
|
||||
|
||||
36
src/utils.js
Normal file
36
src/utils.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
function printACL (repoACL) {
|
||||
console.log('Repo Visibility:', '\t', repoACL.visibility)
|
||||
console.log('Protected Branch(s):', '\t', repoACL.protectedBranches.join(', '))
|
||||
console.log('User:', '\t', 'Role:')
|
||||
for (const user in repoACL.ACL) {
|
||||
console.log(user, '\t', repoACL.ACL[user])
|
||||
}
|
||||
}
|
||||
|
||||
function printACLForUser (repoACL, u) {
|
||||
console.log('Repo Visibility:', '\t', repoACL.visibility)
|
||||
console.log('Protected Branch(s):', '\t', repoACL.protectedBranches.join(', '))
|
||||
console.log('User:', u, '\t', repoACL.ACL[u])
|
||||
}
|
||||
|
||||
function checkIfGitRepo (p) {
|
||||
if (!fs.existsSync(path.join(p, '.git'))) {
|
||||
console.error(` ${p} is not a git repo`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function logBranches (repoACL) {
|
||||
console.log('Repo Visibility:', '\t', repoACL.visibility)
|
||||
console.log('Protected Branch(s):', '\t', repoACL.protectedBranches.join(', '))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
printACL,
|
||||
printACLForUser,
|
||||
checkIfGitRepo,
|
||||
logBranches
|
||||
}
|
||||
@@ -7,8 +7,8 @@ test('acl', async t => {
|
||||
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(aclObj.protectedBranches.length, 2)
|
||||
t.ok(aclObj.protectedBranches.includes('master') && aclObj.protectedBranches.includes('main'))
|
||||
t.is(Object.keys(aclObj.ACL).length, 0)
|
||||
})
|
||||
|
||||
@@ -34,12 +34,12 @@ test('acl', async t => {
|
||||
|
||||
test('addProtectedBranch', async t => {
|
||||
acl.addProtectedBranch(repoName, 'branch1')
|
||||
t.is(acl.getACL(repoName).protectedBranches.length, 2)
|
||||
t.is(acl.getACL(repoName).protectedBranches.length, 3)
|
||||
})
|
||||
|
||||
test('removeProtectedBranch', async t => {
|
||||
acl.removeProtectedBranch(repoName, 'branch1')
|
||||
t.is(acl.getACL(repoName).protectedBranches.length, 1)
|
||||
t.is(acl.getACL(repoName).protectedBranches.length, 2)
|
||||
})
|
||||
|
||||
test('getAdmins', async t => {
|
||||
|
||||
@@ -51,13 +51,13 @@ test('git - uploadPack (w have)', { skip: true }, async t => {
|
||||
})
|
||||
|
||||
test('git - createBareRepo', async t => {
|
||||
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test-git', 'code')))
|
||||
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test-git')))
|
||||
home.createAppFolder('test-git')
|
||||
|
||||
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test-git', 'code', 'HEAD')))
|
||||
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test-git', 'HEAD')))
|
||||
await git.createBareRepo('test-git')
|
||||
|
||||
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test-git', 'code', 'HEAD')))
|
||||
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test-git', 'HEAD')))
|
||||
|
||||
t.teardown(() => {
|
||||
fs.rmSync(path.join(home.APP_HOME, 'test-git'), { recursive: true })
|
||||
|
||||
@@ -11,7 +11,7 @@ test('getAppHome', t => {
|
||||
test('createAppFolder, share, is shared, unshare, isInitialized, list, getCodePath', t => {
|
||||
home.createAppFolder('test_code')
|
||||
|
||||
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test_code', 'code')))
|
||||
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test_code')))
|
||||
|
||||
t.absent(home.isShared('test_code'))
|
||||
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok')))
|
||||
@@ -32,7 +32,7 @@ test('createAppFolder, share, is shared, unshare, isInitialized, list, getCodePa
|
||||
t.alike(new Set(home.list()), new Set(['foo', 'bar', 'zar']))
|
||||
t.alike(new Set(home.list(true)), new Set(['foo', 'bar']))
|
||||
|
||||
t.alike(path.resolve(home.getCodePath('test_code')), path.resolve(path.join(home.APP_HOME, 'test_code', 'code')))
|
||||
t.alike(path.resolve(home.getCodePath('test_code')), path.resolve(path.join(home.APP_HOME, 'test_code')))
|
||||
|
||||
t.teardown(() => {
|
||||
fs.rmSync(path.join(home.APP_HOME, 'test_code'), { recursive: true })
|
||||
|
||||
@@ -56,7 +56,7 @@ test('e2e', async t => {
|
||||
|
||||
await drive.core.update({ wait: true })
|
||||
|
||||
payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-refs', data: repoName }}))
|
||||
payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-refs', data: repoName } }))
|
||||
const refsRes = await rpc.request('get-refs', payload)
|
||||
t.ok(refsRes)
|
||||
|
||||
|
||||
BIN
test_home.tar.gz
BIN
test_home.tar.gz
Binary file not shown.
Reference in New Issue
Block a user