Merge pull request #16 from dzdidi/advanced-acl

Advanced acl
This commit is contained in:
DZ
2024-01-29 09:47:16 +00:00
committed by GitHub
15 changed files with 922 additions and 68 deletions

View File

@@ -32,27 +32,48 @@ npm nix
See `./result` - for binaries build by nix. To make the available add to path by running `PATH="${PATH:+${PATH}:}${PWD}/result/bin/"` See `./result` - for binaries build by nix. To make the available add to path by running `PATH="${PATH:+${PATH}:}${PWD}/result/bin/"`
## ## Running
All data will be persisted in application directory (default `~/.gitpear`). To change it. Provide environment variable `GIT_PEAR` All data will be persisted in application directory (default `~/.gitpear`). To change it. Provide environment variable `GIT_PEAR`
* `git pear daemon <-s, --start | -k, --stop>` - start or stop daemon * `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 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 [-s, --share] <path>` - 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` or call `gitpear share <path>` later * `git pear init [-s, --share] <path>` - 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` or call `gitpear share <path>` later
* `git pear share <path>` - makes repository sharable * `git pear share <path>` - makes repository sharable
* `git pear unshare <path>` - stop sharing repository * `git pear unshare <path>` - stop sharing repository
* `git pear list [-s, --shared]` - list all or (only shared) repositories * `git pear list [-s, --shared]` - list all or (only shared) repositories
## Usage example ### ACL (for authenticated access to enable support of PUSH)
Please not this is only remote helper and its intention is only to enable direct `clone|fetch|pull` of repository hosted on private computer. Support of `push` capabilities only enabled for authenticated users. Currently supported authentication is based on [NIP98](https://github.com/nostr-protocol/nips/blob/master/98.md).
To start daemon with authenticated support provide environment varibales `GIT_PEAR_AUTH` with value `nip98` and `GIT_PEAR_AUTH_NSEC` with value of your [NIP19 nsec](https://github.com/nostr-protocol/nips/blob/master/19.md).
For example:
```
GIT_PEAR_AUTH=nip98 GIT_PEAR_AUTH_NSEC=nsec.... git pear daemon -s
```
Collaboration is possible however with the following flow between Alice and Bob in a pure peer-to-peer manner of git. 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
### 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
# Examples of usage
## Un authenticated usage example (no push)
Collaboration is possible with the following flow between Alice and Bob in a peer-to-peer manner.
1. Both Alice and Bob have gitpear installed and Alice wants Bob to help her with repo Repo 1. Both Alice and Bob have gitpear installed and Alice wants Bob to help her with repo Repo
2. Alice steps are: 2. Alice steps are:
@@ -94,3 +115,41 @@ git checkout master
git fetch origin git fetch origin
git pull 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.
### Carol steps (as a server of code)
1. Start daemon
* `GIT_PEAR_AUTH_NSEC=<Carol's nsec> GIT_PEAR_AUTH='nip98' git pear daemon -s`
2. Go to repository
* `cd repo`
3. Initialize git pear repository
* `git pear init .`
4. Share repository wit hviben visibility () - (default is `public`)
* `git pear share . <private|public>`
5. Add Daviv as a `contirbutor`.
6. List David's npub as a contributor
* `git pear acl add <David npub>:contributor`
7. Retreive repo url and share it with Dave
* `git pear list -s`
### Dave side (a collaborator for code)
1. Start daemon. This will be needed later for push. Not that no auth or sec are provided which means that push to this place will not be supportedd.
* `git pear daemon -s`
2. Clone repository. Authorization data and type are necesary for server (Carol) to grant corresponding access persmissions
* `GIT_PEAR_AUTH_NSEC=<David's nsec> GIT_PEAR_AUTH='nip98' git clone pear://<Carol's url>/<repo name>`
3. Do the necessary change in separate branch
* `git checkout -b feat/david`
* do change
* `git add .`
* `git commit -s -m 'made by David'`
4. Push branch to origin
* `GIT_PEAR_AUTH_NSEC=<David's nsec> GIT_PEAR_AUTH='nip98' git push origin feat/david`
### Carol steps
1. For Carol the changes will arrive as branch `feat/david` into her `pear`
* `git fetch pear`
2. From there she can do
* `git diff pear/feat/david` or `git pull pear feat/david` ... merge to master and push to `pear`

210
npm-shrinkwrap.json generated
View File

@@ -12,10 +12,11 @@
"dependencies": { "dependencies": {
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"commander": "^11.0.0", "commander": "^11.0.0",
"corestore": "^6.10.1", "corestore": "^6.15.13",
"hyperdrive": "^11.5.3", "hyperdrive": "^11.6.3",
"hyperswarm": "^4.5.1", "hyperswarm": "^4.7.13",
"protomux-rpc": "^1.4.1", "nostr-tools": "^2.1.5",
"protomux-rpc": "^1.5.1",
"random-access-memory": "^6.2.0" "random-access-memory": "^6.2.0"
}, },
"bin": { "bin": {
@@ -239,6 +240,47 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@noble/ciphers": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
"integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -274,6 +316,53 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
]
},
"node_modules/@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"dependencies": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32/node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"dependencies": {
"@noble/hashes": "1.3.1"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"dependencies": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@types/istanbul-lib-coverage": { "node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -3008,6 +3097,36 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/nostr-tools": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.1.5.tgz",
"integrity": "sha512-Gug/j54YGQ0ewB09dZW3mS9qfXWFlcOQMlyb1MmqQsuNO/95mfNOQSBi+jZ61O++Y+jG99SzAUPFLopUsKf0MA==",
"dependencies": {
"@noble/ciphers": "0.2.0",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1"
},
"optionalDependencies": {
"nostr-wasm": "v0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/nostr-wasm": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
"optional": true
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4390,7 +4509,7 @@
"version": "5.3.3", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true, "devOptional": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -4838,6 +4957,31 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"@noble/ciphers": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
"integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw=="
},
"@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"requires": {
"@noble/hashes": "1.3.2"
},
"dependencies": {
"@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="
}
}
},
"@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="
},
"@nodelib/fs.scandir": { "@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4864,6 +5008,40 @@
"fastq": "^1.6.0" "fastq": "^1.6.0"
} }
}, },
"@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
},
"@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"requires": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
},
"dependencies": {
"@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"requires": {
"@noble/hashes": "1.3.1"
}
}
}
},
"@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"requires": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
}
},
"@types/istanbul-lib-coverage": { "@types/istanbul-lib-coverage": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -6982,6 +7160,26 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
}, },
"nostr-tools": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.1.5.tgz",
"integrity": "sha512-Gug/j54YGQ0ewB09dZW3mS9qfXWFlcOQMlyb1MmqQsuNO/95mfNOQSBi+jZ61O++Y+jG99SzAUPFLopUsKf0MA==",
"requires": {
"@noble/ciphers": "0.2.0",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1",
"nostr-wasm": "v0.1.0"
}
},
"nostr-wasm": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
"optional": true
},
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -8028,7 +8226,7 @@
"version": "5.3.3", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true "devOptional": true
}, },
"udx-native": { "udx-native": {
"version": "1.7.12", "version": "1.7.12",

View File

@@ -46,6 +46,7 @@
"corestore": "^6.15.13", "corestore": "^6.15.13",
"hyperdrive": "^11.6.3", "hyperdrive": "^11.6.3",
"hyperswarm": "^4.7.13", "hyperswarm": "^4.7.13",
"nostr-tools": "^2.1.5",
"protomux-rpc": "^1.5.1", "protomux-rpc": "^1.5.1",
"random-access-memory": "^6.2.0" "random-access-memory": "^6.2.0"
} }

