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