ProjectNeedsSecuring.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common'
  2. import { AnimatePresence, motion } from 'framer-motion'
  3. import { useRouter } from 'next/router'
  4. import { PropsWithChildren, useMemo } from 'react'
  5. import type {
  6. ProjectSecurityActionDetails,
  7. ProjectSecurityActionType,
  8. } from './ProjectNeedsSecuring.types'
  9. import { getExposedSchemas, getTableKey, sortTables } from './ProjectNeedsSecuring.utils'
  10. import { ProjectNeedsSecuringView } from './ProjectNeedsSecuringView'
  11. import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
  12. import { useProjectLintsQuery } from '@/data/lint/lint-query'
  13. import { useTablePrivilegesQuery } from '@/data/privileges/table-privileges-query'
  14. import { useTablesQuery } from '@/data/tables/tables-query'
  15. import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
  16. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  17. import { isApiAccessRole, isApiPrivilegeType } from '@/lib/data-api-types'
  18. import { useTrack } from '@/lib/telemetry/track'
  19. const PROJECT_SECURITY_FEATURE_FLAG = 'projectNeedsSecuring'
  20. const PROJECT_HOME_PATHNAME = '/project/[ref]'
  21. const ProjectNeedsSecuringGate = ({ children }: PropsWithChildren) => {
  22. const router = useRouter()
  23. const track = useTrack()
  24. const { ref: projectRef } = useParams()
  25. const { data: project } = useSelectedProjectQuery()
  26. const [securityDismissedAt, setSecurityDismissedAt, { isLoading: isLoadingDismissedAt }] =
  27. useLocalStorageQuery<string | null>(
  28. projectRef
  29. ? LOCAL_STORAGE_KEYS.PROJECT_SECURITY_DISMISSED_AT(projectRef)
  30. : 'project-security-dismissed-at-unknown',
  31. null
  32. )
  33. const isProjectHomeRoute = router.pathname === PROJECT_HOME_PATHNAME
  34. const { data: lints = [], isPending: isLoadingLints } = useProjectLintsQuery(
  35. { projectRef },
  36. { enabled: isProjectHomeRoute && !!projectRef }
  37. )
  38. const rlsIssueKeys = useMemo(() => {
  39. return new Set(
  40. lints
  41. .filter((lint) => lint.name === 'rls_disabled_in_public' && lint.level === 'ERROR')
  42. .map((lint) => {
  43. const schema = typeof lint.metadata?.schema === 'string' ? lint.metadata.schema : null
  44. const name = typeof lint.metadata?.name === 'string' ? lint.metadata.name : null
  45. return schema && name ? getTableKey({ schema, name }) : null
  46. })
  47. .filter((value): value is string => value !== null)
  48. )
  49. }, [lints])
  50. const hasRlsIssues = rlsIssueKeys.size > 0
  51. const shouldRenderGate =
  52. isProjectHomeRoute &&
  53. !!projectRef &&
  54. !isLoadingDismissedAt &&
  55. hasRlsIssues &&
  56. securityDismissedAt === null
  57. const {
  58. data: tables,
  59. error: tablesError,
  60. isPending: isLoadingTables,
  61. } = useTablesQuery(
  62. {
  63. projectRef,
  64. connectionString: project?.connectionString,
  65. includeColumns: false,
  66. },
  67. { enabled: shouldRenderGate }
  68. )
  69. const handleTrackAction = (
  70. type: ProjectSecurityActionType,
  71. details?: ProjectSecurityActionDetails
  72. ) => {
  73. track('project_security_cta_clicked', {
  74. type,
  75. ...details,
  76. })
  77. }
  78. const {
  79. data: dbSchema,
  80. error: postgrestConfigError,
  81. isPending: isLoadingPostgrestConfig,
  82. } = useProjectPostgrestConfigQuery(
  83. { projectRef },
  84. {
  85. enabled: shouldRenderGate,
  86. select: ({ db_schema }) => db_schema,
  87. }
  88. )
  89. const {
  90. data: tablePrivileges,
  91. error: tablePrivilegesError,
  92. isPending: isLoadingTablePrivileges,
  93. } = useTablePrivilegesQuery(
  94. { projectRef, connectionString: project?.connectionString },
  95. { enabled: shouldRenderGate }
  96. )
  97. const tableRows = useMemo(() => {
  98. if (!tables) return []
  99. const exposedSchemas = getExposedSchemas(dbSchema)
  100. const dataApiAccessByTable = new Map<string, boolean>()
  101. for (const entry of tablePrivileges ?? []) {
  102. const key = getTableKey(entry)
  103. const hasDataApiAccess = entry.privileges.some(
  104. (privilege) =>
  105. isApiAccessRole(privilege.grantee) && isApiPrivilegeType(privilege.privilege_type)
  106. )
  107. if (hasDataApiAccess) {
  108. dataApiAccessByTable.set(key, true)
  109. }
  110. }
  111. return sortTables(
  112. tables
  113. .filter((table) => exposedSchemas.includes(table.schema))
  114. .filter((table) => !table.rls_enabled && rlsIssueKeys.has(getTableKey(table)))
  115. .map((table) => {
  116. const key = getTableKey(table)
  117. return {
  118. id: table.id,
  119. name: table.name,
  120. schema: table.schema,
  121. rlsEnabled: table.rls_enabled,
  122. dataApiAccessible: dataApiAccessByTable.get(key) === true,
  123. hasRlsIssue: rlsIssueKeys.has(key),
  124. }
  125. })
  126. )
  127. }, [dbSchema, rlsIssueKeys, tablePrivileges, tables])
  128. if (!isProjectHomeRoute || !projectRef || isLoadingLints || !hasRlsIssues) {
  129. return <>{children}</>
  130. }
  131. return (
  132. <AnimatePresence mode="wait">
  133. {shouldRenderGate ? (
  134. <motion.div
  135. key="project-needs-securing"
  136. className="flex flex-1 min-h-0 w-full"
  137. initial={{ opacity: 0 }}
  138. animate={{ opacity: 1 }}
  139. exit={{ opacity: 0 }}
  140. transition={{ duration: 0.2, ease: 'easeOut' }}
  141. >
  142. <ProjectNeedsSecuringView
  143. projectRef={projectRef}
  144. issueCount={rlsIssueKeys.size}
  145. tables={tableRows}
  146. isLoading={isLoadingTables || isLoadingPostgrestConfig || isLoadingTablePrivileges}
  147. error={tablesError ?? postgrestConfigError ?? tablePrivilegesError}
  148. onDismiss={() => setSecurityDismissedAt(new Date().toISOString())}
  149. onTrackAction={handleTrackAction}
  150. />
  151. </motion.div>
  152. ) : (
  153. <motion.div
  154. key="project-needs-securing-children"
  155. className="flex flex-1 min-h-0 w-full"
  156. initial={{ opacity: 0 }}
  157. animate={{ opacity: 1 }}
  158. exit={{ opacity: 0 }}
  159. transition={{ duration: 0.2, ease: 'easeOut' }}
  160. >
  161. {children}
  162. </motion.div>
  163. )}
  164. </AnimatePresence>
  165. )
  166. }
  167. export const ProjectNeedsSecuring = ({ children }: PropsWithChildren) => {
  168. const isEnabled = useFlag(PROJECT_SECURITY_FEATURE_FLAG)
  169. if (!isEnabled) return <>{children}</>
  170. return <ProjectNeedsSecuringGate>{children}</ProjectNeedsSecuringGate>
  171. }