index.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { useState } from 'react'
  2. import { Badge, Card, CardContent, CardHeader, CardTitle, cn } from 'ui'
  3. import { AnonIcon, AuthenticatedIcon, ServiceRoleIcon } from './Icons'
  4. import { RoleImpersonationRadio } from './RoleImpersonationRadio'
  5. import { UserImpersonationSelector } from './UserImpersonationSelector'
  6. import { DocsButton } from '@/components/ui/DocsButton'
  7. import { DOCS_URL } from '@/lib/constants'
  8. import { PostgrestRole } from '@/lib/role-impersonation'
  9. import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
  10. export interface RoleImpersonationSelectorProps {
  11. header?: string
  12. serviceRoleLabel?: string
  13. disallowAuthenticatedOption?: boolean
  14. title?: string
  15. orientation?: 'horizontal' | 'vertical'
  16. }
  17. export const RoleImpersonationSelector = ({
  18. header = 'Impersonate a database role',
  19. serviceRoleLabel = 'Postgres',
  20. disallowAuthenticatedOption = false,
  21. orientation = 'horizontal',
  22. }: RoleImpersonationSelectorProps) => {
  23. const isVertical = orientation === 'vertical'
  24. const state = useRoleImpersonationStateSnapshot()
  25. const [selectedOption, setSelectedOption] = useState<PostgrestRole | undefined>(() => {
  26. if (
  27. state.role?.type === 'postgrest' &&
  28. (state.role.role === 'anon' || state.role.role === 'authenticated')
  29. ) {
  30. return state.role.role
  31. }
  32. return 'service_role'
  33. })
  34. const isAuthenticatedOptionFullySelected = Boolean(
  35. selectedOption === 'authenticated' &&
  36. state.role?.type === 'postgrest' &&
  37. state.role.role === 'authenticated' &&
  38. (('user' in state.role && state.role.user) ||
  39. ('externalAuth' in state.role && state.role.externalAuth)) // Check for either auth type
  40. )
  41. function onSelectedChange(value: PostgrestRole) {
  42. if (value === 'service_role') {
  43. // do not set a role for service role
  44. // as the default role is the "service role"
  45. state.setRole(undefined)
  46. }
  47. if (value === 'anon') {
  48. state.setRole({
  49. type: 'postgrest',
  50. role: value,
  51. })
  52. }
  53. setSelectedOption(value)
  54. }
  55. return (
  56. <Card className="border-none">
  57. <CardHeader className="flex-row items-center justify-between py-3 space-y-0">
  58. <CardTitle>{header}</CardTitle>
  59. <DocsButton
  60. href={`${DOCS_URL}/guides/database/postgres/row-level-security#authenticated-and-unauthenticated-roles`}
  61. />
  62. </CardHeader>
  63. <CardContent className="flex flex-col gap-y-4">
  64. <form
  65. onSubmit={(e) => {
  66. // don't allow form submission
  67. e.preventDefault()
  68. }}
  69. >
  70. <fieldset className={cn('flex gap-3', isVertical && 'flex-col gap-2')}>
  71. <RoleImpersonationRadio
  72. value="service_role"
  73. isSelected={selectedOption === 'service_role'}
  74. onSelectedChange={onSelectedChange}
  75. label={serviceRoleLabel}
  76. description="Superuser"
  77. icon={<ServiceRoleIcon isSelected={selectedOption === 'service_role'} />}
  78. fullWidth={isVertical}
  79. />
  80. <RoleImpersonationRadio
  81. value="anon"
  82. label="Anonymous"
  83. isSelected={selectedOption === 'anon'}
  84. onSelectedChange={onSelectedChange}
  85. description="Not logged in"
  86. icon={<AnonIcon isSelected={selectedOption === 'anon'} />}
  87. fullWidth={isVertical}
  88. />
  89. {!disallowAuthenticatedOption && (
  90. <RoleImpersonationRadio
  91. value="authenticated"
  92. label="Authenticated"
  93. isSelected={
  94. selectedOption === 'authenticated' &&
  95. (isAuthenticatedOptionFullySelected || 'partially')
  96. }
  97. onSelectedChange={onSelectedChange}
  98. description="Specific logged in user"
  99. icon={<AuthenticatedIcon isSelected={selectedOption === 'authenticated'} />}
  100. fullWidth={isVertical}
  101. />
  102. )}
  103. </fieldset>
  104. </form>
  105. {selectedOption === 'service_role' && (
  106. <div>
  107. <p className="text-sm">
  108. Full admin access
  109. <Badge className="ml-2">Default</Badge>
  110. </p>
  111. <p className="text-foreground-light text-sm">
  112. The <code className="text-code-inline">postgres</code> role, which bypasses all Row
  113. Level Security (RLS) policies.
  114. </p>
  115. </div>
  116. )}
  117. {selectedOption === 'anon' && (
  118. <div>
  119. <p className="text-sm">For unauthenticated access</p>
  120. <p className="text-foreground-light text-sm">
  121. The <code className="text-code-inline">anon</code> role, which the API (PostgREST)
  122. uses when a user is not logged in.
  123. <br />
  124. Row Level Security (RLS) policies apply.
  125. </p>
  126. </div>
  127. )}
  128. {selectedOption === 'authenticated' && (
  129. <div>
  130. <p className="text-sm">For authenticated access</p>
  131. <p className="text-foreground-light text-sm">
  132. The <code className="text-code-inline">authenticated</code> role, which the API
  133. (PostgREST) uses when a user is logged in.
  134. <br />
  135. Row Level Security (RLS) policies apply.
  136. </p>
  137. </div>
  138. )}
  139. </CardContent>
  140. {selectedOption === 'authenticated' && (
  141. <CardContent className="p-0">
  142. <UserImpersonationSelector />
  143. </CardContent>
  144. )}
  145. </Card>
  146. )
  147. }