feat(auth): add sessionCanManageApiKeys function and update access control logic
CI / changes (push) Successful in 5s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 53s
CI / go (push) Successful in 56s
CI / bird2 (push) Successful in 17s
CI / release (push) Successful in 4m2s

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.
This commit is contained in:
Denozordec
2026-07-21 03:18:59 +07:00
parent fd2fd8298d
commit 2e3e1493f5
5 changed files with 123 additions and 42 deletions
+24
View File
@@ -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')) {
+53 -26
View File
@@ -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 (
<div className="flex flex-col gap-6">
<PageHeader
title="Права доступа"
description="API-ключи control plane и текущая сессия Bearer-токена."
actions={
isOperator ? (
canManageKeys ? (
<Button variant="outline" size="sm" onClick={refetchAll} disabled={refreshing}>
<RefreshCw className={refreshing ? 'animate-spin' : ''} />
Обновить
@@ -94,36 +110,48 @@ function AccessComponent() {
{session ? (
<PanelCard
title="Текущая сессия"
description="Tenant и роль ключа, с которым открыта панель."
description="Tenant и права текущего Bearer (API-ключ или portal JWT)."
contentClassName="grid gap-3 py-4 text-sm sm:grid-cols-2"
>
<div>
<p className="text-muted-foreground">Tenant</p>
<p className="break-all font-mono text-xs">{session.tenant_id}</p>
</div>
<div>
<p className="text-muted-foreground">Доступ</p>
<p className="font-mono text-xs">{sessionAccessLabel}</p>
</div>
{sessionKindLabel ? (
<div>
<p className="text-muted-foreground">Tenant</p>
<p className="break-all font-mono text-xs">{session.tenant_id}</p>
<p className="text-muted-foreground">Тип</p>
<p className="font-mono text-xs">{sessionKindLabel}</p>
</div>
) : null}
{session.email ? (
<div>
<p className="text-muted-foreground">Роль</p>
<p className="font-mono">{session.role}</p>
<p className="text-muted-foreground">Email</p>
<p className="break-all text-xs">{session.email}</p>
</div>
) : null}
</PanelCard>
) : (
<PanelCard contentClassName="py-4 text-sm text-muted-foreground">
Не удалось определить сессию. Укажите токен в{' '}
<Link
to="/settings"
search={{ tab: 'connection' }}
className="text-primary underline-offset-4 hover:underline"
>
настройках
</Link>{' '}
(для dev-окружения <code className="text-xs">dev</code> при включённом demo-seed).
{sessionQuery.isError && sessionQuery.error instanceof Error ? (
<span className="mt-2 block text-destructive">{sessionQuery.error.message}</span>
) : null}
Не удалось определить сессию. Укажите токен в{' '}
<Link
to="/settings"
search={{ tab: 'connection' }}
className="text-primary underline-offset-4 hover:underline"
>
настройках
</Link>{' '}
(для dev-окружения <code className="text-xs">dev</code> при включённом demo-seed).
{sessionQuery.isError && sessionQuery.error instanceof Error ? (
<span className="mt-2 block text-destructive">{sessionQuery.error.message}</span>
) : null}
</PanelCard>
)}
{isOperator ? (
{canManageKeys ? (
<>
{keysQuery.isLoading ? (
<SectionCardsSkeleton count={3} />
@@ -140,10 +168,9 @@ function AccessComponent() {
</>
) : session ? (
<PanelCard contentClassName="py-4 text-sm text-muted-foreground">
Управление API-ключами доступно только роли <strong>operator</strong>. Текущая роль:{' '}
<span className="font-mono">{session.role}</span>. Для выдачи ключей войдите с
operator-ключом или создайте ключ через API / переменную{' '}
<code className="text-xs">EVOBGP_API_KEYS</code>.
Управление API-ключами доступно роли <strong>operator</strong> (API-ключ) или portal JWT
с <strong>is_admin</strong> / правом <code className="text-xs">bgp:access:admin</code>.
Текущий доступ: <span className="font-mono">{sessionAccessLabel}</span>.
</PanelCard>
) : null}
</div>
+10 -3
View File
@@ -341,13 +341,20 @@ export type JobsResponse = Page<JobRow>
export type AppSettings = Record<string, unknown>
// ---- 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
+15 -11
View File
@@ -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.
+21 -2
View File
@@ -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