mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +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?
Extracts the token view sheet slice of #48742
(w3b6x9/scoped-pat-access-feedback, commit 56ef87a). The role-evaluation
logic (estimateRoleLevel, computeTokenRoleContext,
applySelectionToRoleContext, groupFailingResources) already landed on
master via #48805 and #48809 — this PR only wires the view sheet up to
it:
- Bindings whose project/org was deleted (FGA bindings erased) render a
"resources no longer exist" state; bindings the user can no longer reach
render an anonymous count with a "No longer accessible" badge and a
"removed from" admonition.
- Accessible resources list their name plus ref/slug; capabilities show
"Exceeds your role" pills and the risk badge reflects what the owner's
current role actually allows.
- Header split into separate "Access control" and "API docs" buttons.
- Everything recomputes from live org/project/permission queries (no
stored state) and degrades to no warnings while loading or on
self-hosted.
Also gives DocsButton an optional `label` prop (defaults preserve
existing behavior for every other consumer) so the two header buttons
can carry distinct text.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Enhanced access-token details with permission categories, risk
summaries, endpoint, and MCP information.
* Added warnings for permissions exceeding the token’s role.
* Clearly identifies inaccessible, deleted, or unavailable organizations
and projects.
* Added resource details and remediation guidance for unusable tokens.
* Documentation links can now display custom labels.
* **Bug Fixes**
* Improved access evaluation when organization or project data is
incomplete or access has changed.
* Deferred resource loading until token details are opened.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
import { permissions } from '@supabase/shared-types'
|
|
import { components } from 'api-types'
|
|
|
|
export type ScopedAccessTokenPermission =
|
|
components['schemas']['CreateScopedAccessTokenBody']['permissions'][number]
|
|
|
|
export const CUSTOM_EXPIRY_VALUE = 'custom'
|
|
|
|
/** Shared tail for every "this token can no longer be used" message. */
|
|
export const TOKEN_DENIED_REMEDIATION =
|
|
'Requests with this token will be denied. Delete this token and create a new one with the resources and permissions you need.'
|
|
|
|
/** Warning shown on both entry points that create a classic (account-wide) token. */
|
|
export const CLASSIC_TOKEN_WARNING = {
|
|
title: 'Access tokens can be used to control your whole account',
|
|
description: 'Be careful when sharing your tokens',
|
|
} as const
|
|
|
|
export const EXPIRES_AT_OPTIONS = {
|
|
hour: { value: 'hour', label: '1 hour' },
|
|
day: { value: 'day', label: '1 day' },
|
|
week: { value: 'week', label: '7 days' },
|
|
month: { value: 'month', label: '30 days' },
|
|
custom: { value: CUSTOM_EXPIRY_VALUE, label: 'Custom' },
|
|
} as const
|
|
|
|
const FGA = permissions.FgaPermissions
|
|
|
|
const getAction = (key: string): string => {
|
|
if (key.endsWith('_READ')) return 'read'
|
|
if (key.endsWith('_WRITE')) return 'write'
|
|
if (key.endsWith('_CREATE')) return 'create'
|
|
if (key.endsWith('_DELETE')) return 'delete'
|
|
return 'read'
|
|
}
|
|
|
|
const getResource = (key: string): string => {
|
|
return key.replace(/_(READ|WRITE|CREATE|DELETE)$/, '').toLowerCase()
|
|
}
|
|
|
|
const buildPermissionList = () => {
|
|
const list: Array<{
|
|
scope: string
|
|
resource: string
|
|
action: string
|
|
id: string
|
|
title: string
|
|
}> = []
|
|
|
|
for (const [scope, scopePerms] of Object.entries(FGA)) {
|
|
for (const [key, perm] of Object.entries(scopePerms)) {
|
|
list.push({
|
|
scope: scope.toLowerCase(),
|
|
resource: getResource(key),
|
|
action: getAction(key),
|
|
id: perm.id,
|
|
title: perm.title,
|
|
})
|
|
}
|
|
}
|
|
|
|
return list
|
|
}
|
|
|
|
export const PERMISSION_LIST = buildPermissionList()
|
|
|
|
export const ACCESS_TOKEN_RESOURCES = (() => {
|
|
const resourceMap = new Map<string, { resource: string; title: string; actions: string[] }>()
|
|
|
|
for (const p of PERMISSION_LIST) {
|
|
const key = `${p.scope}:${p.resource}`
|
|
if (!resourceMap.has(key)) {
|
|
const cleanTitle = p.title.replace(/^(Read|Manage|Create|Delete)\s+/i, '')
|
|
resourceMap.set(key, { resource: key, title: cleanTitle, actions: [] })
|
|
}
|
|
const entry = resourceMap.get(key)!
|
|
if (!entry.actions.includes(p.action)) {
|
|
entry.actions.push(p.action)
|
|
}
|
|
}
|
|
|
|
return Array.from(resourceMap.values())
|
|
})()
|