OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-identity / test / security.test.js

327 lines · javascript ref:
import test from 'brittle'
import b4a from 'b4a'
import { createApplyFunction } from '../lib/auth/identity-apply.js'
import { encodeOp, OpTypes } from '../lib/auth/identity-operations.js'

/**
 * Security tests for hyper-identity contract validation.
 *
 * These tests verify that rogue Autobase writers cannot inject
 * unauthorized operations. Each test creates a mock Hyperbee view
 * and simulates operations from different writers.
 */

// Helper: mock Hyperbee batch with in-memory Map
function mockView () {
  const data = new Map()
  return {
    data,
    batch: () => ({
      put: async (k, v) => data.set(k, { value: v }),
      get: async (k) => data.get(k) || null,
      del: async (k) => data.delete(k),
      flush: async () => {},
      close: async () => {},
      createReadStream: (opts) => ({
        [Symbol.asyncIterator]: async function * () {
          for (const [k, v] of data) {
            if (opts.gt && k <= opts.gt) continue
            if (opts.lt && k >= opts.lt) continue
            if (opts.limit && --opts.limit < 0) return
            yield { key: k, value: v.value }
          }
        }
      })
    })
  }
}

// Helper: create a node as if appended by a specific writer
function nodeFrom (writerKeyHex, op) {
  return {
    value: encodeOp(op),
    from: { key: b4a.from(writerKeyHex, 'hex') }
  }
}

const ADMIN_KEY = 'aa'.repeat(32)
const USER_KEY = 'bb'.repeat(32)
const ROGUE_KEY = 'cc'.repeat(32)

test('contract: bootstrap — first writer can create admin user', async (t) => {
  const apply = createApplyFunction({ contractValidation: true })
  const view = mockView()

  // Empty registry: first writer is trusted
  const nodes = [nodeFrom(ADMIN_KEY, {
    type: OpTypes.CREATE_USER,
    userId: 'admin-user',
    username: 'admin',
    name: 'Admin',
    globalPermissions: ['read', 'write', 'admin'],
    identities: [ADMIN_KEY],
    createdAt: Date.now()
  })]

  await apply(nodes, view, {})

  t.ok(view.data.has('user:admin-user'), 'admin user created')
  const user = view.data.get('user:admin-user').value
  t.ok(user.globalPermissions.includes('admin'), 'has admin permissions')
})

test('contract: admin can upgrade permissions', async (t) => {
  const apply = createApplyFunction({ contractValidation: true })
  const view = mockView()

  // Setup: create admin user with writer key linked
  view.data.set('user:admin-user', {
    value: {
      userId: 'admin-user',
      globalPermissions: ['read', 'write', 'admin'],
      identities: [ADMIN_KEY]
    }
  })
  view.data.set('pubkey:' + ADMIN_KEY, { value: { identityId: ADMIN_KEY } })
  view.data.set('user-identity:' + ADMIN_KEY, { value: { userId: 'admin-user' } })

  // Setup: create regular user
  view.data.set('user:regular-user', {
    value: {
      userId: 'regular-user',
      globalPermissions: ['read'],
      identities: [USER_KEY]
    }
  })

  // Admin upgrades regular user — should succeed
  const nodes = [nodeFrom(ADMIN_KEY, {
    type: OpTypes.UPGRADE_PERMISSIONS,
    userId: 'regular-user',
    resourceName: '*',
    permissions: ['read', 'write', 'admin']
  })]

  await apply(nodes, view, {})

  const user = view.data.get('user:regular-user').value
  t.ok(user.globalPermissions.includes('admin'), 'permissions upgraded by admin')
})

