index.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { useParams } from 'common'
  2. import { noop } from 'lodash'
  3. import { memo, useMemo } from 'react'
  4. import {
  5. Card,
  6. CardContent,
  7. CardHeader,
  8. cn,
  9. Table,
  10. TableBody,
  11. TableHead,
  12. TableHeader,
  13. TableRow,
  14. } from 'ui'
  15. import { Admonition } from 'ui-patterns'
  16. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  17. import { usePoliciesData } from '../PoliciesDataContext'
  18. import { PolicyRow } from './PolicyRow'
  19. import type { PolicyTable } from './PolicyTableRow.types'
  20. import type { Policy } from './PolicyTableRow.utils'
  21. import { getTableAdmonitionMessage, getTableDataApiStatus } from './PolicyTableRow.utils'
  22. import { PolicyTableRowHeader } from './PolicyTableRowHeader'
  23. import AlertError from '@/components/ui/AlertError'
  24. import { InlineLink } from '@/components/ui/InlineLink'
  25. import { useTableApiAccessQuery } from '@/data/privileges/table-api-access-query'
  26. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  27. export interface PolicyTableRowProps {
  28. table: PolicyTable
  29. isLocked: boolean
  30. onSelectToggleRLS: (table: PolicyTable) => void
  31. onSelectCreatePolicy: (table: PolicyTable) => void
  32. onSelectEditPolicy: (policy: Policy) => void
  33. onSelectDeletePolicy: (policy: Policy) => void
  34. }
  35. const PolicyTableRowComponent = ({
  36. table,
  37. isLocked,
  38. onSelectToggleRLS = noop,
  39. onSelectCreatePolicy = noop,
  40. onSelectEditPolicy = noop,
  41. onSelectDeletePolicy = noop,
  42. }: PolicyTableRowProps) => {
  43. const { ref } = useParams()
  44. const { data: project } = useSelectedProjectQuery()
  45. const { getPoliciesForTable, isPoliciesLoading, isPoliciesError, policiesError, exposedSchemas } =
  46. usePoliciesData()
  47. const policies = useMemo(
  48. () => getPoliciesForTable(table.schema, table.name),
  49. [getPoliciesForTable, table.schema, table.name]
  50. )
  51. // [Joshen] Classification is more granular than "RLS on/off" alone — it also considers
  52. // schema exposure and whether anon/authenticated/service_role actually have grants.
  53. // Ideally we'd rely on the security lints, but they only look at the public schema and
  54. // ignore roles. Once the lints cover both, we can switch to them as the source of truth.
  55. const tableNames = useMemo(() => [table.name], [table.name])
  56. const {
  57. data: apiAccessMap,
  58. isPending: isLoadingRolesAccess,
  59. isError: isRolesAccessError,
  60. } = useTableApiAccessQuery({
  61. projectRef: project?.ref,
  62. connectionString: project?.connectionString,
  63. schemaName: table.schema,
  64. tableNames,
  65. })
  66. const status = useMemo(
  67. () =>
  68. getTableDataApiStatus({
  69. isSchemaExposed: exposedSchemas.has(table.schema),
  70. apiAccessData: apiAccessMap?.[table.name],
  71. isRLSEnabled: table.rls_enabled,
  72. policiesCount: policies.length,
  73. }),
  74. [exposedSchemas, apiAccessMap, table.schema, table.name, table.rls_enabled, policies.length]
  75. )
  76. const hasApiAccess =
  77. status === 'publicly-readable' || status === 'locked-by-rls' || status === 'secured'
  78. const isPubliclyReadable = status === 'publicly-readable'
  79. const isRealtimeSchema = table.schema === 'realtime'
  80. const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages'
  81. const isTableLocked = isRealtimeSchema ? !isRealtimeMessagesTable : isLocked
  82. const showPolicies = !isPoliciesLoading && !isPoliciesError && !isLoadingRolesAccess
  83. const admonitionMessage = useMemo(() => getTableAdmonitionMessage(status), [status])
  84. return (
  85. <Card className={cn(isPubliclyReadable && 'border-warning-500')}>
  86. <CardHeader className={cn('py-3 px-4', status !== 'secured' && 'border-b-0')}>
  87. <PolicyTableRowHeader
  88. table={table}
  89. isLocked={isLocked}
  90. hasApiAccess={hasApiAccess}
  91. isLoadingApiAccess={isLoadingRolesAccess}
  92. onSelectToggleRLS={onSelectToggleRLS}
  93. onSelectCreatePolicy={onSelectCreatePolicy}
  94. />
  95. </CardHeader>
  96. {!isLoadingRolesAccess && !isRolesAccessError && status === 'schema-not-exposed' && (
  97. <Admonition
  98. showIcon={false}
  99. type="warning"
  100. className="border-0 border-y rounded-none min-h-12 flex items-center"
  101. >
  102. <p className="text-foreground-light">
  103. No data will be selectable via Briven APIs as this schema is not exposed. You may
  104. configure this in your project’s{' '}
  105. <InlineLink href={`/project/${ref}/integrations/data_api/settings`}>
  106. API settings
  107. </InlineLink>
  108. .
  109. </p>
  110. </Admonition>
  111. )}
  112. {!isLoadingRolesAccess && !isRolesAccessError && admonitionMessage !== null && (
  113. <Admonition
  114. showIcon={false}
  115. type={isPubliclyReadable ? 'warning' : 'default'}
  116. className="border-0 border-y rounded-none min-h-12 flex items-center"
  117. >
  118. <p>{admonitionMessage}</p>
  119. </Admonition>
  120. )}
  121. {(isPoliciesLoading || isLoadingRolesAccess) && (
  122. <CardContent>
  123. <ShimmeringLoader />
  124. </CardContent>
  125. )}
  126. {isPoliciesError && (
  127. <CardContent>
  128. <AlertError
  129. className="border-0 rounded-none"
  130. error={policiesError}
  131. subject="Failed to retrieve policies"
  132. />
  133. </CardContent>
  134. )}
  135. {showPolicies && (
  136. <CardContent className="p-0">
  137. {policies.length === 0 ? (
  138. <p className="text-foreground-lighter text-sm p-4">No policies created yet</p>
  139. ) : (
  140. <Table className="table-fixed">
  141. <TableHeader>
  142. <TableRow>
  143. <TableHead className="w-[40%]">Name</TableHead>
  144. <TableHead className="w-[20%]">Command</TableHead>
  145. <TableHead className="w-[30%]">Applied to</TableHead>
  146. <TableHead className="text-right">
  147. <span className="sr-only">Actions</span>
  148. </TableHead>
  149. </TableRow>
  150. </TableHeader>
  151. <TableBody>
  152. {policies.map((policy) => (
  153. <PolicyRow
  154. key={policy.id}
  155. policy={policy}
  156. isLocked={isTableLocked}
  157. onSelectEditPolicy={onSelectEditPolicy}
  158. onSelectDeletePolicy={onSelectDeletePolicy}
  159. />
  160. ))}
  161. </TableBody>
  162. </Table>
  163. )}
  164. </CardContent>
  165. )}
  166. </Card>
  167. )
  168. }
  169. export const PolicyTableRow = memo(PolicyTableRowComponent)
  170. PolicyTableRow.displayName = 'PolicyTableRow'