120
src/acl.js Normal file
View File

@@ -0,0 +1,120 @@
const home = require('./home')
const fs = require('fs')
const ROLES = {
admin: {
description: 'Read and write to all branches',
},
contributor: {
description: 'Read and write to all branches except protected ones',
},
viewer: {
description: 'Read all branches',
},
}
const DEFAULT_ACL = {
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 revokeAccessFromUser (repoName, user) {
const acl = getACL(repoName)
delete acl.ACL[user]
setACL(repoName, acl)
}
function addProtectedBranch (repoName, branch) {
const acl = getACL(repoName)
if (acl.protectedBranches.includes(branch)) throw new Error(`${branch} is already protected`)
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,
revokeAccessFromUser,
}

20
src/auth/index.js Normal file
View File

@@ -0,0 +1,20 @@
function getId(data) {
if (!process.env.GIT_PEAR_AUTH) return payload
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
if (process.env.GIT_PEAR_AUTH === 'nip98') {
const nip98 = require('./nip98')
return nip98.getToken(payload)
}
}
module.exports = {
getId,
getToken
}

28
src/auth/nip98.js Normal file
View File

@@ -0,0 +1,28 @@
const { nip98, nip19, finalizeEvent } = require('nostr-tools')
async function getToken({ url, method, data }) {
const { data: sK } = nip19.decode(process.env.GIT_PEAR_AUTH_NSEC)
return nip98.getToken(
url,
method,
(e) => finalizeEvent(e, sK),
false,
data
)
}
// FIXME
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 {
...event,
userId: nip19.npubEncode(event.pubkey)
}
}
module.exports = {
getId,
getToken
}