test('contract: non-admin CANNOT upgrade permissions', async (t) => {
  const apply = createApplyFunction({ contractValidation: true })
  const view = mockView()

  // Setup: admin exists
  view.data.set('user:admin-user', {
    value: { userId: 'admin-user', globalPermissions: ['read', 'write', 'admin'], identities: [ADMIN_KEY] }
  })
  view.data.set('pubkey:' + ADMIN_KEY, { value: { identityId: ADMIN_KEY } })
  view.data.set('user-identity:' + ADMIN_KEY, { value: { userId: 'admin-user' } })

  // Setup: regular user
  view.data.set('user:regular-user', {
    value: { userId: 'regular-user', globalPermissions: ['read'], identities: [USER_KEY] }
  })
  view.data.set('pubkey:' + USER_KEY, { value: { identityId: USER_KEY } })
  view.data.set('user-identity:' + USER_KEY, { value: { userId: 'regular-user' } })

  // Regular user tries to upgrade themselves — should be BLOCKED
  const nodes = [nodeFrom(USER_KEY, {
    type: OpTypes.UPGRADE_PERMISSIONS,
    userId: 'regular-user',
    resourceName: '*',
    permissions: ['read', 'write', 'admin']
  })]

  await apply(nodes, view, {})

  const user = view.data.get('user:regular-user').value
  t.alike(user.globalPermissions, ['read'], 'permissions NOT upgraded — contract blocked it')
})

test('contract: rogue writer CANNOT inject admin ops', async (t) => {
  const apply = createApplyFunction({ contractValidation: true })
  const view = mockView()

  // Setup: admin and regular user exist
  view.data.set('user:admin-user', {
    value: { userId: 'admin-user', globalPermissions: ['read', 'write', 'admin'], identities: [ADMIN_KEY] }
  })
  view.data.set('pubkey:' + ADMIN_KEY, { value: { identityId: ADMIN_KEY } })
  view.data.set('user-identity:' + ADMIN_KEY, { value: { userId: 'admin-user' } })

  // Rogue writer (unknown key, no associated user) tries to:
  // 1. Create an admin user
  // 2. Delete an existing user
  // 3. Merge accounts
  const nodes = [
    // CREATE_USER is allowed (but with read-only perms, autoRegister handles admin promotion)
    nodeFrom(ROGUE_KEY, {
      type: OpTypes.CREATE_USER,
      userId: 'rogue-user',
      name: 'Rogue',
      globalPermissions: ['read', 'write', 'admin'],
      identities: [ROGUE_KEY],
      createdAt: Date.now()
    }),
    // DELETE_USER requires admin — should be BLOCKED
    nodeFrom(ROGUE_KEY, {
      type: OpTypes.DELETE_USER,
      userId: 'admin-user'
    }),
    // MERGE_ACCOUNTS requires admin — should be BLOCKED
    nodeFrom(ROGUE_KEY, {
      type: OpTypes.MERGE_ACCOUNTS,
      primaryUserId: 'rogue-user',
      secondaryUserId: 'admin-user'
    })
  ]

  await apply(nodes, view, {})

  // CREATE_USER goes through (it's always allowed)
  t.ok(view.data.has('user:rogue-user'), 'rogue user created (CREATE_USER is open)')

  // But admin still exists (DELETE was blocked)
  t.ok(view.data.has('user:admin-user'), 'admin NOT deleted — contract blocked it')

  // Admin not merged (MERGE was blocked)
  const admin = view.data.get('user:admin-user').value
  t.not(admin.mergedInto, 'rogue-user', 'admin NOT merged — contract blocked it')
})

test('contract: user can update their own profile', async (t) => {
  const apply = createApplyFunction({ contractValidation: true })
  const view = mockView()

  view.data.set('user:alice', {
    value: { userId: 'alice', name: 'Alice', globalPermissions: ['read'], identities: [USER_KEY] }
  })
  view.data.set('pubkey:' + USER_KEY, { value: { identityId: USER_KEY } })
  view.data.set('user-identity:' + USER_KEY, { value: { userId: 'alice' } })

  // Alice updates her own name — should succeed
  const nodes = [nodeFrom(USER_KEY, {
    type: OpTypes.UPDATE_USER,
    userId: 'alice',
    name: 'Alice Smith'
  })]

  await apply(nodes, view, {})

  const user = view.data.get('user:alice').value
  t.is(user.name, 'Alice Smith', 'self-update succeeded')
})

