meraproject/openspec/changes/archive/2026-07-14-service-project-members/design.md

81 lines
6.1 KiB
Markdown
Raw Permalink Normal View History

## Context
User-reader уже умеет upsert участника проекта через `PUT /api/project-members` (`project_members_write.py`): валидация раздела/роли/сотрудника и запись в `tMerakomisTeamMember`. Авторизация завязана на `X-Acting-Emp-Id` и `can_write_project_member` (паритет PHP `Rules::isRwMember`).
Внешнее приложение знает `project_id`, `emp_id`, `section_id`, `role` и имеет только service API-ключ. Impersonation РП/ГИП для него неудобна и нежелательна.
Справочник разделов проекта (`GET /api/project-sections`) уже отдаёт `section_id`; отдельного поля «раздел + подраздел» в модели нет — в TeamMember хранится один `section`.
## Goals / Non-Goals
**Goals:**
- Дать внешним сервисам upsert участника проекта только по API-ключу.
- Переиспользовать существующую бизнес-валидацию и upsert-логику без дублирования SQL.
- Явно отделить service-контракт от портального acting-потока (отдельный URL).
- Зафиксировать контракт в docs и тестах.
**Non-Goals:**
- Создание/редактирование сотрудника в `tMerakomisEmp`.
- Изменение поведения `PUT /api/project-members` (acting остаётся обязательным).
- Расширение `GET /api/project-sections` (parent / full_name) — не требуется для этого сценария.
- Отдельный API-ключ или scopes на эндпоинт (пока один `USER_READER_API_KEY`).
- Запись разделов проекта (`tMerakomisProjectSection`) или создание команды проекта.
## Decisions
### 1. Отдельный путь `PUT /api/service/project-members` (вариант B)
**Выбор:** новый URL под префиксом `/api/service/…`, а не опциональный acting на существующем PUT.
**Почему:**
- Контракт для интеграций явный: «сервис доверяет ключу», не «человек с правами».
- Не ослабляем портальный эндпоинт случайным вызовом без acting.
- Проще документировать и аудировать.
**Альтернатива (A):** тот же PUT, acting опционален → отвергнута: смешивает модели доверия.
### 2. Переиспользование `ProjectMemberWrite` + `validate_member_write` + `upsert_team_member`
**Выбор:** тот же JSON-контракт и те же helper-функции; новый handler только пропускает acting/permissions.
**Почему:** один источник правил (раздел ∈ проекта, роль валидна, emp не архивный, у проекта есть team).
**Реализация:** тонкий роут в новом модуле (например `project_members_service.py`) или второй handler в `project_members_write.py` с `Depends(require_api_key)` без `parse_acting_emp_id`. Предпочтительно отдельный файл/роутер, чтобы граница service vs portal была видна в `main.py`.
### 3. Авторизация = только API-ключ
**Выбор:** при включённом `USER_READER_API_KEY` — обязателен ключ; при пустом ключе (локальная отладка) — как у остальных защищённых эндпоинтов. Проверки `isRwMember` нет.
**Почему:** соответствует заявленному сценарию внешнего приложения. Риск компенсируется контролем ключа на стороне деплоя.
### 4. Ответ и ошибки
**Выбор:** тот же shape, что у portal PUT: `ok: true` + поля item `GET /api/project-members`. Те же коды валидации (`400`/`404`), без `403 forbidden` из-за прав acting (acting нет). `401` при неверном/отсутствующем ключе.
### 5. Документация
Обновить матрицу доступа и раздел записи в `docs/user-reader-api.md` (+ краткая строка в `DEVELOPERS.md`). В сценариях интеграции указать: внешний сервис → service PUT; UI/табель от имени сотрудника → portal PUT.
## Risks / Trade-offs
| Risk | Mitigation |
|------|------------|
| Утечка API-ключа = полный write состава любых проектов | Ключ только на сервере; ротация; в будущем — scopes / отдельный ключ для write |
| Клиенты путают portal и service PUT | Разные URL + явная секция в docs; portal по-прежнему требует Acting |
| Дублирование логики при копировании кода | Обязательно вызывать существующие validate/upsert, не копировать SQL |
| Нет audit «кто добавил» (нет acting) | Принять для v1; при необходимости позже писать в History с маркером `service` |
## Migration Plan
1. Задеплоить user-reader с новым роутом (обратно совместимо).
2. Внешнее приложение переключить на `PUT /api/service/project-members`.
3. Rollback: убрать роут / откатить образ — portal PUT не затронут.
## Open Questions
- Нужен ли позже audit/history для service-записей? (вне scope v1)
- Нужен ли отдельный write-ключ или scope? (вне scope v1; один общий ключ)