From fe382183349edcc35246e3377d4d6934a6d99621 Mon Sep 17 00:00:00 2001 From: Denis Date: Fri, 22 May 2026 12:23:31 +0300 Subject: [PATCH] =?UTF-8?q?H-3657:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20Personal=20Access=20Tokens=20=D0=B2=20v2=20swagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - info.description: подробная how-to секция «Авторизация по личному API-токену» (создание, формат Bearer-заголовка, tenant-резолв, отсутствие scopes, жизненный цикл и автo-revoke при блокировке юзера, запрет на управление под Bearer). - securitySchemes.BearerAuth: новая схема http/bearer (PAT). - security (global): + BearerAuth, чтобы Try-it-out предлагал три способа auth. - Теги user-api-token и admin-user-api-token. - Эндпоинты: * GET /user-api-token, POST /user-api-token, POST /user-api-token/revoke * GET /admin/user-api-token, POST /admin/user-api-token/revoke Все management-эндпоинты помечены `security: SessionAuth` и явно документируют 403 под Bearer (защита от рекурсии). - Схемы: UserApiToken, UserApiTokenList, UserApiTokenCreated (с plain), UserApiTokenCreateForm, UserApiTokenAdmin (+ user/updatedByUser), UserApiTokenAdminList, UserApiTokenStatus. Co-Authored-By: Claude Opus 4.7 (1M context) --- v2/swagger.yaml | 404 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 403 insertions(+), 1 deletion(-) diff --git a/v2/swagger.yaml b/v2/swagger.yaml index 163908a..84c6649 100644 --- a/v2/swagger.yaml +++ b/v2/swagger.yaml @@ -27,6 +27,72 @@ info: {sessionId} - you will get this form /mobile/auth method {sessionSecret} - you will get this form /mobile/auth method the last one (x-hrbox-embed) is for opening web pages in WebView to remove website footer and header + + --- + + # Авторизация по личному API-токену (Personal Access Token, PAT) + + Для серверных интеграций (web-zaim и т.п.) удобнее не держать сессию, + а использовать постоянный токен. Токен работает **только на `/api/v2/*`** + (для `/api/v1/*`, `/api/v3/*`, `/admin/*` и веб-страниц — игнорируется). + + ## Получение токена + + 1. Юзеру, от имени которого будет работать интеграция, нужно право + `api-tokens-manage` (выдаётся точечно администратором). + 2. Юзер открывает `/profile/settings/api-tokens` в веб-интерфейсе и жмёт + «Создать токен» — задаёт название и (опционально) дату истечения. + Plain-секрет **показывается один раз** в этом окне, после закрытия + восстановить нельзя. + 3. Альтернативно — `POST /api/v2/user-api-token` под session-авторизацией + (см. ниже про management-эндпоинты). Plain также возвращается + один раз в поле `data.plain`. + + Формат plain-токена: `hrb_` + 32 url-safe символов = 36 символов всего. + В БД хранится только HMAC-SHA-256 хеш и видимый префикс (первые 12 + символов с `hrb_`). + + ## Использование токена + + Подписывайте каждый запрос заголовком `Authorization`: + + ``` + Authorization: Bearer hrb_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + ``` + + Tenant авторезолвится по `Host`-заголовку запроса, поэтому + `x-hrbox-tenant-id` / `x-hrbox-tenant-domain` **необязательны**. + Если всё же передаёте — будут строго провалидированы, невалидный + идентификатор тенанта → 400. `x-hrbox-session-id` / `x-hrbox-session-code` + при Bearer-аутентификации **не нужны**. + + ## Права и scopes + + ⚠️ **Гранулярных scopes у токена нет.** Токен наследует **все права того + юзера**, под которым выпущен. Это означает что для интеграции нужно + создать **отдельного системного пользователя** и выдать ему ровно те + права, которые требуются (например, `wallets-transactions` для нового + `/api/v2/admin/wallet*`). Не используйте свой личный аккаунт. + + ## Отзыв и жизненный цикл + + - Юзер отзывает свой токен через `POST /api/v2/user-api-token/revoke` + (или из UI). + - Администратор с правом `api-tokens-admin` может посмотреть/отозвать + любой токен тенанта через `/api/v2/admin/user-api-token*`. Plain + админу **никогда не виден**, только префикс. + - При блокировке юзера (`reg_status_id = BLOCKED`) все его токены + автоматически отзываются. При разблокировке **обратно не восстанавливаются** — + нужно выпускать новые. + - Если у токена выставлен `expires_at` — после этой даты он перестаёт + работать сам (статус `EXPIRED`). + + ## Управление токенами под Bearer запрещено + + Management-эндпоинты (`/user-api-token*`, `/admin/user-api-token*`) + отдают `403`, если запрос аутентифицирован Bearer-токеном — нельзя + выпускать или отзывать токены от имени токена. Управляйте только + под обычной web-сессией. contact: email: yuriy@hrbox.io @@ -67,6 +133,16 @@ tags: description: Админский API транзакций кошелька — проведение начислений/списаний (form/excel) и просмотр. - name: file-processing description: Статусы асинхронных обработок файлов (excel-импорт/экспорт, архивы). Видны только записи текущего пользователя. + - name: user-api-token + description: | + Личные API-токены (Personal Access Tokens) для серверных интеграций. + Юзер управляет только своими токенами. Permission `api-tokens-manage`. + Management-эндпоинты доступны **только под обычной session-авторизацией** + (Bearer запрещён). См. секцию «Авторизация по личному API-токену» выше. + - name: admin-user-api-token + description: | + Админский обзор и отзыв любых PAT тенанта. Permission `api-tokens-admin`. + Plain-секрет недоступен — виден только `token_prefix`. paths: /mobile/bind/{id}/{token}: @@ -7265,6 +7341,199 @@ paths: schema: $ref: '#/components/schemas/FileProcessing' + /user-api-token: + get: + tags: [user-api-token] + summary: Список своих API-токенов + description: | + Возвращает PAT текущего юзера. Plain-секрет уже недоступен — + для идентификации используется `token_prefix` (первые 12 символов, + начинаются с `hrb_`). + operationId: userApiTokenIndex + security: + - SessionAuth: [] + parameters: + - name: page + in: query + schema: { type: integer, minimum: 1, default: 1 } + - name: per-page + in: query + schema: { type: integer, minimum: 1, default: 20 } + - name: sort + in: query + schema: + type: string + enum: [created_at, "-created_at", last_used_at, "-last_used_at", expires_at, "-expires_at"] + description: По умолчанию `-created_at`. + responses: + 200: + description: Список токенов + content: + application/json: + schema: + $ref: '#/components/schemas/UserApiTokenList' + 403: + description: Запрос пришёл под Bearer-токеном (запрещено) или нет права `api-tokens-manage`. + post: + tags: [user-api-token] + summary: Создать новый API-токен (plain возвращается один раз) + description: | + Создаёт PAT для текущего юзера. **Plain-секрет (`data.plain`) + возвращается только в этом ответе и больше нигде не доступен** — + сохраните его сразу. + + `expires_at` опционален; если задан, должен быть не в прошлом + (валидируется по началу текущего дня). Токен работает весь + указанный день включительно (23:59:59). + + Доступно только под session-авторизацией (запрос под Bearer → 403). + operationId: userApiTokenCreate + security: + - SessionAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserApiTokenCreateForm' + responses: + 200: + description: Токен создан (либо ошибки валидации в стандартном формате `httpJsonResponse`). + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/httpJsonResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/UserApiTokenCreated' + 403: + description: Запрос пришёл под Bearer-токеном или нет права `api-tokens-manage`. + + /user-api-token/revoke: + post: + tags: [user-api-token] + summary: Отозвать свой API-токен + description: | + Идемпотентно: повторный revoke уже отозванного токена просто ничего не делает. + Чужой токен отозвать нельзя — будет 403/404. + + Доступно только под session-авторизацией (запрос под Bearer → 403). + operationId: userApiTokenRevoke + security: + - SessionAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [id] + properties: + id: + type: string + format: uuid + responses: + 200: + description: Успех + content: + application/json: + schema: + $ref: '#/components/schemas/httpJsonResponse' + 400: + description: Параметр `id` не строка или не uuid. + 403: + description: Токен принадлежит другому юзеру, либо запрос под Bearer, либо нет права `api-tokens-manage`. + 404: + description: Токен не найден. + + /admin/user-api-token: + get: + tags: [admin-user-api-token] + summary: Список API-токенов всех юзеров тенанта + description: | + Админский обзор PAT. Permission `api-tokens-admin`. Plain-секрет + здесь **никогда не возвращается** (только `token_prefix`). + + Доступно только под session-авторизацией (запрос под Bearer → 403). + operationId: adminUserApiTokenIndex + security: + - SessionAuth: [] + parameters: + - name: page + in: query + schema: { type: integer, minimum: 1, default: 1 } + - name: per-page + in: query + schema: { type: integer, minimum: 1, default: 20 } + - name: sort + in: query + schema: + type: string + enum: [created_at, "-created_at", last_used_at, "-last_used_at", expires_at, "-expires_at"] + - name: user_id + in: query + schema: { type: string, format: uuid } + description: Фильтр по владельцу токена. + - name: name + in: query + schema: { type: string, maxLength: 100 } + description: Like-поиск по названию токена. + - name: token_prefix + in: query + schema: { type: string, maxLength: 16 } + description: 'Like-поиск по префиксу (например `hrb_abc12345`).' + - name: status_id + in: query + schema: { type: string } + description: | + Comma-separated `UserApiTokenStatus`: `1` — Активен, `2` — Отозван, + `3` — Истёк. Без параметра — все статусы. + responses: + 200: + description: Список + content: + application/json: + schema: + $ref: '#/components/schemas/UserApiTokenAdminList' + 403: + description: Запрос под Bearer, либо нет права `api-tokens-admin`. + + /admin/user-api-token/revoke: + post: + tags: [admin-user-api-token] + summary: Отозвать любой токен тенанта + description: | + Идемпотентно. Доступно только под session-авторизацией (Bearer → 403). + operationId: adminUserApiTokenRevoke + security: + - SessionAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [id] + properties: + id: + type: string + format: uuid + responses: + 200: + description: Успех + content: + application/json: + schema: + $ref: '#/components/schemas/httpJsonResponse' + 400: + description: Параметр `id` не строка или не uuid. + 403: + description: Запрос под Bearer, либо нет права `api-tokens-admin`. + 404: + description: Токен не найден. + components: securitySchemes: SessionAuth: @@ -7277,6 +7546,16 @@ components: in: header name: X-Hrbox-Client-Id description: "Авторизация с использованием API-ключа. Требуемые заголовки: Content-Type, X-Hrbox-Client-Id, X-Hrbox-Client-Secret." + BearerAuth: + type: http + scheme: bearer + bearerFormat: PAT + description: | + Personal Access Token (постоянный API-токен пользователя). Заголовок: + `Authorization: Bearer hrb_<32 url-safe>`. Tenant авторезолвится по + `Host`, `x-hrbox-tenant-id` опционален. Работает только на `/api/v2/*`. + Токен наследует все права юзера, под которым выпущен (granular scopes + не поддерживаются). Подробности — в описании API. schemas: httpJsonResponse: type: object @@ -12220,6 +12499,128 @@ components: _meta: $ref: '#/components/schemas/_meta' + UserApiTokenStatus: + type: integer + description: | + Статус личного API-токена (вычисляется по `revoked_at` / `expires_at`): + * `1` — Активен + * `2` — Отозван + * `3` — Истёк + enum: [1, 2, 3] + example: 1 + + UserApiToken: + type: object + description: | + Личный API-токен (PAT). Plain-секрет в этой схеме отсутствует — он + возвращается только в ответе на создание (см. `UserApiTokenCreated`). + properties: + id: { type: string, format: uuid } + user_id: { type: string, format: uuid } + name: + type: string + maxLength: 100 + description: Человекочитаемое название от юзера (HTML-теги вырезаются на бэке). + token_prefix: + type: string + description: | + Первые 12 символов токена с приставкой `hrb_`. Безопасно для отображения + и поиска (например `hrb_abc12345`). + example: "hrb_abc12345" + created_at: { type: string, format: date-time } + expires_at: + type: string + format: date-time + nullable: true + description: Если null — без срока. Срок включает указанный день целиком (до 23:59:59). + last_used_at: + type: string + format: date-time + nullable: true + description: Обновляется при использовании, не чаще раза в минуту. + revoked_at: + type: string + format: date-time + nullable: true + description: Заполняется при revoke; если null — токен не отозван. + + UserApiTokenList: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/UserApiToken' + _meta: + $ref: '#/components/schemas/_meta' + + UserApiTokenCreated: + description: | + Расширение `UserApiToken` дополнительным полем `plain` — это + единственный момент, когда секрет возвращается клиенту. + allOf: + - $ref: '#/components/schemas/UserApiToken' + - type: object + required: [plain] + properties: + plain: + type: string + description: | + Полный plain-токен. Формат `hrb_<32 url-safe>`. **Сохраните + немедленно — повторно его получить невозможно.** + example: "hrb_aBcD3fGhIjKlMnOpQrStUvWxYz012345" + + UserApiTokenCreateForm: + type: object + required: [name] + properties: + name: + type: string + maxLength: 100 + description: Название токена. Сохраняется после HTML-purify. + example: "Интеграция web-zaim — wallet" + expires_at: + type: string + format: date + nullable: true + description: | + Дата истечения `yyyy-MM-dd`. Не может быть в прошлом (валидируется + по началу текущего дня). На бэке к ней приклеивается время + `23:59:59`, токен работает весь указанный день включительно. + Если опущено — токен бессрочный. + example: "2026-12-31" + + UserApiTokenAdmin: + description: | + Запись токена в админском списке: к базовым полям добавляются вложенные + `user` и `updatedByUser` (последний — кто последним менял запись; + для авто-revoke при блокировке — система). + allOf: + - $ref: '#/components/schemas/UserApiToken' + - type: object + properties: + user: + type: object + properties: + id: { type: string, format: uuid } + name: { type: string } + updatedByUser: + type: object + nullable: true + properties: + id: { type: string, format: uuid } + name: { type: string } + + UserApiTokenAdminList: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/UserApiTokenAdmin' + _meta: + $ref: '#/components/schemas/_meta' + parameters: boardDateTypeParam: in: query @@ -12405,4 +12806,5 @@ components: security: - SessionAuth: [ ] - - ApiKeyAuth: [ ] \ No newline at end of file + - ApiKeyAuth: [ ] + - BearerAuth: [ ] \ No newline at end of file