tests/app-creator-apikeys.test.ts
Rama main · réplica de lectura del repositorio original.
import type { Database } from '../src/types/database.types.ts'
import { randomUUID } from 'node:crypto'
import { env } from 'node:process'
import { createClient } from '@supabase/supabase-js'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { fetchTestRequest, getAuthHeaders, getAuthHeadersForCredentials, getEndpointUrl, getSupabaseClient, SUPABASE_ANON_KEY, SUPABASE_BASE_URL, USER_ID } from './test-utils.ts'
const USE_CLOUDFLARE = env.USE_CLOUDFLARE_WORKERS === 'true'
// Dedicated org: the shared test user is super admin, a fresh user is org_member.
const TEST_ID = randomUUID().slice(0, 8)
const ORG_ID = randomUUID()
const CUSTOMER_ID = `cus_app_creator_${TEST_ID}`
const MEMBER_EMAIL = `app-creator-member-${TEST_ID}@capgo.app`
const MEMBER_PASSWORD = 'testtest'
let adminHeaders: Record<string, string>
let memberHeaders: Record<string, string>
let memberId = ''
const createdAppIds: string[] = []
const createdKeyIds: number[] = []
async function createApp(headers: Record<string, string>, suffix: string) {
const appId = `com.test.appcreator.${TEST_ID}.${suffix}`
const response = await fetchTestRequest(getEndpointUrl('/app'), {
method: 'POST',
headers,
body: JSON.stringify({ owner_org: ORG_ID, app_id: appId, name: `App creator ${suffix}` }),
})
const body = await response.json() as { id: string, app_id: string }
expect(response.status, JSON.stringify(body)).toBe(200)
createdAppIds.push(appId)
return body
}
async function appRolesOf(userId: string, appUuid: string) {
const { data, error } = await getSupabaseClient()
.from('role_bindings')
.select('roles(name)')
.eq('principal_type', 'user')
.eq('principal_id', userId)
.eq('scope_type', 'app')
.eq('app_id', appUuid)
if (error)
throw error
return (data ?? []).map(row => (row as unknown as { roles: { name: string } }).roles.name)
}
function createKey(headers: Record<string, string>, bindings: Record<string, unknown>[]) {
return fetchTestRequest(getEndpointUrl('/apikey'), {
method: 'POST',
headers,
body: JSON.stringify({ name: `app-creator-key-${randomUUID().slice(0, 8)}`, bindings }),
})
}
beforeAll(async () => {
const capgo = getSupabaseClient()
adminHeaders = await getAuthHeaders()
const { error: stripeError } = await capgo.from('stripe_info').insert({
customer_id: CUSTOMER_ID,
product_id: 'prod_LQIregjtNduh4q',
status: 'succeeded',
is_good_plan: true,
})
if (stripeError)
throw stripeError
const { error: orgError } = await capgo.from('orgs').insert({
id: ORG_ID,
created_by: USER_ID,
name: `App Creator Org ${TEST_ID}`,
management_email: `app-creator-${TEST_ID}@capgo.app`,
customer_id: CUSTOMER_ID,
})
if (orgError)
throw orgError
const { error: adminMembershipError } = await capgo.from('org_users').insert({ org_id: ORG_ID, user_id: USER_ID, rbac_role_name: 'org_super_admin' })
if (adminMembershipError)
throw adminMembershipError
const { data: member, error: memberError } = await capgo.auth.admin.createUser({
email: MEMBER_EMAIL,
password: MEMBER_PASSWORD,
email_confirm: true,
})
if (memberError || !member.user)
throw memberError ?? new Error('Failed to create member user')
memberId = member.user.id
const { error: profileError } = await capgo.from('users').upsert({ id: memberId, email: MEMBER_EMAIL })
if (profileError)
throw profileError
const { error: memberMembershipError } = await capgo.from('org_users').insert({ org_id: ORG_ID, user_id: memberId, rbac_role_name: 'org_member' })
if (memberMembershipError)
throw memberMembershipError
const { data: memberRole, error: memberRoleError } = await capgo.from('roles').select('id').eq('name', 'org_member').single()
if (memberRoleError)
throw memberRoleError
const { error: memberBindingError } = await capgo.from('role_bindings').insert({
principal_type: 'user',
principal_id: memberId,
role_id: memberRole.id,
scope_type: 'org',
org_id: ORG_ID,
granted_by: USER_ID,
})
if (memberBindingError)
throw memberBindingError
memberHeaders = await getAuthHeadersForCredentials(MEMBER_EMAIL, MEMBER_PASSWORD)
})
afterAll(async () => {
const capgo = getSupabaseClient()
for (const keyId of createdKeyIds)
await capgo.from('apikeys').delete().eq('id', keyId)
for (const appId of createdAppIds)
await capgo.from('apps').delete().eq('app_id', appId)
await capgo.from('role_bindings').delete().eq('org_id', ORG_ID)
await capgo.from('org_users').delete().eq('org_id', ORG_ID)
await capgo.from('orgs').delete().eq('id', ORG_ID)
await capgo.from('stripe_info').delete().eq('customer_id', CUSTOMER_ID)
if (memberId)
await capgo.auth.admin.deleteUser(memberId)
})
describe.skipIf(USE_CLOUDFLARE)('app creators and app-scoped API keys', () => {
it('makes an org_member admin of the app they create, and only of that one', async () => {
const memberApp = await createApp(memberHeaders, 'member')
expect(await appRolesOf(memberId, memberApp.id)).toEqual(['app_admin'])
// The creator can now read their app through RLS, and only that one.
const memberClient = createClient<Database>(SUPABASE_BASE_URL!, SUPABASE_ANON_KEY!, { auth: { persistSession: false, autoRefreshToken: false } })
const { error: signInError } = await memberClient.auth.signInWithPassword({ email: MEMBER_EMAIL, password: MEMBER_PASSWORD })
if (signInError)
throw signInError
// An org admin already administers every app: no redundant app binding.
const adminApp = await createApp(adminHeaders, 'admin')
expect(await appRolesOf(USER_ID, adminApp.id)).toEqual([])
const { data: visibleApps } = await memberClient.from('apps').select('id').eq('owner_org', ORG_ID)
expect((visibleApps ?? []).map(app => app.id)).toEqual([memberApp.id])
})
it('lets an app admin create keys limited to their own app, nothing wider', async () => {
const memberApp = await createApp(memberHeaders, 'keys')
const adminApp = await createApp(adminHeaders, 'keys-admin')
const ownApp = await createKey(memberHeaders, [{ role_name: 'app_developer', scope_type: 'app', org_id: ORG_ID, app_id: memberApp.id }])
const ownAppBody = await ownApp.json() as { id: number }
expect(ownApp.status, JSON.stringify(ownAppBody)).toBe(200)
createdKeyIds.push(ownAppBody.id)
const orgWide = await createKey(memberHeaders, [{ role_name: 'org_member', scope_type: 'org', org_id: ORG_ID }])
expect(orgWide.status).toBe(403)
const otherApp = await createKey(memberHeaders, [{ role_name: 'app_reader', scope_type: 'app', org_id: ORG_ID, app_id: adminApp.id }])
expect(otherApp.status).toBe(403)
// Capped by the creator: app admins cannot hand out app_admin itself.
const escalation = await createKey(memberHeaders, [{ role_name: 'app_admin', scope_type: 'app', org_id: ORG_ID, app_id: memberApp.id }])
expect(escalation.status).toBe(403)
})
})