| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- import { ident, safeSql } from '@supabase/pg-meta/src/pg-format'
- import { useQueryClient } from '@tanstack/react-query'
- import { EyeOff, Lock } from 'lucide-react'
- import { useState } from 'react'
- import { toast } from 'sonner'
- import { Badge, Button } from 'ui'
- import { Admonition } from 'ui-patterns'
- import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
- import { InlineLink } from '@/components/ui/InlineLink'
- import { lintKeys } from '@/data/lint/keys'
- import { Lint } from '@/data/lint/lint-query'
- import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- const GRAPHQL_EXPOSURE_LINT_NAMES = [
- 'pg_graphql_anon_table_exposed',
- 'pg_graphql_authenticated_table_exposed',
- ] as const
- export type GraphqlExposureLintName = (typeof GRAPHQL_EXPOSURE_LINT_NAMES)[number]
- export const asGraphqlExposureLint = (
- name: string | undefined | null
- ): GraphqlExposureLintName | null =>
- !!name && (GRAPHQL_EXPOSURE_LINT_NAMES as readonly string[]).includes(name)
- ? (name as GraphqlExposureLintName)
- : null
- interface GraphqlExposureLintCTAProps {
- lintName: GraphqlExposureLintName
- projectRef: string
- metadata: Lint['metadata']
- onAfterAction?: () => void
- }
- const ROLE_BY_LINT: Record<GraphqlExposureLintName, 'anon' | 'authenticated'> = {
- pg_graphql_anon_table_exposed: 'anon',
- pg_graphql_authenticated_table_exposed: 'authenticated',
- }
- const AUDIENCE: Record<GraphqlExposureLintName, { lower: string; upper: string }> = {
- pg_graphql_anon_table_exposed: { lower: 'anonymous users', upper: 'Anonymous users' },
- pg_graphql_authenticated_table_exposed: { lower: 'signed-in users', upper: 'Signed-in users' },
- }
- const TRIGGER_LABEL: Record<GraphqlExposureLintName, string> = {
- pg_graphql_anon_table_exposed: 'Remove access for anonymous users',
- pg_graphql_authenticated_table_exposed: 'Remove access for signed-in users',
- }
- export const GraphqlExposureLintCTA = ({
- lintName,
- projectRef,
- metadata,
- onAfterAction,
- }: GraphqlExposureLintCTAProps) => {
- const { data: project } = useSelectedProjectQuery()
- const queryClient = useQueryClient()
- const [showConfirmRevoke, setShowConfirmRevoke] = useState(false)
- const schema = metadata?.schema
- const name = metadata?.name
- const objectType = metadata?.type ?? 'object'
- const role = ROLE_BY_LINT[lintName]
- const audience = AUDIENCE[lintName]
- const canAct = !!schema && !!name
- const revokeSql =
- schema && name
- ? safeSql`revoke all on ${ident(schema)}.${ident(name)} from ${ident(role)};`
- : undefined
- const { mutate: executeSql, isPending: isRevoking } = useExecuteSqlMutation({
- onSuccess: async () => {
- toast.success(
- `Revoked access to ${schema}.${name} from ${role}. ${audience.upper} can no longer query this ${objectType} via GraphQL or Data API.`
- )
- setShowConfirmRevoke(false)
- await queryClient.invalidateQueries({ queryKey: lintKeys.lint(projectRef) })
- onAfterAction?.()
- },
- onError: (error) => {
- toast.error(`Failed to revoke access: ${error.message}`)
- },
- })
- const handleRevoke = () => {
- if (!revokeSql) return
- executeSql({
- projectRef,
- connectionString: project?.connectionString,
- sql: revokeSql,
- })
- }
- return (
- <>
- <Button type="primary" disabled={!canAct} onClick={() => setShowConfirmRevoke(true)}>
- {TRIGGER_LABEL[lintName]}
- </Button>
- <ConfirmationModal
- visible={showConfirmRevoke}
- size="xlarge"
- title={
- canAct
- ? `Remove access to ${schema}.${name} for ${audience.lower}?`
- : `Remove access for ${audience.lower}?`
- }
- confirmLabel="Remove access"
- confirmLabelLoading="Removing access..."
- cancelLabel="Cancel"
- loading={isRevoking}
- onCancel={() => setShowConfirmRevoke(false)}
- onConfirm={handleRevoke}
- >
- <div className="text-sm text-foreground mb-6">
- <p>This change affects both schema visibility and data access for {audience.lower}.</p>
- <p>
- Alternatively, you can{' '}
- <InlineLink href={`/project/${projectRef}/database/extensions`}>
- disable GraphQL
- </InlineLink>{' '}
- to remove schema visibility.
- </p>
- </div>
- <div className="space-y-5">
- <div className="flex gap-3">
- <Lock className="text-foreground-light shrink-0 mt-0.5" size={20} strokeWidth={1.5} />
- <div>
- <div className="flex items-center gap-2">
- <p className="text-sm text-foreground">Data API access removed</p>
- <Badge variant="warning">Breaking change</Badge>
- </div>
- <p className="text-sm text-foreground-light mt-1">
- {audience.upper} will no longer be able to read or write to this {objectType} via
- Briven APIs (GraphQL or Data API), even if RLS policies allow it.
- </p>
- </div>
- </div>
- <div className="flex gap-3">
- <EyeOff className="text-foreground-light shrink-0 mt-0.5" size={20} strokeWidth={1.5} />
- <div>
- <p className="text-sm text-foreground">Schema hidden from GraphQL</p>
- <p className="text-sm text-foreground-light mt-1">
- This {objectType} will no longer appear in the GraphQL schema. {audience.upper}{' '}
- won't be able to discover its name, columns, or relationships.
- </p>
- </div>
- </div>
- </div>
- <Admonition
- type="warning"
- title="When to keep access"
- description={`If your app needs ${audience.lower} to query this ${objectType}, keep access and ignore this warning. Be aware that this ${objectType}'s schema will remain visible via the GraphQL API.`}
- className="mt-6"
- />
- <p className="text-sm text-foreground-light mt-6">
- The following statement will be executed:
- </p>
- <pre className="mt-2 px-3 py-2 rounded bg-surface-200 text-xs font-mono whitespace-pre-wrap break-all">
- {revokeSql}
- </pre>
- </ConfirmationModal>
- </>
- )
- }
- export const GraphqlExposureCallout = ({ projectRef }: { projectRef: string }) => {
- return (
- <Admonition
- type="default"
- title="Why this appears"
- description={
- <p>
- These warnings are triggered by GraphQL exposing your table schemas. If you're not using
- GraphQL, disable it from the{' '}
- <InlineLink href={`/project/${projectRef}/database/extensions`}>
- Database extensions page
- </InlineLink>
- .
- </p>
- }
- />
- )
- }
|