View File

@@ -9,6 +9,7 @@ const path = require('path')
const fs = require('fs') const fs = require('fs')
const home = require('./home') const home = require('./home')
const acl = require('./acl')
const git = require('./git') const git = require('./git')
const pkg = require('../package.json') const pkg = require('../package.json')
@@ -32,19 +33,28 @@ program
const name = fullPath.split(path.sep).pop() const name = fullPath.split(path.sep).pop()
if ((home.isInitialized(name))) { if ((home.isInitialized(name))) {
console.error(`${name} is already initialized`) console.error(`${name} is already initialized`)
await git.addRemote(name)
console.log(`Added git remote for "${name}" as "pear"`)
process.exit(1) process.exit(1)
} }
home.createAppFolder(name) try {
console.log(`Added project "${name}" to gitpear`) home.createAppFolder(name)
await git.createBareRepo(name) console.log(`Added project "${name}" to gitpear`)
console.log(`Created bare repo for "${name}"`) } catch (e) { }
await git.addRemote(name) try {
console.log(`Added git remote for "${name}" as "pear"`) await git.createBareRepo(name)
console.log(`Created bare repo for "${name}"`)
} catch (e) { }
try {
await git.addRemote(name)
console.log(`Added git remote for "${name}" as "pear"`)
} catch (e) { }
if (options.share) { if (options.share) {
home.shareAppFolder(name) try { home.shareAppFolder(name) } catch (e) { }
await git.push() try { acl.setACL(name) } catch (e) { }
try { await git.push() } catch (e) { }
console.log(`Shared "${name}" project`) console.log(`Shared "${name}" project`)
} }
}) })
@@ -53,25 +63,153 @@ program
.command('share') .command('share')
.description('share a gitpear repo') .description('share a gitpear repo')
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.')) .addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
.action(async (p, options) => { .addArgument(new commander.Argument('[v]', 'visibility of the repo').default('public'))
const name = path.resolve(p).split(path.sep).pop() .action(async (p, v, options) => {
if ((home.isInitialized(name))) { const fullPath = path.resolve(p)
home.shareAppFolder(name) if (!fs.existsSync(path.join(fullPath, '.git'))) {
await git.push() console.error('Not a git repo')
console.log(`Shared "${name}" project`) process.exit(1)
}
const name = fullPath.split(path.sep).pop()
if (!home.isInitialized(name)) {
console.error(`${name} is not initialized`)
process.exit(1)
}
try { home.shareAppFolder(name) } catch (e) { }
try { acl.setACL(name, { visibility: v }) } catch (e) { }
try { await git.push() } catch (e) { }
console.log(`Shared "${name}" project, as ${v} repo`)
return
})
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')
.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)
}
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 return
} }
console.error(`${name} is not initialized`) if (a === 'list') {
process.exit(1) 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)
}
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}`)
return
}
}) })
program program
.command('unshare') .command('unshare')
.description('unshare a gitpear repo') .description('unshare a gitpear repo')
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.')) .addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
.action((p, options) => { .action((p, options) => {
const name = path.resolve(p).split(path.sep).pop() 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))) { if ((home.isInitialized(name))) {
home.unshareAppFolder(name) home.unshareAppFolder(name)
console.log(`Unshared "${name}" project`) console.log(`Unshared "${name}" project`)

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
const { spawn } = require('child_process')
const ProtomuxRPC = require('protomux-rpc') const ProtomuxRPC = require('protomux-rpc')
const RAM = require('random-access-memory') const RAM = require('random-access-memory')
@@ -9,6 +10,11 @@ const Hyperdrive = require('hyperdrive')
const crypto = require('hypercore-crypto') const crypto = require('hypercore-crypto')
const git = require('./git.js') const git = require('./git.js')
const home = require('./home')
const auth = require('./auth')
const acl = require('./acl')
const fs = require('fs')
const url = process.argv[3] const url = process.argv[3]
const matches = url.match(/pear:\/\/([a-f0-9]{64})\/(.*)/) const matches = url.match(/pear:\/\/([a-f0-9]{64})\/(.*)/)
@@ -22,7 +28,12 @@ const targetKey = matches[1]
const repoName = matches[2] const repoName = matches[2]
const store = new Corestore(RAM) const store = new Corestore(RAM)
const swarm = new Hyperswarm() const swarm = new Hyperswarm({ keypair: home.getKeyPair() })
if (!home.isDaemonRunning()) {
console.error('Please start git pear daemon')
process.exit(1)
}
swarm.join(crypto.discoveryKey(Buffer.from(targetKey, 'hex')), { server: false }) swarm.join(crypto.discoveryKey(Buffer.from(targetKey, 'hex')), { server: false })
@@ -30,7 +41,12 @@ swarm.on('connection', async (socket) => {
store.replicate(socket) store.replicate(socket)
const rpc = new ProtomuxRPC(socket) const rpc = new ProtomuxRPC(socket)
const reposRes = await rpc.request('get-repos') let payload = { body: { url, method: 'get-repos' } }
if (process.env.GIT_PEAR_AUTH) {
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()) const repositories = JSON.parse(reposRes.toString())
if (!repositories) { if (!repositories) {
console.error('Failed to retrieve repositories') console.error('Failed to retrieve repositories')
@@ -56,27 +72,87 @@ swarm.on('connection', async (socket) => {
await drive.core.update({ wait: true }) await drive.core.update({ wait: true })
const refsRes = await rpc.request('get-refs', Buffer.from(repoName)) payload = { body: { url, method: 'get-refs', data: repoName }}
if (process.env.GIT_PEAR_AUTH) {
payload.header = await auth.getToken(payload.body)
}
const refsRes = await rpc.request('get-refs', Buffer.from(JSON.stringify(payload)))
await talkToGit(JSON.parse(refsRes.toString()), drive) let commit
try {
commit = await git.getCommit()
} catch (e) { }
await talkToGit(JSON.parse(refsRes.toString()), drive, repoName, rpc, commit)
}) })
async function talkToGit (refs, drive) { async function talkToGit (refs, drive, repoName, rpc, commit) {
for (const ref in refs) {
console.warn(refs[ref] + '\t' + ref)
}
process.stdin.setEncoding('utf8') process.stdin.setEncoding('utf8')
const didFetch = false const didFetch = false
process.stdin.on('readable', async function () { process.stdin.on('readable', async function () {
const chunk = process.stdin.read() const chunk = process.stdin.read()
if (chunk === 'capabilities\n') { if (chunk === 'capabilities\n') {
process.stdout.write('list\n')
process.stdout.write('push\n')
process.stdout.write('fetch\n\n') process.stdout.write('fetch\n\n')
} else if (chunk === 'list\n') { } else if (chunk && chunk.search(/^push/) !== -1) {
const [_command, path] = chunk.split(' ')
let [src, dst] = path.split(':')
const isDelete = !src
const isForce = src.startsWith('+')
if (!home.isShared(repoName)) {
home.shareAppFolder(name)
}
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'
} else if (isForce) {
console.warn('To', url)
await git.push(src, isForce)
src = src.replace('+', '')
method = 'f-push'
} else {
console.warn('To', url)
await git.push(src)
method = 'push'
}
const publicKey = home.readPk()
let payload = { body: {
url: `pear://${publicKey}/${repoName}`,
data: `${dst}#${commit}`,
method
} }
if (process.env.GIT_PEAR_AUTH) {
payload.header = await auth.getToken(payload.body)
}
const res = await rpc.request(method, Buffer.from(JSON.stringify(payload)))
process.stdout.write('\n\n')
process.exit(0)
} else if (chunk && chunk.search(/^list/) !== -1) { // list && list for-push
for (const ref in refs) {
console.warn(refs[ref] + '\t' + ref)
}
Object.keys(refs).forEach(function (branch, i) { Object.keys(refs).forEach(function (branch, i) {
process.stdout.write(refs[branch] + ' ' + branch + '\n') process.stdout.write(refs[branch] + ' ' + branch + '\n')
}) })
process.stdout.write('\n') process.stdout.write('\n')
} else if (chunk && chunk.search(/^fetch/) !== -1) { } else if (chunk && chunk.search(/^fetch/) !== -1) {
for (const ref in refs) {
console.warn(refs[ref] + '\t' + ref)
}
const lines = chunk.split(/\n/).filter(l => l !== '') const lines = chunk.split(/\n/).filter(l => l !== '')
const targets = [] const targets = []

View File

@@ -1,6 +1,25 @@
const { getCodePath } = require('./home') const { getCodePath } = require('./home')
const { spawn } = require('child_process') const { spawn } = require('child_process')
async function getCommit () {
return await new Promise((resolve, reject) => {
const process = spawn('git', ['rev-parse', 'HEAD'])
let outBuffer = Buffer.from('')
process.stdout.on('data', data => {
outBuffer = Buffer.concat([outBuffer, data])
})
let errBuffer = Buffer.from('')
process.stderr.on('err', data => {
errBuffer = Buffer.concat([errBuffer, data])
})
process.on('close', code => {
return code === 0 ? resolve(outBuffer.toString().replace('\n', '')) : reject(errBuffer)
})
})
}
async function lsPromise (url) { async function lsPromise (url) {
const ls = spawn('git', ['ls-remote', url]) const ls = spawn('git', ['ls-remote', url])
const res = {} const res = {}
@@ -31,8 +50,10 @@ async function addRemote (name) {
return await doGit(init) return await doGit(init)
} }
async function push (branch = 'master') { async function push (branch = 'master', force = false) {
const push = spawn('git', ['push', 'pear', branch]) const args = ['push', 'pear', branch]
if (force) args.push('-f')
const push = spawn('git', args)
return await doGit(push) return await doGit(push)
} }
@@ -180,4 +201,4 @@ async function unpackStream (packStream) {
}) })
} }
module.exports = { lsPromise, uploadPack, unpackFile, unpackStream, createBareRepo, addRemote, push } module.exports = { lsPromise, uploadPack, unpackFile, unpackStream, createBareRepo, addRemote, push, getCommit }

