UnsafeEntitiesConfirmModal.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { ChevronRight, ChevronUp } from 'lucide-react'
  2. import { useMemo, useState } from 'react'
  3. import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui'
  4. import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
  5. import { type ExposedEntity } from './DataApiEnableSwitch.utils'
  6. interface UnsafeEntitiesConfirmModalProps {
  7. visible: boolean
  8. loading: boolean
  9. unsafeEntities: Array<ExposedEntity>
  10. onCancel: () => void
  11. onConfirm: () => void
  12. }
  13. const ENTITY_TYPE_META: Record<
  14. ExposedEntity['type'],
  15. { heading: string; recommendation: string; docsUrl: string }
  16. > = {
  17. table: {
  18. heading: 'Tables without Row Level Security',
  19. recommendation: 'Enable RLS on these tables to control access per-row.',
  20. docsUrl:
  21. 'https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public',
  22. },
  23. 'foreign table': {
  24. heading: 'Foreign tables',
  25. recommendation:
  26. 'Foreign tables do not support RLS. Revoke access from the anon and authenticated roles.',
  27. docsUrl:
  28. 'https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api',
  29. },
  30. 'materialized view': {
  31. heading: 'Materialized views',
  32. recommendation:
  33. 'Materialized views do not support RLS. Revoke access from the anon and authenticated roles.',
  34. docsUrl:
  35. 'https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api',
  36. },
  37. view: {
  38. heading: 'Views without SECURITY INVOKER',
  39. recommendation:
  40. 'These views run with the permissions of the view creator, not the querying user. Set SECURITY INVOKER to enforce caller permissions.',
  41. docsUrl:
  42. 'https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view',
  43. },
  44. }
  45. const ENTITY_TYPE_ORDER: Array<ExposedEntity['type']> = [
  46. 'table',
  47. 'foreign table',
  48. 'materialized view',
  49. 'view',
  50. ]
  51. const COLLAPSE_THRESHOLD = 3
  52. export const UnsafeEntitiesConfirmModal = ({
  53. visible,
  54. loading,
  55. unsafeEntities,
  56. onCancel,
  57. onConfirm,
  58. }: UnsafeEntitiesConfirmModalProps) => {
  59. const groupedEntities = useMemo(() => {
  60. const groups = new Map<ExposedEntity['type'], Array<ExposedEntity>>()
  61. for (const entity of unsafeEntities) {
  62. const group = groups.get(entity.type)
  63. if (group) {
  64. group.push(entity)
  65. } else {
  66. groups.set(entity.type, [entity])
  67. }
  68. }
  69. return ENTITY_TYPE_ORDER.filter((type) => groups.has(type)).map((type) => ({
  70. type,
  71. ...ENTITY_TYPE_META[type],
  72. entities: groups.get(type) ?? [],
  73. }))
  74. }, [unsafeEntities])
  75. return (
  76. <ConfirmationModal
  77. variant="warning"
  78. visible={visible}
  79. loading={loading}
  80. title="Insecure objects detected"
  81. confirmLabel="Enable Data API"
  82. confirmLabelLoading="Enabling"
  83. onCancel={onCancel}
  84. onConfirm={onConfirm}
  85. className="max-h-[50vh] overflow-y-auto"
  86. >
  87. <div className="text-sm text-foreground-light space-y-4">
  88. <p>
  89. The following objects will be publicly accessible through the Data API and are insecure.
  90. </p>
  91. {groupedEntities.map(({ type, heading, recommendation, docsUrl, entities }) => (
  92. <div key={type} className="space-y-1">
  93. <h4 className="text-foreground font-medium">{heading}</h4>
  94. <EntityList entities={entities} />
  95. <p className="text-foreground-lighter text-xs">
  96. {recommendation}{' '}
  97. <a
  98. href={docsUrl}
  99. target="_blank"
  100. rel="noopener noreferrer"
  101. className="underline hover:text-foreground"
  102. >
  103. Learn more
  104. </a>
  105. </p>
  106. </div>
  107. ))}
  108. </div>
  109. </ConfirmationModal>
  110. )
  111. }
  112. const EntityListItem = ({ entity }: { entity: ExposedEntity }) => (
  113. <li>
  114. <code className="text-xs">
  115. {entity.schema}.{entity.name}
  116. </code>
  117. </li>
  118. )
  119. const EntityList = ({ entities }: { entities: Array<ExposedEntity> }) => {
  120. const [open, setOpen] = useState(false)
  121. const shouldCollapse = entities.length > COLLAPSE_THRESHOLD
  122. const visibleEntities = entities.slice(0, COLLAPSE_THRESHOLD)
  123. const hiddenEntities = entities.slice(COLLAPSE_THRESHOLD)
  124. if (!shouldCollapse) {
  125. return (
  126. <ul className="list-disc pl-5 space-y-0.5">
  127. {entities.map((entity) => (
  128. <EntityListItem key={`${entity.schema}.${entity.name}`} entity={entity} />
  129. ))}
  130. </ul>
  131. )
  132. }
  133. return (
  134. <Collapsible open={open} onOpenChange={setOpen}>
  135. <ul className="list-disc pl-5 space-y-0.5">
  136. {visibleEntities.map((entity) => (
  137. <EntityListItem key={`${entity.schema}.${entity.name}`} entity={entity} />
  138. ))}
  139. </ul>
  140. <CollapsibleContent className="transition-all data-closed:animate-collapsible-up data-open:animate-collapsible-down">
  141. <ul className="list-disc pl-5 space-y-0.5">
  142. {hiddenEntities.map((entity) => (
  143. <EntityListItem key={`${entity.schema}.${entity.name}`} entity={entity} />
  144. ))}
  145. </ul>
  146. </CollapsibleContent>
  147. <CollapsibleTrigger asChild>
  148. <Button
  149. type="text"
  150. size="tiny"
  151. className="px-0 h-auto text-xs text-foreground-lighter hover:text-foreground"
  152. >
  153. <div className="flex items-center gap-1">
  154. {open ? <ChevronUp size={12} /> : <ChevronRight size={12} />}
  155. <span>{open ? 'Show less' : `Show ${hiddenEntities.length} more`}</span>
  156. </div>
  157. </Button>
  158. </CollapsibleTrigger>
  159. </Collapsible>
  160. )
  161. }