mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
docs: add Personal Access Tokens guide with generated permission tables (#49732)
Add a guide that compares classic and scoped personal access tokens, explains how account roles constrain token permissions, and walks through creating and testing a project-scoped token. Include generated tables mapping permissions to Management API endpoints and MCP tools, and link the guide from docs navigation and Studio token sheets. Move the scoped-token permission catalog from Studio into shared-data. Studio and docs generation now share permission names, categories, descriptions, risk metadata, modes, scopes, and display order. Generate the tables from the shared catalog, OpenAPI x-fga-permissions, and the downloaded MCP permission map. Exclude Workers permissions until the feature is live. Run regeneration through the docs Makefile, verify checked-in output in CI, and refresh it in the weekly Management API workflow. Add Dashboard and Docs ownership plus contributor guidance so permission changes stay synchronized.
This commit is contained in:
1 parent
b278b1ec8a
commit
2681a21f5c
24 files changed
+1474
-634
No files matched your search
@@ -87,7 +87,7 @@ at hand — they cite each other where context matters.
|
||||
| [`reference/llm-agent-parity.md`](./reference/llm-agent-parity.md) | HTML↔markdown fidelity (e.g. AI prompts), search caveat, agent onboarding guides, in-flux wiring. |
|
||||
| [`reference/federated-docs.md`](./reference/federated-docs.md) | How docs pulls markdown from external repos at build time. Routes, `pageMap`, remark/rehype plugins, link transforms, known failure modes. |
|
||||
| [`reference/ci-and-lint.md`](./reference/ci-and-lint.md) | GitHub Actions on every PR — `docs_lint`, `Docs Tests`, typecheck, prettier, Vercel preview gate. Where to add a check before creating a new one. |
|
||||
| [`reference/management-api-reference.md`](./reference/management-api-reference.md) | Management API OpenAPI download → Redocly bundle → codegen → `ApiEndpointSection`; why not to swap in Scalar/Redoc. |
|
||||
| [`reference/management-api-reference.md`](./reference/management-api-reference.md) | Management API OpenAPI → reference generation, including scoped PAT permission tables; why not to swap in Scalar/Redoc. |
|
||||
| [`reference/gotchas.md`](./reference/gotchas.md) | Specific traps to watch for. One-liner per item. |
|
||||
|
||||
## How to use during a chat
|
||||
|
||||
@@ -14,7 +14,7 @@ PR opened / updated
|
||||
│
|
||||
├── docs_lint (MDX/content linting; required)
|
||||
├── docs_lint_comment_external (posts results as PR comments for external PRs)
|
||||
├── Docs Tests (pnpm test:docs on .ts* file changes)
|
||||
├── Docs Tests (pnpm test:docs on relevant docs code/spec changes)
|
||||
├── TypeScript & Lint (tsc + eslint)
|
||||
├── Prettier (format check)
|
||||
├── reviewdog (inline annotations)
|
||||
@@ -36,15 +36,16 @@ check before merging.** Backed by `supa-mdx-lint`.
|
||||
|
||||
### 2. Docs Tests (`docs-tests.yml`)
|
||||
|
||||
Triggered on PRs to master when files matching `apps/docs/**/*.ts*` or
|
||||
`apps/docs/spec/**/*.json` change. Runs on a Blacksmith 4-vCPU Ubuntu runner
|
||||
with concurrency controls to cancel stale builds.
|
||||
Triggered on relevant docs code/spec changes, including the generated scoped
|
||||
PAT partials and their shared permission catalog. Runs on a Blacksmith 4-vCPU
|
||||
Ubuntu runner with concurrency controls to cancel stale builds.
|
||||
|
||||
- Sparse checkout of `apps/docs`, `examples`, `packages`, `supabase`, and
|
||||
`patches`.
|
||||
- Install pnpm (pinned hash).
|
||||
- Set up Node.js from `.nvmrc`.
|
||||
- `pnpm install --frozen-lockfile`.
|
||||
- Regenerate the scoped PAT partials and fail if the committed output drifts.
|
||||
- Run `pnpm run test:docs` (with dummy GitHub OAuth env vars to prevent local
|
||||
Supabase startup errors).
|
||||
|
||||
|
||||
@@ -92,6 +92,15 @@ the custom cycle-safe resolve remains required.
|
||||
| `apps/docs/features/docs/Reference.sections.tsx` | `ApiEndpointSection` UI |
|
||||
| `apps/docs/internals/generate-reference-markdown.ts` | agent markdown export |
|
||||
|
||||
## Scoped personal access token permission tables
|
||||
|
||||
The "Personal Access Tokens" guide's permission and MCP tables are generated
|
||||
from the Management API specs, MCP permission map, and the same shared catalog
|
||||
Studio uses. Regenerate `content/_partials/access-control/scoped_pat_*.mdx` with
|
||||
`make -C apps/docs/spec generate.partials.access-control`. Docs Tests runs this
|
||||
on relevant pull requests and fails if the partials drift; the weekly Management
|
||||
API update also runs it after refreshing the live inputs.
|
||||
|
||||
## Related
|
||||
|
||||
- [`build-pipeline.md`](./build-pipeline.md) — where `codegen:references`
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ pnpm api:codegen # platform Management API types → packages/api-ty
|
||||
|
||||
Every PR must pass typecheck + lint (one workflow), Prettier, and a typos check. Other checks are path-filtered: Studio unit tests/build and the lint ratchet (ESLint warning count must not increase) run on `apps/studio/**` changes; app-specific test suites run on their own paths.
|
||||
|
||||
Never hand-edit generated files: `packages/api-types/types/**`, `**/routeTree.gen.ts`, `**/__generated__/**`, `apps/docs/features/docs/generated/**`, `apps/www/.generated/**`, `supabase/functions/common/database-types.ts`.
|
||||
Never hand-edit generated files: `packages/api-types/types/**`, `**/routeTree.gen.ts`, `**/__generated__/**`, `apps/docs/features/docs/generated/**`, `apps/www/.generated/**`, `supabase/functions/common/database-types.ts`, `apps/docs/content/_partials/access-control/scoped_pat_*.mdx` (run `make -C apps/docs/spec generate.partials.access-control`).
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/packages/ui/ @supabase/design
|
||||
/packages/shared-data/pricing.ts @supabase/billing
|
||||
/packages/shared-data/plans.ts @supabase/billing
|
||||
/packages/shared-data/scoped-access-token-permissions.ts @supabase/Dashboard @supabase/docs
|
||||
/packages/common/telemetry-constants.ts @supabase/growth-eng
|
||||
/packages/dev-tools/ @supabase/growth-eng
|
||||
# /packages/pg-meta @supabase/postgres @avallete
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Update Mgmt Api Docs
|
||||
on:
|
||||
schedule:
|
||||
# Run at 00:00 UTC every Monday
|
||||
- cron: "0 0 * * 1"
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -19,10 +19,13 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.ref }}
|
||||
# The PAT tables generator imports the shared catalog and its base tsconfig.
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
patches
|
||||
packages/generator
|
||||
packages/shared-data
|
||||
packages/tsconfig
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
name: Install pnpm
|
||||
@@ -32,15 +35,15 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Change to apps/docs/spec directory and run make command
|
||||
- name: Refresh Management API docs
|
||||
working-directory: apps/docs/spec
|
||||
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
|
||||
run: make download.api.v1 download.mcp-tools-permissions dereference.api.v1 generate.sections.api.v1 generate.partials.access-control format
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
@@ -55,8 +58,18 @@ jobs:
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
commit-message: "feat: update mgmt api docs"
|
||||
title: "feat: update mgmt api docs"
|
||||
body: "This PR updates mgmt api docs automatically."
|
||||
branch: "gha/auto-update-mgmt-api-docs"
|
||||
base: "master"
|
||||
commit-message: 'feat: update mgmt api docs'
|
||||
title: 'feat: update mgmt api docs'
|
||||
body: |
|
||||
This PR updates Management API docs automatically.
|
||||
|
||||
This regenerates:
|
||||
|
||||
- Management API specs and sections
|
||||
- Personal Access Tokens permission-to-endpoint table
|
||||
- Personal Access Tokens MCP tool permissions table
|
||||
|
||||
Sources include the live Management API specs, MCP permission map,
|
||||
and Studio's shared permission catalog.
|
||||
branch: 'gha/auto-update-mgmt-api-docs'
|
||||
base: 'master'
|
||||
@@ -6,8 +6,13 @@ on:
|
||||
paths:
|
||||
- 'apps/docs/**/*.ts*'
|
||||
- 'apps/docs/spec/**/*.json'
|
||||
- 'apps/docs/spec/Makefile'
|
||||
- 'apps/docs/spec/sections/generateAccessControlPartials.mts'
|
||||
- 'apps/docs/content/_partials/access-control/**'
|
||||
- 'apps/docs/.env.development'
|
||||
- 'apps/docs/package.json'
|
||||
- 'packages/shared-data/package.json'
|
||||
- 'packages/shared-data/scoped-access-token-permissions.ts'
|
||||
- 'e2e/docs/local-smoke/**'
|
||||
- 'e2e/docs/playwright.local-smoke.config.ts'
|
||||
- 'e2e/docs/package.json'
|
||||
@@ -52,6 +57,13 @@ jobs:
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Check access-control partials
|
||||
run: |
|
||||
make -C apps/docs/spec generate.partials.access-control
|
||||
git diff --exit-code -- \
|
||||
apps/docs/content/_partials/access-control/scoped_pat_permissions.mdx \
|
||||
apps/docs/content/_partials/access-control/scoped_pat_mcp_tools.mdx
|
||||
|
||||
- name: Download JS reference TypeDoc dumps
|
||||
# The source dumps under apps/docs/spec/reference/<lib>/<ver>/*.json are
|
||||
# gitignored — `make download.tsdoc.v2` re-fetches them from
|
||||
|
||||
@@ -2728,6 +2728,11 @@ export const platform: NavMenuConstant = {
|
||||
name: 'Access Control',
|
||||
url: '/guides/platform/access-control' as `/${string}`,
|
||||
},
|
||||
{
|
||||
name: 'Personal Access Tokens',
|
||||
url: '/guides/platform/personal-access-tokens' as `/${string}`,
|
||||
enabled: fullPlatformEnabled,
|
||||
},
|
||||
{
|
||||
name: 'Multi-factor Authentication',
|
||||
url: '/guides/platform/multi-factor-authentication',
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{/* Generated by `make -C apps/docs/spec generate.partials.access-control`. Do not hand-edit; see supabase/platform#37175 and apps/docs/spec/Makefile. */}
|
||||
|
||||
| MCP tool | Required permission |
|
||||
| --------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `apply_migration` | **Migrations** (Read-write) |
|
||||
| `confirm_cost` | None (always available) |
|
||||
| `create_branch` | **Development Branches** (Read-write) or **Production Branches** (Read-write) |
|
||||
| `create_project` | **Organization Projects** (Read-write) |
|
||||
| `delete_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) |
|
||||
| `deploy_edge_function` | **Edge Functions** (Read-write) |
|
||||
| `execute_sql` | **Database** (Read) |
|
||||
| `generate_typescript_types` | **Database** (Read) |
|
||||
| `get_advisors` | **Advisors** (Read) |
|
||||
| `get_cost` | **Organization Settings** (Read) and **Projects (account-wide)** (Read) |
|
||||
| `get_edge_function` | **Edge Functions** (Read) |
|
||||
| `get_logs` | **Logs** (Read) |
|
||||
| `get_organization` | **Organization Settings** (Read) |
|
||||
| `get_project` | **Project Settings** (Read) |
|
||||
| `get_project_url` | **Project Settings** (Read) |
|
||||
| `get_publishable_keys` | **API Keys** (Read) |
|
||||
| `get_storage_config` | **Storage Config** (Read) |
|
||||
| `list_branches` | **Development Branches** (Read) or **Production Branches** (Read) |
|
||||
| `list_edge_functions` | **Edge Functions** (Read) |
|
||||
| `list_extensions` | **Database** (Read) |
|
||||
| `list_migrations` | **Migrations** (Read) |
|
||||
| `list_organizations` | **Organizations** (Read) |
|
||||
| `list_projects` | **Projects (account-wide)** (Read) |
|
||||
| `list_storage_buckets` | **Storage** (Read) |
|
||||
| `list_tables` | **Database** (Read) |
|
||||
| `merge_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) |
|
||||
| `pause_project` | **Project Settings** (Read-write) |
|
||||
| `query_logs` | **Logs** (Read) |
|
||||
| `rebase_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) |
|
||||
| `reset_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) |
|
||||
| `restore_project` | **Project Settings** (Read-write) |
|
||||
| `search_docs` | None (always available) |
|
||||
| `update_storage_config` | **Storage Config** (Read-write) |
|
||||
@@ -0,0 +1,236 @@
|
||||
{/* Generated by `make -C apps/docs/spec generate.partials.access-control`. Do not hand-edit; see supabase/platform#37175 and apps/docs/spec/Makefile. */}
|
||||
|
||||
| Permission | Access required | Management API endpoint |
|
||||
| -------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Project** | | |
|
||||
| Project Settings | Read | [Get JIT access config](/docs/reference/api/v1-get-jit-access-config) |
|
||||
| | | [Get postgres upgrade eligibility](/docs/reference/api/v1-get-postgres-upgrade-eligibility)[^1] |
|
||||
| | | [Get postgres upgrade status](/docs/reference/api/v1-get-postgres-upgrade-status)[^1] |
|
||||
| | | [Get project](/docs/reference/api/v1-get-project) |
|
||||
| | | [Get services health](/docs/reference/api/v1-get-services-health) |
|
||||
| | | [List available restore versions](/docs/reference/api/v1-list-available-restore-versions) |
|
||||
| | | [List private link associations](/docs/reference/api/v2-list-private-link-associations) |
|
||||
| | | [Preview a project transfer](/docs/reference/api/v2-preview-a-project-transfer) |
|
||||
| | Read-write | [Cancel a project restoration](/docs/reference/api/v1-cancel-a-project-restoration) |
|
||||
| | | [Create private link association](/docs/reference/api/v2-create-private-link-association) |
|
||||
| | | [Delete a project](/docs/reference/api/v1-delete-a-project) |
|
||||
| | | [Delete private link association](/docs/reference/api/v2-delete-private-link-association) |
|
||||
| | | [Delete private link association for database](/docs/reference/api/v2-delete-private-link-association-for-database) |
|
||||
| | | [Get pgsodium config](/docs/reference/api/v1-get-pgsodium-config) |
|
||||
| | | [OAuth authorize project claim](/docs/reference/api/v1-oauth-authorize-project-claim)[^2] |
|
||||
| | | [Pause a project](/docs/reference/api/v1-pause-a-project) |
|
||||
| | | [Restart a project](/docs/reference/api/v1-restart-a-project) |
|
||||
| | | [Restore a project](/docs/reference/api/v1-restore-a-project) |
|
||||
| | | [Update a project](/docs/reference/api/v1-update-a-project) |
|
||||
| | | [Update auth service config](/docs/reference/api/v1-update-auth-service-config)[^3] |
|
||||
| | | [Update JIT access config](/docs/reference/api/v1-update-jit-access-config) |
|
||||
| | | [Update pgsodium config](/docs/reference/api/v1-update-pgsodium-config) |
|
||||
| | | [Upgrade postgres version](/docs/reference/api/v1-upgrade-postgres-version)[^4] |
|
||||
| Action Runs | Read | [Count action runs](/docs/reference/api/v1-count-action-runs) |
|
||||
| | | [Get action run](/docs/reference/api/v1-get-action-run) |
|
||||
| | | [Get action run logs](/docs/reference/api/v1-get-action-run-logs) |
|
||||
| | | [List action runs](/docs/reference/api/v1-list-action-runs) |
|
||||
| | Read-write | [Update action run status](/docs/reference/api/v1-update-action-run-status) |
|
||||
| Advisors | Read | [Get performance advisors](/docs/reference/api/v1-get-performance-advisors) |
|
||||
| | | [Get security advisors](/docs/reference/api/v1-get-security-advisors) |
|
||||
| Analytics Config | Read | [List log drains](/docs/reference/api/v2-list-log-drains) |
|
||||
| | Read-write | [Create log drain](/docs/reference/api/v2-create-log-drain) |
|
||||
| | | [Delete log drain](/docs/reference/api/v2-delete-log-drain) |
|
||||
| | | [Update log drain](/docs/reference/api/v2-update-log-drain) |
|
||||
| Logs | Read | [Get project logs](/docs/reference/api/v1-get-project-logs) |
|
||||
| | | [Get project logs all](/docs/reference/api/v1-get-project-logs-all) |
|
||||
| | | [Scrape project metrics](/docs/reference/api/v1-scrape-project-metrics) |
|
||||
| Usage Analytics | Read | [Get project function combined stats](/docs/reference/api/v1-get-project-function-combined-stats) |
|
||||
| | | [Get project usage API count](/docs/reference/api/v1-get-project-usage-api-count) |
|
||||
| | | [Get project usage request count](/docs/reference/api/v1-get-project-usage-request-count) |
|
||||
| Platform Webhooks | Read | [Get delivery](/docs/reference/api/v2-projects-ref-webhooks-deliveries-id-get) |
|
||||
| | | [Get endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-get) |
|
||||
| | | [List deliveries](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-deliveries-get) |
|
||||
| | | [List endpoints](/docs/reference/api/v2-projects-ref-webhooks-endpoints-get) |
|
||||
| | Read-write | [Create endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-post) |
|
||||
| | | [Delete all endpoints](/docs/reference/api/v2-projects-ref-webhooks-endpoints-delete) |
|
||||
| | | [Delete endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-delete) |
|
||||
| | | [Retry delivery](/docs/reference/api/v2-projects-ref-webhooks-deliveries-id-retry-post) |
|
||||
| | | [Send test event](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-test-post) |
|
||||
| | | [Update endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-patch) |
|
||||
| **Database** | | |
|
||||
| Backups | Read | [Get backup schedule](/docs/reference/api/v1-get-backup-schedule) |
|
||||
| | | [List all backups](/docs/reference/api/v1-list-all-backups) |
|
||||
| | Read-write | [Restore PITR backup](/docs/reference/api/v1-restore-pitr-backup) |
|
||||
| | | [Update backup schedule](/docs/reference/api/v1-update-backup-schedule) |
|
||||
| Database | Read | [Generate typescript types](/docs/reference/api/v1-generate-typescript-types) |
|
||||
| | | [Get database metadata](/docs/reference/api/v1-get-database-metadata) |
|
||||
| | | [Get database openapi](/docs/reference/api/v1-get-database-openapi) |
|
||||
| | | [Get postgres upgrade eligibility](/docs/reference/api/v1-get-postgres-upgrade-eligibility)[^1] |
|
||||
| | | [Get postgres upgrade status](/docs/reference/api/v1-get-postgres-upgrade-status)[^1] |
|
||||
| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
|
||||
| | | [Get project PgBouncer config](/docs/reference/api/v1-get-project-pgbouncer-config) |
|
||||
| | | [Read only query](/docs/reference/api/v1-read-only-query) |
|
||||
| | | [Run a query](/docs/reference/api/v1-run-a-query) |
|
||||
| | Read-write | [Create login role](/docs/reference/api/v1-create-login-role) |
|
||||
| | | [Delete login roles](/docs/reference/api/v1-delete-login-roles) |
|
||||
| | | [Run a query](/docs/reference/api/v1-run-a-query) |
|
||||
| | | [Upgrade postgres version](/docs/reference/api/v1-upgrade-postgres-version)[^4] |
|
||||
| Database Config | Read | [Get postgres config](/docs/reference/api/v1-get-postgres-config) |
|
||||
| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
|
||||
| | Read-write | [Update database password](/docs/reference/api/v1-update-database-password) |
|
||||
| | | [Update postgres config](/docs/reference/api/v1-update-postgres-config) |
|
||||
| Database JIT | Read | [Authorize JIT access](/docs/reference/api/v1-authorize-jit-access) |
|
||||
| | | [Get JIT access](/docs/reference/api/v1-get-jit-access) |
|
||||
| | Read-write | [Delete invite external JIT access](/docs/reference/api/v1-delete-invite-external-jit-access) |
|
||||
| | | [Delete JIT access](/docs/reference/api/v1-delete-jit-access) |
|
||||
| | | [Invite external JIT access](/docs/reference/api/v1-invite-external-jit-access) |
|
||||
| | | [List JIT access](/docs/reference/api/v1-list-jit-access) |
|
||||
| | | [Update JIT access](/docs/reference/api/v1-update-jit-access) |
|
||||
| Network Bans | Read | [List all network bans](/docs/reference/api/v1-list-all-network-bans) |
|
||||
| | | [List all network bans enriched](/docs/reference/api/v1-list-all-network-bans-enriched) |
|
||||
| | Read-write | [Delete network bans](/docs/reference/api/v1-delete-network-bans) |
|
||||
| Network Restrictions | Read | [Get network restrictions](/docs/reference/api/v1-get-network-restrictions) |
|
||||
| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
|
||||
| | Read-write | [Patch network restrictions](/docs/reference/api/v1-patch-network-restrictions) |
|
||||
| | | [Update network restrictions](/docs/reference/api/v1-update-network-restrictions) |
|
||||
| Migrations | Read | [Get a migration](/docs/reference/api/v1-get-a-migration) |
|
||||
| | | [List migration history](/docs/reference/api/v1-list-migration-history) |
|
||||
| | Read-write | [Apply a migration](/docs/reference/api/v1-apply-a-migration) |
|
||||
| | | [Patch a migration](/docs/reference/api/v1-patch-a-migration) |
|
||||
| | | [Rollback migrations](/docs/reference/api/v1-rollback-migrations) |
|
||||
| | | [Upsert a migration](/docs/reference/api/v1-upsert-a-migration) |
|
||||
| Connection Pooling | Read | [Get pooler config](/docs/reference/api/v1-get-pooler-config) |
|
||||
| | Read-write | [Update pooler config](/docs/reference/api/v1-update-pooler-config) |
|
||||
| Read-only Mode | Read | [Get read-only mode status](/docs/reference/api/v1-get-readonly-mode-status) |
|
||||
| | Read-write | [Disable read-only mode temporarily](/docs/reference/api/v1-disable-readonly-mode-temporarily) |
|
||||
| SSL Enforcement | Read | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
|
||||
| | | [Get SSL enforcement config](/docs/reference/api/v1-get-ssl-enforcement-config) |
|
||||
| | Read-write | [Update SSL enforcement config](/docs/reference/api/v1-update-ssl-enforcement-config) |
|
||||
| Database Webhooks | Read-write | [Enable database webhook](/docs/reference/api/v1-enable-database-webhook) |
|
||||
| **Application services** | | |
|
||||
| API Keys | Read | [Get project API key](/docs/reference/api/v1-get-project-api-key) |
|
||||
| | | [Get project API keys](/docs/reference/api/v1-get-project-api-keys) |
|
||||
| | | [Get project legacy API keys](/docs/reference/api/v1-get-project-legacy-api-keys) |
|
||||
| | Read-write | [Create project API key](/docs/reference/api/v1-create-project-api-key) |
|
||||
| | | [Delete project API key](/docs/reference/api/v1-delete-project-api-key) |
|
||||
| | | [Update project API key](/docs/reference/api/v1-update-project-api-key) |
|
||||
| | | [Update project legacy API keys](/docs/reference/api/v1-update-project-legacy-api-keys) |
|
||||
| Auth Config | Read | [Get a SSO provider](/docs/reference/api/v1-get-a-sso-provider) |
|
||||
| | | [Get auth service config](/docs/reference/api/v1-get-auth-service-config) |
|
||||
| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
|
||||
| | | [Get project TPA integration](/docs/reference/api/v1-get-project-tpa-integration) |
|
||||
| | | [List all SSO provider](/docs/reference/api/v1-list-all-sso-provider) |
|
||||
| | | [List project TPA integrations](/docs/reference/api/v1-list-project-tpa-integrations) |
|
||||
| | Read-write | [Create a SSO provider](/docs/reference/api/v1-create-a-sso-provider) |
|
||||
| | | [Create project TPA integration](/docs/reference/api/v1-create-project-tpa-integration) |
|
||||
| | | [Delete a SSO provider](/docs/reference/api/v1-delete-a-sso-provider) |
|
||||
| | | [Delete project TPA integration](/docs/reference/api/v1-delete-project-tpa-integration) |
|
||||
| | | [Update a SSO provider](/docs/reference/api/v1-update-a-sso-provider) |
|
||||
| | | [Update auth service config](/docs/reference/api/v1-update-auth-service-config)[^3] |
|
||||
| Auth Signing Keys | Read | [Get legacy signing key](/docs/reference/api/v1-get-legacy-signing-key) |
|
||||
| | | [Get project signing key](/docs/reference/api/v1-get-project-signing-key) |
|
||||
| | | [Get project signing keys](/docs/reference/api/v1-get-project-signing-keys) |
|
||||
| | Read-write | [Create legacy signing key](/docs/reference/api/v1-create-legacy-signing-key) |
|
||||
| | | [Create project signing key](/docs/reference/api/v1-create-project-signing-key) |
|
||||
| | | [Remove project signing key](/docs/reference/api/v1-remove-project-signing-key) |
|
||||
| | | [Update project signing key](/docs/reference/api/v1-update-project-signing-key) |
|
||||
| Data API Config | Read | [Get PostgREST service config](/docs/reference/api/v1-get-postgrest-service-config) |
|
||||
| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
|
||||
| | Read-write | [Update PostgREST service config](/docs/reference/api/v1-update-postgrest-service-config) |
|
||||
| Edge Functions | Read | [Get a function](/docs/reference/api/v1-get-a-function) |
|
||||
| | | [Get a function body](/docs/reference/api/v1-get-a-function-body) |
|
||||
| | | [List all functions](/docs/reference/api/v1-list-all-functions) |
|
||||
| | Read-write | [Bulk update functions](/docs/reference/api/v1-bulk-update-functions) |
|
||||
| | | [Create a function](/docs/reference/api/v1-create-a-function) |
|
||||
| | | [Delete a function](/docs/reference/api/v1-delete-a-function) |
|
||||
| | | [Deploy a function](/docs/reference/api/v1-deploy-a-function) |
|
||||
| | | [Update a function](/docs/reference/api/v1-update-a-function) |
|
||||
| Edge Function Secrets | Read | [List all secrets](/docs/reference/api/v1-list-all-secrets) |
|
||||
| | Read-write | [Bulk create secrets](/docs/reference/api/v1-bulk-create-secrets) |
|
||||
| | | [Bulk delete secrets](/docs/reference/api/v1-bulk-delete-secrets) |
|
||||
| Realtime Config | Read | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
|
||||
| | | [Get realtime config](/docs/reference/api/v1-get-realtime-config) |
|
||||
| | Read-write | [Shutdown realtime](/docs/reference/api/v1-shutdown-realtime) |
|
||||
| | | [Update realtime config](/docs/reference/api/v1-update-realtime-config) |
|
||||
| Storage | Read | [List all buckets](/docs/reference/api/v1-list-all-buckets) |
|
||||
| Storage Config | Read | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
|
||||
| | | [Get storage config](/docs/reference/api/v1-get-storage-config) |
|
||||
| | Read-write | [Update storage config](/docs/reference/api/v1-update-storage-config) |
|
||||
| **Infrastructure and delivery** | | |
|
||||
| Development Branches | Read | [Get a branch](/docs/reference/api/v1-get-a-branch) |
|
||||
| | | [Get a branch config](/docs/reference/api/v1-get-a-branch-config) |
|
||||
| | | [List all branches](/docs/reference/api/v1-list-all-branches) |
|
||||
| | Read-write | [Create a branch](/docs/reference/api/v1-create-a-branch) |
|
||||
| | | [Delete a branch](/docs/reference/api/v1-delete-a-branch) |
|
||||
| | | [Diff a branch](/docs/reference/api/v1-diff-a-branch) |
|
||||
| | | [Merge a branch](/docs/reference/api/v1-merge-a-branch) |
|
||||
| | | [Push a branch](/docs/reference/api/v1-push-a-branch) |
|
||||
| | | [Reset a branch](/docs/reference/api/v1-reset-a-branch) |
|
||||
| | | [Restore a branch](/docs/reference/api/v1-restore-a-branch) |
|
||||
| | | [Update a branch config](/docs/reference/api/v1-update-a-branch-config) |
|
||||
| Production Branches | Read | [Get a branch](/docs/reference/api/v1-get-a-branch) |
|
||||
| | | [Get a branch config](/docs/reference/api/v1-get-a-branch-config) |
|
||||
| | | [List all branches](/docs/reference/api/v1-list-all-branches) |
|
||||
| | Read-write | [Create a branch](/docs/reference/api/v1-create-a-branch) |
|
||||
| | | [Delete a branch](/docs/reference/api/v1-delete-a-branch) |
|
||||
| | | [Diff a branch](/docs/reference/api/v1-diff-a-branch) |
|
||||
| | | [Disable preview branching](/docs/reference/api/v1-disable-preview-branching) |
|
||||
| | | [Merge a branch](/docs/reference/api/v1-merge-a-branch) |
|
||||
| | | [Push a branch](/docs/reference/api/v1-push-a-branch) |
|
||||
| | | [Reset a branch](/docs/reference/api/v1-reset-a-branch) |
|
||||
| | | [Restore a branch](/docs/reference/api/v1-restore-a-branch) |
|
||||
| | | [Update a branch config](/docs/reference/api/v1-update-a-branch-config) |
|
||||
| Custom Domains | Read | [Get hostname config](/docs/reference/api/v1-get-hostname-config) |
|
||||
| | Read-write | [Activate custom hostname](/docs/reference/api/v1-activate-custom-hostname) |
|
||||
| | | Delete hostname config |
|
||||
| | | [Update hostname config](/docs/reference/api/v1-update-hostname-config) |
|
||||
| | | [Verify DNS config](/docs/reference/api/v1-verify-dns-config) |
|
||||
| Add-ons | Read | [List project add-ons](/docs/reference/api/v1-list-project-addons) |
|
||||
| | Read-write | [Apply project add-on](/docs/reference/api/v1-apply-project-addon) |
|
||||
| | | [Remove project add-on](/docs/reference/api/v1-remove-project-addon) |
|
||||
| Disk Config | Read | [Get database disk](/docs/reference/api/v1-get-database-disk) |
|
||||
| | | [Get disk utilization](/docs/reference/api/v1-get-disk-utilization) |
|
||||
| | | [Get project disk auto-scaling config](/docs/reference/api/v1-get-project-disk-autoscale-config) |
|
||||
| | Read-write | [Modify database disk](/docs/reference/api/v1-modify-database-disk) |
|
||||
| Read Replicas | Read-write | [Remove a read replica](/docs/reference/api/v1-remove-a-read-replica) |
|
||||
| | | [Setup a read replica](/docs/reference/api/v1-setup-a-read-replica) |
|
||||
| Vanity Subdomain | Read | [Get vanity subdomain config](/docs/reference/api/v1-get-vanity-subdomain-config) |
|
||||
| | Read-write | [Activate vanity subdomain config](/docs/reference/api/v1-activate-vanity-subdomain-config) |
|
||||
| | | [Check vanity subdomain availability](/docs/reference/api/v1-check-vanity-subdomain-availability) |
|
||||
| | | [Deactivate vanity subdomain config](/docs/reference/api/v1-deactivate-vanity-subdomain-config) |
|
||||
| **Account and organization** | | |
|
||||
| Organizations | Read | [List all organizations](/docs/reference/api/v1-list-all-organizations) |
|
||||
| | Read-write | [Create an organization](/docs/reference/api/v1-create-an-organization) |
|
||||
| Projects (account-wide) | Read | [List all projects](/docs/reference/api/v1-list-all-projects) |
|
||||
| SQL Snippets (account-wide) | Read | [Get a snippet](/docs/reference/api/v1-get-a-snippet) |
|
||||
| | | [List all snippets](/docs/reference/api/v1-list-all-snippets) |
|
||||
| Organization Settings | Read | [Get an organization](/docs/reference/api/v1-get-an-organization) |
|
||||
| | | [Get organization entitlements](/docs/reference/api/v1-get-organization-entitlements) |
|
||||
| | Read-write | [Assign organization member role](/docs/reference/api/v2-assign-organization-member-role) |
|
||||
| | | [OAuth authorize project claim](/docs/reference/api/v1-oauth-authorize-project-claim)[^2] |
|
||||
| | | [Transfer a project](/docs/reference/api/v2-transfer-a-project) |
|
||||
| Organization Members | Read | [List organization members](/docs/reference/api/v1-list-organization-members) |
|
||||
| | | [List organization members](/docs/reference/api/v2-list-organization-members) |
|
||||
| | | [List organization roles](/docs/reference/api/v2-list-organization-roles) |
|
||||
| | Read-write | [Create organization invitations](/docs/reference/api/v2-create-organization-invitations) |
|
||||
| | | [Delete organization invitations](/docs/reference/api/v2-delete-organization-invitations) |
|
||||
| Organization Projects | Read | [Get all projects for organization](/docs/reference/api/v1-get-all-projects-for-organization) |
|
||||
| | | [List organization GitHub connections](/docs/reference/api/v2-list-organization-github-connections) |
|
||||
| | | [List organization projects](/docs/reference/api/v2-list-organization-projects) |
|
||||
| | Read-write | [Create a project](/docs/reference/api/v1-create-a-project) |
|
||||
| Platform Webhooks (organization) | Read | [Get delivery](/docs/reference/api/v2-organizations-slug-webhooks-deliveries-id-get) |
|
||||
| | | [Get endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-get) |
|
||||
| | | [List deliveries](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-deliveries-get) |
|
||||
| | | [List endpoints](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-get) |
|
||||
| | Read-write | [Create endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-post) |
|
||||
| | | [Delete all endpoints](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-delete) |
|
||||
| | | [Delete endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-delete) |
|
||||
| | | [Retry delivery](/docs/reference/api/v2-organizations-slug-webhooks-deliveries-id-retry-post) |
|
||||
| | | [Send test event](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-test-post) |
|
||||
| | | [Update endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-patch) |
|
||||
|
||||
[^1]: Requires **Project Settings** (Read) and **Database** (Read).
|
||||
|
||||
[^2]: Requires **Organization Settings** (Read-write) and **Project Settings** (Read-write).
|
||||
|
||||
[^3]: Requires **Auth Config** (Read-write) and **Project Settings** (Read-write).
|
||||
|
||||
[^4]: Requires **Project Settings** (Read-write) and **Database** (Read-write).
|
||||
|
||||
[^5]: Requires **Database Config** (Read), **Database** (Read), **SSL Enforcement** (Read), **Network Restrictions** (Read), **Auth Config** (Read), **Data API Config** (Read), **Realtime Config** (Read), and **Storage Config** (Read).
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: 'Personal Access Tokens'
|
||||
description: 'Scope personal access tokens to specific organizations, projects, and permissions'
|
||||
---
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Scoped personal access tokens are in **public alpha** and rolling out gradually. If you don't see the option to choose permissions when creating a token, your account doesn't have access yet. File a [support ticket](https://supabase.help) to get early access.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Personal access tokens (PATs) authenticate you to the [Management API](/docs/reference/api/introduction) and the tools built on it, like the Supabase CLI and the [MCP server](/docs/guides/ai-tools/mcp). They come in two flavors:
|
||||
|
||||
- **Classic tokens** carry your account's full access. That means every permission, on every organization and every project you belong to today, and on every one you create or join in the future. A classic token created a year ago can touch a project you created today.
|
||||
- **Scoped tokens** carry only the organizations, projects, and permissions you choose. For example: read one project's database and view its logs, with no access to billing or organization settings.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
We recommend scoped tokens for everything, especially AI agents, automation scripts, and CI environments. If a token leaks, the blast radius stays small.
|
||||
|
||||
</Admonition>
|
||||
|
||||
To use scoped personal access tokens you need a Supabase account with a role on the organization or project you want the token to reach. A scoped personal access token's permissions only ever narrow what your account can already do. They never grant more. If your role doesn't include a permission (see [Access Control](/docs/guides/platform/access-control)), granting that permission to a token has no effect: the token still can't do it.
|
||||
|
||||
You create scoped tokens the same way as classic tokens, from your [access tokens](/dashboard/account/tokens) settings. Choose which permissions to grant during creation instead of leaving the token with full access.
|
||||
|
||||
## Create and use a scoped personal access token
|
||||
|
||||
This example creates a token that can only read one project's settings, then calls an endpoint outside that scope. Both calls are reads available to every organization role, including Read-Only, so anyone can run it.
|
||||
|
||||
1. Go to your [access tokens](/dashboard/account/tokens) settings and generate a new token.
|
||||
2. While creating it, scope the token to one project and grant only the **Project Settings** permission with **Read** access.
|
||||
3. Copy the token (scoped personal access tokens start with `sbp_fc`) and use it against the Management API, replacing `your-project-ref` with the project's ref:
|
||||
|
||||
```bash
|
||||
export SUPABASE_ACCESS_TOKEN="sbp_fc..."
|
||||
|
||||
# Allowed: Project Settings Read unlocks this endpoint
|
||||
curl "https://api.supabase.com/v1/projects/your-project-ref" \
|
||||
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
|
||||
# Returns the project's details
|
||||
|
||||
# Denied: this endpoint needs the Database permission, which the token lacks
|
||||
curl -i "https://api.supabase.com/v1/projects/your-project-ref/types/typescript" \
|
||||
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
|
||||
# Returns HTTP 403
|
||||
```
|
||||
|
||||
The tables below list which permission unlocks which endpoints, and which permission each [MCP tool](#mcp-tools) requires, so you can grant exactly what a workflow needs.
|
||||
|
||||
## Permission scopes
|
||||
|
||||
Each permission controls read or read-write access to one resource, for example **Database**, **Edge Functions**, or **Production Branches**. The permissions and names below match those shown when creating a scoped personal access token. Permissions that currently unlock neither a public Management API endpoint nor an MCP tool are omitted. A few endpoints need more than one permission. The footnotes call these out.
|
||||
|
||||
<$Partial path="access-control/scoped_pat_permissions.mdx" />
|
||||
|
||||
## MCP tools
|
||||
|
||||
A scoped personal access token used to authenticate the [MCP server](/docs/guides/ai-tools/mcp) can only call the tools its granted permissions unlock. See [Available tools](/docs/guides/ai-tools/mcp#available-tools) for what each tool does.
|
||||
|
||||
<$Partial path="access-control/scoped_pat_mcp_tools.mdx" />
|
||||
+18
-3
@@ -1,7 +1,7 @@
|
||||
REPO_DIR=$(shell pwd)
|
||||
GENERATOR_DIR=../../../packages/generator
|
||||
|
||||
.PHONY: run download download.api.v1 download.storage.v1 download.tsdoc.v2 download.server.v1 transform dereference.api.v1 dereference.auth.v1 dereference.storage.v0 generate generate.sections.api.v1 format
|
||||
.PHONY: run download download.api.v1 download.mcp-tools-permissions download.storage.v1 download.tsdoc.v2 download.server.v1 transform dereference.api.v1 dereference.auth.v1 dereference.storage.v0 generate generate.sections.api.v1 generate.partials.access-control format
|
||||
|
||||
run: download transform generate format
|
||||
|
||||
@@ -11,12 +11,17 @@ run: download transform generate format
|
||||
###############################################################################
|
||||
# comment out download.auth.v1 temporarily, we're manually creating the file
|
||||
# download: download.api.v1 download.auth.v1 download.storage.v1 download.tsdoc.v2
|
||||
download: download.api.v1 download.storage.v1 download.tsdoc.v2 download.server.v1
|
||||
download: download.api.v1 download.mcp-tools-permissions download.storage.v1 download.tsdoc.v2 download.server.v1
|
||||
|
||||
download.api.v1:
|
||||
curl -sS https://api.supabase.com/api/v1-json > $(REPO_DIR)/api_v1_openapi.json
|
||||
curl -sS https://api.supabase.com/api/v2-json > $(REPO_DIR)/api_v2_openapi.json
|
||||
|
||||
# Download tool permissions from the public Management API projection of its enforcement rules.
|
||||
download.mcp-tools-permissions:
|
||||
curl -sSf https://api.supabase.com/platform/mcp-tools-permissions > $(REPO_DIR)/mcp_tools_permissions.json
|
||||
npx prettier --write $(REPO_DIR)/mcp_tools_permissions.json
|
||||
|
||||
# This flow needs to be updated, so we'l comment out for the moment
|
||||
# Manual flow for now:
|
||||
# — get swagger.json (https://supabase.github.io/gotrue/swagger.json)
|
||||
@@ -81,7 +86,7 @@ dereference.analytics.v0:
|
||||
###############################################################################
|
||||
# Generate sections from OpenAPI 3.0
|
||||
###############################################################################
|
||||
generate: generate.sections.api.v1
|
||||
generate: generate.sections.api.v1 generate.partials.access-control
|
||||
|
||||
generate.sections.api.v1:
|
||||
npx tsx $(REPO_DIR)/sections/generateMgmtApiSections.cts \
|
||||
@@ -89,6 +94,16 @@ generate.sections.api.v1:
|
||||
$(REPO_DIR)/transforms/api_v2_openapi_deparsed.json \
|
||||
$(REPO_DIR)/common-api-sections.json
|
||||
|
||||
# Generate PAT guide tables from Management API inputs and Studio's shared permission catalog.
|
||||
generate.partials.access-control:
|
||||
npx tsx $(REPO_DIR)/sections/generateAccessControlPartials.mts \
|
||||
$(REPO_DIR)/transforms/api_v1_openapi_deparsed.json \
|
||||
$(REPO_DIR)/transforms/api_v2_openapi_deparsed.json \
|
||||
$(REPO_DIR)/mcp_tools_permissions.json \
|
||||
$(REPO_DIR)/../content/_partials/access-control/scoped_pat_permissions.mdx \
|
||||
$(REPO_DIR)/../content/_partials/access-control/scoped_pat_mcp_tools.mdx
|
||||
npx prettier --write $(REPO_DIR)/../content/_partials/access-control/*.mdx
|
||||
|
||||
###############################################################################
|
||||
# Validate OpenAPI 3.0
|
||||
###############################################################################
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"list_organizations": [["organizations_read"]],
|
||||
"get_organization": [["organization_admin_read"]],
|
||||
"list_projects": [["projects_read"]],
|
||||
"get_project": [["project_admin_read"]],
|
||||
"create_project": [["organization_projects_create"]],
|
||||
"pause_project": [["project_admin_write"]],
|
||||
"restore_project": [["project_admin_write"]],
|
||||
"list_branches": [["branching_development_read"], ["branching_production_read"]],
|
||||
"create_branch": [["branching_development_create"], ["branching_production_create"]],
|
||||
"delete_branch": [["branching_production_delete"], ["branching_development_delete"]],
|
||||
"merge_branch": [["branching_production_write"], ["branching_development_write"]],
|
||||
"reset_branch": [["branching_production_write"], ["branching_development_write"]],
|
||||
"rebase_branch": [["branching_production_write"], ["branching_development_write"]],
|
||||
"execute_sql": [["database_read"]],
|
||||
"list_tables": [["database_read"]],
|
||||
"list_extensions": [["database_read"]],
|
||||
"list_migrations": [["database_migrations_read"]],
|
||||
"apply_migration": [["database_migrations_write"]],
|
||||
"get_logs": [["analytics_logs_read"]],
|
||||
"query_logs": [["analytics_logs_read"]],
|
||||
"get_advisors": [["advisors_read"]],
|
||||
"generate_typescript_types": [["database_read"]],
|
||||
"get_project_url": [["project_admin_read"]],
|
||||
"get_publishable_keys": [["api_gateway_keys_read"]],
|
||||
"list_edge_functions": [["edge_functions_read"]],
|
||||
"get_edge_function": [["edge_functions_read"]],
|
||||
"deploy_edge_function": [["edge_functions_write"]],
|
||||
"get_storage_config": [["storage_config_read"]],
|
||||
"update_storage_config": [["storage_config_write"]],
|
||||
"list_storage_buckets": [["storage_read"]],
|
||||
"get_cost": [["organization_admin_read", "projects_read"]],
|
||||
"confirm_cost": [[]],
|
||||
"search_docs": [[]]
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import fs from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import path from 'node:path'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const { PERMISSION_CATALOG_BY_CATEGORY, PERMISSION_MODE_LABEL } =
|
||||
require('shared-data/scoped-access-token-permissions') as typeof import('shared-data/scoped-access-token-permissions')
|
||||
|
||||
type ScopeGroupAlternatives = string[][]
|
||||
type McpMap = Record<string, ScopeGroupAlternatives>
|
||||
|
||||
type Operation = {
|
||||
operationId?: string
|
||||
summary?: string
|
||||
'x-fga-permissions'?: ScopeGroupAlternatives
|
||||
'x-internal'?: boolean
|
||||
}
|
||||
|
||||
type Endpoint = {
|
||||
operationId: string
|
||||
label: string
|
||||
groups: ScopeGroupAlternatives
|
||||
}
|
||||
|
||||
type PermissionRow = {
|
||||
resource: string
|
||||
access: string
|
||||
category: string
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
const GENERATED_NOTICE =
|
||||
'{/* Generated by `make -C apps/docs/spec generate.partials.access-control`. Do not hand-edit; see supabase/platform#37175 and apps/docs/spec/Makefile. */}\n'
|
||||
|
||||
const WORD_FIXES: Record<string, string> = {
|
||||
api: 'API',
|
||||
sso: 'SSO',
|
||||
tpa: 'TPA',
|
||||
pitr: 'PITR',
|
||||
dns: 'DNS',
|
||||
jit: 'JIT',
|
||||
ssl: 'SSL',
|
||||
oauth: 'OAuth',
|
||||
github: 'GitHub',
|
||||
postgrest: 'PostgREST',
|
||||
pgbouncer: 'PgBouncer',
|
||||
readonly: 'read-only',
|
||||
addon: 'add-on',
|
||||
addons: 'add-ons',
|
||||
autoscale: 'auto-scaling',
|
||||
}
|
||||
|
||||
const readJson = (filePath: string) => JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
||||
|
||||
function endpointLabel(operationId: string) {
|
||||
const words = operationId
|
||||
.replace(/^v\d+-?/, '')
|
||||
.split('-')
|
||||
.filter(Boolean)
|
||||
.map((word) => WORD_FIXES[word] ?? word)
|
||||
.join(' ')
|
||||
return words.charAt(0).toUpperCase() + words.slice(1)
|
||||
}
|
||||
|
||||
const permissionRows: PermissionRow[] = PERMISSION_CATALOG_BY_CATEGORY.flatMap((category) =>
|
||||
category.entries.flatMap((entry) => [
|
||||
...(entry.readScopes.length > 0
|
||||
? [
|
||||
{
|
||||
resource: entry.name,
|
||||
access: PERMISSION_MODE_LABEL.read,
|
||||
category: category.name,
|
||||
scopes: entry.readScopes,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(entry.writeScopes.length > 0
|
||||
? [
|
||||
{
|
||||
resource: entry.name,
|
||||
access: PERMISSION_MODE_LABEL.readwrite,
|
||||
category: category.name,
|
||||
scopes: entry.writeScopes,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
])
|
||||
)
|
||||
|
||||
const rowByScope = new Map(permissionRows.flatMap((row) => row.scopes.map((scope) => [scope, row])))
|
||||
|
||||
// Workers permissions are present in the API spec but are not live for scoped PATs yet.
|
||||
const EXCLUDED_SCOPES = new Set(['workers_read', 'workers_write'])
|
||||
|
||||
// The public v2 webhook operations currently omit x-fga-permissions from the OpenAPI projection.
|
||||
// Keep this fallback narrow so the generated table can still link those endpoints, and fail below
|
||||
// if any other public operation has not been classified for the scoped-PAT table.
|
||||
const WEBHOOK_PERMISSION_SCOPES = [
|
||||
{
|
||||
routePrefix: '/v2/projects/{ref}/webhooks/',
|
||||
read: 'platform_webhooks_projects_read',
|
||||
write: 'platform_webhooks_projects_write',
|
||||
},
|
||||
{
|
||||
routePrefix: '/v2/organizations/{slug}/webhooks/',
|
||||
read: 'platform_webhooks_organization_read',
|
||||
write: 'platform_webhooks_organization_write',
|
||||
},
|
||||
]
|
||||
|
||||
// These public operations sit outside the scoped-PAT permission table.
|
||||
const OPERATIONS_OUTSIDE_SCOPED_PAT_TABLE = new Set([
|
||||
'v1-accept-invite-external-jit-access',
|
||||
'v1-authorize-user',
|
||||
'v1-exchange-oauth-token',
|
||||
'v1-get-available-regions',
|
||||
'v1-get-profile',
|
||||
'v1-revoke-token',
|
||||
])
|
||||
|
||||
function webhookPermissionGroups(
|
||||
route: string,
|
||||
method: string
|
||||
): ScopeGroupAlternatives | undefined {
|
||||
const scopes = WEBHOOK_PERMISSION_SCOPES.find(({ routePrefix }) => route.startsWith(routePrefix))
|
||||
if (!scopes) return undefined
|
||||
|
||||
const access = ['get', 'head'].includes(method)
|
||||
? 'read'
|
||||
: ['post', 'put', 'patch', 'delete'].includes(method)
|
||||
? 'write'
|
||||
: undefined
|
||||
if (!access) return undefined
|
||||
return [[scopes[access]]]
|
||||
}
|
||||
|
||||
function knownGroups(groups: ScopeGroupAlternatives, missing: Set<string>) {
|
||||
return groups.filter((group) => {
|
||||
if (group.some((scope) => EXCLUDED_SCOPES.has(scope))) return false
|
||||
const unknown = group.filter((scope) => !rowByScope.has(scope))
|
||||
unknown.forEach((scope) => missing.add(scope))
|
||||
return unknown.length === 0
|
||||
})
|
||||
}
|
||||
|
||||
function joinList(items: string[]) {
|
||||
if (items.length < 2) return items[0] ?? ''
|
||||
if (items.length === 2) return `${items[0]} and ${items[1]}`
|
||||
return `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`
|
||||
}
|
||||
|
||||
function formatRequirement(groups: ScopeGroupAlternatives) {
|
||||
const alternatives = Array.from(
|
||||
new Set(
|
||||
groups.map((group) =>
|
||||
joinList(
|
||||
Array.from(
|
||||
new Set(
|
||||
group.map((scope) => {
|
||||
const row = rowByScope.get(scope)!
|
||||
return `**${row.resource}** (${row.access})`
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
return alternatives.join(alternatives.some((item) => item.includes(' and ')) ? ', or ' : ' or ')
|
||||
}
|
||||
|
||||
function missingScopesNotice(missing: Set<string>) {
|
||||
if (missing.size === 0) return []
|
||||
return [
|
||||
'',
|
||||
`{/* Not documented, missing from the shared permission catalog ` +
|
||||
`(packages/shared-data/scoped-access-token-permissions.ts): ${[...missing].sort().join(', ')} */}`,
|
||||
]
|
||||
}
|
||||
|
||||
function collectEndpoints(specPaths: string[]) {
|
||||
const endpoints = new Map<string, Endpoint>()
|
||||
const unclassifiedOperations: string[] = []
|
||||
|
||||
for (const specPath of specPaths) {
|
||||
const spec = readJson(specPath)
|
||||
for (const [route, methods] of Object.entries<Record<string, Operation>>(spec.paths ?? {})) {
|
||||
for (const [method, operation] of Object.entries(methods)) {
|
||||
if (!operation?.operationId || operation['x-internal']) continue
|
||||
|
||||
const key = `${method.toUpperCase()} ${route}`
|
||||
const fallbackGroups = webhookPermissionGroups(route, method)
|
||||
const groups = operation['x-fga-permissions'] ?? fallbackGroups ?? []
|
||||
if (groups.length === 0) {
|
||||
if (!OPERATIONS_OUTSIDE_SCOPED_PAT_TABLE.has(operation.operationId)) {
|
||||
unclassifiedOperations.push(`${key} (${operation.operationId})`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
endpoints.set(key, {
|
||||
operationId: operation.operationId,
|
||||
label: fallbackGroups
|
||||
? (operation.summary ?? endpointLabel(operation.operationId))
|
||||
: endpointLabel(operation.operationId),
|
||||
groups,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unclassifiedOperations.length > 0) {
|
||||
throw new Error(
|
||||
`Public Management API operations are not classified for the scoped-PAT table:\n${unclassifiedOperations.join('\n')}`
|
||||
)
|
||||
}
|
||||
|
||||
return [...endpoints.values()]
|
||||
}
|
||||
|
||||
function generatePermissionsPartial(specPaths: string[], tools: McpMap, outputPath: string) {
|
||||
const missing = new Set<string>()
|
||||
const endpoints = collectEndpoints(specPaths).map((endpoint) => ({
|
||||
...endpoint,
|
||||
groups: knownGroups(endpoint.groups, missing),
|
||||
}))
|
||||
const mcpToolScopes = new Set(
|
||||
Object.values(tools).flatMap((groups) => knownGroups(groups, missing).flat())
|
||||
)
|
||||
const footnotes = new Map<string, string>()
|
||||
const lines = [
|
||||
GENERATED_NOTICE,
|
||||
'| Permission | Access required | Management API endpoint |',
|
||||
'| ---------- | --------------- | ----------------------- |',
|
||||
]
|
||||
let previousCategory = ''
|
||||
let previousResource = ''
|
||||
|
||||
for (const row of permissionRows) {
|
||||
const rowScopes = new Set(row.scopes)
|
||||
const rowEndpoints = endpoints
|
||||
.filter((endpoint) =>
|
||||
endpoint.groups.some((group) => group.some((scope) => rowScopes.has(scope)))
|
||||
)
|
||||
.sort((a, b) => a.label.localeCompare(b.label) || a.operationId.localeCompare(b.operationId))
|
||||
|
||||
if (rowEndpoints.length === 0 && !row.scopes.some((scope) => mcpToolScopes.has(scope))) continue
|
||||
|
||||
if (row.category !== previousCategory) {
|
||||
lines.push(`| **${row.category}** | | |`)
|
||||
previousCategory = row.category
|
||||
previousResource = ''
|
||||
}
|
||||
|
||||
const permissionCell = row.resource === previousResource ? '' : row.resource
|
||||
previousResource = row.resource
|
||||
|
||||
if (rowEndpoints.length === 0) {
|
||||
lines.push(`| ${permissionCell} | ${row.access} | No public Management API endpoints |`)
|
||||
continue
|
||||
}
|
||||
|
||||
rowEndpoints.forEach((endpoint, index) => {
|
||||
const label = endpoint.label.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\s+/g, ' ')
|
||||
const link = /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(endpoint.operationId)
|
||||
? `[${label}](/docs/reference/api/${endpoint.operationId})`
|
||||
: label
|
||||
const unlocksAlone = endpoint.groups.some((group) =>
|
||||
group.every((scope) => rowScopes.has(scope))
|
||||
)
|
||||
let requirement = ''
|
||||
if (!unlocksAlone) {
|
||||
const text = `Requires ${formatRequirement(endpoint.groups)}.`
|
||||
const id = footnotes.get(text) ?? String(footnotes.size + 1)
|
||||
footnotes.set(text, id)
|
||||
requirement = `[^${id}]`
|
||||
}
|
||||
lines.push(
|
||||
`| ${index === 0 ? permissionCell : ''} | ${index === 0 ? row.access : ''} | ${link}${requirement} |`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const definitions = Array.from(footnotes, ([text, id]) => `[^${id}]: ${text}`)
|
||||
writeOutput(outputPath, [
|
||||
...lines,
|
||||
...(definitions.length > 0 ? ['', ...definitions] : []),
|
||||
...missingScopesNotice(missing),
|
||||
'',
|
||||
])
|
||||
}
|
||||
|
||||
function generateMcpToolsPartial(tools: McpMap, outputPath: string) {
|
||||
const missing = new Set<string>()
|
||||
const rows = Object.entries(tools)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([tool, groups]) => {
|
||||
const publishable = knownGroups(groups, missing)
|
||||
const requirement = publishable.some((group) => group.length === 0)
|
||||
? 'None (always available)'
|
||||
: publishable.length === 0
|
||||
? 'Not available to scoped personal access tokens'
|
||||
: formatRequirement(publishable)
|
||||
return `| \`${tool}\` | ${requirement} |`
|
||||
})
|
||||
|
||||
writeOutput(outputPath, [
|
||||
GENERATED_NOTICE,
|
||||
'| MCP tool | Required permission |',
|
||||
'| -------- | ------------------- |',
|
||||
...rows,
|
||||
...missingScopesNotice(missing),
|
||||
'',
|
||||
])
|
||||
}
|
||||
|
||||
function writeOutput(outputPath: string, lines: string[]) {
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
|
||||
fs.writeFileSync(outputPath, lines.join('\n'), 'utf8')
|
||||
console.log(`Wrote ${outputPath}`)
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2).map((arg) => path.resolve(arg))
|
||||
if (args.length !== 5) {
|
||||
console.error(
|
||||
'Usage: generateAccessControlPartials.mts <api-v1.json> <api-v2.json> ' +
|
||||
'<mcp-tools.json> <permissions-output.mdx> <mcp-tools-output.mdx>'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const tools: McpMap = readJson(args[2])
|
||||
generatePermissionsPartial(args.slice(0, 2), tools, args[3])
|
||||
generateMcpToolsPartial(tools, args[4])
|
||||
@@ -37,6 +37,7 @@ Studio is migrating from the Next.js pages router (`pages/**`) to TanStack Start
|
||||
- **Telemetry** — `useTrack()` from `lib/telemetry/track`; event types live in `packages/common/telemetry-constants.ts`.
|
||||
- **Tests** — default to including relevant tests with any change: a couple of unit tests for extracted logic, component tests for UI behavior, E2E only when the scope demands it (`studio-testing` has the decision tree). Not every PR needs them, but "no tests" should be a considered choice, not the default. Tooling: vitest + MSW; component tests use `customRender` + `addAPIMock` from `tests/lib/`; unhandled network requests fail tests. Don't `vi.mock('@/data/...')`.
|
||||
- **Shortcuts** — use the registry in `state/shortcuts/` and `components/ui/Shortcut*.tsx`; keep `G then …` chords for navigation; no one-off keyboard listeners.
|
||||
- **Scoped PAT catalog** — `packages/shared-data/scoped-access-token-permissions.ts` feeds Studio and the generated Personal Access Tokens guide. After changing it, run `make -C apps/docs/spec generate.partials.access-control`; Docs Tests rejects stale tables.
|
||||
- **Reuse first** — before writing a new hook or helper, search for an existing one (`hooks/`, `lib/`, `packages/common`, `packages/ui-patterns`). If you do need a new one, make it as reusable as possible: general naming, no page-specific coupling, placed where other callers can find it.
|
||||
- Co-locate sub-components with their parent; avoid barrel re-export files.
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { permissions } from '@supabase/shared-types'
|
||||
import { components } from 'api-types'
|
||||
import {
|
||||
getAction,
|
||||
getResource,
|
||||
PERMISSION_CATALOG,
|
||||
type FgaAction,
|
||||
} from 'shared-data/scoped-access-token-permissions'
|
||||
|
||||
export type ScopedAccessTokenPermission =
|
||||
components['schemas']['CreateScopedAccessTokenBody']['permissions'][number]
|
||||
@@ -26,25 +32,12 @@ export const EXPIRES_AT_OPTIONS = {
|
||||
|
||||
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
|
||||
action: FgaAction
|
||||
id: string
|
||||
title: string
|
||||
}> = []
|
||||
|
||||
for (const [scope, scopePerms] of Object.entries(FGA)) {
|
||||
@@ -54,7 +47,6 @@ const buildPermissionList = () => {
|
||||
resource: getResource(key),
|
||||
action: getAction(key),
|
||||
id: perm.id,
|
||||
title: perm.title,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -64,20 +56,12 @@ const buildPermissionList = () => {
|
||||
|
||||
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())
|
||||
})()
|
||||
/**
|
||||
* Resources shown in token permission summaries (e.g. the post-creation banner).
|
||||
* Titles come from the shared permission catalog so they match the creation form
|
||||
* and the generated docs tables.
|
||||
*/
|
||||
export const ACCESS_TOKEN_RESOURCES = PERMISSION_CATALOG.map((entry) => ({
|
||||
resource: entry.key,
|
||||
title: entry.name,
|
||||
}))
|
||||
+24
-561
@@ -1,563 +1,32 @@
|
||||
import { permissions } from '@supabase/shared-types'
|
||||
import {
|
||||
getCatalogEntry,
|
||||
PERMISSION_CATALOG,
|
||||
PERMISSION_CATALOG_BY_CATEGORY,
|
||||
PERMISSION_MODE_LABEL,
|
||||
type PermissionCatalogEntry,
|
||||
type PermissionCategoryKey,
|
||||
type PermissionMode,
|
||||
type RiskLevel,
|
||||
} from 'shared-data/scoped-access-token-permissions'
|
||||
|
||||
import type { ScopedAccessTokenPermission } from './AccessToken.constants'
|
||||
|
||||
export {
|
||||
getCatalogEntry,
|
||||
PERMISSION_CATALOG,
|
||||
PERMISSION_CATALOG_BY_CATEGORY,
|
||||
PERMISSION_MODE_LABEL,
|
||||
}
|
||||
export type { PermissionCatalogEntry, PermissionMode, RiskLevel }
|
||||
|
||||
/**
|
||||
* Data model for the scoped access-token creation flow.
|
||||
* Selection model for the scoped personal access token creation flow: none/read/readwrite modes per
|
||||
* catalog entry, and the conversions between a selection and concrete FGA scope ids.
|
||||
*
|
||||
* The real permission scopes come from `@supabase/shared-types` (`FgaPermissions`). Those scopes
|
||||
* carry no category or risk metadata, so this file layers editable presentation data on top:
|
||||
* - PERMISSION_CATEGORIES groups every scope into one of five UI categories.
|
||||
* - RESOURCE_METADATA assigns each resource a display name, description, category and risk.
|
||||
*
|
||||
* TODO(product): the risk levels, reasons and "Allows" copy below are proposed defaults — review
|
||||
* and adjust. Where a resource has no explicit metadata entry we fall back to a heuristic.
|
||||
* The catalog itself (resources, categories, risk metadata, scopes) lives in
|
||||
* shared-data/scoped-access-token-permissions.ts so the docs generator can consume it too.
|
||||
*/
|
||||
|
||||
const FGA = permissions.FgaPermissions
|
||||
|
||||
export type PermissionMode = 'none' | 'read' | 'readwrite'
|
||||
export type RiskLevel = 'low' | 'medium' | 'high'
|
||||
export type PermissionCategoryKey = 'account' | 'project' | 'database' | 'appsvc' | 'infra'
|
||||
|
||||
export interface PermissionCategory {
|
||||
key: PermissionCategoryKey
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
/** Display order matches the accordion, where every category starts collapsed. */
|
||||
export const PERMISSION_CATEGORIES: PermissionCategory[] = [
|
||||
{
|
||||
key: 'project',
|
||||
name: 'Project',
|
||||
description: 'Core project visibility, settings, and diagnostics.',
|
||||
},
|
||||
{
|
||||
key: 'database',
|
||||
name: 'Database',
|
||||
description: 'SQL access, migrations, backups, and data operations.',
|
||||
},
|
||||
{
|
||||
key: 'appsvc',
|
||||
name: 'Application Services',
|
||||
description: 'Auth, storage, realtime, edge functions, and service configuration.',
|
||||
},
|
||||
{
|
||||
key: 'infra',
|
||||
name: 'Infrastructure & Delivery',
|
||||
description: 'Branch automation, domains, add-ons, and network.',
|
||||
},
|
||||
{
|
||||
key: 'account',
|
||||
name: 'Account & Organization',
|
||||
description: 'Account-wide and organization-level access that spans projects.',
|
||||
},
|
||||
]
|
||||
|
||||
interface ResourceMeta {
|
||||
category: PermissionCategoryKey
|
||||
name: string
|
||||
description: string
|
||||
risk: RiskLevel
|
||||
riskReason: string
|
||||
allowsRead?: string[]
|
||||
allowsWrite?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-resource presentation metadata, keyed by the derived `scope:resource` key (see
|
||||
* AccessToken.constants → ACCESS_TOKEN_RESOURCES). Every resource returned from FgaPermissions
|
||||
* should have an entry; RESOURCE_METADATA_FALLBACK covers anything that slips through.
|
||||
*/
|
||||
const RESOURCE_METADATA: Record<string, ResourceMeta> = {
|
||||
// --- Account & Organization ---
|
||||
'user:organizations': {
|
||||
category: 'account',
|
||||
name: 'Organizations',
|
||||
description: 'Organizations you belong to.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can create new organizations under your account.',
|
||||
allowsRead: ['List your organizations'],
|
||||
allowsWrite: ['Create organizations'],
|
||||
},
|
||||
'user:projects': {
|
||||
category: 'account',
|
||||
name: 'Projects (account-wide)',
|
||||
description: 'Projects across all your organizations.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-only listing of the projects you can access.',
|
||||
allowsRead: ['List your projects'],
|
||||
},
|
||||
'user:snippets': {
|
||||
category: 'account',
|
||||
name: 'SQL Snippets (account-wide)',
|
||||
description: 'Saved SQL snippets across your account.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-only access to your saved snippets.',
|
||||
allowsRead: ['Read your SQL snippets'],
|
||||
},
|
||||
'organization:admin': {
|
||||
category: 'account',
|
||||
name: 'Organization Settings',
|
||||
description: 'Organization settings and project transfers.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read-write grants elevated access to organization settings and project transfers.',
|
||||
allowsRead: ['Read organization settings'],
|
||||
allowsWrite: ['Manage organization settings', 'Transfer projects'],
|
||||
},
|
||||
'organization:members': {
|
||||
category: 'account',
|
||||
name: 'Organization Members',
|
||||
description: 'Members and roles within the organization.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read-write can add or remove members and change roles across your organization.',
|
||||
allowsRead: ['Read organization members'],
|
||||
allowsWrite: ['Add or remove members', 'Change member roles'],
|
||||
},
|
||||
'organization:projects': {
|
||||
category: 'account',
|
||||
name: 'Organization Projects',
|
||||
description: 'Projects within the organization.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can create new projects in the organization.',
|
||||
allowsRead: ['List organization projects'],
|
||||
allowsWrite: ['Create organization projects'],
|
||||
},
|
||||
|
||||
// --- Project ---
|
||||
'project:admin': {
|
||||
category: 'project',
|
||||
name: 'Project Settings',
|
||||
description: 'Project metadata and settings.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read-write grants elevated access to change project settings and configuration.',
|
||||
allowsRead: ['Read project metadata'],
|
||||
allowsWrite: ['Update project settings'],
|
||||
},
|
||||
'project:action_runs': {
|
||||
category: 'project',
|
||||
name: 'Action Runs',
|
||||
description: 'Project action run status and logs.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can trigger action runs that execute project workflows.',
|
||||
allowsRead: ['Read action run status', 'Read run logs'],
|
||||
allowsWrite: ['Trigger action runs'],
|
||||
},
|
||||
'project:advisors': {
|
||||
category: 'project',
|
||||
name: 'Advisors',
|
||||
description: 'Security and performance advisor results.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-only access to advisor findings — no changes possible.',
|
||||
allowsRead: ['Read security advisors', 'Read performance advisors'],
|
||||
},
|
||||
'project:analytics_logs': {
|
||||
category: 'project',
|
||||
name: 'Logs',
|
||||
description: 'Operational logs and log analytics.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-only access to project logs.',
|
||||
allowsRead: ['Read project logs'],
|
||||
},
|
||||
'project:analytics_usage': {
|
||||
category: 'project',
|
||||
name: 'Usage Analytics',
|
||||
description: 'Project usage and analytics data.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-only access to usage analytics.',
|
||||
allowsRead: ['Read usage analytics'],
|
||||
},
|
||||
'project:snippets': {
|
||||
category: 'project',
|
||||
name: 'SQL Snippets',
|
||||
description: 'Saved SQL snippets for the project.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-write can create and edit saved SQL snippets.',
|
||||
allowsRead: ['Read project SQL snippets'],
|
||||
allowsWrite: ['Manage project SQL snippets'],
|
||||
},
|
||||
|
||||
// --- Database ---
|
||||
'project:database': {
|
||||
category: 'database',
|
||||
name: 'Database',
|
||||
description: 'Database access and data operations.',
|
||||
risk: 'high',
|
||||
riskReason:
|
||||
'Read-write lets this token run arbitrary SQL, so it can modify or delete any data in your database.',
|
||||
allowsRead: ['Read tables and schema', 'Run read-only queries'],
|
||||
allowsWrite: ['Run arbitrary SQL'],
|
||||
},
|
||||
'project:database_migrations': {
|
||||
category: 'database',
|
||||
name: 'Migrations',
|
||||
description: 'Database migration history and application.',
|
||||
risk: 'high',
|
||||
riskReason:
|
||||
'Read-write can apply schema changes that alter or drop tables across your database.',
|
||||
allowsRead: ['Read migration history'],
|
||||
allowsWrite: ['Apply migrations'],
|
||||
},
|
||||
'project:backups': {
|
||||
category: 'database',
|
||||
name: 'Backups',
|
||||
description: 'Database backups, restore points, and restore.',
|
||||
risk: 'high',
|
||||
riskReason:
|
||||
'Read-write can trigger restores that overwrite current data with an earlier snapshot.',
|
||||
allowsRead: ['Read backups and restore points'],
|
||||
allowsWrite: ['Trigger restores'],
|
||||
},
|
||||
'project:database_config': {
|
||||
category: 'database',
|
||||
name: 'Database Config',
|
||||
description: 'Database configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change database configuration.',
|
||||
allowsRead: ['Read database configuration'],
|
||||
allowsWrite: ['Update database configuration'],
|
||||
},
|
||||
'project:database_jit': {
|
||||
category: 'database',
|
||||
name: 'Database JIT',
|
||||
description: 'Just-in-time database access settings.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change just-in-time database access settings.',
|
||||
allowsRead: ['Read JIT settings'],
|
||||
allowsWrite: ['Manage JIT settings'],
|
||||
},
|
||||
'project:database_pooling_config': {
|
||||
category: 'database',
|
||||
name: 'Connection Pooling',
|
||||
description: 'Database connection pooling.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change connection pooling behavior.',
|
||||
allowsRead: ['Read pooling configuration'],
|
||||
allowsWrite: ['Update pooling configuration'],
|
||||
},
|
||||
'project:database_readonly_config': {
|
||||
category: 'database',
|
||||
name: 'Read-only Mode',
|
||||
description: 'Database read-only mode.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can toggle the database into or out of read-only mode.',
|
||||
allowsRead: ['Read read-only mode status'],
|
||||
allowsWrite: ['Toggle read-only mode'],
|
||||
},
|
||||
'project:database_ssl_config': {
|
||||
category: 'database',
|
||||
name: 'SSL Enforcement',
|
||||
description: 'Database SSL configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change SSL enforcement for database connections.',
|
||||
allowsRead: ['Read SSL configuration'],
|
||||
allowsWrite: ['Manage SSL enforcement'],
|
||||
},
|
||||
'project:database_webhooks_config': {
|
||||
category: 'database',
|
||||
name: 'Database Webhooks',
|
||||
description: 'Webhooks triggered from the database.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change database webhook configuration.',
|
||||
allowsRead: ['Read webhook configuration'],
|
||||
allowsWrite: ['Manage database webhooks'],
|
||||
},
|
||||
'project:database_network_bans': {
|
||||
category: 'database',
|
||||
name: 'Network Bans',
|
||||
description: 'Banned IPs for the database.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can ban or unban IP addresses from reaching the database.',
|
||||
allowsRead: ['Read banned IPs'],
|
||||
allowsWrite: ['Manage banned IPs'],
|
||||
},
|
||||
'project:database_network_restrictions': {
|
||||
category: 'database',
|
||||
name: 'Network Restrictions',
|
||||
description: 'Network restrictions for the database.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read-write can change which networks are allowed to reach the database.',
|
||||
allowsRead: ['Read network restrictions'],
|
||||
allowsWrite: ['Manage network restrictions'],
|
||||
},
|
||||
|
||||
// --- Application Services ---
|
||||
'project:auth_config': {
|
||||
category: 'appsvc',
|
||||
name: 'Auth Config',
|
||||
description: 'Authentication provider and settings.',
|
||||
risk: 'high',
|
||||
riskReason:
|
||||
'Read-write can change authentication providers and settings, affecting how users sign in.',
|
||||
allowsRead: ['Read auth configuration'],
|
||||
allowsWrite: ['Update auth providers and settings'],
|
||||
},
|
||||
'project:auth_signing_keys': {
|
||||
category: 'appsvc',
|
||||
name: 'Auth Signing Keys',
|
||||
description: 'Authentication signing keys.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read-write can rotate signing keys, invalidating existing sessions and tokens.',
|
||||
allowsRead: ['Read signing keys'],
|
||||
allowsWrite: ['Manage signing keys'],
|
||||
},
|
||||
'project:api_gateway_keys': {
|
||||
category: 'appsvc',
|
||||
name: 'API Keys',
|
||||
description: 'Project API keys.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read exposes API keys; read-write grants elevated access to create new keys.',
|
||||
allowsRead: ['Read project API keys'],
|
||||
allowsWrite: ['Create and revoke API keys'],
|
||||
},
|
||||
'project:edge_functions': {
|
||||
category: 'appsvc',
|
||||
name: 'Edge Functions',
|
||||
description: 'Edge functions.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can deploy or delete edge functions.',
|
||||
allowsRead: ['Read edge functions'],
|
||||
allowsWrite: ['Deploy and delete edge functions'],
|
||||
},
|
||||
'project:edge_functions_secrets': {
|
||||
category: 'appsvc',
|
||||
name: 'Edge Function Secrets',
|
||||
description: 'Secrets available to edge functions.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read exposes function secrets; read-write can set new secret values.',
|
||||
allowsRead: ['Read edge function secrets'],
|
||||
allowsWrite: ['Set edge function secrets'],
|
||||
},
|
||||
'project:realtime_config': {
|
||||
category: 'appsvc',
|
||||
name: 'Realtime Config',
|
||||
description: 'Realtime configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change realtime settings and shut down active connections.',
|
||||
allowsRead: ['Read realtime configuration'],
|
||||
allowsWrite: ['Update realtime settings'],
|
||||
},
|
||||
'project:storage': {
|
||||
category: 'appsvc',
|
||||
name: 'Storage',
|
||||
description: 'File storage buckets and objects.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can modify or delete stored files.',
|
||||
allowsRead: ['Read storage buckets and objects'],
|
||||
allowsWrite: ['Manage storage buckets and objects'],
|
||||
},
|
||||
'project:storage_config': {
|
||||
category: 'appsvc',
|
||||
name: 'Storage Config',
|
||||
description: 'Storage bucket configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change storage configuration.',
|
||||
allowsRead: ['Read storage configuration'],
|
||||
allowsWrite: ['Update storage configuration'],
|
||||
},
|
||||
'project:data_api_config': {
|
||||
category: 'appsvc',
|
||||
name: 'Data API Config',
|
||||
description: 'PostgREST behavior and settings.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change how the auto-generated Data API behaves.',
|
||||
allowsRead: ['Read Data API configuration'],
|
||||
allowsWrite: ['Update Data API configuration'],
|
||||
},
|
||||
|
||||
// --- Infrastructure & Delivery ---
|
||||
'project:branching_development': {
|
||||
category: 'infra',
|
||||
name: 'Development Branches',
|
||||
description: 'Development branch automation.',
|
||||
risk: 'low',
|
||||
riskReason: 'Branch automation for development workflows — limited blast radius.',
|
||||
allowsRead: ['Read development branches'],
|
||||
allowsWrite: ['Create, update, and delete development branches'],
|
||||
},
|
||||
'project:branching_production': {
|
||||
category: 'infra',
|
||||
name: 'Production Branches',
|
||||
description: 'Production branch automation.',
|
||||
risk: 'high',
|
||||
riskReason:
|
||||
'Read-write grants elevated access to create, merge, or delete production branches.',
|
||||
allowsRead: ['Read production branches'],
|
||||
allowsWrite: ['Create, merge, and delete production branches'],
|
||||
},
|
||||
'project:custom_domain': {
|
||||
category: 'infra',
|
||||
name: 'Custom Domains',
|
||||
description: 'Custom hostnames.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change custom hostnames, affecting how your project is reached.',
|
||||
allowsRead: ['Read custom domain configuration'],
|
||||
allowsWrite: ['Set custom hostnames'],
|
||||
},
|
||||
'project:vanity_subdomain': {
|
||||
category: 'infra',
|
||||
name: 'Vanity Subdomain',
|
||||
description: 'Project vanity subdomain.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change the project vanity subdomain.',
|
||||
allowsRead: ['Read vanity subdomain'],
|
||||
allowsWrite: ['Manage vanity subdomain'],
|
||||
},
|
||||
'project:infra_addons': {
|
||||
category: 'infra',
|
||||
name: 'Add-ons',
|
||||
description: 'Infrastructure add-ons.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can enable or change paid infrastructure add-ons.',
|
||||
allowsRead: ['Read infrastructure add-ons'],
|
||||
allowsWrite: ['Manage infrastructure add-ons'],
|
||||
},
|
||||
'project:infra_disk_config': {
|
||||
category: 'infra',
|
||||
name: 'Disk Config',
|
||||
description: 'Disk configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change disk size and configuration, which may incur cost.',
|
||||
allowsRead: ['Read disk configuration'],
|
||||
allowsWrite: ['Manage disk configuration'],
|
||||
},
|
||||
'project:read_replicas': {
|
||||
category: 'infra',
|
||||
name: 'Read Replicas',
|
||||
description: 'Read replica configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can provision or remove read replicas, which may incur cost.',
|
||||
allowsRead: ['Read read-replica configuration'],
|
||||
allowsWrite: ['Manage read replicas'],
|
||||
},
|
||||
}
|
||||
|
||||
const RESOURCE_METADATA_FALLBACK = (
|
||||
resourceKey: string,
|
||||
title: string,
|
||||
hasWrite: boolean
|
||||
): ResourceMeta => ({
|
||||
category: resourceKey.startsWith('project:') ? 'project' : 'account',
|
||||
name: title.replace(/^(Read|Manage|Create|Delete)\s+/i, ''),
|
||||
description: title,
|
||||
risk: hasWrite ? 'medium' : 'low',
|
||||
riskReason: hasWrite
|
||||
? 'Read-write can modify this resource.'
|
||||
: 'Read-only access to this resource.',
|
||||
})
|
||||
|
||||
const PERMISSION_LEVELS = ['user', 'organization', 'project'] as const
|
||||
|
||||
export type PermissionLevel = (typeof PERMISSION_LEVELS)[number]
|
||||
|
||||
/**
|
||||
* Runtime guard for the FGA namespaces: role evaluation branches on the level, so an unrecognized
|
||||
* namespace must fail loudly (at module load, caught by any test importing the catalog) rather
|
||||
* than silently evaluate as project-level.
|
||||
*/
|
||||
const toPermissionLevel = (scope: string): PermissionLevel => {
|
||||
const level = scope.toLowerCase()
|
||||
const match = PERMISSION_LEVELS.find((candidate) => candidate === level)
|
||||
if (match === undefined) throw new Error(`Unknown FGA namespace: ${scope}`)
|
||||
return match
|
||||
}
|
||||
|
||||
export interface PermissionCatalogEntry {
|
||||
/** Derived resource key, e.g. "project:database" */
|
||||
key: string
|
||||
/** Which FGA namespace the resource lives in — decides which role (org vs project) governs it. */
|
||||
level: PermissionLevel
|
||||
category: PermissionCategoryKey
|
||||
name: string
|
||||
description: string
|
||||
risk: RiskLevel
|
||||
riskReason: string
|
||||
allowsRead: string[]
|
||||
allowsWrite: string[]
|
||||
/** Whether a Read-write mode is offered (false => read-only resource). */
|
||||
writable: boolean
|
||||
/** FGA scope ids granted at Read (and above). */
|
||||
readScopes: ScopedAccessTokenPermission[]
|
||||
/** Additional FGA scope ids granted at Read-write (write / create / delete). */
|
||||
writeScopes: ScopedAccessTokenPermission[]
|
||||
}
|
||||
|
||||
const getAction = (key: string): 'read' | 'write' | 'create' | 'delete' => {
|
||||
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 =>
|
||||
key.replace(/_(READ|WRITE|CREATE|DELETE)$/, '').toLowerCase()
|
||||
|
||||
/**
|
||||
* Builds the permission catalog from the real FgaPermissions. Each unique `scope:resource` becomes
|
||||
* one row; its read scope maps to Read mode and its write/create/delete scopes to Read-write mode.
|
||||
*/
|
||||
const buildCatalog = (): PermissionCatalogEntry[] => {
|
||||
const byResource = new Map<
|
||||
string,
|
||||
{ level: PermissionLevel; title: string; readScopes: string[]; writeScopes: string[] }
|
||||
>()
|
||||
|
||||
for (const [scope, scopePerms] of Object.entries(FGA)) {
|
||||
const level = toPermissionLevel(scope)
|
||||
for (const [permKey, perm] of Object.entries(scopePerms)) {
|
||||
const resourceKey = `${level}:${getResource(permKey)}`
|
||||
const action = getAction(permKey)
|
||||
if (!byResource.has(resourceKey)) {
|
||||
byResource.set(resourceKey, { level, title: perm.title, readScopes: [], writeScopes: [] })
|
||||
}
|
||||
const entry = byResource.get(resourceKey)!
|
||||
if (action === 'read') entry.readScopes.push(perm.id)
|
||||
else entry.writeScopes.push(perm.id)
|
||||
}
|
||||
}
|
||||
|
||||
const catalog: PermissionCatalogEntry[] = []
|
||||
for (const [key, { level, title, readScopes, writeScopes }] of byResource.entries()) {
|
||||
const meta =
|
||||
RESOURCE_METADATA[key] ?? RESOURCE_METADATA_FALLBACK(key, title, writeScopes.length > 0)
|
||||
catalog.push({
|
||||
key,
|
||||
level,
|
||||
category: meta.category,
|
||||
name: meta.name,
|
||||
description: meta.description,
|
||||
risk: meta.risk,
|
||||
riskReason: meta.riskReason,
|
||||
allowsRead: meta.allowsRead ?? [`Read ${meta.name.toLowerCase()}`],
|
||||
allowsWrite:
|
||||
meta.allowsWrite ?? (writeScopes.length > 0 ? [`Modify ${meta.name.toLowerCase()}`] : []),
|
||||
writable: writeScopes.length > 0,
|
||||
readScopes: readScopes as ScopedAccessTokenPermission[],
|
||||
writeScopes: writeScopes as ScopedAccessTokenPermission[],
|
||||
})
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
export const PERMISSION_CATALOG = buildCatalog()
|
||||
|
||||
const CATALOG_BY_KEY = new Map(PERMISSION_CATALOG.map((entry) => [entry.key, entry]))
|
||||
|
||||
export const getCatalogEntry = (key: string) => CATALOG_BY_KEY.get(key)
|
||||
|
||||
export interface CategoryWithEntries extends PermissionCategory {
|
||||
entries: PermissionCatalogEntry[]
|
||||
}
|
||||
|
||||
/** Catalog grouped by category, in category display order, dropping empty categories. */
|
||||
export const PERMISSION_CATALOG_BY_CATEGORY: CategoryWithEntries[] = PERMISSION_CATEGORIES.map(
|
||||
(category) => ({
|
||||
...category,
|
||||
entries: PERMISSION_CATALOG.filter((entry) => entry.category === category.key),
|
||||
})
|
||||
).filter((category) => category.entries.length > 0)
|
||||
|
||||
/** Map of resource key -> selected mode. Absent keys are treated as 'none'. */
|
||||
export type PermissionSelection = Record<string, PermissionMode>
|
||||
|
||||
@@ -577,7 +46,7 @@ export const selectionToScopes = (
|
||||
): ScopedAccessTokenPermission[] => {
|
||||
const scopes: ScopedAccessTokenPermission[] = []
|
||||
for (const [key, mode] of Object.entries(selection)) {
|
||||
const entry = CATALOG_BY_KEY.get(key)
|
||||
const entry = getCatalogEntry(key)
|
||||
if (!entry) continue
|
||||
scopes.push(...getEntryScopes(entry, mode))
|
||||
}
|
||||
@@ -590,7 +59,7 @@ export const selectionToScopes = (
|
||||
* Tokens created through the Management API can hold arbitrary scope subsets that the
|
||||
* none/read/readwrite modes cannot represent exactly (e.g. a lone branching_development_create).
|
||||
* Any granted scope of an entry marks it at the corresponding mode, so a partial grant is never
|
||||
* dropped — the mode is an upper bound and may name specific operations the token lacks, but it
|
||||
* dropped. The mode is an upper bound and may name specific operations the token lacks, but it
|
||||
* never understates the token's authority or risk. The endpoint and MCP-tool lists, computed
|
||||
* from the actual granted scopes, remain the precise view.
|
||||
*/
|
||||
@@ -623,12 +92,6 @@ export const RISK_LEVEL_LABEL: Record<RiskLevel, string> = {
|
||||
high: 'High risk',
|
||||
}
|
||||
|
||||
export const PERMISSION_MODE_LABEL: Record<PermissionMode, string> = {
|
||||
none: 'None',
|
||||
read: 'Read',
|
||||
readwrite: 'Read-write',
|
||||
}
|
||||
|
||||
export type ResourceAccessMode = 'project' | 'organization' | 'account'
|
||||
|
||||
export const RISK_TONE_VARIANT: Record<RiskLevel, 'success' | 'warning' | 'destructive'> = {
|
||||
|
||||
+1
-7
@@ -13,8 +13,6 @@ import { getActivePreset, type PermissionPreset } from '../../AccessToken.preset
|
||||
import type { TokenAccessEvaluation } from '../../AccessToken.roles'
|
||||
import { PermissionPresetSelect } from './PermissionPresetSelect'
|
||||
import { PermissionRow } from './PermissionRow'
|
||||
import { InlineLink } from '@/components/ui/InlineLink'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
interface PermissionsAccordionProps {
|
||||
selection: PermissionSelection
|
||||
@@ -42,11 +40,7 @@ export const PermissionsAccordion = ({
|
||||
description={
|
||||
<p className="text-foreground-lighter text-sm">
|
||||
Grant the minimum access this token needs. Everything defaults to None. Permissions
|
||||
follow your role in the organizations and projects you're a member of — see{' '}
|
||||
<InlineLink href={`${DOCS_URL}/guides/platform/access-control`}>
|
||||
access control
|
||||
</InlineLink>{' '}
|
||||
for how roles work.
|
||||
follow your role in the organizations and projects you're a member of.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
|
||||
+3
-1
@@ -15,6 +15,7 @@ import { ExperimentalTokenDropdown } from '../Classic/ExperimentalTokenDropdown'
|
||||
import { NewScopedTokenForm } from './Form/NewScopedTokenForm'
|
||||
import { getExpiryDate, type TokenFormValues } from './Form/NewScopedTokenForm.utils'
|
||||
import { NewScopedTokenSuccess } from './Form/NewScopedTokenSuccess'
|
||||
import { TokenDocsButtons } from './TokenDocsButtons'
|
||||
import {
|
||||
useAccessTokenCreateMutation,
|
||||
type NewAccessToken,
|
||||
@@ -124,11 +125,12 @@ export const NewScopedTokenSheet = ({ onCreateExperimentalToken }: NewScopedToke
|
||||
size="default"
|
||||
className="flex h-full flex-col gap-0 sm:w-[656px] lg:w-[800px]"
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetHeader className="flex flex-col md:flex-row justify-between gap-4 items-start md:items-center">
|
||||
<SheetTitle>{step === 'success' ? 'Token created' : 'Generate token'}</SheetTitle>
|
||||
<SheetDescription className="sr-only">
|
||||
Configure and create a new access token.
|
||||
</SheetDescription>
|
||||
{step !== 'success' && <TokenDocsButtons />}
|
||||
</SheetHeader>
|
||||
{step === 'success' && createdToken ? (
|
||||
<NewScopedTokenSuccess
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { DocsButton } from '@/components/ui/DocsButton'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
/** Docs links shown in the header of the scoped token sheets. */
|
||||
export const TokenDocsButtons = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<DocsButton
|
||||
href={`${DOCS_URL}/guides/platform/personal-access-tokens`}
|
||||
topic="Personal access tokens"
|
||||
label="Access tokens docs"
|
||||
/>
|
||||
<DocsButton
|
||||
href={`${DOCS_URL}/guides/platform/access-control`}
|
||||
topic="Access control"
|
||||
label="Access control docs"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
getCapabilityDensityTier,
|
||||
type CapabilityLevelFilter,
|
||||
} from './TokenCapabilities/TokenCapabilities.utils'
|
||||
import { DocsButton } from '@/components/ui/DocsButton'
|
||||
import { TokenDocsButtons } from './TokenDocsButtons'
|
||||
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
|
||||
import { useProjectsInfiniteQuery } from '@/data/projects/projects-infinite-query'
|
||||
import {
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
useGetEnabledEndpointsForCapability,
|
||||
} from '@/data/scoped-access-tokens/permission-scope-map-query'
|
||||
import { useScopedAccessTokenQuery } from '@/data/scoped-access-tokens/scoped-access-token-query'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
interface ViewTokenSheetProps {
|
||||
visible: boolean
|
||||
@@ -148,18 +147,7 @@ export function ViewTokenSheet({ visible, tokenId, onClose }: ViewTokenSheetProp
|
||||
<p className="truncate" title={`View access for ${token?.name}`}>
|
||||
View access for {token?.name}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<DocsButton
|
||||
href={`${DOCS_URL}/guides/platform/access-control`}
|
||||
topic="Access control"
|
||||
label="Access control docs"
|
||||
/>
|
||||
<DocsButton
|
||||
href={`${DOCS_URL}/reference/api/introduction`}
|
||||
topic="Management API"
|
||||
label="API docs"
|
||||
/>
|
||||
</div>
|
||||
<TokenDocsButtons />
|
||||
</SheetHeader>
|
||||
{/* Radix wraps viewport children in an inline-styled display:table div that grows to fit
|
||||
the widest child, which would let one long endpoint path expand the sheet instead of
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/shared-types": "0.1.91",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
import { permissions } from '@supabase/shared-types'
|
||||
|
||||
/**
|
||||
* The permission catalog for scoped personal access tokens: every FGA scope grouped into resources with
|
||||
* display names, categories, and risk metadata.
|
||||
*
|
||||
* The real permission scopes come from `@supabase/shared-types` (`FgaPermissions`). Those scopes
|
||||
* carry no category or risk metadata, so this file layers editable presentation data on top:
|
||||
* - PERMISSION_CATEGORIES groups every scope into a display category.
|
||||
* - RESOURCE_METADATA assigns each resource a display name, description, category and risk.
|
||||
*
|
||||
* Lives in shared-data (not apps/studio) because two apps consume it: Studio's scoped personal
|
||||
* access token creation form, and the docs generator that renders the "Personal Access Tokens"
|
||||
* guide's permission tables (apps/docs/spec/sections/generateAccessControlPartials.mts).
|
||||
* The docs must show the same names, categories, and order as the form.
|
||||
*
|
||||
* The `name` and `category` fields are published copy: they are the permission and section labels
|
||||
* in both the form and the docs guide, so renaming one renames both on the next regeneration.
|
||||
*
|
||||
* TODO(product): the risk levels, risk reasons, and "Allows" copy below are proposed defaults and
|
||||
* still need review. They render in the form only, not in the docs. Where a resource has no
|
||||
* explicit metadata entry we fall back to a heuristic.
|
||||
*/
|
||||
|
||||
const FGA = permissions.FgaPermissions
|
||||
|
||||
type PermissionsOf<T> = T extends Record<string, infer P> ? P : never
|
||||
/** Union of every FGA scope id literal published by @supabase/shared-types. */
|
||||
export type FgaScopeId =
|
||||
PermissionsOf<(typeof FGA)[keyof typeof FGA]> extends { id: infer Id } ? Id : never
|
||||
|
||||
export type RiskLevel = 'low' | 'medium' | 'high'
|
||||
|
||||
/**
|
||||
* Display labels for permission modes. Part of the form<->docs contract like resource names:
|
||||
* Studio's form and the docs guide's tables must label modes identically.
|
||||
*/
|
||||
export const PERMISSION_MODE_LABEL = {
|
||||
none: 'None',
|
||||
read: 'Read',
|
||||
readwrite: 'Read-write',
|
||||
} as const
|
||||
|
||||
/** Selection modes for a catalog entry. The label map's keys are the single source of truth. */
|
||||
export type PermissionMode = keyof typeof PERMISSION_MODE_LABEL
|
||||
|
||||
export type PermissionCategoryKey = 'account' | 'project' | 'database' | 'appsvc' | 'infra'
|
||||
|
||||
export interface PermissionCategory {
|
||||
key: PermissionCategoryKey
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
/** Display order. The form's accordion sections and the docs guide's sections both follow it. */
|
||||
const PERMISSION_CATEGORIES: PermissionCategory[] = [
|
||||
{
|
||||
key: 'project',
|
||||
name: 'Project',
|
||||
description: 'Core project visibility, settings, and diagnostics.',
|
||||
},
|
||||
{
|
||||
key: 'database',
|
||||
name: 'Database',
|
||||
description: 'SQL access, migrations, backups, and data operations.',
|
||||
},
|
||||
{
|
||||
key: 'appsvc',
|
||||
name: 'Application services',
|
||||
description: 'Auth, storage, realtime, edge functions, and service configuration.',
|
||||
},
|
||||
{
|
||||
key: 'infra',
|
||||
name: 'Infrastructure and delivery',
|
||||
description: 'Branch automation, domains, add-ons, and network.',
|
||||
},
|
||||
{
|
||||
key: 'account',
|
||||
name: 'Account and organization',
|
||||
description: 'Account-wide and organization-level access that spans projects.',
|
||||
},
|
||||
]
|
||||
|
||||
interface ResourceMeta {
|
||||
category: PermissionCategoryKey
|
||||
name: string
|
||||
description: string
|
||||
risk: RiskLevel
|
||||
riskReason: string
|
||||
allowsRead?: string[]
|
||||
allowsWrite?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-resource presentation metadata, keyed by the derived `scope:resource` key (derived in
|
||||
* `buildCatalog` below). Every resource returned from FgaPermissions should have an entry;
|
||||
* RESOURCE_METADATA_FALLBACK covers anything that slips through.
|
||||
*/
|
||||
const RESOURCE_METADATA: Record<string, ResourceMeta> = {
|
||||
// --- Account and organization ---
|
||||
'user:organizations': {
|
||||
category: 'account',
|
||||
name: 'Organizations',
|
||||
description: 'Organizations you belong to.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can create new organizations under your account.',
|
||||
allowsRead: ['List your organizations'],
|
||||
allowsWrite: ['Create organizations'],
|
||||
},
|
||||
'user:projects': {
|
||||
category: 'account',
|
||||
name: 'Projects (account-wide)',
|
||||
description: 'Projects across all your organizations.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-only listing of the projects you can access.',
|
||||
allowsRead: ['List your projects'],
|
||||
},
|
||||
'user:snippets': {
|
||||
category: 'account',
|
||||
name: 'SQL Snippets (account-wide)',
|
||||
description: 'Saved SQL snippets across your account.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-only access to your saved snippets.',
|
||||
allowsRead: ['Read your SQL snippets'],
|
||||
},
|
||||
'organization:admin': {
|
||||
category: 'account',
|
||||
name: 'Organization Settings',
|
||||
description: 'Organization settings and project transfers.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read-write grants elevated access to organization settings and project transfers.',
|
||||
allowsRead: ['Read organization settings'],
|
||||
allowsWrite: ['Manage organization settings', 'Transfer projects'],
|
||||
},
|
||||
'organization:members': {
|
||||
category: 'account',
|
||||
name: 'Organization Members',
|
||||
description: 'Members and roles within the organization.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read-write can add or remove members and change roles across your organization.',
|
||||
allowsRead: ['Read organization members'],
|
||||
allowsWrite: ['Add or remove members', 'Change member roles'],
|
||||
},
|
||||
'organization:projects': {
|
||||
category: 'account',
|
||||
name: 'Organization Projects',
|
||||
description: 'Projects within the organization.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can create new projects in the organization.',
|
||||
allowsRead: ['List organization projects'],
|
||||
allowsWrite: ['Create organization projects'],
|
||||
},
|
||||
'organization:platform_webhooks': {
|
||||
category: 'account',
|
||||
name: 'Platform Webhooks (organization)',
|
||||
description: 'Platform webhook endpoints and deliveries for the organization.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can create webhook endpoints that receive organization events.',
|
||||
allowsRead: ['Read webhook endpoints and deliveries'],
|
||||
allowsWrite: ['Manage webhook endpoints'],
|
||||
},
|
||||
|
||||
// --- Project ---
|
||||
'project:admin': {
|
||||
category: 'project',
|
||||
name: 'Project Settings',
|
||||
description: 'Project metadata and settings.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read-write grants elevated access to change project settings and configuration.',
|
||||
allowsRead: ['Read project metadata'],
|
||||
allowsWrite: ['Update project settings'],
|
||||
},
|
||||
'project:action_runs': {
|
||||
category: 'project',
|
||||
name: 'Action Runs',
|
||||
description: 'Project action run status and logs.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can trigger action runs that execute project workflows.',
|
||||
allowsRead: ['Read action run status', 'Read run logs'],
|
||||
allowsWrite: ['Trigger action runs'],
|
||||
},
|
||||
'project:advisors': {
|
||||
category: 'project',
|
||||
name: 'Advisors',
|
||||
description: 'Security and performance advisor results.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-only access to advisor findings. No changes possible.',
|
||||
allowsRead: ['Read security advisors', 'Read performance advisors'],
|
||||
},
|
||||
'project:analytics_logs': {
|
||||
category: 'project',
|
||||
name: 'Logs',
|
||||
description: 'Operational logs and log analytics.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-only access to project logs.',
|
||||
allowsRead: ['Read project logs'],
|
||||
},
|
||||
'project:analytics_usage': {
|
||||
category: 'project',
|
||||
name: 'Usage Analytics',
|
||||
description: 'Project usage and analytics data.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-only access to usage analytics.',
|
||||
allowsRead: ['Read usage analytics'],
|
||||
},
|
||||
'project:analytics_config': {
|
||||
category: 'project',
|
||||
name: 'Analytics Config',
|
||||
description: 'Log drains and analytics configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can create log drains that export project logs.',
|
||||
allowsRead: ['Read analytics configuration'],
|
||||
allowsWrite: ['Manage log drains'],
|
||||
},
|
||||
'project:platform_webhooks': {
|
||||
category: 'project',
|
||||
name: 'Platform Webhooks',
|
||||
description: 'Platform webhook endpoints and deliveries for the project.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can create webhook endpoints that receive project events.',
|
||||
allowsRead: ['Read webhook endpoints and deliveries'],
|
||||
allowsWrite: ['Manage webhook endpoints'],
|
||||
},
|
||||
'project:snippets': {
|
||||
category: 'project',
|
||||
name: 'SQL Snippets',
|
||||
description: 'Saved SQL snippets for the project.',
|
||||
risk: 'low',
|
||||
riskReason: 'Read-write can create and edit saved SQL snippets.',
|
||||
allowsRead: ['Read project SQL snippets'],
|
||||
allowsWrite: ['Manage project SQL snippets'],
|
||||
},
|
||||
|
||||
// --- Database ---
|
||||
'project:database': {
|
||||
category: 'database',
|
||||
name: 'Database',
|
||||
description: 'Database access and data operations.',
|
||||
risk: 'high',
|
||||
riskReason:
|
||||
'Read-write lets this token run arbitrary SQL, so it can modify or delete any data in your database.',
|
||||
allowsRead: ['Read tables and schema', 'Run read-only queries'],
|
||||
allowsWrite: ['Run arbitrary SQL'],
|
||||
},
|
||||
'project:database_migrations': {
|
||||
category: 'database',
|
||||
name: 'Migrations',
|
||||
description: 'Database migration history and application.',
|
||||
risk: 'high',
|
||||
riskReason:
|
||||
'Read-write can apply schema changes that alter or drop tables across your database.',
|
||||
allowsRead: ['Read migration history'],
|
||||
allowsWrite: ['Apply migrations'],
|
||||
},
|
||||
'project:backups': {
|
||||
category: 'database',
|
||||
name: 'Backups',
|
||||
description: 'Database backups, restore points, and restore.',
|
||||
risk: 'high',
|
||||
riskReason:
|
||||
'Read-write can trigger restores that overwrite current data with an earlier snapshot.',
|
||||
allowsRead: ['Read backups and restore points'],
|
||||
allowsWrite: ['Trigger restores'],
|
||||
},
|
||||
'project:database_config': {
|
||||
category: 'database',
|
||||
name: 'Database Config',
|
||||
description: 'Database configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change database configuration.',
|
||||
allowsRead: ['Read database configuration'],
|
||||
allowsWrite: ['Update database configuration'],
|
||||
},
|
||||
'project:database_jit': {
|
||||
category: 'database',
|
||||
name: 'Database JIT',
|
||||
description: 'Just-in-time database access settings.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change just-in-time database access settings.',
|
||||
allowsRead: ['Read JIT settings'],
|
||||
allowsWrite: ['Manage JIT settings'],
|
||||
},
|
||||
'project:database_pooling_config': {
|
||||
category: 'database',
|
||||
name: 'Connection Pooling',
|
||||
description: 'Database connection pooling.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change connection pooling behavior.',
|
||||
allowsRead: ['Read pooling configuration'],
|
||||
allowsWrite: ['Update pooling configuration'],
|
||||
},
|
||||
'project:database_readonly_config': {
|
||||
category: 'database',
|
||||
name: 'Read-only Mode',
|
||||
description: 'Database read-only mode.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can toggle the database into or out of read-only mode.',
|
||||
allowsRead: ['Read read-only mode status'],
|
||||
allowsWrite: ['Toggle read-only mode'],
|
||||
},
|
||||
'project:database_ssl_config': {
|
||||
category: 'database',
|
||||
name: 'SSL Enforcement',
|
||||
description: 'Database SSL configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change SSL enforcement for database connections.',
|
||||
allowsRead: ['Read SSL configuration'],
|
||||
allowsWrite: ['Manage SSL enforcement'],
|
||||
},
|
||||
'project:database_webhooks_config': {
|
||||
category: 'database',
|
||||
name: 'Database Webhooks',
|
||||
description: 'Webhooks triggered from the database.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change database webhook configuration.',
|
||||
allowsRead: ['Read webhook configuration'],
|
||||
allowsWrite: ['Manage database webhooks'],
|
||||
},
|
||||
'project:database_network_bans': {
|
||||
category: 'database',
|
||||
name: 'Network Bans',
|
||||
description: 'Banned IPs for the database.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can ban or unban IP addresses from reaching the database.',
|
||||
allowsRead: ['Read banned IPs'],
|
||||
allowsWrite: ['Manage banned IPs'],
|
||||
},
|
||||
'project:database_network_restrictions': {
|
||||
category: 'database',
|
||||
name: 'Network Restrictions',
|
||||
description: 'Network restrictions for the database.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read-write can change which networks are allowed to reach the database.',
|
||||
allowsRead: ['Read network restrictions'],
|
||||
allowsWrite: ['Manage network restrictions'],
|
||||
},
|
||||
|
||||
// --- Application services ---
|
||||
'project:auth_config': {
|
||||
category: 'appsvc',
|
||||
name: 'Auth Config',
|
||||
description: 'Authentication provider and settings.',
|
||||
risk: 'high',
|
||||
riskReason:
|
||||
'Read-write can change authentication providers and settings, affecting how users sign in.',
|
||||
allowsRead: ['Read auth configuration'],
|
||||
allowsWrite: ['Update auth providers and settings'],
|
||||
},
|
||||
'project:auth_signing_keys': {
|
||||
category: 'appsvc',
|
||||
name: 'Auth Signing Keys',
|
||||
description: 'Authentication signing keys.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read-write can rotate signing keys, invalidating existing sessions and tokens.',
|
||||
allowsRead: ['Read signing keys'],
|
||||
allowsWrite: ['Manage signing keys'],
|
||||
},
|
||||
'project:api_gateway_keys': {
|
||||
category: 'appsvc',
|
||||
name: 'API Keys',
|
||||
description: 'Project API keys.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read exposes API keys; read-write grants elevated access to create new keys.',
|
||||
allowsRead: ['Read project API keys'],
|
||||
allowsWrite: ['Create and revoke API keys'],
|
||||
},
|
||||
'project:edge_functions': {
|
||||
category: 'appsvc',
|
||||
name: 'Edge Functions',
|
||||
description: 'Edge functions.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can deploy or delete edge functions.',
|
||||
allowsRead: ['Read edge functions'],
|
||||
allowsWrite: ['Deploy and delete edge functions'],
|
||||
},
|
||||
'project:edge_functions_secrets': {
|
||||
category: 'appsvc',
|
||||
name: 'Edge Function Secrets',
|
||||
description: 'Secrets available to edge functions.',
|
||||
risk: 'high',
|
||||
riskReason: 'Read exposes function secrets; read-write can set new secret values.',
|
||||
allowsRead: ['Read edge function secrets'],
|
||||
allowsWrite: ['Set edge function secrets'],
|
||||
},
|
||||
'project:realtime_config': {
|
||||
category: 'appsvc',
|
||||
name: 'Realtime Config',
|
||||
description: 'Realtime configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change realtime settings and shut down active connections.',
|
||||
allowsRead: ['Read realtime configuration'],
|
||||
allowsWrite: ['Update realtime settings'],
|
||||
},
|
||||
'project:storage': {
|
||||
category: 'appsvc',
|
||||
name: 'Storage',
|
||||
description: 'File storage buckets and objects.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can modify or delete stored files.',
|
||||
allowsRead: ['Read storage buckets and objects'],
|
||||
allowsWrite: ['Manage storage buckets and objects'],
|
||||
},
|
||||
'project:storage_config': {
|
||||
category: 'appsvc',
|
||||
name: 'Storage Config',
|
||||
description: 'Storage bucket configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change storage configuration.',
|
||||
allowsRead: ['Read storage configuration'],
|
||||
allowsWrite: ['Update storage configuration'],
|
||||
},
|
||||
'project:data_api_config': {
|
||||
category: 'appsvc',
|
||||
name: 'Data API Config',
|
||||
description: 'PostgREST behavior and settings.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change how the auto-generated Data API behaves.',
|
||||
allowsRead: ['Read Data API configuration'],
|
||||
allowsWrite: ['Update Data API configuration'],
|
||||
},
|
||||
|
||||
// --- Infrastructure and delivery ---
|
||||
'project:branching_development': {
|
||||
category: 'infra',
|
||||
name: 'Development Branches',
|
||||
description: 'Development branch automation.',
|
||||
risk: 'low',
|
||||
riskReason: 'Branch automation for development workflows with limited blast radius.',
|
||||
allowsRead: ['Read development branches'],
|
||||
allowsWrite: ['Create, update, and delete development branches'],
|
||||
},
|
||||
'project:branching_production': {
|
||||
category: 'infra',
|
||||
name: 'Production Branches',
|
||||
description: 'Production branch automation.',
|
||||
risk: 'high',
|
||||
riskReason:
|
||||
'Read-write grants elevated access to create, merge, or delete production branches.',
|
||||
allowsRead: ['Read production branches'],
|
||||
allowsWrite: ['Create, merge, and delete production branches'],
|
||||
},
|
||||
'project:custom_domain': {
|
||||
category: 'infra',
|
||||
name: 'Custom Domains',
|
||||
description: 'Custom hostnames.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change custom hostnames, affecting how your project is reached.',
|
||||
allowsRead: ['Read custom domain configuration'],
|
||||
allowsWrite: ['Set custom hostnames'],
|
||||
},
|
||||
'project:vanity_subdomain': {
|
||||
category: 'infra',
|
||||
name: 'Vanity Subdomain',
|
||||
description: 'Project vanity subdomain.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change the project vanity subdomain.',
|
||||
allowsRead: ['Read vanity subdomain'],
|
||||
allowsWrite: ['Manage vanity subdomain'],
|
||||
},
|
||||
'project:infra_addons': {
|
||||
category: 'infra',
|
||||
name: 'Add-ons',
|
||||
description: 'Infrastructure add-ons.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can enable or change paid infrastructure add-ons.',
|
||||
allowsRead: ['Read infrastructure add-ons'],
|
||||
allowsWrite: ['Manage infrastructure add-ons'],
|
||||
},
|
||||
'project:infra_disk_config': {
|
||||
category: 'infra',
|
||||
name: 'Disk Config',
|
||||
description: 'Disk configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can change disk size and configuration, which may incur cost.',
|
||||
allowsRead: ['Read disk configuration'],
|
||||
allowsWrite: ['Manage disk configuration'],
|
||||
},
|
||||
'project:read_replicas': {
|
||||
category: 'infra',
|
||||
name: 'Read Replicas',
|
||||
description: 'Read replica configuration.',
|
||||
risk: 'medium',
|
||||
riskReason: 'Read-write can provision or remove read replicas, which may incur cost.',
|
||||
allowsRead: ['Read read-replica configuration'],
|
||||
allowsWrite: ['Manage read replicas'],
|
||||
},
|
||||
}
|
||||
|
||||
const RESOURCE_METADATA_FALLBACK = (
|
||||
resourceKey: string,
|
||||
title: string,
|
||||
hasWrite: boolean
|
||||
): ResourceMeta => ({
|
||||
category: resourceKey.startsWith('project:') ? 'project' : 'account',
|
||||
// Title-cased so an uncurated resource doesn't ship a lowercase FGA-derived name
|
||||
// (e.g. "project analytics configurations") next to the curated Title Case entries.
|
||||
name: title
|
||||
.replace(/^(Read|Manage|Create|Delete)\s+/i, '')
|
||||
.replace(/\b[a-z]/g, (char) => char.toUpperCase()),
|
||||
description: title,
|
||||
risk: hasWrite ? 'medium' : 'low',
|
||||
riskReason: hasWrite
|
||||
? 'Read-write can modify this resource.'
|
||||
: 'Read-only access to this resource.',
|
||||
})
|
||||
|
||||
const PERMISSION_LEVELS = ['user', 'organization', 'project'] as const
|
||||
|
||||
export type PermissionLevel = (typeof PERMISSION_LEVELS)[number]
|
||||
|
||||
/**
|
||||
* Runtime guard for the FGA namespaces: role evaluation branches on the level, so an unrecognized
|
||||
* namespace must fail loudly (at module load, caught by any test importing the catalog) rather
|
||||
* than silently evaluate as project-level.
|
||||
*/
|
||||
const toPermissionLevel = (scope: string): PermissionLevel => {
|
||||
const level = scope.toLowerCase()
|
||||
const match = PERMISSION_LEVELS.find((candidate) => candidate === level)
|
||||
if (match === undefined) throw new Error(`Unknown FGA namespace: ${scope}`)
|
||||
return match
|
||||
}
|
||||
|
||||
export interface PermissionCatalogEntry {
|
||||
/** Derived resource key, e.g. "project:database" */
|
||||
key: string
|
||||
/** Which FGA namespace the resource lives in. Decides which role (org vs project) governs it. */
|
||||
level: PermissionLevel
|
||||
category: PermissionCategoryKey
|
||||
name: string
|
||||
description: string
|
||||
risk: RiskLevel
|
||||
riskReason: string
|
||||
allowsRead: string[]
|
||||
allowsWrite: string[]
|
||||
/** Whether a Read-write mode is offered (false => read-only resource). */
|
||||
writable: boolean
|
||||
/** FGA scope ids granted at Read (and above). */
|
||||
readScopes: FgaScopeId[]
|
||||
/** Additional FGA scope ids granted at Read-write (write / create / delete). */
|
||||
writeScopes: FgaScopeId[]
|
||||
}
|
||||
|
||||
/** Action classes an FGA permission key's suffix can map to. */
|
||||
export type FgaAction = 'read' | 'write' | 'create' | 'delete'
|
||||
|
||||
/**
|
||||
* Classifies an FGA permission key by its action suffix: "PROJECTS_READ" -> "read".
|
||||
* Keys without a write, create, or delete suffix are treated as read.
|
||||
*/
|
||||
export const getAction = (key: string): FgaAction => {
|
||||
if (key.endsWith('_WRITE')) return 'write'
|
||||
if (key.endsWith('_CREATE')) return 'create'
|
||||
if (key.endsWith('_DELETE')) return 'delete'
|
||||
return 'read'
|
||||
}
|
||||
|
||||
const isReadScope = (key: string): boolean => getAction(key) === 'read'
|
||||
|
||||
/** Strips the FGA action suffix off a permission key: "PROJECTS_READ" -> "projects". */
|
||||
export const getResource = (key: string): string =>
|
||||
key.replace(/_(READ|WRITE|CREATE|DELETE)$/, '').toLowerCase()
|
||||
|
||||
/**
|
||||
* Builds the permission catalog from the real FgaPermissions. Each unique `scope:resource` becomes
|
||||
* one row; its read scope maps to Read mode and its write/create/delete scopes to Read-write mode.
|
||||
*/
|
||||
const buildCatalog = (): PermissionCatalogEntry[] => {
|
||||
const byResource = new Map<
|
||||
string,
|
||||
{ level: PermissionLevel; title: string; readScopes: string[]; writeScopes: string[] }
|
||||
>()
|
||||
|
||||
for (const [scope, scopePerms] of Object.entries(FGA)) {
|
||||
const level = toPermissionLevel(scope)
|
||||
for (const [permKey, perm] of Object.entries(scopePerms)) {
|
||||
const resourceKey = `${level}:${getResource(permKey)}`
|
||||
if (!byResource.has(resourceKey)) {
|
||||
byResource.set(resourceKey, { level, title: perm.title, readScopes: [], writeScopes: [] })
|
||||
}
|
||||
const entry = byResource.get(resourceKey)!
|
||||
if (isReadScope(permKey)) entry.readScopes.push(perm.id)
|
||||
else entry.writeScopes.push(perm.id)
|
||||
}
|
||||
}
|
||||
|
||||
const catalog: PermissionCatalogEntry[] = []
|
||||
for (const [key, { level, title, readScopes, writeScopes }] of byResource.entries()) {
|
||||
const meta =
|
||||
RESOURCE_METADATA[key] ?? RESOURCE_METADATA_FALLBACK(key, title, writeScopes.length > 0)
|
||||
catalog.push({
|
||||
key,
|
||||
level,
|
||||
category: meta.category,
|
||||
name: meta.name,
|
||||
description: meta.description,
|
||||
risk: meta.risk,
|
||||
riskReason: meta.riskReason,
|
||||
allowsRead: meta.allowsRead ?? [`Read ${meta.name.toLowerCase()}`],
|
||||
allowsWrite:
|
||||
meta.allowsWrite ?? (writeScopes.length > 0 ? [`Modify ${meta.name.toLowerCase()}`] : []),
|
||||
writable: writeScopes.length > 0,
|
||||
readScopes: readScopes as FgaScopeId[],
|
||||
writeScopes: writeScopes as FgaScopeId[],
|
||||
})
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
export const PERMISSION_CATALOG = buildCatalog()
|
||||
|
||||
const CATALOG_BY_KEY = new Map(PERMISSION_CATALOG.map((entry) => [entry.key, entry]))
|
||||
|
||||
export const getCatalogEntry = (key: string) => CATALOG_BY_KEY.get(key)
|
||||
|
||||
export interface CategoryWithEntries extends PermissionCategory {
|
||||
entries: PermissionCatalogEntry[]
|
||||
}
|
||||
|
||||
/** Catalog grouped by category, in category display order, dropping empty categories. */
|
||||
export const PERMISSION_CATALOG_BY_CATEGORY: CategoryWithEntries[] = PERMISSION_CATEGORIES.map(
|
||||
(category) => ({
|
||||
...category,
|
||||
entries: PERMISSION_CATALOG.filter((entry) => entry.category === category.key),
|
||||
})
|
||||
).filter((category) => category.entries.length > 0)
|
||||
Generated
+3
@@ -2592,6 +2592,9 @@ importers:
|
||||
|
||||
packages/shared-data:
|
||||
dependencies:
|
||||
'@supabase/shared-types':
|
||||
specifier: 0.1.91
|
||||
version: 0.1.91
|
||||
zod:
|
||||
specifier: 'catalog:'
|
||||
version: 3.25.76
|
||||
|
||||
Reference in new issue
Block a user