NetworkRestrictions.tsx 12 KB

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