Expose PUT /api/service/project-members for external apps that authenticate with only the API key, reusing portal validation while keeping acting-based writes unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
6.1 KiB
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
- Задеплоить user-reader с новым роутом (обратно совместимо).
- Внешнее приложение переключить на
PUT /api/service/project-members. - Rollback: убрать роут / откатить образ — portal PUT не затронут.
Open Questions
- Нужен ли позже audit/history для service-записей? (вне scope v1)
- Нужен ли отдельный write-ключ или scope? (вне scope v1; один общий ключ)