Files
supabase/apps/studio/components/interfaces/Account/AccessTokens/AccessToken.presets.test.ts
kemal.earth 31497ba127 feat(studio): add permission presets to scoped pat creation form (#49381)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

This adds a quick presets selector to scoped pat permissions. No access,
read-only and full access.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added permission presets for scoped access tokens: No access,
Read-only, and Full access.
  * Added a selector to quickly configure permissions across resources.
  * Displays “Custom” when individual permissions differ from a preset.
  * Shows warnings and guidance for high-risk full-access permissions.
* Automatically uses read-only access for resources that do not support
write permissions.

* **Tests**
* Added coverage for preset selection, application, warnings, ordering,
and custom configurations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-21 11:12:37 +01:00

118 lines
4.2 KiB
TypeScript

import { describe, expect, test } from 'vitest'
import {
getCatalogEntry,
PERMISSION_CATALOG,
PERMISSION_CATALOG_BY_CATEGORY,
type PermissionSelection,
} from './AccessToken.permissions'
import {
applyPreset,
getActivePresetId,
getFullAccessDescription,
getPreset,
PERMISSION_PRESETS,
} from './AccessToken.presets'
const NONE = getPreset('none')!
const READ = getPreset('read')!
const FULL = getPreset('full')!
describe('PERMISSION_PRESETS', () => {
test('offers no access, read-only and full access in that order', () => {
expect(PERMISSION_PRESETS.map((preset) => preset.id)).toEqual(['none', 'read', 'full'])
})
test('resolves every catalog entry to a mode its row can render', () => {
for (const entry of PERMISSION_CATALOG) {
expect(NONE.resolve(entry)).toBe('none')
expect(READ.resolve(entry)).toBe('read')
expect(FULL.resolve(entry)).toBe(entry.writable ? 'readwrite' : 'read')
}
})
test('caps full access at read for resources with no write scopes', () => {
const readOnlyEntries = PERMISSION_CATALOG.filter((entry) => !entry.writable)
expect(readOnlyEntries.length).toBeGreaterThan(0)
for (const entry of readOnlyEntries) {
expect(FULL.resolve(entry)).toBe('read')
}
})
test('only marks full access as risky, and only it carries a description', () => {
expect(
PERMISSION_PRESETS.filter((preset) => preset.isRisky).map((preset) => preset.id)
).toEqual(['full'])
expect(
PERMISSION_PRESETS.filter((preset) => preset.description !== undefined).map(
(preset) => preset.id
)
).toEqual(['full'])
})
})
describe('getFullAccessDescription', () => {
test('names high-risk resources that exist in the catalog', () => {
const description = getFullAccessDescription()
expect(description).toBe(
'Grants the highest access each resource offers, including write access to your database, API keys, and organization members.'
)
for (const key of ['project:database', 'project:api_gateway_keys', 'organization:members']) {
expect(getCatalogEntry(key)).toBeDefined()
expect(getCatalogEntry(key)!.risk).toBe('high')
// The copy claims write access on these specifically, so they have to be writable
expect(getCatalogEntry(key)!.writable).toBe(true)
}
})
})
describe('applyPreset', () => {
test('sets every catalog entry by default', () => {
const selection = applyPreset(READ, {})
expect(Object.keys(selection)).toHaveLength(PERMISSION_CATALOG.length)
expect(Object.values(selection).every((mode) => mode === 'read')).toBe(true)
})
test('overwrites existing manual choices', () => {
const selection = applyPreset(NONE, { 'project:database': 'readwrite' })
expect(selection['project:database']).toBe('none')
})
test('leaves entries outside the given subset untouched', () => {
const database = PERMISSION_CATALOG_BY_CATEGORY.find((category) => category.key === 'database')!
const before: PermissionSelection = { 'project:advisors': 'read' }
const selection = applyPreset(FULL, before, database.entries)
expect(selection['project:advisors']).toBe('read')
expect(selection['project:database']).toBe('readwrite')
expect(Object.keys(selection)).toHaveLength(database.entries.length + 1)
})
})
describe('getActivePresetId', () => {
test('reads an empty selection as no access', () => {
expect(getActivePresetId({})).toBe('none')
})
test('identifies a selection produced by each preset', () => {
for (const preset of PERMISSION_PRESETS) {
expect(getActivePresetId(applyPreset(preset, {}))).toBe(preset.id)
}
})
test('returns null once a single row diverges', () => {
const selection = applyPreset(READ, {})
selection['project:storage'] = 'readwrite'
expect(getActivePresetId(selection)).toBeNull()
})
test('ignores rows outside the given subset', () => {
const database = PERMISSION_CATALOG_BY_CATEGORY.find((category) => category.key === 'database')!
const selection = applyPreset(READ, {}, database.entries)
selection['project:storage'] = 'readwrite'
expect(getActivePresetId(selection)).toBeNull()
expect(getActivePresetId(selection, database.entries)).toBe('read')
})
})