From 2e3e1493f5b229299dbcd1cef7e7ada2d2840ce4 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 21 Jul 2026 03:18:59 +0700 Subject: [PATCH] feat(auth): add sessionCanManageApiKeys function and update access control logic Introduced the sessionCanManageApiKeys function to determine if a session can manage API keys based on role, permissions, and admin status. Updated the AccessComponent to utilize this new function for enabling/disabling API key management features. Enhanced documentation to reflect changes in API key management roles and permissions, including updates to the OpenAPI specification. --- apps/web/src/lib/auth.ts | 24 +++++++++ apps/web/src/routes/_auth/access.tsx | 79 +++++++++++++++++++--------- apps/web/src/types/api.ts | 13 +++-- docs/access.md | 26 +++++---- docs/openapi.yaml | 23 +++++++- 5 files changed, 123 insertions(+), 42 deletions(-) diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts index e072cc2..a612927 100644 --- a/apps/web/src/lib/auth.ts +++ b/apps/web/src/lib/auth.ts @@ -259,6 +259,30 @@ export function can(required: string): boolean { return hasPermission(claims.permissions, required) } +/** + * Whether /v1/auth/session may manage API keys (`bgp:access:admin`). + * Mirrors backend `requirePerm` for JWT (is_admin / permissions) and API-key operator. + */ +export function sessionCanManageApiKeys(session: { + role?: string + kind?: string + is_admin?: boolean + permissions?: readonly string[] +} | null | undefined): boolean { + if (!session) return false + const jwtPath = + session.kind === 'jwt' || + session.is_admin === true || + (session.permissions?.length ?? 0) > 0 + if (jwtPath) { + return ( + session.is_admin === true || + hasPermission(session.permissions ?? [], 'bgp:access:admin') + ) + } + return session.role === 'operator' +} + /** Nav path → minimum permission to show the item. Sync with app-shell NAV. */ export function permissionForPath(pathname: string): string | null { if (pathname === '/' || pathname.startsWith('/dashboard')) { diff --git a/apps/web/src/routes/_auth/access.tsx b/apps/web/src/routes/_auth/access.tsx index 74ea51b..674d051 100644 --- a/apps/web/src/routes/_auth/access.tsx +++ b/apps/web/src/routes/_auth/access.tsx @@ -11,6 +11,7 @@ import { PageHeader } from '@/components/page-header' import { Badge } from '@/components/reui/badge' import { SectionCards, type SectionCardItem } from '@/components/section-cards' import { SectionCardsSkeleton } from '@/components/skeletons' +import { sessionCanManageApiKeys } from '@/lib/auth' import { authSessionQueryOptions } from '@/queries/auth' import { apiKeysQueryOptions } from '@/queries/api-keys' @@ -21,11 +22,11 @@ export const Route = createFileRoute('/_auth/access')({ function AccessComponent() { const sessionQuery = useQuery(authSessionQueryOptions()) const session = sessionQuery.data ?? null - const isOperator = session?.role === 'operator' + const canManageKeys = sessionCanManageApiKeys(session) const keysQuery = useQuery({ ...apiKeysQueryOptions(), - enabled: isOperator, + enabled: canManageKeys, }) const keys = keysQuery.data ?? [] @@ -73,16 +74,31 @@ function AccessComponent() { function refetchAll() { void sessionQuery.refetch() - if (isOperator) void keysQuery.refetch() + if (canManageKeys) void keysQuery.refetch() } + const sessionKindLabel = + session?.kind === 'jwt' ? 'Portal JWT' : session?.kind === 'apikey' ? 'API-ключ' : null + + const sessionAccessLabel = (() => { + if (!session) return null + if (session.kind === 'jwt' || session.is_admin || (session.permissions?.length ?? 0) > 0) { + if (session.is_admin) return 'admin (portal)' + if (sessionCanManageApiKeys(session)) return 'bgp:access:admin' + return session.permissions?.length + ? session.permissions.slice(0, 3).join(', ') + : 'без access:admin' + } + return session.role || '—' + })() + return (
Обновить @@ -94,36 +110,48 @@ function AccessComponent() { {session ? ( +
+

Tenant

+

{session.tenant_id}

+
+
+

Доступ

+

{sessionAccessLabel}

+
+ {sessionKindLabel ? (
-

Tenant

-

{session.tenant_id}

+

Тип

+

{sessionKindLabel}

+ ) : null} + {session.email ? (
-

Роль

-

{session.role}

+

Email

+

{session.email}

+ ) : null}
) : ( - Не удалось определить сессию. Укажите токен в{' '} - - настройках - {' '} - (для dev-окружения — dev при включённом demo-seed). - {sessionQuery.isError && sessionQuery.error instanceof Error ? ( - {sessionQuery.error.message} - ) : null} + Не удалось определить сессию. Укажите токен в{' '} + + настройках + {' '} + (для dev-окружения — dev при включённом demo-seed). + {sessionQuery.isError && sessionQuery.error instanceof Error ? ( + {sessionQuery.error.message} + ) : null} )} - {isOperator ? ( + {canManageKeys ? ( <> {keysQuery.isLoading ? ( @@ -140,10 +168,9 @@ function AccessComponent() { ) : session ? ( - Управление API-ключами доступно только роли operator. Текущая роль:{' '} - {session.role}. Для выдачи ключей войдите с - operator-ключом или создайте ключ через API / переменную{' '} - EVOBGP_API_KEYS. + Управление API-ключами доступно роли operator (API-ключ) или portal JWT + с is_admin / правом bgp:access:admin. + Текущий доступ: {sessionAccessLabel}. ) : null}
diff --git a/apps/web/src/types/api.ts b/apps/web/src/types/api.ts index b1aa61a..092b96a 100644 --- a/apps/web/src/types/api.ts +++ b/apps/web/src/types/api.ts @@ -341,13 +341,20 @@ export type JobsResponse = Page export type AppSettings = Record // ---- Auth / API keys ---- +export type ApiKeyRole = 'viewer' | 'editor' | 'operator' | 'node' + +/** GET /v1/auth/session — API key has role; portal JWT uses kind/permissions/is_admin. */ export type AuthSession = { tenant_id: string - role: 'viewer' | 'editor' | 'operator' | 'node' + /** API-key role; empty for portal JWT sessions. */ + role: ApiKeyRole | '' + kind?: 'apikey' | 'jwt' + user_id?: string + email?: string + permissions?: string[] + is_admin?: boolean } -export type ApiKeyRole = AuthSession['role'] - export type ApiKey = { id: string name: string diff --git a/docs/access.md b/docs/access.md index 99b010a..2877e79 100644 --- a/docs/access.md +++ b/docs/access.md @@ -60,14 +60,18 @@ opkey|01ARZ3NDEKTSV4RRFFQ69G5FAV|operator,nodekey|01ARZ3NDEKTSV4RRFFQ69G5FAV|nod ### Управление через API и UI -При подключённой БД operator может: +При подключённой БД управлять ключами может: -- `GET|POST /v1/api-keys`, `GET|PATCH|DELETE /v1/api-keys/{id}`, `POST /v1/api-keys/{id}/rotate` — см. OpenAPI, тег **API keys**. -- В веб-панели: **Права доступа** (`/access`) → блок «API-ключи» (только для роли `operator`). Токен для браузера — в **Настройки** (`/settings`). +- API-ключ с ролью **`operator`**, или +- portal JWT с **`is_admin`** / правом **`bgp:access:admin`** (админ auth-portal). + +Эндпоинты: `GET|POST /v1/api-keys`, `GET|PATCH|DELETE /v1/api-keys/{id}`, `POST /v1/api-keys/{id}/rotate` — см. OpenAPI, тег **API keys**. + +В веб-панели: **Права доступа** (`/access`) → блок «API-ключи». Токен для браузера (API-key gate) — в **Настройки** (`/settings`). Полный токен возвращается **один раз** в ответе `201` (создание) и `200` (ротация). В списках — только `prefix` (первые 8 символов). В БД хранится SHA-256 токена, не plaintext. -`GET /v1/auth/session` — текущие `tenant_id` и `role` (для UI). +`GET /v1/auth/session` — `tenant_id`, `kind` (`apikey`|`jwt`), для API-ключа — `role`; для JWT — `user_id`, `email`, `permissions`, `is_admin`. ### Роли @@ -178,13 +182,13 @@ http://localhost:5173,http://127.0.0.1:5173,https://ui.example.com ## Краткая матрица (ориентир) -| Действие | viewer | editor | operator | node | -|----------|--------|--------|----------|------| -| GET модули, ревизии, peers, speakers | да | да | да | нет | -| POST/PATCH/DELETE CRUD сущностей | нет | да | да | нет | -| apply, rollback, PATCH settings | нет | нет | да | нет | -| Управление API-ключами (`/v1/api-keys`) | нет | нет | да | нет | -| bundle, latest revision, enroll | нет | нет | нет | да | +| Действие | viewer | editor | operator | node | portal admin / `bgp:access:admin` | +|----------|--------|--------|----------|------|-----------------------------------| +| GET модули, ревизии, peers, speakers | да | да | да | нет | по permissions | +| POST/PATCH/DELETE CRUD сущностей | нет | да | да | нет | по permissions | +| apply, rollback, PATCH settings | нет | нет | да | нет | `bgp:operations:admin` | +| Управление API-ключами (`/v1/api-keys`) | нет | нет | да | нет | да | +| bundle, latest revision, enroll | нет | нет | нет | да | нет | Точные проверки по каждому маршруту — в коде `internal/httpapi` и в схеме безопасности операций в OpenAPI. diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 3490676..b849fbf 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -676,13 +676,32 @@ components: AuthSession: type: object - required: [tenant_id, role] + required: [tenant_id, kind] properties: tenant_id: $ref: "#/components/schemas/ResourceId" + kind: + type: string + enum: [apikey, jwt] + description: apikey — Bearer API key; jwt — portal SSO token. role: type: string - enum: [viewer, editor, operator, node] + description: > + API-key role (viewer|editor|operator|node). Empty string for portal JWT sessions. + user_id: + type: string + description: JWT sub (portal sessions only). + email: + type: string + description: JWT email claim (portal sessions only). + permissions: + type: array + items: + type: string + description: JWT permissions (bgp:*); portal sessions only. + is_admin: + type: boolean + description: Portal is_admin claim; grants all bgp permissions. ApiKey: type: object