AutoEnableRLSNotice.tsx184 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { LOCAL_STORAGE_KEYS, useParams } from 'common'
3import { ShieldCheck, X } from 'lucide-react'
4import { useMemo, useState } from 'react'
5import { toast } from 'sonner'
6import {
7 Button,
8 Card,
9 CardContent,
10 Dialog,
11 DialogContent,
12 DialogDescription,
13 DialogFooter,
14 DialogHeader,
15 DialogSection,
16 DialogSectionSeparator,
17 DialogTitle,
18 DialogTrigger,
19} from 'ui'
20import { CodeBlock } from 'ui-patterns/CodeBlock'
21
22import { AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL } from '@/components/interfaces/Database/Triggers/EventTriggersList/EventTriggers.constants'
23import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
24import { useDatabaseEventTriggerCreateMutation } from '@/data/database-event-triggers/database-event-trigger-create-mutation'
25import { useDatabaseEventTriggersQuery } from '@/data/database-event-triggers/database-event-triggers-query'
26import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
27import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
28import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
29import { useTrack } from '@/lib/telemetry/track'
30
31export const AutoEnableRLSNotice = ({ iconOnly }: { iconOnly?: boolean }) => {
32 const { ref } = useParams()
33 const { data: project } = useSelectedProjectQuery()
34 const projectRef = ref ?? project?.ref
35
36 // [Joshen] Changing the behaviour of this to not be dismissible, only minimized
37 // Given that its a security measure that we highly advise. Otherwise there's no way for users to revisit this
38 const [, setIsMinimized] = useLocalStorageQuery(
39 LOCAL_STORAGE_KEYS.RLS_EVENT_TRIGGER_BANNER_DISMISSED(projectRef ?? 'unknown'),
40 false
41 )
42
43 const { data: eventTriggers = [], isLoading: isLoadingEventTriggers } =
44 useDatabaseEventTriggersQuery({
45 projectRef: project?.ref,
46 connectionString: project?.connectionString,
47 })
48 const hasDefaultTrigger = useMemo(
49 () =>
50 eventTriggers.some(
51 (trigger) => trigger.name === 'ensure_rls' || trigger.function_name === 'rls_auto_enable'
52 ),
53 [eventTriggers]
54 )
55
56 if (!projectRef || isLoadingEventTriggers || hasDefaultTrigger) return null
57
58 if (iconOnly) {
59 return <CreateEnsureRLSTriggerDialog iconOnly />
60 }
61
62 return (
63 <Card>
64 <CardContent className="flex items-center justify-between">
65 <div className="flex items-center gap-x-4">
66 <div className="rounded-lg bg-surface-300 text-foreground-light w-10 h-10 flex items-center justify-center">
67 <ShieldCheck size={18} />
68 </div>
69 <div className="text-sm">
70 <p>Auto-enable RLS for new tables</p>
71 <p className="text-foreground-lighter">
72 Create an event trigger that enables Row Level Security on all new tables
73 </p>
74 </div>
75 </div>
76
77 <div className="flex items-center gap-x-2">
78 <CreateEnsureRLSTriggerDialog />
79 <ButtonTooltip
80 icon={<X />}
81 type="text"
82 className="w-7"
83 tooltip={{ content: { side: 'bottom', text: 'Minimize' } }}
84 onClick={() => setIsMinimized(true)}
85 />
86 </div>
87 </CardContent>
88 </Card>
89 )
90}
91
92const CreateEnsureRLSTriggerDialog = ({ iconOnly }: { iconOnly?: boolean }) => {
93 const track = useTrack()
94 const { data: project } = useSelectedProjectQuery()
95
96 const [open, setOpen] = useState(false)
97
98 const { can: canCreateTriggers } = useAsyncCheckPermissions(
99 PermissionAction.TENANT_SQL_ADMIN_WRITE,
100 'triggers'
101 )
102
103 const { mutate: createEventTrigger, isPending: isCreating } =
104 useDatabaseEventTriggerCreateMutation({
105 onSuccess: () => {
106 toast.success(
107 'Successfully set up database trigger to automatically enable RLS on all new tables'
108 )
109 setOpen(false)
110 },
111 })
112
113 const handleCreateTrigger = () => {
114 if (!project) return
115 track('rls_event_trigger_banner_create_button_clicked')
116 createEventTrigger({
117 projectRef: project.ref,
118 connectionString: project.connectionString,
119 sql: AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL,
120 })
121 }
122
123 return (
124 <Dialog open={open} onOpenChange={setOpen}>
125 <DialogTrigger asChild>
126 {iconOnly ? (
127 <ButtonTooltip
128 type="default"
129 icon={<ShieldCheck />}
130 className="w-7"
131 tooltip={{ content: { side: 'bottom', text: 'Auto-enable RLS for new tables' } }}
132 />
133 ) : (
134 <Button type="primary">Learn more</Button>
135 )}
136 </DialogTrigger>
137 <DialogContent size="large">
138 <DialogHeader>
139 <DialogTitle>Automatically enable RLS for newly created tables</DialogTitle>
140 <DialogDescription>Secure your data using Postgres Row Level Security</DialogDescription>
141 </DialogHeader>
142
143 <DialogSectionSeparator />
144
145 <DialogSection className="text-sm flex flex-col gap-y-2">
146 <p>
147 Tables in exposed schemas (default being the{' '}
148 <code className="text-code-inline">public</code> schema) are accessible to anyone.
149 Hence, we highly recommend enabling RLS on all such tables.
150 </p>
151 <p>
152 You can set up a database trigger to enable RLS automatically on all new tables with the
153 following SQL:
154 </p>
155 </DialogSection>
156
157 <CodeBlock language="sql" className="language-sql px-0 border-x-0 rounded-none h-64">
158 {AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL.trim()}
159 </CodeBlock>
160
161 <DialogFooter>
162 <Button type="default" disabled={isCreating} onClick={() => setOpen(false)}>
163 Close
164 </Button>
165 <ButtonTooltip
166 disabled={!canCreateTriggers}
167 loading={isCreating}
168 onClick={handleCreateTrigger}
169 tooltip={{
170 content: {
171 side: 'bottom',
172 text: !canCreateTriggers
173 ? 'You need additional permissions to create triggers'
174 : undefined,
175 },
176 }}
177 >
178 Create ensure_rls trigger
179 </ButtonTooltip>
180 </DialogFooter>
181 </DialogContent>
182 </Dialog>
183 )
184}