role-impersonation-state.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import { ident, literal, safeSql } from '@supabase/pg-meta/src/pg-format'
  2. import { useConstant } from 'common'
  3. import { createContext, PropsWithChildren, useCallback, useContext, useEffect } from 'react'
  4. import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
  5. import { CustomAccessTokenHookDetails } from '../hooks/misc/useCustomAccessTokenHookDetails'
  6. import { executeSql } from '@/data/sql/execute-sql-query'
  7. import useLatest from '@/hooks/misc/useLatest'
  8. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  9. import { getPostgrestClaims, ImpersonationRole } from '@/lib/role-impersonation'
  10. export function createRoleImpersonationState(
  11. projectRef: string,
  12. customizeAccessTokenRef: {
  13. current: (args: {
  14. schema: string
  15. functionName: string
  16. claims: ReturnType<typeof getPostgrestClaims>
  17. }) => Promise<any>
  18. }
  19. ) {
  20. const roleImpersonationState = proxy({
  21. projectRef,
  22. role: undefined as ImpersonationRole | undefined,
  23. claims: undefined as ReturnType<typeof getPostgrestClaims> | undefined,
  24. setRole: async (
  25. role: ImpersonationRole | undefined,
  26. customAccessTokenHookDetails?: CustomAccessTokenHookDetails
  27. ) => {
  28. let claims = role?.type === 'postgrest' ? getPostgrestClaims(projectRef, role) : undefined
  29. if (customAccessTokenHookDetails?.type === 'postgres' && claims !== undefined) {
  30. const { schema, functionName } = customAccessTokenHookDetails
  31. const updatedClaims = await customizeAccessTokenRef.current({
  32. schema,
  33. functionName,
  34. claims,
  35. })
  36. if (updatedClaims) {
  37. claims = updatedClaims
  38. }
  39. }
  40. roleImpersonationState.role = role
  41. if (claims) {
  42. roleImpersonationState.claims = claims
  43. }
  44. },
  45. })
  46. return roleImpersonationState
  47. }
  48. export type RoleImpersonationState = ReturnType<typeof createRoleImpersonationState>
  49. export const RoleImpersonationStateContext = createContext<RoleImpersonationState>(
  50. createRoleImpersonationState('', { current: async () => {} })
  51. )
  52. export const RoleImpersonationStateContextProvider = ({ children }: PropsWithChildren) => {
  53. const { data: project } = useSelectedProjectQuery()
  54. async function customizeAccessToken({
  55. schema,
  56. functionName,
  57. claims,
  58. }: {
  59. schema: string
  60. functionName: string
  61. claims: ReturnType<typeof getPostgrestClaims>
  62. }) {
  63. const event = { user_id: claims.sub, claims, authentication_method: 'password' }
  64. const result = await executeSql({
  65. projectRef: project?.ref,
  66. connectionString: project?.connectionString,
  67. sql: safeSql`select ${ident(schema)}.${ident(functionName)}(${literal(JSON.stringify(event))}::jsonb) as event;`,
  68. queryKey: ['customize-access-token', project?.ref],
  69. })
  70. return result?.result?.[0]?.event?.claims
  71. }
  72. const customizeAccessTokenRef = useLatest(customizeAccessToken)
  73. const state = useConstant(() =>
  74. createRoleImpersonationState(project?.ref ?? '', customizeAccessTokenRef)
  75. )
  76. return (
  77. <RoleImpersonationStateContext.Provider value={state}>
  78. {children}
  79. </RoleImpersonationStateContext.Provider>
  80. )
  81. }
  82. export function useRoleImpersonationStateSnapshot(options?: Parameters<typeof useSnapshot>[1]) {
  83. const roleImpersonationState = useContext(RoleImpersonationStateContext)
  84. return useSnapshot(roleImpersonationState, options)
  85. }
  86. export function useGetImpersonatedRoleState() {
  87. const roleImpersonationState = useContext(RoleImpersonationStateContext)
  88. return useCallback(
  89. // [Alaister]: typeof roleImpersonationState is needed to avoid readonly type errors everywhere
  90. () => snapshot(roleImpersonationState) as typeof roleImpersonationState,
  91. [roleImpersonationState]
  92. )
  93. }
  94. export function useSubscribeToImpersonatedRole(
  95. onChange: (role: ImpersonationRole | undefined) => void
  96. ) {
  97. const roleImpersonationState = useContext(RoleImpersonationStateContext)
  98. const onChangeRef = useLatest(onChange)
  99. useEffect(() => {
  100. return subscribe(roleImpersonationState, () => {
  101. onChangeRef.current(snapshot(roleImpersonationState).role)
  102. })
  103. }, [roleImpersonationState])
  104. }
  105. export function isRoleImpersonationEnabled(impersonationRole?: ImpersonationRole) {
  106. return impersonationRole?.type === 'postgrest'
  107. }
  108. export const useIsImpersonatingAnon = () => {
  109. const state = useRoleImpersonationStateSnapshot()
  110. return state.role?.type === 'postgrest' && state.role.role === 'anon'
  111. }
  112. export const useImpersonatedUser = () => {
  113. const state = useRoleImpersonationStateSnapshot()
  114. return state.role?.type === 'postgrest' &&
  115. state.role.role === 'authenticated' &&
  116. state.role.userType === 'native'
  117. ? state.role.user
  118. : undefined
  119. }
  120. export const useImpersonatedExternalAuth = () => {
  121. const state = useRoleImpersonationStateSnapshot()
  122. return state.role?.type === 'postgrest' &&
  123. state.role.role === 'authenticated' &&
  124. state.role.userType === 'external' &&
  125. state.role.externalAuth
  126. ? state.role.externalAuth.sub
  127. : undefined
  128. }
  129. export const useImpersonatedAAL = () => {
  130. const state = useRoleImpersonationStateSnapshot()
  131. return (
  132. state.role?.type === 'postgrest' &&
  133. state.role.role === 'authenticated' &&
  134. state.role.userType === 'external' &&
  135. state.role.aal
  136. )
  137. }