NetworkRestrictions.tsx318 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useParams } from 'common'
3import { AlertCircle, ChevronDown, Globe, Lock } from 'lucide-react'
4import { useState } from 'react'
5import {
6 Badge,
7 Button,
8 Card,
9 CardContent,
10 CardDescription,
11 CardHeader,
12 DropdownMenu,
13 DropdownMenuContent,
14 DropdownMenuItem,
15 DropdownMenuTrigger,
16 Tooltip,
17 TooltipContent,
18 TooltipTrigger,
19} from 'ui'
20import {
21 PageSection,
22 PageSectionAside,
23 PageSectionContent,
24 PageSectionMeta,
25 PageSectionSummary,
26 PageSectionTitle,
27} from 'ui-patterns'
28import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
29
30import AddRestrictionModal from './AddRestrictionModal'
31import AllowAllModal from './AllowAllModal'
32import DisallowAllModal from './DisallowAllModal'
33import RemoveRestrictionModal from './RemoveRestrictionModal'
34import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
35import { DocsButton } from '@/components/ui/DocsButton'
36import { useNetworkRestrictionsQuery } from '@/data/network-restrictions/network-restrictions-query'
37import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
38import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
39import { DOCS_URL } from '@/lib/constants'
40
41interface AccessButtonProps {
42 disabled: boolean
43 onClick: (value: boolean) => void
44}
45
46const AllowAllAccessButton = ({ disabled, onClick }: AccessButtonProps) => (
47 <Tooltip>
48 <TooltipTrigger asChild>
49 <Button type="default" disabled={disabled} onClick={() => onClick(true)}>
50 Allow all access
51 </Button>
52 </TooltipTrigger>
53 {disabled && (
54 <TooltipContent side="bottom">
55 You need additional permissions to update network restrictions
56 </TooltipContent>
57 )}
58 </Tooltip>
59)
60
61const DisallowAllAccessButton = ({ disabled, onClick }: AccessButtonProps) => (
62 <ButtonTooltip
63 disabled={disabled}
64 type="default"
65 onClick={() => onClick(true)}
66 tooltip={{
67 content: {
68 side: 'bottom',
69 text: disabled
70 ? 'You need additional permissions to update network restrictions'
71 : undefined,
72 },
73 }}
74 >
75 Restrict all access
76 </ButtonTooltip>
77)
78
79export const NetworkRestrictions = () => {
80 const { ref } = useParams()
81 const { data: project } = useSelectedProjectQuery()
82 const [isAddingAddress, setIsAddingAddress] = useState<undefined | 'IPv4' | 'IPv6'>()
83 const [isAllowingAll, setIsAllowingAll] = useState(false)
84 const [isDisallowingAll, setIsDisallowingAll] = useState(false)
85 const [selectedRestrictionToRemove, setSelectedRestrictionToRemove] = useState<string>()
86
87 const { data, isPending: isLoading } = useNetworkRestrictionsQuery({ projectRef: ref })
88 const { can: canUpdateNetworkRestrictions } = useAsyncCheckPermissions(
89 PermissionAction.UPDATE,
90 'projects',
91 {
92 resource: {
93 project_id: project?.id,
94 },
95 }
96 )
97
98 const hasAccessToRestrictions = data?.entitlement === 'allowed'
99 const ipv4Restrictions = data?.config?.dbAllowedCidrs ?? []
100 // @ts-ignore [Joshen] API typing issue
101 const ipv6Restrictions = data?.config?.dbAllowedCidrsV6 ?? []
102 const restrictedIps = ipv4Restrictions.concat(ipv6Restrictions)
103 const restrictionStatus = data?.status ?? ''
104
105 const hasApplyError = restrictionStatus === 'stored'
106 const isUninitialized = restrictedIps.length === 0 && restrictionStatus.length === 0
107 const isAllowedAll = restrictedIps.includes('0.0.0.0/0') && restrictedIps.includes('::/0')
108 const isDisallowedAll = restrictedIps.length === 0
109
110 if (!hasAccessToRestrictions) return null
111
112 return (
113 <>
114 <PageSection id="network-restrictions">
115 <PageSectionMeta>
116 <PageSectionSummary>
117 <PageSectionTitle>Network restrictions</PageSectionTitle>
118 </PageSectionSummary>
119 <PageSectionAside className="flex items-center gap-x-2">
120 <DocsButton href={`${DOCS_URL}/guides/platform/network-restrictions`} />
121 {!canUpdateNetworkRestrictions ? (
122 <ButtonTooltip
123 disabled
124 type="primary"
125 tooltip={{
126 content: {
127 side: 'bottom',
128 text: 'You need additional permissions to update network restrictions',
129 },
130 }}
131 >
132 Add restriction
133 </ButtonTooltip>
134 ) : (
135 <DropdownMenu>
136 <DropdownMenuTrigger asChild>
137 <Button
138 type="primary"
139 disabled={!canUpdateNetworkRestrictions}
140 iconRight={<ChevronDown size={14} />}
141 >
142 Add restriction
143 </Button>
144 </DropdownMenuTrigger>
145 <DropdownMenuContent align="end" side="bottom" className="w-48">
146 <DropdownMenuItem
147 key="IPv4"
148 disabled={isLoading}
149 onClick={() => setIsAddingAddress('IPv4')}
150 >
151 <p className="block text-foreground">Add IPv4 restriction</p>
152 </DropdownMenuItem>
153 <DropdownMenuItem
154 key="IPv6"
155 disabled={isLoading}
156 onClick={() => setIsAddingAddress('IPv6')}
157 >
158 <p className="block text-foreground">Add IPv6 restriction</p>
159 </DropdownMenuItem>
160 </DropdownMenuContent>
161 </DropdownMenu>
162 )}
163 </PageSectionAside>
164 </PageSectionMeta>
165 <PageSectionContent>
166 {isLoading ? (
167 <Card>
168 <CardContent>
169 <div className="space-y-2">
170 <ShimmeringLoader />
171 <ShimmeringLoader className="w-[70%]" />
172 <ShimmeringLoader className="w-[50%]" />
173 </div>
174 </CardContent>
175 </Card>
176 ) : hasApplyError ? (
177 <Card>
178 <CardContent>
179 <div className="flex items-center justify-between">
180 <div className="space-y-2">
181 <div className="flex items-center space-x-2">
182 <AlertCircle size={20} strokeWidth={1.5} className="text-foreground-light" />
183 <p className="text-sm">
184 Your network restrictions were not applied correctly
185 </p>
186 </div>
187 <p className="text-sm text-foreground-light">
188 Please try to add your network restrictions again
189 </p>
190 </div>
191 <div className="flex items-center space-x-2">
192 <AllowAllAccessButton
193 disabled={!canUpdateNetworkRestrictions}
194 onClick={setIsAllowingAll}
195 />
196 <DisallowAllAccessButton
197 disabled={!canUpdateNetworkRestrictions}
198 onClick={setIsDisallowingAll}
199 />
200 </div>
201 </div>
202 </CardContent>
203 </Card>
204 ) : (
205 <Card>
206 {isUninitialized || isAllowedAll ? (
207 <CardContent className="flex items-center justify-between">
208 <div className="flex items-start space-x-4">
209 <div className="space-y-0.5">
210 <p className="text-foreground text-sm">
211 Your database can be accessed by all IP addresses
212 </p>
213 <p className="text-foreground-light text-sm">
214 You may start limiting access to your database by adding a network
215 restriction.
216 </p>
217 </div>
218 </div>
219 <div>
220 <DisallowAllAccessButton
221 disabled={!canUpdateNetworkRestrictions}
222 onClick={setIsDisallowingAll}
223 />
224 </div>
225 </CardContent>
226 ) : isDisallowedAll ? (
227 <CardContent className="flex items-center justify-between">
228 <div className="flex items-start space-x-4">
229 <Lock size={20} className="text-foreground-light" strokeWidth={1.5} />
230 <div className="space-y-1">
231 <p className="text-foreground-light text-sm">
232 Your database <span className="text-amber-900 opacity-80">cannot</span> be
233 accessed externally
234 </p>
235 <p className="text-foreground-light text-sm">
236 All external IP addresses have been disallowed from accessing your project's
237 database.
238 </p>
239 <p className="text-foreground-light text-sm">
240 Note: Restrictions only apply to your database, and not to Briven services
241 </p>
242 </div>
243 </div>
244 <div>
245 <AllowAllAccessButton
246 disabled={!canUpdateNetworkRestrictions}
247 onClick={setIsAllowingAll}
248 />
249 </div>
250 </CardContent>
251 ) : (
252 <>
253 <CardHeader className="md:flex-row md:items-center justify-between">
254 <CardDescription className="text-foreground-light">
255 <p>Only the following IP addresses have access to your database.</p>
256 <p>
257 You may remove all of them to allow all IP addresses to have access to your
258 database.
259 </p>
260 <p>
261 Note: Restrictions only apply to your database, and not to Briven services
262 </p>
263 </CardDescription>
264 <div className="flex items-center space-x-2">
265 <AllowAllAccessButton
266 disabled={!canUpdateNetworkRestrictions}
267 onClick={setIsAllowingAll}
268 />
269 <DisallowAllAccessButton
270 disabled={!canUpdateNetworkRestrictions}
271 onClick={setIsDisallowingAll}
272 />
273 </div>
274 </CardHeader>
275 <CardContent className="py-0">
276 <div className="divide-y">
277 {restrictedIps.map((ip) => {
278 return (
279 <div key={ip} className="py-4 flex items-center justify-between">
280 <div className="flex items-center space-x-5">
281 <Globe size={16} className="text-foreground-lighter" />
282 <Badge>{ipv4Restrictions.includes(ip) ? 'IPv4' : 'IPv6'}</Badge>
283 <p className="text-sm font-mono">{ip}</p>
284 </div>
285 <Button
286 type="default"
287 onClick={() => setSelectedRestrictionToRemove(ip)}
288 >
289 Remove
290 </Button>
291 </div>
292 )
293 })}
294 </div>
295 </CardContent>
296 </>
297 )}
298 </Card>
299 )}
300 </PageSectionContent>
301 </PageSection>
302
303 <AllowAllModal visible={isAllowingAll} onClose={() => setIsAllowingAll(false)} />
304 <DisallowAllModal visible={isDisallowingAll} onClose={() => setIsDisallowingAll(false)} />
305
306 <AddRestrictionModal
307 type={isAddingAddress}
308 hasOverachingRestriction={isAllowedAll || isDisallowedAll}
309 onClose={() => setIsAddingAddress(undefined)}
310 />
311 <RemoveRestrictionModal
312 visible={selectedRestrictionToRemove !== undefined}
313 selectedRestriction={selectedRestrictionToRemove}
314 onClose={() => setSelectedRestrictionToRemove(undefined)}
315 />
316 </>
317 )
318}