OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / require-identity.test.js

94 lines · javascript ref:
/**
 * requireIdentity — the gate for SELF-SCOPED reads.
 *
 * A newly TOFU-registered user gets `['read']` by default
 * (handlers/users.js:457). requireWriter passes only 'admin' or 'write', so
 * gating a self-scoped handler with it locks a read-only identity out of its
 * OWN data. That is what stopped a fresh identity from listing its own devices
 * and copying its link invite: the daemon's rpc-audit-log recorded
 *   {"method":"list-my-devices","trust":"authenticated","error":"writer permission required"}
 * alongside anonymous denials of the same method.
 *
 * requireIdentity must sit strictly between anonymous and writer:
 *   - anonymous  -> DENIED (does not reopen the #12 default-deny surface)
 *   - revoked    -> DENIED (#14: a revoked identity keeps its perms, so the
 *                   revocation gate must run before/without a perms check)
 *   - read-only  -> ALLOWED  <- the regression this fixes
 *   - uds        -> ALLOWED (local transport is trusted)
 */

import test from 'brittle'

import { requireIdentity, requireWriter } from '../lib/auth/check.js'

const PUB = 'a'.repeat(64)

// Minimal registry stand-in. isIdentityRevoked only calls registry.isRevoked;
// requireWriter additionally calls getUserByIdentity for the perms lookup.
function makeRegistry ({ perms = ['read'], revoked = false } = {}) {
  return {
    async isRevoked () { return revoked },
    async getUserByIdentity () { return { userId: 'u1', globalPermissions: perms } }
  }
}

function ctxFor (publicKey) {
  return { transport: 'http', identity: publicKey ? { publicKey } : null }
}

test('read-only identity is ALLOWED (the regression)', async (t) => {
  await t.execution(
    requireIdentity(ctxFor(PUB), makeRegistry({ perms: ['read'] })),
    'a default ["read"] user reaches its own self-scoped data'
  )
})

test('the same read-only identity is REJECTED by requireWriter', async (t) => {
  // Documents precisely why the gate had to change rather than the perms.
  await t.exception(
    requireWriter(ctxFor(PUB), makeRegistry({ perms: ['read'] })),
    /writer permission required/,
    'requireWriter denies a read-only user — the observed audit-log error'
  )
})

test('anonymous is REJECTED — #12 default-deny is not reopened', async (t) => {
  await t.exception(
    requireIdentity(ctxFor(null), makeRegistry()),
    /authentication required/,
    'no identity means no "my" anything'
  )
})

test('revoked identity is REJECTED even though its perms would pass', async (t) => {
  // #14: revocation must be denied BEFORE perms are consulted, because a
  // revoked identity retains its User record and globalPermissions.
  await t.exception(
    requireIdentity(ctxFor(PUB), makeRegistry({ perms: ['admin'], revoked: true })),
    /identity revoked/,
    'revocation gate still runs'
  )
})

test('uds transport is trusted', async (t) => {
  await t.execution(
    requireIdentity({ transport: 'uds', identity: null }, makeRegistry()),
    'local UDS bypasses, matching requireWriter'
  )
})

test('intrinsic owner passes without a registry record', async (t) => {
  await t.execution(
    requireIdentity({ transport: 'http', identity: { publicKey: PUB, ownerMatched: true } }, null),
    'ownerMatched short-circuits, matching requireWriter'
  )
})

test('missing registry is a 500, not a silent allow', async (t) => {
  await t.exception(
    requireIdentity(ctxFor(PUB), null),
    /identity registry not available/,
    'cannot verify revocation => refuse rather than pass'
  )
})