mirror of
https://github.com/supabase/supabase.git
synced 2026-06-13 10:09:12 +08:00
Fixes the RLS policies page showing self-contradictory or wrong
admonitions for tables with partial grants. Classifies each table using
the same `granted / custom / revoked` semantics used by the Data API
settings page so the two views agree on what counts as "exposed".
**Changed:**
- `PolicyTableRow` now uses `useTableApiAccessQuery` (shared cache with
the Table Editor sidebar) instead of a bespoke
`tables-roles-access-query`
- Boolean soup collapsed into a single `TableDataApiStatus`
discriminated union (`schema-not-exposed | no-grants | custom-grants |
publicly-readable | locked-by-rls | secured`) via a pure helper
- Admonition copy for `no-grants` and `locked-by-rls` updated; a table
with no policies but full grants now reads "No data will be returned via
the Data API as no RLS policies exist on this table." instead of the
earlier self-contradictory "can be accessed but no data will be
returned"
- `table-api-access-query.ts` now exposes a `grantStatus: 'granted' |
'custom'` on `access` entries — `granted` = all 3 API roles × all 4 CRUD
privileges (matches `getTableGrantsCTEs` in pg-meta)
**Added:**
- New `custom-grants` admonition: "This table has custom Data API
permissions — access may be restricted for some roles or operations."
- Unit tests for `getTableDataApiStatus`, `getTableAdmonitionMessage`,
and `isFullyGranted`
**Removed:**
- `data/tables/tables-roles-access-query.ts` and the `rolesAccess` key —
no more callers
## To test
On a project with the `public` schema exposed, for each scenario check
the admonition shown on `/project/{ref}/auth/policies`:
1. Table with full standard grants, RLS on, no policies → "No data will
be returned via the Data API as no RLS policies exist on this table."
2. Table with full standard grants, RLS off → yellow warning "can be
accessed by anyone"
3. Table with partial grants (e.g. only `GRANT SELECT ON t TO anon`) →
new "custom Data API permissions" admonition regardless of RLS state
4. Table with no anon/authenticated/service_role grants → "cannot be
accessed via the Data API"
5. Schema not in the exposed list → "schema not exposed" admonition with
link
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Tests**
* Added unit tests covering table Data API/RLS status classification and
API grant validation.
* **Refactor**
* Introduced a unified per-table API/RLS status model and reusable
utilities to derive display status and admonitions.
* Simplified UI logic to drive access indicators and warnings from the
new status.
* **Chores**
* Removed legacy role-based access query and its related keying logic.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
185 lines
6.0 KiB
TypeScript
185 lines
6.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
|
||
import { getTableAdmonitionMessage, getTableDataApiStatus } from './PolicyTableRow.utils'
|
||
import type { TableApiAccessData } from '@/data/privileges/table-api-access-query'
|
||
import type { ApiPrivilegesByRole } from '@/lib/data-api-types'
|
||
|
||
const FULL_PRIVILEGES: ApiPrivilegesByRole = {
|
||
anon: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
||
authenticated: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
||
service_role: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
||
}
|
||
|
||
const PARTIAL_PRIVILEGES: ApiPrivilegesByRole = {
|
||
anon: ['SELECT'],
|
||
authenticated: [],
|
||
service_role: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
||
}
|
||
|
||
const grantedAccess: TableApiAccessData = {
|
||
apiAccessType: 'access',
|
||
grantStatus: 'granted',
|
||
privileges: FULL_PRIVILEGES,
|
||
}
|
||
|
||
const customAccess: TableApiAccessData = {
|
||
apiAccessType: 'access',
|
||
grantStatus: 'custom',
|
||
privileges: PARTIAL_PRIVILEGES,
|
||
}
|
||
|
||
const noGrants: TableApiAccessData = { apiAccessType: 'exposed-schema-no-grants' }
|
||
const schemaNotExposedData: TableApiAccessData = { apiAccessType: 'none' }
|
||
|
||
describe('getTableDataApiStatus', () => {
|
||
it('returns schema-not-exposed when the schema is not in the exposed list', () => {
|
||
const status = getTableDataApiStatus({
|
||
isSchemaExposed: false,
|
||
apiAccessData: grantedAccess,
|
||
isRLSEnabled: true,
|
||
policiesCount: 1,
|
||
})
|
||
expect(status).toBe('schema-not-exposed')
|
||
})
|
||
|
||
it('returns no-grants when schema is exposed but no API roles have privileges', () => {
|
||
const status = getTableDataApiStatus({
|
||
isSchemaExposed: true,
|
||
apiAccessData: noGrants,
|
||
isRLSEnabled: true,
|
||
policiesCount: 0,
|
||
})
|
||
expect(status).toBe('no-grants')
|
||
})
|
||
|
||
it('returns custom-grants for partial/non-standard grants — even if RLS is off', () => {
|
||
const status = getTableDataApiStatus({
|
||
isSchemaExposed: true,
|
||
apiAccessData: customAccess,
|
||
isRLSEnabled: false,
|
||
policiesCount: 0,
|
||
})
|
||
expect(status).toBe('custom-grants')
|
||
})
|
||
|
||
it('returns publicly-readable when fully granted and RLS is off', () => {
|
||
const status = getTableDataApiStatus({
|
||
isSchemaExposed: true,
|
||
apiAccessData: grantedAccess,
|
||
isRLSEnabled: false,
|
||
policiesCount: 3,
|
||
})
|
||
expect(status).toBe('publicly-readable')
|
||
})
|
||
|
||
it('returns locked-by-rls when fully granted + RLS on + no policies', () => {
|
||
const status = getTableDataApiStatus({
|
||
isSchemaExposed: true,
|
||
apiAccessData: grantedAccess,
|
||
isRLSEnabled: true,
|
||
policiesCount: 0,
|
||
})
|
||
expect(status).toBe('locked-by-rls')
|
||
})
|
||
|
||
it('returns secured when fully granted + RLS on + policies exist', () => {
|
||
const status = getTableDataApiStatus({
|
||
isSchemaExposed: true,
|
||
apiAccessData: grantedAccess,
|
||
isRLSEnabled: true,
|
||
policiesCount: 2,
|
||
})
|
||
expect(status).toBe('secured')
|
||
})
|
||
|
||
it('returns unknown when apiAccessData is still loading or errored', () => {
|
||
// apiAccessData is undefined during loading AND on query error (isPending flips false
|
||
// but data stays undefined). We must not fall through to 'schema-not-exposed' — that
|
||
// would tell the user to reconfigure API settings for a schema that is in fact exposed.
|
||
const status = getTableDataApiStatus({
|
||
isSchemaExposed: true,
|
||
apiAccessData: undefined,
|
||
isRLSEnabled: true,
|
||
policiesCount: 0,
|
||
})
|
||
expect(status).toBe('unknown')
|
||
})
|
||
|
||
it('returns unknown when apiAccessData reports apiAccessType=none on an exposed schema', () => {
|
||
// Defensive: the query shouldn't emit apiAccessType=none when schema is exposed,
|
||
// but if it does we still don't want the false "schema not exposed" admonition.
|
||
const status = getTableDataApiStatus({
|
||
isSchemaExposed: true,
|
||
apiAccessData: schemaNotExposedData,
|
||
isRLSEnabled: true,
|
||
policiesCount: 0,
|
||
})
|
||
expect(status).toBe('unknown')
|
||
})
|
||
|
||
it('isSchemaExposed=false wins over any apiAccessData value', () => {
|
||
const status = getTableDataApiStatus({
|
||
isSchemaExposed: false,
|
||
apiAccessData: noGrants,
|
||
isRLSEnabled: true,
|
||
policiesCount: 0,
|
||
})
|
||
expect(status).toBe('schema-not-exposed')
|
||
})
|
||
|
||
it('custom-grants wins over RLS state — we never claim public-readable for partial grants', () => {
|
||
const rlsOff = getTableDataApiStatus({
|
||
isSchemaExposed: true,
|
||
apiAccessData: customAccess,
|
||
isRLSEnabled: false,
|
||
policiesCount: 0,
|
||
})
|
||
const rlsOnNoPolicies = getTableDataApiStatus({
|
||
isSchemaExposed: true,
|
||
apiAccessData: customAccess,
|
||
isRLSEnabled: true,
|
||
policiesCount: 0,
|
||
})
|
||
expect(rlsOff).toBe('custom-grants')
|
||
expect(rlsOnNoPolicies).toBe('custom-grants')
|
||
})
|
||
})
|
||
|
||
describe('getTableAdmonitionMessage', () => {
|
||
it('returns the custom-grants copy', () => {
|
||
expect(getTableAdmonitionMessage('custom-grants')).toBe(
|
||
'This table has custom Data API permissions — access may be restricted for some roles or operations.'
|
||
)
|
||
})
|
||
|
||
it('returns the no-grants copy', () => {
|
||
expect(getTableAdmonitionMessage('no-grants')).toBe(
|
||
'This table cannot be accessed via the Data API. Enable access in your project’s Data API settings.'
|
||
)
|
||
})
|
||
|
||
it('returns the publicly-readable copy', () => {
|
||
expect(getTableAdmonitionMessage('publicly-readable')).toBe(
|
||
'This table can be accessed by anyone via the Data API as RLS is disabled.'
|
||
)
|
||
})
|
||
|
||
it('returns the locked-by-rls copy', () => {
|
||
expect(getTableAdmonitionMessage('locked-by-rls')).toBe(
|
||
'No data will be returned via the Data API as no RLS policies exist on this table.'
|
||
)
|
||
})
|
||
|
||
it('returns null for secured — no admonition needed', () => {
|
||
expect(getTableAdmonitionMessage('secured')).toBeNull()
|
||
})
|
||
|
||
it('returns null for schema-not-exposed — handled by a separate admonition with a link', () => {
|
||
expect(getTableAdmonitionMessage('schema-not-exposed')).toBeNull()
|
||
})
|
||
|
||
it('returns null for unknown — caller should stay silent during loading/errored state', () => {
|
||
expect(getTableAdmonitionMessage('unknown')).toBeNull()
|
||
})
|
||
})
|