mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
## 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 -->
118 lines
4.2 KiB
TypeScript
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')
|
|
})
|
|
})
|