View File

@@ -14,6 +14,11 @@ 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`)
} }
@@ -90,6 +95,10 @@ function getDaemonPid () {
} }
} }
function isDaemonRunning () {
return fs.existsSync(`${APP_HOME}/.daemon.pid`)
}
function removeDaemonPid () { function removeDaemonPid () {
try { try {
fs.unlinkSync(`${APP_HOME}/.daemon.pid`) fs.unlinkSync(`${APP_HOME}/.daemon.pid`)
@@ -114,5 +123,7 @@ module.exports = {
getErrStream, getErrStream,
storeDaemonPid, storeDaemonPid,
getDaemonPid, getDaemonPid,
removeDaemonPid isDaemonRunning,
removeDaemonPid,
getACLFilePath
} }

135
src/rpc.js Normal file → Executable file
View File

@@ -1,4 +1,8 @@
const ProtomuxRPC = require('protomux-rpc') 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 { module.exports = class RPC {
constructor (announcedRefs, repositories, drives) { constructor (announcedRefs, repositories, drives) {
@@ -16,23 +20,138 @@ module.exports = class RPC {
// for example check of peerInfo.publicKey is in a list of allowed keys // for example check of peerInfo.publicKey is in a list of allowed keys
// 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
rpc.respond('get-repos', req => this.getReposHandler(req)) /* -- PULL HANDLERS -- */
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))
if (process.env.GIT_PEAR_AUTH) {
/* -- PUSH HANDLERS -- */
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 this.connections[peerInfo.publicKey] = rpc
} }
getReposHandler (_req) { async getReposHandler (publicKey, req) {
const { branch, url, userId } = await this.parseReq(publicKey, req)
const res = {} const res = {}
for (const repo in this.repositories) { for (const repoName in this.repositories) {
res[repo] = this.drives[repo].key.toString('hex') // 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)) return Buffer.from(JSON.stringify(res))
} }
getRefsHandler (req) { async getRefsHandler (publicKey, req) {
const res = this.repositories[req.toString()] const { repoName, branch, url, userId } = await this.parseReq(publicKey, req)
const res = this.repositories[repoName]
return Buffer.from(JSON.stringify(res)) 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
}
async authenticate (publicKey, request) {
if (!process.env.GIT_PEAR_AUTH) return publicKey.toString('hex')
if (!request.header) throw new Error('You are not allowed to access this repo')
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'])
})
})

View File

@@ -9,33 +9,33 @@ test('getAppHome', t => {
}) })
test('createAppFolder, share, is shared, unshare, isInitialized, list, getCodePath', t => { test('createAppFolder, share, is shared, unshare, isInitialized, list, getCodePath', t => {
home.createAppFolder('test') home.createAppFolder('test_code')
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test', 'code'))) t.ok(fs.existsSync(path.join(home.APP_HOME, 'test_code', 'code')))
t.absent(home.isShared('test')) t.absent(home.isShared('test_code'))
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test', '.git-daemon-export-ok'))) t.absent(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok')))
home.shareAppFolder('test') home.shareAppFolder('test_code')
t.ok(home.isShared('test')) t.ok(home.isShared('test_code'))
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test', '.git-daemon-export-ok'))) t.ok(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok')))
home.unshareAppFolder('test') home.unshareAppFolder('test_code')
t.absent(home.isShared('test')) t.absent(home.isShared('test_code'))
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test', '.git-daemon-export-ok'))) t.absent(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok')))
t.absent(home.isInitialized('test')) t.absent(home.isInitialized('test_code'))
t.ok(home.isInitialized('foo')) t.ok(home.isInitialized('foo'))
t.alike(new Set(home.list()), new Set(['foo', 'bar', 'zar'])) t.alike(new Set(home.list()), new Set(['foo', 'bar', 'zar']))
t.alike(new Set(home.list(true)), new Set(['foo', 'bar'])) t.alike(new Set(home.list(true)), new Set(['foo', 'bar']))
t.alike(path.resolve(home.getCodePath('test')), path.resolve(path.join(home.APP_HOME, 'test', 'code'))) t.alike(path.resolve(home.getCodePath('test_code')), path.resolve(path.join(home.APP_HOME, 'test_code', 'code')))
t.teardown(() => { t.teardown(() => {
fs.rmSync(path.join(home.APP_HOME, 'test', 'code'), { recursive: true }) fs.rmSync(path.join(home.APP_HOME, 'test_code'), { recursive: true })
}) })
}) })

View File

@@ -40,7 +40,10 @@ 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') const repoName = 'foo'
const url = `${serverSwarm.keyPair.publicKey.toString('hex')}/${repoName}`
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 +56,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: repoName }}))
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]

Binary file not shown.