Ctrl K
ComunidadGuías…

tests/apikey-subkey-middleware.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { randomUUID } from 'node:crypto'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import {
  APIKEY_MANAGEMENT_APIKEY_MANAGER,
  APIKEY_MANAGEMENT_ORG_SUPER_ADMIN,
  BASE_URL,
  createDirectApiKeyWithBindings,
  getAuthHeadersForCredentials,
  ORG_ID_APIKEY_MANAGEMENT,
  USER_EMAIL_APIKEY_MANAGEMENT,
  USER_ID_APIKEY_MANAGEMENT,
  USER_PASSWORD,
} from './test-utils.ts'

describe('x-limited-key-id delegation containment', () => {
  const runId = randomUUID().replaceAll('-', '')
  const appA = `com.subkey.contain.a.${runId}`
  const appB = `com.subkey.contain.b.${runId}`
  const createdKeyIds: number[] = []
  const createdKeyRbacIds: string[] = []

  let managerAppAdminSiblingId = 0
  let appScopedParentKey = ''
  let orgScopedSiblingId = 0
  let appAScopedParentKey = ''
  let appBSiblingId = 0
  let limitedChildId = 0
  let privilegedParentKey = APIKEY_MANAGEMENT_ORG_SUPER_ADMIN

  beforeAll(async () => {
    const authHeaders = await getAuthHeadersForCredentials(USER_EMAIL_APIKEY_MANAGEMENT, USER_PASSWORD)

    for (const appId of [appA, appB]) {
      const createApp = await fetch(`${BASE_URL}/app`, {
        method: 'POST',
        headers: authHeaders,
        body: JSON.stringify({
          owner_org: ORG_ID_APIKEY_MANAGEMENT,
          app_id: appId,
          name: `Subkey containment ${appId}`,
          icon: 'https://cdn.example/test-icon.png',
        }),
      })
      if (createApp.status !== 200) {
        const body = await createApp.json().catch(() => null) as { error?: string } | null
        expect(createApp.status, JSON.stringify(body)).toBe(200)
      }
    }

    const appAdminSibling = await createDirectApiKeyWithBindings({
      userId: USER_ID_APIKEY_MANAGEMENT,
      key: randomUUID(),
      name: `app admin sibling ${runId}`,
      orgId: ORG_ID_APIKEY_MANAGEMENT,
      roleName: 'org_member',
      appId: appA,
      appRoleName: 'app_admin',
    })
    managerAppAdminSiblingId = appAdminSibling.id
    createdKeyIds.push(appAdminSibling.id)
    createdKeyRbacIds.push(appAdminSibling.rbac_id)

    const appScopedParent = await createDirectApiKeyWithBindings({
      userId: USER_ID_APIKEY_MANAGEMENT,
      key: randomUUID(),
      name: `app scoped parent ${runId}`,
      orgId: ORG_ID_APIKEY_MANAGEMENT,
      roleName: 'org_member',
      appId: appA,
      appRoleName: 'app_admin',
    })
    appScopedParentKey = appScopedParent.key ?? ''
    createdKeyIds.push(appScopedParent.id)
    createdKeyRbacIds.push(appScopedParent.rbac_id)

    const orgScopedSibling = await createDirectApiKeyWithBindings({
      userId: USER_ID_APIKEY_MANAGEMENT,
      key: randomUUID(),
      name: `org scoped sibling ${runId}`,
      orgId: ORG_ID_APIKEY_MANAGEMENT,
      roleName: 'org_admin',
    })
    orgScopedSiblingId = orgScopedSibling.id
    createdKeyIds.push(orgScopedSibling.id)
    createdKeyRbacIds.push(orgScopedSibling.rbac_id)

    const appAParent = await createDirectApiKeyWithBindings({
      userId: USER_ID_APIKEY_MANAGEMENT,
      key: randomUUID(),
      name: `app A parent ${runId}`,
      orgId: ORG_ID_APIKEY_MANAGEMENT,
      roleName: 'org_member',
      appId: appA,
      appRoleName: 'app_admin',
    })
    appAScopedParentKey = appAParent.key ?? ''
    createdKeyIds.push(appAParent.id)
    createdKeyRbacIds.push(appAParent.rbac_id)

    const appBSibling = await createDirectApiKeyWithBindings({
      userId: USER_ID_APIKEY_MANAGEMENT,
      key: randomUUID(),
      name: `app B sibling ${runId}`,
      orgId: ORG_ID_APIKEY_MANAGEMENT,
      roleName: 'org_member',
      appId: appB,
      appRoleName: 'app_admin',
    })
    appBSiblingId = appBSibling.id
    createdKeyIds.push(appBSibling.id)
    createdKeyRbacIds.push(appBSibling.rbac_id)

    const limitedChild = await createDirectApiKeyWithBindings({
      userId: USER_ID_APIKEY_MANAGEMENT,
      key: randomUUID(),
      name: `limited child ${runId}`,
      orgId: ORG_ID_APIKEY_MANAGEMENT,
      roleName: 'org_member',
      appId: appA,
      appRoleName: 'app_admin',
    })
    limitedChildId = limitedChild.id
    createdKeyIds.push(limitedChild.id)
    createdKeyRbacIds.push(limitedChild.rbac_id)
  })

  afterAll(async () => {
    const { getSupabaseClient } = await import('./test-utils.ts')
    const supabase = getSupabaseClient()

    for (const rbacId of createdKeyRbacIds) {
      const { error } = await supabase.from('role_bindings').delete().eq('principal_id', rbacId)
      if (error) {
        console.warn(`Failed to delete role_bindings for ${rbacId}:`, error.message)
      }
    }

    for (const keyId of createdKeyIds) {
      const { error } = await supabase.from('apikeys').delete().eq('id', keyId)
      if (error) {
        console.warn(`Failed to delete apikey ${keyId}:`, error.message)
      }
    }

    const { error: appDeleteError } = await supabase.from('apps').delete().in('app_id', [appA, appB])
    if (appDeleteError) {
      console.warn('Failed to delete containment test apps:', appDeleteError.message)
    }
  })

  it.concurrent('rejects apikey_manager parent impersonating same-owner app_admin sibling for app update', async () => {
    const response = await fetch(`${BASE_URL}/app/${appA}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'capgkey': APIKEY_MANAGEMENT_APIKEY_MANAGER,
        'x-limited-key-id': String(managerAppAdminSiblingId),
      },
      body: JSON.stringify({ name: `Escalated ${runId}` }),
    })

    expect(response.status).toBe(401)
    const data = await response.json() as { error?: string }
    expect(data.error).toBe('invalid_subkey')
  })

  it.concurrent('rejects app-scoped parent with broader org-scoped sibling', async () => {
    const response = await fetch(`${BASE_URL}/app/${appA}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'authorization': appScopedParentKey,
        'x-limited-key-id': String(orgScopedSiblingId),
      },
      body: JSON.stringify({ name: `Org scoped sibling ${runId}` }),
    })

    expect(response.status).toBe(401)
    const data = await response.json() as { error?: string }
    expect(data.error).toBe('invalid_subkey')
  })

  it.concurrent('rejects unrelated same-owner sibling with disjoint app scope', async () => {
    const response = await fetch(`${BASE_URL}/app/${appB}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'authorization': appAScopedParentKey,
        'x-limited-key-id': String(appBSiblingId),
      },
      body: JSON.stringify({ name: `Disjoint sibling ${runId}` }),
    })

    expect(response.status).toBe(401)
    const data = await response.json() as { error?: string }
    expect(data.error).toBe('invalid_subkey')
  })

  it.concurrent('allows privileged parent to adopt a limited child subkey', async () => {
    const response = await fetch(`${BASE_URL}/app/${appA}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'capgkey': privilegedParentKey,
        'x-limited-key-id': String(limitedChildId),
      },
      body: JSON.stringify({ name: `Delegated limited child ${runId}` }),
    })

    expect(response.status).toBe(200)
  })

  it.concurrent('rejects apikey_manager app update without x-limited-key-id', async () => {
    const response = await fetch(`${BASE_URL}/app/${appA}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'capgkey': APIKEY_MANAGEMENT_APIKEY_MANAGER,
      },
      body: JSON.stringify({ name: `Manager direct ${runId}` }),
    })

    expect(response.status).toBe(401)
  })
})