| … | … | @@ -0,0 +1,102 @@ |
| 1 | // Push ACL gating in the swarm connection handler (issue #37, item 2). |
| 2 | // |
| 3 | // git-upload-pack (read) is always open; git-receive-pack (push) is |
| 4 | // rejected unless the `authorize` hook approves it. Drives the real |
| 5 | // _onConnection logic with a fake conn + stubbed _spawnGitService so no |
| 6 | // swarm or real git process is involved. |
| 7 | |
| 8 | const test = require('brittle'); |
| 9 | const fs = require('fs'); |
| 10 | const os = require('os'); |
| 11 | const path = require('path'); |
| 12 | const EventEmitter = require('events'); |
| 13 | |
| 14 | const HyperSync = require('../'); |
| 15 | |
| 16 | function makeReposDir () { |
| 17 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hs-acl-')); |
| 18 | fs.mkdirSync(path.join(dir, 'r.git'), { recursive: true }); // existsSync(repoPath) → true |
| 19 | return dir; |
| 20 | } |
| 21 | |
| 22 | function fakeConn () { |
| 23 | const c = new EventEmitter(); |
| 24 | c.destroyed = false; |
| 25 | c.paused = false; |
| 26 | c.remotePublicKey = Buffer.from('ab'.repeat(32), 'hex'); |
| 27 | c.destroy = () => { c.destroyed = true; c.emit('close'); }; |
| 28 | c.pause = () => { c.paused = true; }; |
| 29 | c.resume = () => { c.paused = false; }; |
| 30 | c.write = () => true; |
| 31 | return c; |
| 32 | } |
| 33 | |
| 34 | // Minimal `this` with just what _onConnection touches — exercises the real |
| 35 | // gating branch without constructing a swarm-backed HyperSync. |
| 36 | function ctx (reposDir, authorize, spawned) { |
| 37 | return { |
| 38 | _reposDir: reposDir, |
| 39 | _topics: new Map([['r', {}]]), |
| 40 | _authorize: authorize, |
| 41 | emit () {}, |
| 42 | _spawnGitService (conn, service) { spawned.push(service); } |
| 43 | }; |
| 44 | } |
| 45 | |
| 46 | function header (service, repo) { |
| 47 | return Buffer.from(service + '\0' + repo + '\0'); |
| 48 | } |
| 49 | |
| 50 | const tick = () => new Promise((r) => setTimeout(r, 30)); |
| 51 | |
| 52 | test('upload-pack (read) is always served — no authorize hook needed', async (t) => { |
| 53 | const reposDir = makeReposDir(); |
| 54 | const spawned = []; |
| 55 | const conn = fakeConn(); |
| 56 | HyperSync.prototype._onConnection.call(ctx(reposDir, null, spawned), conn, {}); |
| 57 | conn.emit('data', header('git-upload-pack', 'r')); |
| 58 | await tick(); |
| 59 | t.alike(spawned, ['git-upload-pack'], 'upload-pack spawned'); |
| 60 | t.absent(conn.destroyed, 'read connection not destroyed'); |
| 61 | }); |
| 62 | |
| 63 | test('receive-pack (push) is rejected when no authorize hook is configured (back-compat)', async (t) => { |
| 64 | const reposDir = makeReposDir(); |
| 65 | const spawned = []; |
| 66 | const conn = fakeConn(); |
| 67 | HyperSync.prototype._onConnection.call(ctx(reposDir, null, spawned), conn, {}); |
| 68 | conn.emit('data', header('git-receive-pack', 'r')); |
| 69 | await tick(); |
| 70 | t.alike(spawned, [], 'nothing spawned'); |
| 71 | t.ok(conn.destroyed, 'push connection destroyed (read-only default)'); |
| 72 | }); |
| 73 | |
| 74 | test('receive-pack is served when authorize approves', async (t) => { |
| 75 | const reposDir = makeReposDir(); |
| 76 | const spawned = []; |
| 77 | const seen = []; |
| 78 | const authorize = async (info) => { seen.push(info); return true; }; |
| 79 | const conn = fakeConn(); |
| 80 | HyperSync.prototype._onConnection.call(ctx(reposDir, authorize, spawned), conn, {}); |
| 81 | conn.emit('data', header('git-receive-pack', 'r')); |
| 82 | await tick(); |
| 83 | t.is(seen.length, 1, 'authorize was consulted'); |
| 84 | t.is(seen[0].service, 'git-receive-pack', 'authorize got the service'); |
| 85 | t.is(seen[0].repo, 'r', 'authorize got the repo'); |
| 86 | t.ok(seen[0].remotePublicKey && seen[0].remotePublicKey.equals(conn.remotePublicKey), |
| 87 | 'authorize got the peer noise key'); |
| 88 | t.alike(spawned, ['git-receive-pack'], 'receive-pack spawned after approval'); |
| 89 | t.absent(conn.destroyed, 'approved push not destroyed'); |
| 90 | }); |
| 91 | |
| 92 | test('receive-pack is rejected when authorize denies', async (t) => { |
| 93 | const reposDir = makeReposDir(); |
| 94 | const spawned = []; |
| 95 | const authorize = async () => false; |
| 96 | const conn = fakeConn(); |
| 97 | HyperSync.prototype._onConnection.call(ctx(reposDir, authorize, spawned), conn, {}); |
| 98 | conn.emit('data', header('git-receive-pack', 'r')); |
| 99 | await tick(); |
| 100 | t.alike(spawned, [], 'nothing spawned on denial'); |
| 101 | t.ok(conn.destroyed, 'denied push destroyed'); |
| 102 | }); |