iac/apps/resources/openapi.yaml

842 lines
33 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

openapi: 3.0.3
info:
title: Sarex Resources API
version: "1.0.0"
description: |
REST API сервиса **sarex-resources** (`platform/sarex-resources`) — управление
ресурсами/проектами, их типами (иерархия), локациями, а также разрешениями
сервисных аккаунтов на ресурсы и на компании.
Сервис написан на Python (**Django 4.1 + Django REST Framework**, GeoDjango/PostGIS).
Роутинг задаётся в `server/config/urls.py`:
- `resource-management/` — Django-admin;
- `api/v1/` — основной API (`apps.resource.urls`);
- `api/v2/` — расширенный API ресурсов (`apps.resource.urls_v2`).
Списочные и CRUD-эндпоинты строятся `rest_framework.routers.DefaultRouter`
поверh `ModelViewSet`; часть операций — отдельные `APIView`.
### Аутентификация
На уровне приложения аутентификация и permission-классы DRF **отключены**
(`DEFAULT_AUTHENTICATION_CLASSES = []`, permission-классы не заданы —
эффективно `AllowAny`). Ограничение доступа обеспечивается сетевым слоем
(istio/ingress, внутрикластерный доступ к `sarex-resources-service`).
### Пагинация
Используется `rest_framework.pagination.LimitOffsetPagination` с очень большим
`PAGE_SIZE` (100000). Списочные ответы оборачиваются в
`{ count, next, previous, results }`; постранично управляется параметрами
`limit` и `offset`.
### Идентификаторы ресурсов
У ресурса есть внутренний `id` (int, в API почти не используется), публичный
`public_id`/`id` (UUID — основной идентификатор в URL detail-эндпоинтов v1/v2)
и устаревший `target_id` (`_target_id`, int) для обратной совместимости.
Удаление — «мягкое» (`deleted=True`), объекты с `deleted=True` из выборок
исключаются.
### Замечания (расхождения кода)
- Detail-роуты `resource` в v1/v2 ищут объект по `public_id` (UUID), а не по
первичному ключу; путь при этом выглядит как `/api/v1/resource/{public_id}/`.
- Ряд аналитических эндпоинтов (`users-grouped-by-resource`,
`users-with-resources`, `resources-grouped-by-sa`) возвращают
«сырые» структуры (`dict`/списки), не обёрнутые в пагинацию.
- `bulk_delete/resource_permission/` и `permissions-bulk/` выполняют
**жёсткое** удаление разрешений (`.delete()`), в отличие от «мягкого»
удаления сущностей.
- `RetrieveResourceByTargetIdAPIView` при отсутствии ресурса возвращает
`404` без тела.
contact:
name: sarex-resources
url: https://gitlab/platform/sarex-resources
servers:
- url: https://api.sarex.io/resource-management
description: Production admin (istio VirtualService, prefix /resource-management)
- url: http://sarex-resources-service.resources-prod
description: Внутрикластерный адрес (ClusterIP), production namespace
- url: http://sarex-resources-service.resources-stage
description: Внутрикластерный адрес (ClusterIP), stage namespace
- url: http://localhost:8888
description: Локальный запуск (docker-compose, 8888 → 8000)
tags:
- name: resources
description: Ресурсы (проекты)
- name: resources_v2
description: Расширенные ресурсы (api/v2)
- name: resource_types
description: Типы ресурсов (иерархия)
- name: locations
description: Локации (гео-данные)
- name: resource_permissions
description: Разрешения сервисных аккаунтов на ресурсы
- name: company_permissions
description: Разрешения сервисных аккаунтов на компании
- name: analytics
description: Служебные/аналитические эндпоинты (группировки, интеграция с Sarex)
paths:
/api/v1/resource/:
get:
tags: [resources]
summary: Список ресурсов
parameters:
- { name: tenant_id, in: query, schema: { type: string }, description: "ID компании; поддерживает список через запятую" }
- { name: type, in: query, schema: { type: integer } }
- { name: parent, in: query, schema: { type: integer }, description: "ID родителя; фильтрация по поддереву (ltree)" }
- { name: show_in_overview, in: query, schema: { type: boolean } }
- { name: id, in: query, schema: { type: string }, description: "public_id (UUID); список через запятую" }
- { name: target_id, in: query, schema: { type: string }, description: "устаревший target_id; список через запятую" }
- { name: service_accounts, in: query, schema: { type: string }, description: "UUID сервисных аккаунтов через запятую (доступные ресурсы)" }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/PaginatedResourceList"
post:
tags: [resources]
summary: Создать ресурс
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceWrite" }
responses:
"201":
description: Created
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceWrite" }
/api/v1/resource/{public_id}/:
parameters:
- { name: public_id, in: path, required: true, schema: { type: string, format: uuid } }
get:
tags: [resources]
summary: Получить ресурс по public_id
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceRetrieve" }
"404": { description: Не найден }
delete:
tags: [resources]
summary: Мягкое удаление ресурса (deleted=true)
responses:
"204": { description: No Content }
/api/v1/resource_type/:
get:
tags: [resource_types]
summary: Список типов ресурсов
parameters:
- { name: parent, in: query, schema: { type: integer }, description: "ID родителя; фильтрация по поддереву" }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/PaginatedResourceTypeList" }
post:
tags: [resource_types]
summary: Создать тип ресурса
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceTypeWrite" }
responses:
"201":
description: Created
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceTypeWrite" }
/api/v1/resource_type/filters/:
get:
tags: [resource_types]
summary: "Значения фильтра типов ресурсов (потомки родителя)"
parameters:
- { name: parent, in: query, schema: { type: integer }, description: "ID родителя; вернёт его потомков" }
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
parent:
type: array
items:
type: object
properties:
id: { type: integer }
name: { type: string }
/api/v1/resource_type/{id}/:
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
get:
tags: [resource_types]
summary: Получить тип ресурса
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceTypeRetrieve" }
delete:
tags: [resource_types]
summary: Мягкое удаление типа ресурса
responses:
"204": { description: No Content }
/api/v1/location/:
get:
tags: [locations]
summary: Список локаций
parameters:
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/PaginatedLocationList" }
post:
tags: [locations]
summary: Создать локацию
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/LocationWrite" }
responses:
"201":
description: Created
content:
application/json:
schema: { $ref: "#/components/schemas/LocationWrite" }
/api/v1/location/{id}/:
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
get:
tags: [locations]
summary: Получить локацию
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/LocationRetrieve" }
delete:
tags: [locations]
summary: Мягкое удаление локации
responses:
"204": { description: No Content }
/api/v1/resource_permission/:
get:
tags: [resource_permissions]
summary: Список разрешений на ресурсы
parameters:
- { name: public_resource_id, in: query, schema: { type: string }, description: "UUID через запятую" }
- { name: resource_id, in: query, schema: { type: string }, description: "внутренние id через запятую" }
- { name: service_account, in: query, schema: { type: string }, description: "UUID через запятую" }
- { name: tenant_id, in: query, schema: { type: string }, description: "ID компаний через запятую (по resource.tenant_id)" }
- { name: company_id, in: query, schema: { type: string }, description: "синоним tenant_id" }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/PaginatedResourcePermissionList" }
post:
tags: [resource_permissions]
summary: "Создать разрешение(я) на ресурс"
description: "Принимает один объект или массив (bulk). Поле resource определяется по public_resource_id."
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/ResourcePermissionWrite"
- type: array
items: { $ref: "#/components/schemas/ResourcePermissionWrite" }
responses:
"201":
description: Created
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/ResourcePermissionWrite"
- type: array
items: { $ref: "#/components/schemas/ResourcePermissionWrite" }
/api/v1/resource_permission/{id}/:
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
get:
tags: [resource_permissions]
summary: Получить разрешение на ресурс
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourcePermissionRetrieve" }
delete:
tags: [resource_permissions]
summary: Мягкое удаление разрешения
responses:
"204": { description: No Content }
/api/v1/company_resource_permission/:
get:
tags: [company_permissions]
summary: Список разрешений на компании
parameters:
- { name: service_account, in: query, schema: { type: string }, description: "UUID через запятую" }
- { name: tenant_id, in: query, schema: { type: string }, description: "ID компаний через запятую" }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/PaginatedCompanyPermissionList" }
post:
tags: [company_permissions]
summary: "Создать разрешение(я) на компанию"
description: "Принимает один объект или массив (bulk_create)."
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/CompanyPermission"
- type: array
items: { $ref: "#/components/schemas/CompanyPermission" }
responses:
"201":
description: Created
/api/v1/company_resource_permission/{id}/:
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
get:
tags: [company_permissions]
summary: Получить разрешение на компанию
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/CompanyPermission" }
delete:
tags: [company_permissions]
summary: Мягкое удаление разрешения на компанию
responses:
"204": { description: No Content }
/api/v1/targets/{target_id}/resource/:
get:
tags: [resources]
summary: Получить ресурс по устаревшему target_id
parameters:
- { name: target_id, in: path, required: true, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceRetrieve" }
"404": { description: Не найден }
/api/v1/bulk_delete/resource_permission/:
post:
tags: [resource_permissions]
summary: Массовое (жёсткое) удаление разрешений на ресурсы
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: array
items: { type: integer }
required: [id]
responses:
"200": { description: OK }
"400": { description: "id не передан или содержит не-int" }
/api/v1/service-accounts/:
get:
tags: [analytics]
summary: Сервисные аккаунты, имеющие доступ к ресурсу
description: "Нужно указать ровно один из resource_id (UUID) или target_id (int)."
parameters:
- { name: resource_id, in: query, schema: { type: string, format: uuid } }
- { name: target_id, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
count: { type: integer }
results:
type: array
items: { type: string, format: uuid }
"400": { description: "не указан либо указаны оба параметра / ошибка парсинга" }
"404": { description: Ресурс не найден }
/api/v1/users-grouped-by-resource/:
get:
tags: [analytics]
summary: "Пользователи, сгруппированные по ресурсам"
description: "Обращается к ядру Sarex за списком пользователей. Возвращает map resource_id → [user_id]."
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
additionalProperties:
type: array
items: { type: integer }
/api/v1/resources-grouped-by-sa/:
get:
tags: [analytics]
summary: "Ресурсы, сгруппированные по сервисным аккаунтам"
description: "Учитывает прямые (read) и компанейские разрешения. Возвращает map service_account → [public_resource_id]."
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
additionalProperties:
type: array
items: { type: string, format: uuid }
/api/v1/users-with-resources/:
post:
tags: [analytics]
summary: "Доступные ресурсы для списка пользователей в рамках компании"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: array
items: { type: integer }
tenant_id: { type: integer }
required: [id, tenant_id]
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
results:
type: array
items:
type: object
properties:
id: { type: integer }
unrestricted_access: { type: boolean }
resources:
type: array
items:
type: object
properties:
id: { type: string, format: uuid }
name: { type: string }
"400": { description: "не передан id или tenant_id / некорректный tenant_id" }
/api/v1/permissions-bulk/:
patch:
tags: [resource_permissions]
summary: "Синхронизация (bulk) разрешений на ресурсы и компании"
description: |
Приводит разрешения к переданному состоянию: создаёт недостающие,
удаляет лишние (жёстко). `permissions` — map service_account → [resource_id],
`unrestricted_permissions` — map service_account → bool (доступ ко всей компании).
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
tenant_id: { type: integer }
permissions:
type: object
additionalProperties:
type: array
items: { type: string, format: uuid }
unrestricted_permissions:
type: object
additionalProperties: { type: boolean }
required: [tenant_id, permissions]
responses:
"200": { description: OK }
/api/v2/resource/:
get:
tags: [resources_v2]
summary: Список расширенных ресурсов
parameters:
- { name: tenant_id, in: query, schema: { type: string }, description: "список через запятую" }
- { name: type, in: query, schema: { type: integer } }
- { name: parent, in: query, schema: { type: integer } }
- { name: show_in_overview, in: query, schema: { type: boolean } }
- { name: id, in: query, schema: { type: string }, description: "public_id (UUID) через запятую" }
- { name: target_id, in: query, schema: { type: integer } }
- { name: service_accounts, in: query, schema: { type: string } }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/PaginatedResourceExpandedList" }
post:
tags: [resources_v2]
summary: Создать расширенный ресурс
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceExpandedWrite" }
responses:
"201":
description: Created
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceExpandedWrite" }
/api/v2/resource/{public_id}/:
parameters:
- { name: public_id, in: path, required: true, schema: { type: string, format: uuid } }
- { name: tenant_id, in: query, schema: { type: string }, description: "фильтр по компаниям (через запятую) при retrieve" }
get:
tags: [resources_v2]
summary: Получить расширенный ресурс по public_id
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceExpandedRetrieve" }
"404": { description: "there is no resource with provided id" }
patch:
tags: [resources_v2]
summary: Частичное обновление расширенного ресурса
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceExpandedPartialUpdate" }
responses:
"200":
description: OK
content:
application/json:
schema: { $ref: "#/components/schemas/ResourceExpandedPartialUpdate" }
delete:
tags: [resources_v2]
summary: Мягкое удаление расширенного ресурса
responses:
"204": { description: No Content }
components:
schemas:
PermissionType:
type: integer
description: "0=read, 1=write, 2=delete, 3=admin"
enum: [0, 1, 2, 3]
ResourceRetrieve:
type: object
properties:
id: { type: string, format: uuid, description: "public_id" }
name: { type: string }
type: { type: integer, nullable: true }
tenant_id: { type: integer }
location: { type: integer, nullable: true }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
target_id: { type: integer, nullable: true }
code: { type: string }
ResourceWrite:
type: object
properties:
id: { type: string, format: uuid, nullable: true, readOnly: true }
name: { type: string }
type: { type: integer, nullable: true }
tenant_id: { type: integer }
location: { type: integer, nullable: true }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
target_id: { type: integer, nullable: true }
code: { type: string }
created_at: { type: string, format: date-time, readOnly: true }
updated_at: { type: string, format: date-time, readOnly: true }
required: [name, tenant_id, target_id]
PaginatedResourceList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/ResourceRetrieve" }
ResourceTypeRetrieve:
type: object
properties:
id: { type: integer }
name: { type: string }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
ResourceTypeWrite:
type: object
properties:
id: { type: integer, readOnly: true }
name: { type: string }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
created_at: { type: string, format: date-time, readOnly: true }
updated_at: { type: string, format: date-time, readOnly: true }
required: [name]
PaginatedResourceTypeList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/ResourceTypeRetrieve" }
LocationRetrieve:
type: object
properties:
id: { type: integer }
name: { type: string }
coordinate_system: { type: integer }
geometry: { type: object, description: "GeoJSON-геометрия" }
geography: { type: object, description: "GeoJSON-геометрия (geography)" }
latitude: { type: string, format: decimal }
longitude: { type: string, format: decimal }
created_by: { type: integer }
LocationWrite:
allOf:
- $ref: "#/components/schemas/LocationRetrieve"
- type: object
properties:
created_at: { type: string, format: date-time, readOnly: true }
updated_at: { type: string, format: date-time, readOnly: true }
required: [name, coordinate_system, geometry, geography, latitude, longitude]
PaginatedLocationList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/LocationRetrieve" }
ResourcePermissionRetrieve:
type: object
properties:
id: { type: integer }
service_account: { type: string, format: uuid }
public_resource_id: { type: string, format: uuid }
type: { $ref: "#/components/schemas/PermissionType" }
created_by: { type: integer }
ResourcePermissionWrite:
type: object
properties:
id: { type: integer, readOnly: true }
service_account: { type: string, format: uuid }
public_resource_id: { type: string, format: uuid }
type: { $ref: "#/components/schemas/PermissionType" }
created_by: { type: integer }
resource: { type: integer, description: "заполняется сервером по public_resource_id" }
created_at: { type: string, format: date-time, readOnly: true }
updated_at: { type: string, format: date-time, readOnly: true }
required: [service_account, public_resource_id]
PaginatedResourcePermissionList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/ResourcePermissionRetrieve" }
CompanyPermission:
type: object
properties:
id: { type: integer, readOnly: true }
service_account: { type: string, format: uuid }
tenant_id: { type: integer }
created_by: { type: integer }
created_at: { type: string, format: date-time, readOnly: true }
updated_at: { type: string, format: date-time, readOnly: true }
required: [service_account, tenant_id]
PaginatedCompanyPermissionList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/CompanyPermission" }
ResourcePhoto:
type: object
properties:
id: { type: integer }
author_id: { type: integer }
created_at: { type: string, format: date-time }
image: { type: string, format: uri }
ResourceExpandedRead:
type: object
properties:
id: { type: string, format: uuid }
name: { type: string }
type: { type: integer, nullable: true }
tenant_id: { type: integer }
location: { type: integer, nullable: true }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
target_id: { type: integer, nullable: true }
code: { type: string }
description: { type: string }
location_verbose: { type: string }
latitude: { type: number, format: float }
longitude: { type: number, format: float }
photos:
type: array
items: { $ref: "#/components/schemas/ResourcePhoto" }
attributes: { type: object }
planning_widget_id: { type: integer, nullable: true }
work_schedule_project_id: { type: integer, nullable: true }
show_in_overview: { type: boolean }
widgets: { type: object }
meta: { type: object }
ResourceExpandedRetrieve:
allOf:
- $ref: "#/components/schemas/ResourceExpandedRead"
PaginatedResourceExpandedList:
type: object
properties:
count: { type: integer }
next: { type: string, nullable: true }
previous: { type: string, nullable: true }
results:
type: array
items: { $ref: "#/components/schemas/ResourceExpandedRead" }
ResourceExpandedWrite:
type: object
properties:
id: { type: string, format: uuid, nullable: true, readOnly: true }
name: { type: string }
type: { type: integer, nullable: true }
tenant_id: { type: integer }
parent_id: { type: integer, nullable: true }
created_by: { type: integer }
target_id: { type: integer, nullable: true }
description: { type: string }
location_verbose: { type: string }
latitude: { type: number, format: float }
longitude: { type: number, format: float }
attributes: { type: object }
planning_widget_id: { type: integer, nullable: true }
work_schedule_project_id: { type: integer, nullable: true }
code: { type: string }
show_in_overview: { type: boolean }
widgets: { type: object }
meta: { type: object }
required: [name, tenant_id, target_id]
ResourceExpandedPartialUpdate:
type: object
properties:
id: { type: string, format: uuid, readOnly: true }
name: { type: string }
type: { type: integer, nullable: true }
tenant_id: { type: integer, readOnly: true }
location: { type: integer, nullable: true }
parent_id: { type: integer, nullable: true }
created_by: { type: integer, readOnly: true }
target_id: { type: integer, readOnly: true }
code: { type: string }
description: { type: string }
location_verbose: { type: string }
latitude: { type: number, format: float }
longitude: { type: number, format: float }
attributes: { type: object }
planning_widget_id: { type: integer, nullable: true }
show_in_overview: { type: boolean }
widgets: { type: object }
meta: { type: object }