index.tsx187 lines · main
1import { useParams } from 'common'
2import { noop } from 'lodash'
3import { memo, useMemo } from 'react'
4import {
5 Card,
6 CardContent,
7 CardHeader,
8 cn,
9 Table,
10 TableBody,
11 TableHead,
12 TableHeader,
13 TableRow,
14} from 'ui'
15import { Admonition } from 'ui-patterns'
16import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
17
18import { usePoliciesData } from '../PoliciesDataContext'
19import { PolicyRow } from './PolicyRow'
20import type { PolicyTable } from './PolicyTableRow.types'
21import type { Policy } from './PolicyTableRow.utils'
22import { getTableAdmonitionMessage, getTableDataApiStatus } from './PolicyTableRow.utils'
23import { PolicyTableRowHeader } from './PolicyTableRowHeader'
24import AlertError from '@/components/ui/AlertError'
25import { InlineLink } from '@/components/ui/InlineLink'
26import { useTableApiAccessQuery } from '@/data/privileges/table-api-access-query'
27import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
28
29export interface PolicyTableRowProps {
30 table: PolicyTable
31 isLocked: boolean
32 onSelectToggleRLS: (table: PolicyTable) => void
33 onSelectCreatePolicy: (table: PolicyTable) => void
34 onSelectEditPolicy: (policy: Policy) => void
35 onSelectDeletePolicy: (policy: Policy) => void
36}
37
38const PolicyTableRowComponent = ({
39 table,
40 isLocked,
41 onSelectToggleRLS = noop,
42 onSelectCreatePolicy = noop,
43 onSelectEditPolicy = noop,
44 onSelectDeletePolicy = noop,
45}: PolicyTableRowProps) => {
46 const { ref } = useParams()
47 const { data: project } = useSelectedProjectQuery()
48 const { getPoliciesForTable, isPoliciesLoading, isPoliciesError, policiesError, exposedSchemas } =
49 usePoliciesData()
50
51 const policies = useMemo(
52 () => getPoliciesForTable(table.schema, table.name),
53 [getPoliciesForTable, table.schema, table.name]
54 )
55
56 // [Joshen] Classification is more granular than "RLS on/off" alone — it also considers
57 // schema exposure and whether anon/authenticated/service_role actually have grants.
58 // Ideally we'd rely on the security lints, but they only look at the public schema and
59 // ignore roles. Once the lints cover both, we can switch to them as the source of truth.
60 const tableNames = useMemo(() => [table.name], [table.name])
61 const {
62 data: apiAccessMap,
63 isPending: isLoadingRolesAccess,
64 isError: isRolesAccessError,
65 } = useTableApiAccessQuery({
66 projectRef: project?.ref,
67 connectionString: project?.connectionString,
68 schemaName: table.schema,
69 tableNames,
70 })
71
72 const status = useMemo(
73 () =>
74 getTableDataApiStatus({
75 isSchemaExposed: exposedSchemas.has(table.schema),
76 apiAccessData: apiAccessMap?.[table.name],
77 isRLSEnabled: table.rls_enabled,
78 policiesCount: policies.length,
79 }),
80 [exposedSchemas, apiAccessMap, table.schema, table.name, table.rls_enabled, policies.length]
81 )
82
83 const hasApiAccess =
84 status === 'publicly-readable' || status === 'locked-by-rls' || status === 'secured'
85 const isPubliclyReadable = status === 'publicly-readable'
86
87 const isRealtimeSchema = table.schema === 'realtime'
88 const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages'
89 const isTableLocked = isRealtimeSchema ? !isRealtimeMessagesTable : isLocked
90
91 const showPolicies = !isPoliciesLoading && !isPoliciesError && !isLoadingRolesAccess
92
93 const admonitionMessage = useMemo(() => getTableAdmonitionMessage(status), [status])
94
95 return (
96 <Card className={cn(isPubliclyReadable && 'border-warning-500')}>
97 <CardHeader className={cn('py-3 px-4', status !== 'secured' && 'border-b-0')}>
98 <PolicyTableRowHeader
99 table={table}
100 isLocked={isLocked}
101 hasApiAccess={hasApiAccess}
102 isLoadingApiAccess={isLoadingRolesAccess}
103 onSelectToggleRLS={onSelectToggleRLS}
104 onSelectCreatePolicy={onSelectCreatePolicy}
105 />
106 </CardHeader>
107
108 {!isLoadingRolesAccess && !isRolesAccessError && status === 'schema-not-exposed' && (
109 <Admonition
110 showIcon={false}
111 type="warning"
112 className="border-0 border-y rounded-none min-h-12 flex items-center"
113 >
114 <p className="text-foreground-light">
115 No data will be selectable via Briven APIs as this schema is not exposed. You may
116 configure this in your project’s{' '}
117 <InlineLink href={`/project/${ref}/integrations/data_api/settings`}>
118 API settings
119 </InlineLink>
120 .
121 </p>
122 </Admonition>
123 )}
124
125 {!isLoadingRolesAccess && !isRolesAccessError && admonitionMessage !== null && (
126 <Admonition
127 showIcon={false}
128 type={isPubliclyReadable ? 'warning' : 'default'}
129 className="border-0 border-y rounded-none min-h-12 flex items-center"
130 >
131 <p>{admonitionMessage}</p>
132 </Admonition>
133 )}
134
135 {(isPoliciesLoading || isLoadingRolesAccess) && (
136 <CardContent>
137 <ShimmeringLoader />
138 </CardContent>
139 )}
140
141 {isPoliciesError && (
142 <CardContent>
143 <AlertError
144 className="border-0 rounded-none"
145 error={policiesError}
146 subject="Failed to retrieve policies"
147 />
148 </CardContent>
149 )}
150
151 {showPolicies && (
152 <CardContent className="p-0">
153 {policies.length === 0 ? (
154 <p className="text-foreground-lighter text-sm p-4">No policies created yet</p>
155 ) : (
156 <Table className="table-fixed">
157 <TableHeader>
158 <TableRow>
159 <TableHead className="w-[40%]">Name</TableHead>
160 <TableHead className="w-[20%]">Command</TableHead>
161 <TableHead className="w-[30%]">Applied to</TableHead>
162 <TableHead className="text-right">
163 <span className="sr-only">Actions</span>
164 </TableHead>
165 </TableRow>
166 </TableHeader>
167 <TableBody>
168 {policies.map((policy) => (
169 <PolicyRow
170 key={policy.id}
171 policy={policy}
172 isLocked={isTableLocked}
173 onSelectEditPolicy={onSelectEditPolicy}
174 onSelectDeletePolicy={onSelectDeletePolicy}
175 />
176 ))}
177 </TableBody>
178 </Table>
179 )}
180 </CardContent>
181 )}
182 </Card>
183 )
184}
185
186export const PolicyTableRow = memo(PolicyTableRowComponent)
187PolicyTableRow.displayName = 'PolicyTableRow'