drop expired temporary access roles when they elapse

This commit is contained in:
Danny White committed 2026-08-19 14:44:01 +10:00
1 parent 23c121aa53
commit 2ffbd4258d
3 files changed
+78 -6

No files matched your search

@@ -2,6 +2,7 @@ import { describe, expect, test } from 'vitest'
import {
getActiveTemporaryAccessRoles,
getNextTemporaryAccessExpirySeconds,
isTemporaryAccessRoleExpired,
} from './jit-db-access-roles.utils'
@@ -33,3 +34,28 @@ describe('getActiveTemporaryAccessRoles', () => {
).toEqual(['postgres', 'analytics'])
})
})
describe('getNextTemporaryAccessExpirySeconds', () => {
test('returns the soonest future expiry and ignores missing or past ones', () => {
expect(getNextTemporaryAccessExpirySeconds(undefined, 1_000)).toBeUndefined()
expect(
getNextTemporaryAccessExpirySeconds(
{
user_roles: [
{ role: 'postgres', expires_at: 500 },
{ role: 'analytics' },
{ role: 'postgres', expires_at: 2_000 },
{ role: 'supabase_read_only_user', expires_at: 1_500 },
],
},
1_000
)
).toBe(1_500)
expect(
getNextTemporaryAccessExpirySeconds(
{ user_roles: [{ role: 'postgres', expires_at: 1_701_000_000_000 }] },
1_700_000_000
)
).toBe(1_701_000_000)
})
})
@@ -5,13 +5,32 @@ export type TemporaryAccessRoleGrant = {
const MILLISECONDS_THRESHOLD = 1e12
function toExpirySeconds(expiresAt: number) {
return expiresAt > MILLISECONDS_THRESHOLD ? expiresAt / 1000 : expiresAt
}
export function isTemporaryAccessRoleExpired(
expiresAt: number | undefined,
nowSeconds: number
): boolean {
if (!expiresAt) return false
const expiresSeconds = expiresAt > MILLISECONDS_THRESHOLD ? expiresAt / 1000 : expiresAt
return expiresSeconds <= nowSeconds
return toExpirySeconds(expiresAt) <= nowSeconds
}
export function getNextTemporaryAccessExpirySeconds(
grants: { user_roles?: TemporaryAccessRoleGrant[] } | undefined,
nowSeconds: number
): number | undefined {
if (!grants?.user_roles?.length) return
let next: number | undefined
for (const grant of grants.user_roles) {
if (!grant.role || grant.expires_at == null) continue
const expiresSeconds = toExpirySeconds(grant.expires_at)
if (expiresSeconds <= nowSeconds) continue
if (next == null || expiresSeconds < next) next = expiresSeconds
}
return next
}
export function getActiveTemporaryAccessRoles(
@@ -1,9 +1,16 @@
import { useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useIsJitDbAccessEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { getActiveTemporaryAccessRoles } from '@/data/jit-db-access/jit-db-access-roles.utils'
import {
getActiveTemporaryAccessRoles,
getNextTemporaryAccessExpirySeconds,
} from '@/data/jit-db-access/jit-db-access-roles.utils'
import { useJitDbAccessSelfQuery } from '@/data/jit-db-access/jit-db-access-self-query'
function readNowSeconds() {
return Math.floor(Date.now() / 1000)
}
export function useActiveTemporaryAccessRoles(
projectRef: string | undefined,
{ enabled = true }: { enabled?: boolean } = {}
@@ -13,11 +20,31 @@ export function useActiveTemporaryAccessRoles(
{ projectRef },
{ enabled: enabled && isJitEnabled }
)
const [nowSeconds, setNowSeconds] = useState(readNowSeconds)
useEffect(() => {
if (!enabled || !isJitEnabled || isError) return
const now = readNowSeconds()
if (now !== nowSeconds) {
setNowSeconds(now)
return
}
const nextExpiry = getNextTemporaryAccessExpirySeconds(data, nowSeconds)
if (nextExpiry == null) return
const timeout = window.setTimeout(
() => setNowSeconds(readNowSeconds()),
Math.max(0, nextExpiry * 1000 - Date.now())
)
return () => window.clearTimeout(timeout)
}, [data, enabled, isError, isJitEnabled, nowSeconds])
const activeRoles = useMemo(() => {
if (!isJitEnabled || isError) return []
return getActiveTemporaryAccessRoles(data, Math.floor(Date.now() / 1000))
}, [data, isError, isJitEnabled])
return getActiveTemporaryAccessRoles(data, nowSeconds)
}, [data, isError, isJitEnabled, nowSeconds])
return {
activeRoles,