From bedb2efb87a75845b7635343037906a8fae3bb7b Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:26:59 +1000 Subject: [PATCH] chore(studio): JIT access UI improvements (#44161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? UI and copywriting improvements for temporary access. ## What is the current behavior? The temporary access UI still used older JIT/ephemeral naming in some places, did not clearly explain the setup requirements, and had to infer unavailable states from Platform error message text. ## What is the new behavior? The settings UI now uses temporary access naming consistently, explains that temporary access uses short-lived tokens for manual database connections, and renders clearer unavailable states for projects that require either a Postgres upgrade or a platform migration. The Studio query now consumes Platform’s structured `unavailableReason` contract instead of parsing human-readable error strings, so the UI owns the copy while Platform owns the eligibility reason. Validation: - `pnpm eslint components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessConfiguration.tsx data/jit-db-access/jit-db-access-query.ts` - `pnpm tsc --noEmit --pretty false` ## Summary by CodeRabbit * **New Features** * IP range input now supports one CIDR range per row with add/remove rows and form integration. * **Documentation** * Replaced “JIT” wording with “Temporary” / “Ephemeral token-based” access across UI, dialogs, toasts, and help links. * Added minimum PostgreSQL version requirement (17.6.1.081+). * **Improvements** * Per-row CIDR validation with precise nested error messages. * Refined layout spacing and moved the temporary-access configuration earlier in Database settings. --------- Co-authored-by: Etienne Stalmans Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Joshen Lim --- .../App/FeaturePreview/JitDbAccessPreview.tsx | 17 +-- .../JitDbAccess.constants.ts | 12 -- .../JitDatabaseAccess/JitDbAccess.types.ts | 12 +- .../JitDbAccess.utils.test.ts | 30 ++--- .../JitDatabaseAccess/JitDbAccess.utils.ts | 44 ++++--- .../JitDbAccessConfiguration.tsx | 109 ++++++++++++------ .../JitDbAccessDeleteDialog.tsx | 6 +- .../JitDbAccessRoleGrantFields.tsx | 57 ++++++--- .../JitDbAccessRuleSheet.tsx | 46 ++++---- .../JitDbAccessRulesTable.tsx | 10 +- .../data/jit-db-access/jit-db-access-query.ts | 18 +-- .../jit-db-access-update-mutation.ts | 2 +- .../pages/project/[ref]/database/settings.tsx | 2 +- packages/api-types/types/api.d.ts | 19 ++- 14 files changed, 221 insertions(+), 163 deletions(-) delete mode 100644 apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.constants.ts diff --git a/apps/studio/components/interfaces/App/FeaturePreview/JitDbAccessPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/JitDbAccessPreview.tsx index 3724efd765a..f4f1320ccb2 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/JitDbAccessPreview.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/JitDbAccessPreview.tsx @@ -6,18 +6,21 @@ export const JitDbAccessPreview = () => { const { ref = '_' } = useParams() return ( -
-

- Grant project members temporary database role access through Just-in-Time (JIT) controls in{' '} - Database Settings. +

+

+ Grant project members temporary database role access through short-lived tokens, controlled + in Database Settings.

-
+

Enabling this preview will:

    -
  • Show JIT database access controls in Database Settings
  • -
  • Allow configuring role grants and member-level JIT rules
  • +
  • Show temporary access controls in Database Settings
  • +
  • Allow configuring role grants and member-level temporary access rules
+

+ The minimum Postgres version needed for this feature is 17.6.1.081 (or higher). +

) } diff --git a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.constants.ts b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.constants.ts deleted file mode 100644 index 86bbc9c2706..00000000000 --- a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.constants.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { JitExpiryMode } from './JitDbAccess.types' - -export const JIT_EXPIRY_MODE_OPTIONS: Array<{ value: JitExpiryMode; label: string }> = [ - { value: '1h', label: '1 hour' }, - { value: '1d', label: '1 day' }, - { value: '7d', label: '7 days' }, - { value: '30d', label: '30 days' }, - { value: 'custom', label: 'Custom' }, - { value: 'never', label: 'Never' }, -] - -export const JIT_MAX_CUSTOM_EXPIRY_YEARS = 1 diff --git a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.types.ts b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.types.ts index 8a098eff13e..ea5ef8b7519 100644 --- a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.types.ts +++ b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.types.ts @@ -12,6 +12,11 @@ export type JitStatusBadge = { variant: 'default' | 'success' | 'warning' } +export type JitDbAccessUnavailableReason = + | 'postgres_upgrade_required' + | 'manual_migration_required' + | 'temporarily_unavailable' + export type JitMemberOption = { id: string email: string @@ -23,14 +28,17 @@ export type JitRoleOption = { label: string } +export type JitIpRangeDraft = { + value: string +} + export type JitRoleGrantDraft = { roleId: string enabled: boolean expiryMode: JitExpiryMode hasExpiry: boolean expiry: string - hasIpRestriction: boolean - ipRanges: string + ipRanges: JitIpRangeDraft[] } export type JitUserRuleDraft = { diff --git a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.utils.test.ts b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.utils.test.ts index 5756364e919..38529a0d0bb 100644 --- a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.utils.test.ts +++ b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.utils.test.ts @@ -5,10 +5,9 @@ import type { JitUserRuleDraft } from './JitDbAccess.types' import { computeStatusFromGrants, createEmptyGrant, - getInvalidCidrs, + getInvalidIpRangeRows, getJitMemberOptions, getRelativeDatetimeByMode, - parseCommaSeparatedCidrs, serializeDraftRolesForGrantMutation, } from './JitDbAccess.utils' import type { OrganizationMembersData } from '@/data/organizations/organization-members-query' @@ -31,8 +30,7 @@ describe('jitDbAccess.utils', () => { enabled: true, hasExpiry: true, expiry: dayjs().add(1, 'day').toISOString(), - hasIpRestriction: true, - ipRanges: '192.0.2.0/24', + ipRanges: [{ value: '192.0.2.0/24' }], } const expiredGrant = { @@ -40,8 +38,7 @@ describe('jitDbAccess.utils', () => { enabled: true, hasExpiry: true, expiry: dayjs().subtract(1, 'day').toISOString(), - hasIpRestriction: true, - ipRanges: '203.0.113.0/24', + ipRanges: [{ value: '203.0.113.0/24' }], } const perpetualGrant = { @@ -60,17 +57,15 @@ describe('jitDbAccess.utils', () => { }) }) - it('parses comma-separated CIDR lists and trims whitespace', () => { - expect(parseCommaSeparatedCidrs('192.0.2.0/24, 2001:db8::/64 , ,203.0.113.4/32')).toEqual([ - '192.0.2.0/24', - '2001:db8::/64', - '203.0.113.4/32', - ]) - }) - - it('returns invalid CIDRs from comma-separated input', () => { + it('returns invalid CIDRs from repeated input rows', () => { expect( - getInvalidCidrs('192.0.2.0/24, not-a-cidr, 10.0.0.1/33, 2001:db8::/64, 2001:db8::/129') + getInvalidIpRangeRows([ + { value: '192.0.2.0/24' }, + { value: 'not-a-cidr' }, + { value: '10.0.0.1/33' }, + { value: '2001:db8::/64' }, + { value: '2001:db8::/129' }, + ]) ).toEqual(['not-a-cidr', '10.0.0.1/33', '2001:db8::/129']) }) }) @@ -87,8 +82,7 @@ describe('serializeDraftRolesForGrantMutation', () => { hasExpiry: true, expiryMode: 'custom', expiry, - hasIpRestriction: true, - ipRanges: '192.0.2.0/24, 2001:db8::/64', + ipRanges: [{ value: '192.0.2.0/24' }, { value: ' ' }, { value: '2001:db8::/64' }], }, { ...createEmptyGrant('supabase_read_only_user'), diff --git a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.utils.ts b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.utils.ts index ae6c3653918..234717436ce 100644 --- a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.utils.ts +++ b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccess.utils.ts @@ -3,6 +3,7 @@ import { IPv4CidrRange, IPv6CidrRange } from 'ip-num' import type { JitExpiryMode, + JitIpRangeDraft, JitMemberOption, JitRoleGrantDraft, JitRoleOption, @@ -36,13 +37,24 @@ export function createEmptyGrant(roleId: string): JitRoleGrantDraft { expiryMode: '1h', hasExpiry: true, expiry: getRelativeDatetimeByMode('1h'), - hasIpRestriction: false, - ipRanges: '', + ipRanges: [createEmptyIpRange()], } } +export function createEmptyIpRange(): JitIpRangeDraft { + return { value: '' } +} + +function parseIpRangeRows(value: JitIpRangeDraft[]) { + return value.map((item) => item.value.trim()).filter((item) => item.length > 0) +} + +function cloneIpRanges(ipRanges: JitIpRangeDraft[]) { + return ipRanges.map((ipRange) => ({ ...ipRange })) +} + function cloneGrants(grants: JitRoleGrantDraft[]) { - return grants.map((grant) => ({ ...grant })) + return grants.map((grant) => ({ ...grant, ipRanges: cloneIpRanges(grant.ipRanges) })) } export function createDraft(roleIds: string[]): JitUserRuleDraft { @@ -80,6 +92,7 @@ export function draftFromRule(rule: JitUserRule, baseRoleIds: string[]): JitUser return { ...nextGrant, expiryMode: inferExpiryMode(nextGrant), + ipRanges: cloneIpRanges(nextGrant.ipRanges), } }), } @@ -94,7 +107,7 @@ export function computeStatusFromGrants(grants: JitRoleGrantDraft[]): JitStatus let expiredIp = 0 enabledGrants.forEach((grant) => { - const hasIp = grant.hasIpRestriction && grant.ipRanges.trim().length > 0 + const hasIp = parseIpRangeRows(grant.ipRanges).length > 0 if (!grant.hasExpiry || !grant.expiry) { active += 1 @@ -151,13 +164,6 @@ function toUnixSeconds(datetimeIso: string) { return value.unix() } -export function parseCommaSeparatedCidrs(value: string) { - return value - .split(',') - .map((item) => item.trim()) - .filter((item) => item.length > 0) -} - function isValidCidr(value: string) { try { if (value.includes(':')) { @@ -172,8 +178,8 @@ function isValidCidr(value: string) { } } -export function getInvalidCidrs(value: string) { - return parseCommaSeparatedCidrs(value).filter((cidr) => !isValidCidr(cidr)) +export function getInvalidIpRangeRows(value: JitIpRangeDraft[]) { + return parseIpRangeRows(value).filter((cidr) => !isValidCidr(cidr)) } function isAssignableJitRole(role: PgRole) { @@ -264,8 +270,10 @@ export function mapJitMembersToUserRules( hasExpiry, expiryMode: hasExpiry ? 'custom' : 'never', expiry: hasExpiry ? new Date(expiresAt * 1000).toISOString() : '', - hasIpRestriction: allowedNetworks.length > 0, - ipRanges: allowedNetworks.join(', '), + ipRanges: + allowedNetworks.length > 0 + ? allowedNetworks.map((cidr) => ({ value: cidr })) + : [createEmptyIpRange()], } }) @@ -295,15 +303,13 @@ export function mapJitMembersToUserRules( } export function serializeDraftRolesForGrantMutation(draft: JitUserRuleDraft) { - const serializeAllowedNetworks = (value: string) => { - const cidrs = parseCommaSeparatedCidrs(value) + const serializeAllowedNetworks = (value: JitIpRangeDraft[]) => { + const cidrs = parseIpRangeRows(value) if (cidrs.length === 0) return undefined const allowed_cidrs = cidrs.filter((cidr) => !cidr.includes(':')).map((cidr) => ({ cidr })) const allowed_cidrs_v6 = cidrs.filter((cidr) => cidr.includes(':')).map((cidr) => ({ cidr })) - if (allowed_cidrs.length === 0 && allowed_cidrs_v6.length === 0) return undefined - return { ...(allowed_cidrs.length > 0 ? { allowed_cidrs } : {}), ...(allowed_cidrs_v6.length > 0 ? { allowed_cidrs_v6 } : {}), diff --git a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessConfiguration.tsx b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessConfiguration.tsx index 50af7588a35..7d8f95ed2b1 100644 --- a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessConfiguration.tsx @@ -107,18 +107,18 @@ export const JitDbAccessConfiguration = () => { const nextEnabled = variables.requestedConfig.state === 'enabled' if (nextEnabled) { - toast.success('JIT access enabled') + toast.success('Temporary access enabled') } else { toast.success( activeRuleCount > 0 - ? `JIT access disabled. ${activeRuleCount} configured member${activeRuleCount === 1 ? '' : 's'} can no longer request temporary database access.` - : 'JIT access disabled' + ? `Temporary access disabled. ${activeRuleCount} configured member${activeRuleCount === 1 ? '' : 's'} can no longer request temporary database access.` + : 'Temporary access disabled' ) } }, onError: (error) => { setEnabled(initialIsEnabled ?? false) - toast.error(`Failed to update just-in-time (JIT) database access: ${error.message}`) + toast.error(`Failed to update temporary access: ${error.message}`) }, }) @@ -138,18 +138,13 @@ export const JitDbAccessConfiguration = () => { const isRulesLoading = isLoadingJitMembers || isLoadingProjectMembers const initialIsEnabled = - isSuccessConfiguration && - !!jitDbAccessConfiguration && - 'appliedSuccessfully' in jitDbAccessConfiguration && - jitDbAccessConfiguration.appliedSuccessfully && - 'state' in jitDbAccessConfiguration && - (jitDbAccessConfiguration as { state: string }).state === 'enabled' - - const hasAccessToJitDbAccess = !( - jitDbAccessConfiguration !== undefined && - 'isUnavailable' in jitDbAccessConfiguration && - jitDbAccessConfiguration.isUnavailable - ) + jitDbAccessConfiguration?.state === 'enabled' + ? jitDbAccessConfiguration?.appliedSuccessfully + : false + const isJitDbAccessUnavailable = jitDbAccessConfiguration?.state === 'unavailable' + const unavailableReason = isJitDbAccessUnavailable + ? jitDbAccessConfiguration.unavailableReason + : undefined const roleOptions = useMemo(() => getAssignableJitRoleOptions(databaseRoles), [databaseRoles]) @@ -214,7 +209,7 @@ export const JitDbAccessConfiguration = () => { } const handleJitToggleChange = (checked: boolean) => { - if (!hasAccessToJitDbAccess || !canUpdateJitDbAccess) return + if (isJitDbAccessUnavailable || !canUpdateJitDbAccess) return if (checked && !enabled) { if (activeRuleCount > 0) { @@ -265,6 +260,26 @@ export const JitDbAccessConfiguration = () => { 'appliedSuccessfully' in jitDbAccessConfiguration && !jitDbAccessConfiguration.appliedSuccessfully + const projectReference = ref ? ( + <> + This project {ref} + + ) : ( + 'This project' + ) + const unavailableTitle = + unavailableReason === 'postgres_upgrade_required' + ? 'Postgres upgrade required' + : unavailableReason === 'manual_migration_required' + ? 'Migration required' + : 'Temporary access unavailable' + const unavailableDescription = + unavailableReason === 'postgres_upgrade_required' + ? 'must be upgraded to Postgres 17 or later before temporary access can be enabled.' + : unavailableReason === 'manual_migration_required' + ? 'must be migrated before temporary access can be enabled. Contact support to migrate this project.' + : 'This feature is currently unavailable for this project. Contact support if you need help enabling it.' + useEffect(() => { if (!isLoadingConfiguration && jitDbAccessConfiguration) { setEnabled(initialIsEnabled ?? false) @@ -276,43 +291,64 @@ export const JitDbAccessConfiguration = () => { - Just-in-Time (JIT) + Temporary access - + {isErrorJitDbAccessConfiguration && ( )} - {!isErrorJitDbAccessConfiguration && !hasAccessToJitDbAccess && ( + {!isErrorJitDbAccessConfiguration && isJitDbAccessUnavailable && ( + {projectReference} {unavailableDescription} + + ) + } actions={ - ref ? ( + unavailableReason === 'postgres_upgrade_required' && ref ? ( - ) : undefined + ) : ( + + ) } /> )} - {!isErrorJitDbAccessConfiguration && hasAccessToJitDbAccess && ( + {!isErrorJitDbAccessConfiguration && !isJitDbAccessUnavailable && (
{(isLoadingConfiguration || isUpdatingJitDbAccess) && ( @@ -345,14 +381,14 @@ export const JitDbAccessConfiguration = () => { - The change didn’t apply. Try turning JIT access on or off again, or{' '} + The change didn’t apply. Try enabling or disabling temporary access again, or{' '} @@ -367,13 +403,14 @@ export const JitDbAccessConfiguration = () => { )} - {enabled && hasAccessToJitDbAccess && !isUpdatingJitDbAccess && ( + {enabled && !isJitDbAccessUnavailable && !isUpdatingJitDbAccess && ( <> {isErrorJitMembers && ( )} @@ -408,11 +445,11 @@ export const JitDbAccessConfiguration = () => { - JIT access will activate existing rules + This will activate existing rules

- Enabling JIT will allow {activeRuleCount} configured member + Enabling temporary access will allow {activeRuleCount} pre-configured member {activeRuleCount === 1 ? '' : 's'} to request temporary database access immediately.

@@ -426,7 +463,7 @@ export const JitDbAccessConfiguration = () => { disabled={isUpdatingJitDbAccess} onClick={handleConfirmEnableJit} > - Enable JIT access + Enable temporary access diff --git a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessDeleteDialog.tsx b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessDeleteDialog.tsx index 6f04c91e698..92215a4175f 100644 --- a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessDeleteDialog.tsx +++ b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessDeleteDialog.tsx @@ -31,15 +31,15 @@ export function JitDbAccessDeleteDialog({ !open && !isDeleting && onClose()}> - Delete JIT access rule + Delete temporary access rule

- Remove the JIT access rule for{' '} + Remove the temporary access rule for{' '} {userDisplayName}?

- This revokes any assigned database roles for this member and removes their JIT + This revokes any assigned database roles for this member and removes their temporary access configuration.

diff --git a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRoleGrantFields.tsx b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRoleGrantFields.tsx index 28580e649ca..ef91e457d4e 100644 --- a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRoleGrantFields.tsx +++ b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRoleGrantFields.tsx @@ -1,7 +1,8 @@ import dayjs from 'dayjs' +import type { Control } from 'react-hook-form' import { Checkbox, - Input_Shadcn_, + cn, Select_Shadcn_, SelectContent_Shadcn_, SelectItem_Shadcn_, @@ -11,27 +12,43 @@ import { } from 'ui' import { TimestampInfo } from 'ui-patterns' import { Admonition } from 'ui-patterns/admonition' +import { SingleValueFieldArray } from 'ui-patterns/form/SingleValueFieldArray/SingleValueFieldArray' -import { JIT_EXPIRY_MODE_OPTIONS, JIT_MAX_CUSTOM_EXPIRY_YEARS } from './JitDbAccess.constants' -import type { JitRoleGrantDraft, JitRoleOption } from './JitDbAccess.types' -import { getRelativeDatetimeByMode } from './JitDbAccess.utils' +import type { JitRoleGrantDraft, JitRoleOption, JitUserRuleDraft } from './JitDbAccess.types' +import { createEmptyIpRange, getRelativeDatetimeByMode } from './JitDbAccess.utils' import { DatePicker } from '@/components/ui/DatePicker' import { InlineLink } from '@/components/ui/InlineLink' import { DOCS_URL } from '@/lib/constants' +const EXPIRY_MODE_OPTIONS: Array<{ value: JitRoleGrantDraft['expiryMode']; label: string }> = [ + { value: '1h', label: '1 hour' }, + { value: '1d', label: '1 day' }, + { value: '7d', label: '7 days' }, + { value: '30d', label: '30 days' }, + { value: 'custom', label: 'Custom' }, + { value: 'never', label: 'Never' }, +] + +const MAX_CUSTOM_EXPIRY_YEARS = 1 + interface JitDbAccessRoleGrantFieldsProps { + control: Control + grantIndex: number role: JitRoleOption grant: JitRoleGrantDraft onChange: (next: JitRoleGrantDraft) => void } export function JitDbAccessRoleGrantFields({ + control, + grantIndex, role, grant, onChange, }: JitDbAccessRoleGrantFieldsProps) { const isSuperuserRole = role.id === 'postgres' const isReadOnlyRole = role.id === 'supabase_read_only_user' + const showRoleAdmonition = isSuperuserRole || isReadOnlyRole const checkboxId = `jit-role-${role.id}` return ( @@ -86,6 +103,7 @@ export function JitDbAccessRoleGrantFields({ type="warning" showIcon={false} layout="vertical" + className="rounded-md mb-2" title="Grants full database control" description={ <> @@ -115,11 +133,11 @@ export function JitDbAccessRoleGrantFields({ with only the permissions required. } - className="rounded-md" + className="rounded-md mb-2" /> )} -
+

Expires in

@@ -158,7 +176,7 @@ export function JitDbAccessRoleGrantFields({ - {JIT_EXPIRY_MODE_OPTIONS.map((option) => ( + {EXPIRY_MODE_OPTIONS.map((option) => ( {option.label} @@ -174,7 +192,7 @@ export function JitDbAccessRoleGrantFields({ contentSide="top" to={grant.expiry || undefined} minDate={new Date()} - maxDate={dayjs().add(JIT_MAX_CUSTOM_EXPIRY_YEARS, 'year').toDate()} + maxDate={dayjs().add(MAX_CUSTOM_EXPIRY_YEARS, 'year').toDate()} onChange={(value) => { const selectedDate = value.to || value.from || '' onChange({ @@ -216,18 +234,19 @@ export function JitDbAccessRoleGrantFields({ Restricted IP addresses{' '} (optional)

- - onChange({ - ...grant, - hasIpRestriction: event.target.value.trim().length > 0, - ipRanges: event.target.value, - }) - } - placeholder="e.g. 192.168.0.0/24, 203.0.113.4/32" + -

Comma-separated CIDR ranges

diff --git a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRuleSheet.tsx b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRuleSheet.tsx index 6efa1f0b9d3..f903b2dca0d 100644 --- a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRuleSheet.tsx +++ b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRuleSheet.tsx @@ -36,7 +36,7 @@ import { createDraft, draftFromRule, getAssignableJitRoleOptions, - getInvalidCidrs, + getInvalidIpRangeRows, mapJitMembersToUserRules, serializeDraftRolesForGrantMutation, } from './JitDbAccess.utils' @@ -57,14 +57,13 @@ const grantSchema = z.object({ expiryMode: z.custom(), hasExpiry: z.boolean(), expiry: z.string(), - hasIpRestriction: z.boolean(), - ipRanges: z.string(), + ipRanges: z.array(z.object({ value: z.string() })), }) function createJitRuleSchema(mode: SheetMode, membersWithRules: Set) { return z .object({ - memberId: z.string().min(1, 'Select a member for this JIT access rule.'), + memberId: z.string().min(1, 'Select a member for this temporary access rule.'), grants: z.array(grantSchema), }) .superRefine((data, ctx) => { @@ -73,12 +72,12 @@ function createJitRuleSchema(mode: SheetMode, membersWithRules: Set) { code: z.ZodIssueCode.custom, path: ['memberId'], message: - 'This member already has a JIT access rule. Edit their existing rule from the list.', + 'This member already has a temporary access rule. Edit their existing rule from the list.', }) } - const enabledGrants = data.grants.filter((g) => g.enabled) - if (enabledGrants.length === 0) { + const enabledGrantCount = data.grants.filter((g) => g.enabled).length + if (enabledGrantCount === 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['grants'], @@ -87,19 +86,22 @@ function createJitRuleSchema(mode: SheetMode, membersWithRules: Set) { return } - for (const grant of enabledGrants) { - const invalidCidrs = getInvalidCidrs(grant.ipRanges) - if (invalidCidrs.length > 0) { - const preview = invalidCidrs.slice(0, 3).join(', ') - const overflow = invalidCidrs.length > 3 + data.grants.forEach((grant, grantIndex) => { + if (!grant.enabled) return + + const invalidCidrs = new Set(getInvalidIpRangeRows(grant.ipRanges)) + + grant.ipRanges.forEach((ipRange, ipRangeIndex) => { + const value = ipRange.value.trim() + if (value.length === 0 || !invalidCidrs.has(value)) return + ctx.addIssue({ code: z.ZodIssueCode.custom, - path: ['grants'], - message: `Invalid CIDR range${invalidCidrs.length > 1 ? 's' : ''} for role "${grant.roleId}": ${preview}${overflow ? ', ...' : ''}`, + path: ['grants', grantIndex, 'ipRanges', ipRangeIndex, 'value'], + message: 'Please enter a valid CIDR range', }) - break - } - } + }) + }) }) } @@ -225,10 +227,10 @@ export function JitDbAccessRuleSheet({ > - {mode === 'edit' ? 'Edit JIT access rule' : 'New JIT access rule'} + {mode === 'edit' ? 'Edit temporary access rule' : 'New temporary access rule'} - Configure which database roles a user can request with JIT access. + Configure which database roles a user can request with temporary access. @@ -272,8 +274,8 @@ export function JitDbAccessRuleSheet({ {mode === 'add' && availableMembersForAddCount === 0 && (

- All project members already have JIT access rules. Edit an existing rule - from the table above. + All project members already have temporary access rules. Edit an existing + rule from the table above.

)} @@ -311,6 +313,8 @@ export function JitDbAccessRuleSheet({ {grants.map((grant, index) => (
0 ? 'border-t' : ''}> updateGrant(grant.roleId, () => next)} diff --git a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRulesTable.tsx b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRulesTable.tsx index 8df3cec319f..732be4ad65d 100644 --- a/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRulesTable.tsx +++ b/apps/studio/components/interfaces/Settings/Database/JitDatabaseAccess/JitDbAccessRulesTable.tsx @@ -49,7 +49,7 @@ export function JitDbAccessRulesTable({ const addRuleTooltip = !canUpdate ? 'Additional permissions required' : allProjectMembersHaveRules - ? 'All project members already have JIT access rules' + ? 'All project members already have temporary access rules' : undefined if (isLoading) { @@ -74,9 +74,9 @@ export function JitDbAccessRulesTable({
-

JIT access rules

+

Temporary access rules

- Configure which members can request temporary database access. + Manage member access, allowed roles, and expiry settings.

@@ -107,9 +107,9 @@ export function JitDbAccessRulesTable({ {users.length === 0 ? ( -

No JIT access rules

+

No rules yet

- Add your first JIT access rule above + Add your first temporary access rule above

diff --git a/apps/studio/data/jit-db-access/jit-db-access-query.ts b/apps/studio/data/jit-db-access/jit-db-access-query.ts index 7ebc094d7c8..a5ed202a59b 100644 --- a/apps/studio/data/jit-db-access/jit-db-access-query.ts +++ b/apps/studio/data/jit-db-access/jit-db-access-query.ts @@ -17,23 +17,7 @@ async function getJitDbAccessConfiguration( signal, }) - // jit access might not be available on the project due to - // postgres version - if (error) { - const responseError = error as ResponseError - const isNotAvailableError = - responseError.code === 400 && responseError.message?.includes('unavailable') - - if (isNotAvailableError) { - return { - appliedSuccessfully: false, - state: 'unavailable' as string, - isUnavailable: true, - } as const - } else { - handleError(error) - } - } + if (error) handleError(error) return data } diff --git a/apps/studio/data/jit-db-access/jit-db-access-update-mutation.ts b/apps/studio/data/jit-db-access/jit-db-access-update-mutation.ts index 9d638c18285..3cd9e2391d2 100644 --- a/apps/studio/data/jit-db-access/jit-db-access-update-mutation.ts +++ b/apps/studio/data/jit-db-access/jit-db-access-update-mutation.ts @@ -44,7 +44,7 @@ export const useJitDbAccessUpdateMutation = ({ }, async onError(data, variables, context) { if (onError === undefined) { - toast.error(`Failed to update just-in-time (JIT) database access: ${data.message}`) + toast.error(`Failed to update temporary access: ${data.message}`) } else { onError(data, variables, context) } diff --git a/apps/studio/pages/project/[ref]/database/settings.tsx b/apps/studio/pages/project/[ref]/database/settings.tsx index 64fada053f5..303c9d3539f 100644 --- a/apps/studio/pages/project/[ref]/database/settings.tsx +++ b/apps/studio/pages/project/[ref]/database/settings.tsx @@ -51,9 +51,9 @@ const DatabaseSettings: NextPageWithLayout = () => { + {jitDbAccessEnabled && } - {jitDbAccessEnabled && } {showNewDiskManagementUI ? ( // This form is hidden if Disk and Compute form is enabled, new form is on ./settings/compute-and-disk diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 3163eb5c0db..bf619f41f1c 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -3144,6 +3144,21 @@ export interface components { }[] }[] } + JitStateResponse: + | { + appliedSuccessfully?: boolean + /** @enum {string} */ + state: 'enabled' | 'disabled' + } + | { + /** @enum {string} */ + state: 'unavailable' + /** @enum {string} */ + unavailableReason: + | 'manual_migration_required' + | 'postgres_upgrade_required' + | 'temporarily_unavailable' + } LegacyApiKeysResponse: { enabled: boolean } @@ -11197,7 +11212,7 @@ export interface operations { [name: string]: unknown } content: { - 'application/json': components['schemas']['JitAccessResponse'] + 'application/json': components['schemas']['JitStateResponse'] } } /** @description Unauthorized */ @@ -11251,7 +11266,7 @@ export interface operations { [name: string]: unknown } content: { - 'application/json': components['schemas']['JitAccessResponse'] + 'application/json': components['schemas']['JitStateResponse'] } } /** @description Unauthorized */