RLSTableCard.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import { Check, ChevronDown, Edit, X } from 'lucide-react'
  2. import { useMemo } from 'react'
  3. import { cn, Collapsible, CollapsibleContent, CollapsibleTrigger, WarningIcon } from 'ui'
  4. import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
  5. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  6. interface RLSTableCardProps {
  7. table: { schema: string; name: string; isRLSEnabled: boolean }
  8. role?: string
  9. policies: Policy[]
  10. handleSelectEditPolicy: (policy: Policy) => void
  11. }
  12. export const RLSTableCard = ({
  13. table,
  14. role,
  15. policies,
  16. handleSelectEditPolicy,
  17. }: RLSTableCardProps) => {
  18. const { schema, name, isRLSEnabled } = table
  19. const trueOnlyPolicy = policies.find((x) => x.definition === 'true')
  20. const falseOnlyPolicy = policies.find((x) => x.definition === 'false')
  21. const noPolicies = isRLSEnabled && policies.length === 0
  22. const tableAccessDescription = useMemo(() => {
  23. if (!isRLSEnabled) {
  24. return (
  25. <p>
  26. RLS is disabled and all data is publicly accessible. We highly recommend enabling RLS and
  27. adding policies to restrict access.
  28. </p>
  29. )
  30. }
  31. if (noPolicies) {
  32. return (
  33. <p>
  34. RLS is enabled but no policies exist for the{' '}
  35. <code className="text-code-inline">{role}</code> role on this table - no data will be
  36. returned.
  37. </p>
  38. )
  39. }
  40. if (trueOnlyPolicy) {
  41. return (
  42. <>
  43. <p>
  44. The policy "{trueOnlyPolicy.name}" for the{' '}
  45. <code className="text-code-inline">{role}</code> role on this table evaluates to{' '}
  46. <code className="text-code-inline">true</code>, so all data from this query is
  47. accessible to this user.
  48. </p>
  49. <TableAccessPolicySummary
  50. policies={policies}
  51. handleSelectEditPolicy={handleSelectEditPolicy}
  52. />
  53. </>
  54. )
  55. }
  56. if (falseOnlyPolicy) {
  57. return (
  58. <>
  59. <p>
  60. The policy "{falseOnlyPolicy.name}" for the{' '}
  61. <code className="text-code-inline">{role}</code> role on this table evaluates to{' '}
  62. <code className="text-code-inline">false</code>, so no data from this query is
  63. accessible to this user.
  64. </p>
  65. <TableAccessPolicySummary
  66. policies={policies}
  67. handleSelectEditPolicy={handleSelectEditPolicy}
  68. />
  69. </>
  70. )
  71. }
  72. return (
  73. <>
  74. <p>
  75. {policies.length} {policies.length > 1 ? 'policies apply' : 'policy applies'} for the{' '}
  76. <code className="text-code-inline">{role}</code> role on this table. Only rows that match{' '}
  77. {policies.length > 1 ? 'these conditions' : 'this condition'} are returned.
  78. </p>
  79. <TableAccessPolicySummary
  80. policies={policies}
  81. handleSelectEditPolicy={handleSelectEditPolicy}
  82. />
  83. </>
  84. )
  85. }, [
  86. isRLSEnabled,
  87. noPolicies,
  88. trueOnlyPolicy,
  89. falseOnlyPolicy,
  90. policies,
  91. role,
  92. handleSelectEditPolicy,
  93. ])
  94. return (
  95. <Collapsible
  96. className={cn('border rounded-sm', !isRLSEnabled && 'bg-warning-300 border-warning-500')}
  97. >
  98. <CollapsibleTrigger className="flex items-center justify-between px-3 py-2 w-full [&[data-state=open]>div>svg]:-rotate-180!">
  99. <div className="w-full flex items-center justify-between">
  100. <div className="flex items-center gap-x-2">
  101. {!isRLSEnabled ? (
  102. <WarningIcon />
  103. ) : noPolicies || falseOnlyPolicy ? (
  104. <X size={16} className="text-destructive" />
  105. ) : (
  106. <Check size={16} className="text-brand" />
  107. )}
  108. <p className={cn('text-xs font-mono', !isRLSEnabled && 'font-medium text-foreground')}>
  109. {schema}.{name}
  110. </p>
  111. </div>
  112. </div>
  113. <div className="flex items-center gap-x-2">
  114. <p
  115. className={cn(
  116. 'text-xs text-foreground-light w-max',
  117. !isRLSEnabled && 'text-foreground'
  118. )}
  119. >
  120. {noPolicies || falseOnlyPolicy
  121. ? 'Returns no rows'
  122. : !isRLSEnabled || !!trueOnlyPolicy
  123. ? 'Returns all rows'
  124. : null}
  125. </p>
  126. <ChevronDown className="transition-transform duration-200" strokeWidth={1.5} size={14} />
  127. </div>
  128. </CollapsibleTrigger>
  129. <CollapsibleContent
  130. className={cn(
  131. 'border-t p-3 text-sm text-foreground-light',
  132. !isRLSEnabled && 'border-warning-500'
  133. )}
  134. >
  135. {tableAccessDescription}
  136. </CollapsibleContent>
  137. </Collapsible>
  138. )
  139. }
  140. const TableAccessPolicySummary = ({
  141. policies,
  142. handleSelectEditPolicy,
  143. }: {
  144. policies: Policy[]
  145. handleSelectEditPolicy: (policy: Policy) => void
  146. }) => {
  147. return (
  148. <div className="border rounded-sm mt-4">
  149. <p className="text-xs font-mono text-foreground-light uppercase border-b px-3 py-2">
  150. {policies.length} {policies.length > 1 ? 'policies' : 'policy'} applied
  151. </p>
  152. <ul>
  153. {policies.map((policy) => (
  154. <li key={policy.id} className="px-3 py-2 flex justify-between items-center">
  155. <div>
  156. <p>{policy.name}</p>
  157. <p className="text-foreground-lighter">
  158. Show rows where:{' '}
  159. <code className="text-code-inline text-foreground">{policy.definition}</code>
  160. </p>
  161. </div>
  162. <ButtonTooltip
  163. type="text"
  164. icon={<Edit />}
  165. className="w-7"
  166. tooltip={{ content: { side: 'bottom', text: 'Edit policy' } }}
  167. onClick={() => {
  168. handleSelectEditPolicy(policy)
  169. }}
  170. />
  171. </li>
  172. ))}
  173. </ul>
  174. </div>
  175. )
  176. }