Files
ACORE/apps/mattermost_connector/views.py
T
lokses 04b1625fde Безопасность: закрытие 5 открытых находок ревью 25.07
- 2FA: убраны /api/ и /media/ из ALLOWED_PREFIXES — staff-сессия без TOTP
  больше не дотягивается до API/медиа (устройства в /api/ идут без staff-сессии)
- Nextcloud «Обмен»: writable только для staff (не для любого logged-in)
- Nextcloud путь отдела/адрес аптеки: safe_component() санитайзит сегмент
  (сепараторы/управляющие/ведущие точки) в провижининге и в браузере
- Mattermost: shared secret кнопки «Виконано» перенесён из query string в
  context (тело POST) — без утечки в логи/Referer
- Login: throttle на кэше (5 попыток / 5 мин, 429); logout только POST (CSRF),
  logout-ссылки в шаблонах → POST-формы

Тесты обновлены под enforcement 2FA (login_2fa helper) и новые правила.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:38:44 +03:00

102 lines
4.1 KiB
Python
Executable File
Raw 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.
import json
import logging
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core import signing
from django.http import JsonResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
logger = logging.getLogger(__name__)
Employee = get_user_model()
def _find_employee(mattermost_user_id):
if not mattermost_user_id:
return None
return Employee.objects.filter(mattermost_user_id=mattermost_user_id).first()
@csrf_exempt
@require_POST
def slash_command_tasks(request):
"""Обработчик слэш-команды /tasks — Mattermost шлёт application/x-www-form-urlencoded.
https://developers.mattermost.com/integrate/slash-commands/custom/"""
token = request.POST.get('token', '')
if not settings.MATTERMOST_SLASH_COMMAND_TOKEN or token != settings.MATTERMOST_SLASH_COMMAND_TOKEN:
return HttpResponseForbidden('invalid token')
employee = _find_employee(request.POST.get('user_id'))
if employee is None:
return JsonResponse({
'response_type': 'ephemeral',
'text': 'Не знайшов вашого облікового запису в порталі ACORE. Зверніться до HR.',
})
from apps.tasks.models import Task
tasks = (
Task.objects.for_user(employee)
.exclude(status__in=['done', 'cancelled'])
.order_by('due_date', '-priority')[:10]
)
if not tasks:
text = 'У вас немає активних задач. 🎉'
else:
base_url = settings.PORTAL_BASE_URL.rstrip('/')
lines = ['**Ваші активні задачі:**']
for task in tasks:
due = f' (до {task.due_date:%d.%m.%Y})' if task.due_date else ''
lines.append(f'- [{task.title}]({base_url}/tasks/{task.pk}/){due}')
text = '\n'.join(lines)
return JsonResponse({'response_type': 'ephemeral', 'text': text})
@csrf_exempt
@require_POST
def task_done_action(request):
"""Обработчик кнопки «Виконано» в інтерактивному повідомленні бота.
https://developers.mattermost.com/integrate/interactive-messages/
Fail-closed: потрібен MATTERMOST_INTERACTIVE_TOKEN (у context тіла POST)
+ валідний підписаний task id.
"""
expected = settings.MATTERMOST_INTERACTIVE_TOKEN
if not expected:
return HttpResponseForbidden('invalid token')
try:
payload = json.loads(request.body)
except (json.JSONDecodeError, UnicodeDecodeError):
return HttpResponseForbidden('bad payload')
from apps.mattermost_connector.services.bot_service import TASK_ACTION_SALT, TASK_ACTION_MAX_AGE
context = payload.get('context') or {}
# Токен приходить у context (тіло POST), не в URL — без витоку в логи/Referer.
provided = context.get('token') or payload.get('token') or ''
if provided != expected:
return HttpResponseForbidden('invalid token')
try:
task_id = signing.loads(context.get('t', ''), salt=TASK_ACTION_SALT, max_age=TASK_ACTION_MAX_AGE)
except signing.BadSignature:
return JsonResponse({'ephemeral_text': 'Посилання застаріло або недійсне.'})
employee = _find_employee(payload.get('user_id'))
if employee is None:
return JsonResponse({'ephemeral_text': 'Не знайшов вашого облікового запису в порталі ACORE.'})
from apps.tasks.models import Task
task = Task.objects.filter(pk=task_id).first()
if task is None:
return JsonResponse({'ephemeral_text': 'Задачу не знайдено — можливо, її видалили.'})
if not task.is_participant(employee):
return JsonResponse({'ephemeral_text': 'У вас немає прав змінювати статус цієї задачі.'})
task.set_status('done')
return JsonResponse({'ephemeral_text': f'✅ Задача «{task.title}» позначена виконаною.'})