OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-sync / test / push-acl.test.js

103 lines · javascript ref:
// Push ACL gating in the swarm connection handler (issue #37, item 2).
//
// git-upload-pack (read) is always open; git-receive-pack (push) is
// rejected unless the `authorize` hook approves it. Drives the real
// _onConnection logic with a fake conn + stubbed _spawnGitService so no
// swarm or real git process is involved.

const test = require('brittle');
const fs = require('fs');
const os = require('os');
const path = require('path');
const EventEmitter = require('events');

const HyperSync = require('../');

function makeReposDir () {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hs-acl-'));
  fs.mkdirSync(path.join(dir, 'r.git'), { recursive: true }); // existsSync(repoPath) → true
  return dir;
}

function fakeConn () {
  const c = new EventEmitter();
  c.destroyed = false;
  c.paused = false;
  c.remotePublicKey = Buffer.from('ab'.repeat(32), 'hex');
  c.destroy = () => { c.destroyed = true; c.emit('close'); };
  c.pause = () => { c.paused = true; };
  c.resume = () => { c.paused = false; };
  c.write = () => true;
  return c;
}

// Minimal `this` with just what _onConnection touches — exercises the real
// gating branch without constructing a swarm-backed HyperSync.
function ctx (reposDir, authorize, spawned) {
  return {
    _reposDir: reposDir,
    _topics: new Map([['r', {}]]),
    _authorize: authorize,
    emit () {},
    _spawnGitService (conn, service) { spawned.push(service); }
  };
}

function header (service, repo) {
  return Buffer.from(service + '\0' + repo + '\0');
}

const tick = () => new Promise((r) => setTimeout(r, 30));

test('upload-pack (read) is always served — no authorize hook needed', async (t) => {
  const reposDir = makeReposDir();
  const spawned = [];
  const conn = fakeConn();
  HyperSync.prototype._onConnection.call(ctx(reposDir, null, spawned), conn, {});
  conn.emit('data', header('git-upload-pack', 'r'));
  await tick();
  t.alike(spawned, ['git-upload-pack'], 'upload-pack spawned');
  t.absent(conn.destroyed, 'read connection not destroyed');
});

test('receive-pack (push) is rejected when no authorize hook is configured (back-compat)', async (t) => {
  const reposDir = makeReposDir();
  const spawned = [];
  const conn = fakeConn();
  HyperSync.prototype._onConnection.call(ctx(reposDir, null, spawned), conn, {});
  conn.emit('data', header('git-receive-pack', 'r'));
  await tick();
  t.alike(spawned, [], 'nothing spawned');
  t.ok(conn.destroyed, 'push connection destroyed (read-only default)');
});

test('receive-pack is served when authorize approves', async (t) => {
  const reposDir = makeReposDir();
  const spawned = [];
  const seen = [];
  const authorize = async (info) => { seen.push(info); return true; };
  const conn = fakeConn();
  HyperSync.prototype._onConnection.call(ctx(reposDir, authorize, spawned), conn, {});
  conn.emit('data', header('git-receive-pack', 'r'));
  await tick();
  t.is(seen.length, 1, 'authorize was consulted');
  t.is(seen[0].service, 'git-receive-pack', 'authorize got the service');
  t.is(seen[0].repo, 'r', 'authorize got the repo');
  t.ok(seen[0].remotePublicKey && seen[0].remotePublicKey.equals(conn.remotePublicKey),
    'authorize got the peer noise key');
  t.alike(spawned, ['git-receive-pack'], 'receive-pack spawned after approval');
  t.absent(conn.destroyed, 'approved push not destroyed');
});

test('receive-pack is rejected when authorize denies', async (t) => {
  const reposDir = makeReposDir();
  const spawned = [];
  const authorize = async () => false;
  const conn = fakeConn();
  HyperSync.prototype._onConnection.call(ctx(reposDir, authorize, spawned), conn, {});
  conn.emit('data', header('git-receive-pack', 'r'));
  await tick();
  t.alike(spawned, [], 'nothing spawned on denial');
  t.ok(conn.destroyed, 'denied push destroyed');
});