fix: check for api key permissions before requesting (#40452)

To prevent sending unnecessary requests for API keys when the user can't
view them (Read-Only Role), first check the permissions.
This commit is contained in:
Charis authored and GitHub committed 2025-11-14 09:43:34 -05:00
1 parent d9e5851467
commit a5e2c62ef4
25 files changed
+350 -256

No files matched your search

@@ -1,4 +1,3 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useMemo } from 'react'
import { InputVariants } from '@ui/components/shadcn/ui/input'
@@ -6,7 +5,6 @@ import { useParams } from 'common'
import CopyButton from 'components/ui/CopyButton'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import {
cn,
EyeOffIcon,
@@ -17,6 +15,7 @@ import {
TooltipTrigger,
WarningIcon,
} from 'ui'
import { useApiKeysVisibility } from './hooks/useApiKeysVisibility'
// to add in later with follow up PR
// import CreatePublishableAPIKeyDialog from './CreatePublishableAPIKeyDialog'
@@ -25,22 +24,19 @@ import {
export const PublishableAPIKeys = () => {
const { ref: projectRef } = useParams()
const { canReadAPIKeys, isLoading: isLoadingVisibility } = useApiKeysVisibility()
const {
data: apiKeysData,
isLoading: isLoadingApiKeys,
error,
} = useAPIKeysQuery({ projectRef, reveal: false })
} = useAPIKeysQuery({ projectRef, reveal: false }, { enabled: canReadAPIKeys })
const publishableApiKeys = useMemo(
() => apiKeysData?.filter(({ type }) => type === 'publishable') ?? [],
[apiKeysData]
)
const { can: canReadAPIKeys, isLoading: isPermissionsLoading } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'*'
)
// The default publisahble key will always be the first one
const apiKey = publishableApiKeys[0]
@@ -63,7 +59,7 @@ export const PublishableAPIKeys = () => {
size="tiny"
type="default"
className="px-2 rounded-full"
disabled={isPermissionsLoading || isLoadingApiKeys || !canReadAPIKeys}
disabled={isLoadingVisibility || isLoadingApiKeys || !canReadAPIKeys}
text={apiKey?.api_key}
/>
</TooltipTrigger>
@@ -95,21 +91,17 @@ export const PublishableAPIKeys = () => {
const ApiKeyInput = () => {
const { ref: projectRef } = useParams()
const { canReadAPIKeys, isLoading: isPermissionsLoading } = useApiKeysVisibility()
const {
data: apiKeysData,
isLoading: isApiKeysLoading,
error,
} = useAPIKeysQuery({ projectRef, reveal: false })
} = useAPIKeysQuery({ projectRef, reveal: false }, { enabled: canReadAPIKeys })
const publishableApiKeys = useMemo(
() => apiKeysData?.filter(({ type }) => type === 'publishable') ?? [],
[apiKeysData]
)
const { can: canReadAPIKeys, isLoading: isPermissionsLoading } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'*'
)
// The default publisahble key will always be the first one
const apiKey = publishableApiKeys[0]
@@ -117,15 +109,7 @@ const ApiKeyInput = () => {
'flex-1 grow gap-1 rounded-full min-w-0 max-w-[200px] sm:max-w-[300px] md:max-w-[400px] lg:min-w-[24rem]'
const size = 'tiny'
if (isApiKeysLoading || isPermissionsLoading) {
return (
<div className={cn(InputVariants({ size }), baseClasses, 'items-center')}>
<Skeleton className="h-2 w-48 rounded-full bg-foreground-muted" />
</div>
)
}
if (!canReadAPIKeys) {
if (!canReadAPIKeys && !isPermissionsLoading) {
return (
<div className={cn(InputVariants({ size }), baseClasses, 'items-center gap-2 font-normal')}>
<EyeOffIcon />
@@ -134,6 +118,14 @@ const ApiKeyInput = () => {
)
}
if (isApiKeysLoading || isPermissionsLoading) {
return (
<div className={cn(InputVariants({ size }), baseClasses, 'items-center')}>
<Skeleton className="h-2 w-48 rounded-full bg-foreground-muted" />
</div>
)
}
if (error) {
return (
<div className={cn(InputVariants({ size }), baseClasses, 'items-center gap-2 font-normal')}>
@@ -1,4 +1,3 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import dayjs from 'dayjs'
import { useMemo, useRef } from 'react'
@@ -7,7 +6,6 @@ import AlertError from 'components/ui/AlertError'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { APIKeysData, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import useLogsQuery from 'hooks/analytics/useLogsQuery'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Card, EyeOffIcon } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import {
@@ -19,6 +17,7 @@ import {
} from 'ui/src/components/shadcn/ui/table'
import { APIKeyRow } from './APIKeyRow'
import CreateSecretAPIKeyDialog from './CreateSecretAPIKeyDialog'
import { useApiKeysVisibility } from './hooks/useApiKeysVisibility'
interface LastSeenData {
[hash: string]: { timestamp: string }
@@ -51,17 +50,14 @@ function useLastSeen(projectRef: string): LastSeenData {
export const SecretAPIKeys = () => {
const { ref: projectRef } = useParams()
const { canReadAPIKeys, isLoading: isLoadingPermissions } = useApiKeysVisibility()
const {
data: apiKeysData,
error,
isLoading: isLoadingApiKeys,
isError: isErrorApiKeys,
} = useAPIKeysQuery({ projectRef, reveal: false })
const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'*'
)
} = useAPIKeysQuery({ projectRef, reveal: false }, { enabled: canReadAPIKeys })
const lastSeen = useLastSeen(projectRef!)
@@ -83,9 +79,7 @@ export const SecretAPIKeys = () => {
actions={<CreateSecretAPIKeyDialog />}
/>
{isLoadingApiKeys || isLoadingPermissions ? (
<GenericSkeletonLoader />
) : !canReadAPIKeys ? (
{!canReadAPIKeys && !isLoadingPermissions ? (
<Card>
<div className="!rounded-b-md overflow-hidden py-12 flex flex-col gap-1 items-center justify-center">
<EyeOffIcon />
@@ -97,6 +91,8 @@ export const SecretAPIKeys = () => {
</p>
</div>
</Card>
) : isLoadingApiKeys || isLoadingPermissions ? (
<GenericSkeletonLoader />
) : isErrorApiKeys ? (
<AlertError error={error} subject="Failed to load secret API keys" />
) : empty ? (
@@ -19,12 +19,18 @@ interface ApiKeysVisibilityState {
*/
export function useApiKeysVisibility(): ApiKeysVisibilityState {
const { ref: projectRef } = useParams()
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.READ, 'api_keys')
const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
PermissionAction.SECRETS_READ,
'*'
)
const { data: apiKeysData, isLoading } = useAPIKeysQuery({
projectRef,
reveal: false,
})
const { data: apiKeysData, isLoading: isLoadingApiKeys } = useAPIKeysQuery(
{
projectRef,
reveal: false,
},
{ enabled: canReadAPIKeys }
)
const publishableApiKeys = useMemo(
() => apiKeysData?.filter(({ type }) => type === 'publishable') ?? [],
@@ -36,14 +42,14 @@ export function useApiKeysVisibility(): ApiKeysVisibilityState {
const hasApiKeys = publishableApiKeys.length > 0
// Can initialize API keys when in rollout, has permissions, not loading, and no API keys yet
const canInitApiKeys = canReadAPIKeys && !isLoading && !hasApiKeys
const canInitApiKeys = canReadAPIKeys && !isLoadingApiKeys && !hasApiKeys
// Disable UI for publishable keys and secrets keys if flag is not enabled OR no API keys created yet
const shouldDisableUI = !hasApiKeys
return {
hasApiKeys,
isLoading,
isLoading: isLoadingPermissions || (canReadAPIKeys && isLoadingApiKeys),
canReadAPIKeys,
canInitApiKeys,
shouldDisableUI,
@@ -1,6 +1,7 @@
import { Key } from 'lucide-react'
import { useMemo } from 'react'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Badge, copyToClipboard } from 'ui'
@@ -24,88 +25,92 @@ export function useApiKeysCommands() {
const { data: project } = useSelectedProjectQuery()
const ref = project?.ref || '_'
const { data: apiKeys } = useAPIKeysQuery({ projectRef: project?.ref, reveal: true })
const { anonKey, serviceKey, publishableKey, allSecretKeys } = getKeys(apiKeys)
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery(
{ projectRef: project?.ref, reveal: true },
{ enabled: canReadAPIKeys }
)
const commands = useMemo(() => {
const { anonKey, serviceKey, publishableKey, allSecretKeys } = canReadAPIKeys
? getKeys(apiKeys)
: {}
const commands = useMemo(
() =>
[
project &&
anonKey && {
id: 'anon-key',
name: `Copy anonymous API key`,
action: () => {
copyToClipboard(anonKey.api_key ?? '')
setIsOpen(false)
},
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge>Public</Badge>
<Badge className="capitalize">{anonKey.type}</Badge>
</span>
),
icon: () => <Key />,
return [
project &&
anonKey && {
id: 'anon-key',
name: `Copy anonymous API key`,
action: () => {
copyToClipboard(anonKey.api_key ?? '')
setIsOpen(false)
},
project &&
serviceKey && {
id: 'service-key',
name: `Copy service API key`,
action: () => {
copyToClipboard(serviceKey.api_key ?? '')
setIsOpen(false)
},
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge variant="destructive">Secret</Badge>
<Badge className="capitalize">{serviceKey.type}</Badge>
</span>
),
icon: () => <Key />,
},
project &&
publishableKey && {
id: 'publishable-key',
name: `Copy publishable key`,
action: () => {
copyToClipboard(publishableKey.api_key ?? '')
setIsOpen(false)
},
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge className="capitalize">{publishableKey.type}</Badge>
</span>
),
icon: () => <Key />,
},
...(project && allSecretKeys
? allSecretKeys.map((key) => ({
id: key.id,
name: `Copy secret key (${key.name})`,
action: () => {
copyToClipboard(key.api_key ?? '')
setIsOpen(false)
},
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge className="capitalize">{key.type}</Badge>
</span>
),
icon: () => <Key />,
}))
: []),
!(anonKey || serviceKey) && {
id: 'api-keys-project-settings',
name: 'See API keys in Project Settings',
route: `/project/${ref}/settings/api`,
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge>Public</Badge>
<Badge className="capitalize">{anonKey.type}</Badge>
</span>
),
icon: () => <Key />,
},
].filter(Boolean) as ICommand[],
[anonKey, serviceKey, project, setIsOpen]
)
project &&
serviceKey && {
id: 'service-key',
name: `Copy service API key`,
action: () => {
copyToClipboard(serviceKey.api_key ?? '')
setIsOpen(false)
},
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge variant="destructive">Secret</Badge>
<Badge className="capitalize">{serviceKey.type}</Badge>
</span>
),
icon: () => <Key />,
},
project &&
publishableKey && {
id: 'publishable-key',
name: `Copy publishable key`,
action: () => {
copyToClipboard(publishableKey.api_key ?? '')
setIsOpen(false)
},
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge className="capitalize">{publishableKey.type}</Badge>
</span>
),
icon: () => <Key />,
},
...(project && allSecretKeys
? allSecretKeys.map((key) => ({
id: key.id,
name: `Copy secret key (${key.name})`,
action: () => {
copyToClipboard(key.api_key ?? '')
setIsOpen(false)
},
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge className="capitalize">{key.type}</Badge>
</span>
),
icon: () => <Key />,
}))
: []),
!(anonKey || serviceKey) && {
id: 'api-keys-project-settings',
name: 'See API keys in Project Settings',
route: `/project/${ref}/settings/api-keys`,
icon: () => <Key />,
},
].filter(Boolean) as ICommand[]
}, [apiKeys, canReadAPIKeys, project, ref, setIsOpen])
useRegisterPage(
API_KEYS_PAGE_NAME,
@@ -119,7 +124,7 @@ export function useApiKeysCommands() {
},
],
},
{ deps: [commands], enabled: !!project }
{ deps: [commands], enabled: !!project && commands.length > 0 }
)
useRegisterCommands(
@@ -133,7 +138,7 @@ export function useApiKeysCommands() {
},
],
{
enabled: !!project,
enabled: !!project && commands.length > 0,
orderSection: orderCommandSectionsByPriority,
sectionMeta: { priority: 3 },
}
@@ -217,7 +217,7 @@ export const Connect = () => {
return []
}
const { data: apiKeys } = useAPIKeysQuery({ projectRef })
const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
const { anonKey, publishableKey } = canReadAPIKeys
? getKeys(apiKeys)
: { anonKey: null, publishableKey: null }
@@ -44,6 +44,7 @@ import { AnalyticsBucketFields, BigQueryFields } from './DestinationPanelFields'
import { DestinationTypeSelection } from './DestinationTypeSelection'
import { NoDestinationsAvailable } from './NoDestinationsAvailable'
import { PublicationSelection } from './PublicationSelection'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
const formId = 'destination-editor'
@@ -126,7 +127,11 @@ export const DestinationPanel = ({
pipelineId: existingDestination?.pipelineId,
})
const { data: apiKeys } = useAPIKeysQuery({ projectRef, reveal: true })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery(
{ projectRef, reveal: true },
{ enabled: canReadAPIKeys }
)
const { serviceKey } = getKeys(apiKeys)
const catalogToken = serviceKey?.api_key ?? ''
@@ -3,6 +3,7 @@ import { useMemo, useState } from 'react'
import type { UseFormReturn } from 'react-hook-form'
import { useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { getCatalogURI } from 'components/interfaces/Storage/StorageSettings/StorageSettings.utils'
import { InlineLink } from 'components/ui/InlineLink'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
@@ -125,7 +126,11 @@ export const AnalyticsBucketFields = ({
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: apiKeys } = useAPIKeysQuery({ projectRef, reveal: true })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery(
{ projectRef, reveal: true },
{ enabled: canReadAPIKeys }
)
const { serviceKey } = getKeys(apiKeys)
const serviceApiKey = serviceKey?.api_key ?? ''
@@ -3,6 +3,7 @@ import Image from 'next/legacy/image'
import { MutableRefObject, useEffect } from 'react'
import { useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
@@ -50,7 +51,11 @@ export const FormContents = ({
const restUrl = project?.restUrl
const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co'
const { data: keys = [] } = useAPIKeysQuery({ projectRef: ref, reveal: true })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: keys = [] } = useAPIKeysQuery(
{ projectRef: ref, reveal: true },
{ enabled: canReadAPIKeys }
)
const { data: functions = [], isSuccess: isSuccessEdgeFunctions } = useEdgeFunctionsQuery({
projectRef: ref,
})
@@ -2,6 +2,7 @@ import { ChevronDown, Plus, X } from 'lucide-react'
import Link from 'next/link'
import { useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
@@ -51,7 +52,11 @@ const HTTPRequestFields = ({
const { data: selectedProject } = useSelectedProjectQuery()
const { data: functions } = useEdgeFunctionsQuery({ projectRef: ref })
const { data: apiKeys } = useAPIKeysQuery({ projectRef: ref, reveal: true })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery(
{ projectRef: ref, reveal: true },
{ enabled: canReadAPIKeys }
)
const edgeFunctions = functions ?? []
const { serviceKey, secretKey } = getKeys(apiKeys)
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { useParams } from 'common'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useApiKeysVisibility } from '../APIKeys/hooks/useApiKeysVisibility'
import CodeSnippet from './CodeSnippet'
import Snippets from './Snippets'
@@ -13,7 +14,8 @@ interface AuthenticationProps {
const Authentication = ({ selectedLang, showApiKey }: AuthenticationProps) => {
const { ref: projectRef } = useParams()
const { data: apiKeys } = useAPIKeysQuery({ projectRef })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { anonKey, serviceKey } = getKeys(apiKeys)
@@ -15,6 +15,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ui'
import { useApiKeysVisibility } from '../APIKeys/hooks/useApiKeysVisibility'
const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' }
@@ -33,10 +34,14 @@ export const LangSelector = ({
}: LangSelectorProps) => {
const { ref: projectRef } = useParams()
const { data: apiKeys = [], isLoading: isLoadingAPIKeys } = useAPIKeysQuery({
projectRef,
reveal: false,
})
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys = [], isLoading: isLoadingAPIKeys } = useAPIKeysQuery(
{
projectRef,
reveal: false,
},
{ enabled: canReadAPIKeys }
)
const legacyKeys = useMemo(() => apiKeys.filter(({ type }) => type === 'legacy'), [apiKeys])
const publishableKeys = useMemo(
@@ -70,97 +75,101 @@ export const LangSelector = ({
>
Bash
</button>
{selectedLang == 'bash' && !isLoadingAPIKeys && apiKeys && apiKeys.length > 0 && (
<div className="flex gap-x-1">
<div className="flex items-center gap-2 p-1 pl-2 text-xs text-foreground-lighter">
<Key size={12} strokeWidth={1.5} />
<span>Project API key:</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="outline">
{selectedApiKey.name === 'hide' ? 'Hide keys' : selectedApiKey.name}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">
<DropdownMenuRadioGroup value={selectedApiKey.key}>
<DropdownMenuRadioItem
key="hide"
value={DEFAULT_KEY.key}
onClick={() => setSelectedApiKey(DEFAULT_KEY)}
>
Hide keys
</DropdownMenuRadioItem>
{selectedLang == 'bash' &&
canReadAPIKeys &&
!isLoadingAPIKeys &&
apiKeys &&
apiKeys.length > 0 && (
<div className="flex gap-x-1">
<div className="flex items-center gap-2 p-1 pl-2 text-xs text-foreground-lighter">
<Key size={12} strokeWidth={1.5} />
<span>Project API key:</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="outline">
{selectedApiKey.name === 'hide' ? 'Hide keys' : selectedApiKey.name}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">
<DropdownMenuRadioGroup value={selectedApiKey.key}>
<DropdownMenuRadioItem
key="hide"
value={DEFAULT_KEY.key}
onClick={() => setSelectedApiKey(DEFAULT_KEY)}
>
Hide keys
</DropdownMenuRadioItem>
{publishableKeys.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Publishable keys</DropdownMenuLabel>
{publishableKeys.map((key) => {
{publishableKeys.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Publishable keys</DropdownMenuLabel>
{publishableKeys.map((key) => {
const value = key.api_key
return (
<DropdownMenuRadioItem
key={key.id}
value={value}
onClick={() =>
setSelectedApiKey({
name: `Publishable key: ${key.name}`,
key: value,
})
}
>
{key.name}
</DropdownMenuRadioItem>
)
})}
</>
)}
{secretKeys.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Secret keys</DropdownMenuLabel>
{secretKeys.map((key) => {
const value = key.prefix + '...'
return (
<DropdownMenuRadioItem
key={key.id}
value={value}
onClick={() =>
setSelectedApiKey({ name: `Secret key: ${key.name}`, key: value })
}
>
{key.name}
</DropdownMenuRadioItem>
)
})}
</>
)}
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>JWT-based legacy keys</DropdownMenuLabel>
{legacyKeys.map((key) => {
const value = key.api_key
return (
<DropdownMenuRadioItem
key={key.id}
value={value}
onClick={() =>
setSelectedApiKey({
name: `Publishable key: ${key.name}`,
key: value,
})
setSelectedApiKey({ name: `Legacy key: ${key.name}`, key: value })
}
>
{key.name}
</DropdownMenuRadioItem>
)
})}
</>
)}
{secretKeys.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Secret keys</DropdownMenuLabel>
{secretKeys.map((key) => {
const value = key.prefix + '...'
return (
<DropdownMenuRadioItem
key={key.id}
value={value}
onClick={() =>
setSelectedApiKey({ name: `Secret key: ${key.name}`, key: value })
}
>
{key.name}
</DropdownMenuRadioItem>
)
})}
</>
)}
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>JWT-based legacy keys</DropdownMenuLabel>
{legacyKeys.map((key) => {
const value = key.api_key
return (
<DropdownMenuRadioItem
key={key.id}
value={value}
onClick={() =>
setSelectedApiKey({ name: `Legacy key: ${key.name}`, key: value })
}
>
{key.name}
</DropdownMenuRadioItem>
)
})}
</DropdownMenuGroup>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</DropdownMenuGroup>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div>
</div>
)
@@ -10,6 +10,7 @@ import { toast } from 'sonner'
import z from 'zod'
import { useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import AlertError from 'components/ui/AlertError'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
@@ -83,7 +84,13 @@ export const EdgeFunctionDetails = () => {
'*'
)
const { data: apiKeys } = useAPIKeysQuery({ projectRef })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery(
{
projectRef,
},
{ enabled: canReadAPIKeys }
)
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const {
@@ -5,6 +5,7 @@ import { useFieldArray, useForm } from 'react-hook-form'
import * as z from 'zod'
import { useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { RoleImpersonationPopover } from 'components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query'
@@ -87,7 +88,8 @@ export const EdgeFunctionTesterSheet = ({ visible, onClose }: EdgeFunctionTester
const [response, setResponse] = useState<ResponseData | null>(null)
const [error, setError] = useState<string | null>(null)
const { data: apiKeys } = useAPIKeysQuery({ projectRef })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
const { data: config } = useProjectPostgrestConfigQuery({ projectRef })
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { data: accessToken } = useSessionAccessTokenQuery({ enabled: IS_PLATFORM })
@@ -16,6 +16,7 @@ import {
CollapsibleTrigger_Shadcn_,
Collapsible_Shadcn_,
} from 'ui'
import { useApiKeysVisibility } from '../APIKeys/hooks/useApiKeysVisibility'
import type { Commands } from './Functions.types'
interface TerminalInstructionsProps extends ComponentPropsWithoutRef<typeof Collapsible_Shadcn_> {
@@ -32,7 +33,8 @@ export const TerminalInstructions = forwardRef<
const [showInstructions, setShowInstructions] = useState(!closable)
const { data: tokens } = useAccessTokensQuery()
const { data: apiKeys } = useAPIKeysQuery({ projectRef })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
@@ -1,15 +1,14 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
import { AlertCircle, Loader } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import Panel from 'components/ui/Panel'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { Input, SimpleCodeBlock } from 'ui'
@@ -56,10 +55,10 @@ export const APIKeys = () => {
isLoading: isProjectSettingsLoading,
} = useProjectSettingsV2Query({ projectRef })
const { data: apiKeys } = useAPIKeysQuery({ projectRef })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
const { anonKey, serviceKey } = getKeys(apiKeys)
// API keys should not be empty. However it can be populated with a delay on project creation
const isApiKeysEmpty = !anonKey && !serviceKey
const {
@@ -77,11 +76,6 @@ export const APIKeys = () => {
const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus
const { can: canReadAPIKeys } = useAsyncCheckPermissions(
PermissionAction.READ,
'service_api_keys'
)
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
@@ -2,6 +2,7 @@ import { ChevronDown, Plus, Trash } from 'lucide-react'
import { useFieldArray } from 'react-hook-form'
import { useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import {
Button,
@@ -32,7 +33,11 @@ export const HTTPHeaderFieldsSection = ({ variant }: HTTPHeaderFieldsSectionProp
})
const { ref } = useParams()
const { data: apiKeys } = useAPIKeysQuery({ projectRef: ref, reveal: true })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery(
{ projectRef: ref, reveal: true },
{ enabled: canReadAPIKeys }
)
const { serviceKey, secretKey } = getKeys(apiKeys)
const apiKey = secretKey?.api_key ?? serviceKey?.api_key ?? '[YOUR API KEY]'
@@ -5,6 +5,7 @@ import { useMemo } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import GraphiQL from 'components/interfaces/GraphQL/GraphiQL'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query'
@@ -21,7 +22,11 @@ export const GraphiQLTab = () => {
const { data: accessToken } = useSessionAccessTokenQuery({ enabled: IS_PLATFORM })
const { data: apiKeys, isFetched } = useAPIKeysQuery({ projectRef, reveal: true })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys, isFetched } = useAPIKeysQuery(
{ projectRef, reveal: true },
{ enabled: canReadAPIKeys }
)
const { serviceKey, secretKey } = getKeys(apiKeys)
const { data: config } = useProjectPostgrestConfigQuery({ projectRef })
@@ -1,9 +1,10 @@
import { AnimatePresence } from 'framer-motion'
import { RotateCw, Timer } from 'lucide-react'
import { AlertCircle, RotateCw, Timer } from 'lucide-react'
import { useMemo, useState } from 'react'
import { toast } from 'sonner'
import { useFlag, useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useLegacyAPIKeysStatusQuery } from 'data/api-keys/legacy-api-keys-status-query'
import { useJWTSigningKeyDeleteMutation } from 'data/jwt-signing-keys/jwt-signing-key-delete-mutation'
@@ -57,14 +58,21 @@ export const JWTSecretKeysTable = () => {
const [selectedKeyToUpdate, setSelectedKeyToUpdate] = useState<string>()
const [shownDialog, setShownDialog] = useState<DialogType>()
const { data: signingKeys, isLoading: isLoadingSigningKeys } = useJWTSigningKeysQuery({
projectRef,
})
const { data: legacyKey, isLoading: isLoadingLegacyKey } = useLegacyJWTSigningKeyQuery({
projectRef,
})
const { canReadAPIKeys, isLoading: isLoadingCanReadAPIKeys } = useApiKeysVisibility()
const { data: signingKeys, isLoading: isLoadingSigningKeys } = useJWTSigningKeysQuery(
{
projectRef,
},
{ enabled: canReadAPIKeys }
)
const { data: legacyKey, isLoading: isLoadingLegacyKey } = useLegacyJWTSigningKeyQuery(
{
projectRef,
},
{ enabled: canReadAPIKeys }
)
const { data: legacyAPIKeysStatus, isLoading: isLoadingLegacyAPIKeysStatus } =
useLegacyAPIKeysStatusQuery({ projectRef })
useLegacyAPIKeysStatusQuery({ projectRef }, { enabled: canReadAPIKeys })
const { mutate: migrateJWTSecret, isLoading: isMigrating } = useLegacyJWTSigningKeyCreateMutation(
{
@@ -152,6 +160,20 @@ export const JWTSecretKeysTable = () => {
)
}
if (!canReadAPIKeys && !isLoadingCanReadAPIKeys) {
return (
<div className="bg-surface-100 rounded-md border shadow-sm">
<div className="flex items-center py-8 px-8 space-x-2">
<AlertCircle size={16} strokeWidth={1.5} />
<p className="text-sm text-foreground-light">
You don't have permission to view JWT signing keys. These keys are restricted to users
with higher access levels.
</p>
</div>
</div>
)
}
if (isLoading) {
return <GenericSkeletonLoader />
}
@@ -163,7 +185,7 @@ export const JWTSecretKeysTable = () => {
return (
<>
<div className="-space-y-px">
{legacyKey ? (
{!canReadAPIKeys ? null : legacyKey ? (
<>
{standbyKey && (
<ActionPanel
@@ -52,6 +52,7 @@ import {
import { Admonition } from 'ui-patterns/admonition'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
import { number, object } from 'yup'
import { useApiKeysVisibility } from '../APIKeys/hooks/useApiKeysVisibility'
import {
JWT_SECRET_UPDATE_ERROR_MESSAGES,
JWT_SECRET_UPDATE_PROGRESS_MESSAGES,
@@ -91,17 +92,19 @@ const JWTSettings = () => {
const { mutateAsync: updateJwt, isLoading: isSubmittingJwtSecretUpdateRequest } =
useJwtSecretUpdateMutation()
const { data: legacyKey } = useLegacyJWTSigningKeyQuery({
projectRef,
})
const { data: legacyAPIKeysStatus } = useLegacyAPIKeysStatusQuery({ projectRef })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: legacyKey } = useLegacyJWTSigningKeyQuery(
{
projectRef,
},
{ enabled: canReadAPIKeys }
)
const { data: legacyAPIKeysStatus } = useLegacyAPIKeysStatusQuery(
{ projectRef },
{ enabled: canReadAPIKeys }
)
const {
data: authConfig,
error: authConfigError,
isLoading: isLoadingAuthConfig,
isSuccess: isSuccessAuthConfig,
} = useAuthConfigQuery({ projectRef })
const { data: authConfig, isLoading: isLoadingAuthConfig } = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig, isLoading: isUpdatingAuthConfig } =
useAuthConfigUpdateMutation()
@@ -1,6 +1,7 @@
import { useParams } from 'common'
import { Button, Input, copyToClipboard } from 'ui'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
@@ -13,7 +14,8 @@ import type { ContentProps } from './Content.types'
export const Introduction = ({ showKeys, language, apikey, endpoint }: ContentProps) => {
const { ref } = useParams()
const { data: apiKeys } = useAPIKeysQuery({ projectRef: ref })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery({ projectRef: ref }, { enabled: canReadAPIKeys })
const { data } = useProjectSettingsV2Query({ projectRef: ref })
const { data: org } = useSelectedOrganizationQuery()
const { mutate: sendEvent } = useSendEventMutation()
@@ -6,6 +6,7 @@ import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useAppStateSnapshot } from 'state/app-state'
import { useApiKeysVisibility } from '../APIKeys/hooks/useApiKeysVisibility'
import { Bucket } from './Content/Bucket'
import { EdgeFunction } from './Content/EdgeFunction'
import { EdgeFunctions } from './Content/EdgeFunctions'
@@ -43,9 +44,10 @@ export const ProjectAPIDocs = () => {
const [showKeys, setShowKeys] = useState(false)
const language = snap.docsLanguage
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery(
{ projectRef: ref },
{ enabled: snap.showProjectApiDocs }
{ enabled: snap.showProjectApiDocs && canReadAPIKeys }
)
const { data: settings } = useProjectSettingsV2Query(
{ projectRef: ref },
@@ -2,6 +2,7 @@ import { Dispatch, SetStateAction, useEffect, useRef } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { RoleImpersonationPopover } from 'components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { getTemporaryAPIKey } from 'data/api-keys/temp-api-keys-query'
@@ -23,10 +24,14 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens
const { data: org } = useSelectedOrganizationQuery()
const snap = useRoleImpersonationStateSnapshot()
const { data: apiKeys } = useAPIKeysQuery({
projectRef: config.projectRef,
reveal: true,
})
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery(
{
projectRef: config.projectRef,
reveal: true,
},
{ enabled: canReadAPIKeys }
)
const { anonKey, publishableKey } = getKeys(apiKeys)
const { data: postgrestConfig } = useProjectPostgrestConfigQuery(
@@ -8,6 +8,7 @@ import { toast } from 'sonner'
import z from 'zod'
import { useFlag, useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { convertKVStringArrayToJson } from 'components/interfaces/Integrations/Wrappers/Wrappers.utils'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
@@ -156,7 +157,11 @@ export const ConnectTablesDialogContent = ({
const wrapperValues = convertKVStringArrayToJson(wrapperInstance?.server_options ?? [])
const { data: projectSettings } = useProjectSettingsV2Query({ projectRef })
const { data: apiKeys } = useAPIKeysQuery({ projectRef, reveal: true })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery(
{ projectRef, reveal: true },
{ enabled: canReadAPIKeys }
)
const { serviceKey } = getKeys(apiKeys)
const { sourceId, pipeline, publication } = useAnalyticsBucketAssociatedEntities({
@@ -3,6 +3,7 @@ import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useToggleLegacyAPIKeysMutation } from 'data/api-keys/legacy-api-key-toggle-mutation'
import { useLegacyAPIKeysStatusQuery } from 'data/api-keys/legacy-api-keys-status-query'
@@ -30,16 +31,20 @@ export const ToggleLegacyApiKeysPanel = () => {
const [isConfirmOpen, setIsConfirmOpen] = useState(false)
const [isAppsWarningOpen, setIsAppsWarningOpen] = useState(false)
const { data: legacyAPIKeysStatusData, isSuccess: isLegacyAPIKeysStatusSuccess } =
useLegacyAPIKeysStatusQuery({ projectRef })
const { data: legacyJWTSecret } = useLegacyJWTSigningKeyQuery({ projectRef })
const { canReadAPIKeys } = useApiKeysVisibility()
const { can: canUpdateAPIKeys, isSuccess: isPermissionsSuccess } = useAsyncCheckPermissions(
PermissionAction.SECRETS_WRITE,
'*'
)
const { data: legacyAPIKeysStatusData, isSuccess: isLegacyAPIKeysStatusSuccess } =
useLegacyAPIKeysStatusQuery({ projectRef }, { enabled: canReadAPIKeys })
const { data: legacyJWTSecret } = useLegacyJWTSigningKeyQuery(
{ projectRef },
{ enabled: canReadAPIKeys }
)
const { data: authorizedApps = [], isSuccess: isAuthorizedAppsSuccess } = useAuthorizedAppsQuery({
slug: org?.slug,
})
@@ -1,5 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { WRAPPERS } from 'components/interfaces/Integrations/Wrappers/Wrappers.constants'
import {
getAnalyticsBucketFDWName,
@@ -19,7 +20,11 @@ import { useS3AccessKeyCreateMutation } from './s3-access-key-create-mutation'
export const useIcebergWrapperCreateMutation = () => {
const { data: project } = useSelectedProjectQuery()
const { data: apiKeys } = useAPIKeysQuery({ projectRef: project?.ref, reveal: true })
const { canReadAPIKeys } = useApiKeysVisibility()
const { data: apiKeys } = useAPIKeysQuery(
{ projectRef: project?.ref, reveal: true },
{ enabled: canReadAPIKeys }
)
const { secretKey, serviceKey } = getKeys(apiKeys)
const { data: settings } = useProjectSettingsV2Query({ projectRef: project?.ref })