test('contract: user CANNOT update another user', async (t) => {
  const apply = createApplyFunction({ contractValidation: true })
  const view = mockView()

  view.data.set('user:alice', {
    value: { userId: 'alice', name: 'Alice', globalPermissions: ['read'], identities: [USER_KEY] }
  })
  view.data.set('pubkey:' + USER_KEY, { value: { identityId: USER_KEY } })
  view.data.set('user-identity:' + USER_KEY, { value: { userId: 'alice' } })

  view.data.set('user:bob', {
    value: { userId: 'bob', name: 'Bob', globalPermissions: ['read'], identities: ['dd'.repeat(32)] }
  })

  // Alice tries to update Bob — should be BLOCKED
  const nodes = [nodeFrom(USER_KEY, {
    type: OpTypes.UPDATE_USER,
    userId: 'bob',
    name: 'Hacked'
  })]

  await apply(nodes, view, {})

  const bob = view.data.get('user:bob').value
  t.is(bob.name, 'Bob', 'Bob NOT modified — contract blocked cross-user update')
})

test('contract: revocation requires admin', async (t) => {
  const apply = createApplyFunction({ contractValidation: true })
  const view = mockView()

  view.data.set('user:admin-user', {
    value: { userId: 'admin-user', globalPermissions: ['read', 'write', 'admin'], identities: [ADMIN_KEY] }
  })
  view.data.set('pubkey:' + ADMIN_KEY, { value: { identityId: ADMIN_KEY } })
  view.data.set('user-identity:' + ADMIN_KEY, { value: { userId: 'admin-user' } })

  view.data.set('user:regular-user', {
    value: { userId: 'regular-user', globalPermissions: ['read'], identities: [USER_KEY] }
  })
  view.data.set('pubkey:' + USER_KEY, { value: { identityId: USER_KEY } })
  view.data.set('user-identity:' + USER_KEY, { value: { userId: 'regular-user' } })

  // Regular user tries to revoke admin's identity — BLOCKED
  const rogueRevoke = [nodeFrom(USER_KEY, {
    type: OpTypes.REVOKE_IDENTITY,
    identityId: ADMIN_KEY,
    revokedAt: Date.now(),
    reason: 'hostile takeover'
  })]

  await apply(rogueRevoke, view, {})
  t.not(view.data.has('revoked:' + ADMIN_KEY), 'admin identity NOT revoked by non-admin')

  // Admin revokes regular user's identity — succeeds
  const adminRevoke = [nodeFrom(ADMIN_KEY, {
    type: OpTypes.REVOKE_IDENTITY,
    identityId: USER_KEY,
    revokedAt: Date.now(),
    reason: 'compromised'
  })]

  await apply(adminRevoke, view, {})
  t.ok(view.data.has('revoked:' + USER_KEY), 'identity revoked by admin')
})

test('contract: disabled by default (backwards compat)', async (t) => {
  // Default: no contractValidation option
  const apply = createApplyFunction()
  const view = mockView()

  view.data.set('user:admin-user', {
    value: { userId: 'admin-user', globalPermissions: ['read', 'write', 'admin'], identities: [ADMIN_KEY] }
  })

  // Rogue writer can do anything when contract is disabled
  const nodes = [nodeFrom(ROGUE_KEY, {
    type: OpTypes.DELETE_USER,
    userId: 'admin-user'
  })]

  await apply(nodes, view, {})

  // Without contract validation, the delete goes through
  t.not(view.data.has('user:admin-user'), 'delete went through without contract validation')
})

test('contract: unknown writer blocked after bootstrap', async (t) => {
  const apply = createApplyFunction({ contractValidation: true })
  const view = mockView()

  // Registry has users (not empty bootstrap)
  view.data.set('user:existing', {
    value: { userId: 'existing', globalPermissions: ['read', 'write', 'admin'], identities: [ADMIN_KEY] }
  })
  view.data.set('pubkey:' + ADMIN_KEY, { value: { identityId: ADMIN_KEY } })
  view.data.set('user-identity:' + ADMIN_KEY, { value: { userId: 'existing' } })

  // Unknown writer (not linked to any user) tries to update a user
  const nodes = [nodeFrom(ROGUE_KEY, {
    type: OpTypes.UPDATE_USER,
    userId: 'existing',
    name: 'Hacked'
  })]

  await apply(nodes, view, {})

  const user = view.data.get('user:existing').value
  t.is(user.name, undefined, 'update blocked — unknown writer has no associated user')
})