tests/apikey-subkey-middleware.test.ts
Rama main · réplica de lectura del repositorio original.
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)
})
})