diff --git a/backend/api/v1/documents/permissions.py b/backend/api/v1/documents/permissions.py index f7df2e26..6f1dba54 100644 --- a/backend/api/v1/documents/permissions.py +++ b/backend/api/v1/documents/permissions.py @@ -54,9 +54,14 @@ class DocumentReadOrModelPerms(AdminOrActionOrModelPerms): if request.method in SAFE_METHODS or action in ("acknowledge", "download"): return bool(request.user and request.user.is_authenticated) - # FSM actions и update/destroy - проверяем на уровне объекта + # CREATE → разрешаем всем аутентифицированным (документы создаются в draft) + # FSM workflow обеспечит контроль: draft → review → approve → publish + if action == "create": + return bool(request.user and request.user.is_authenticated) + + # FSM actions, update/destroy и related documents - проверяем на уровне объекта # Разрешаем здесь, has_object_permission проверит django-rules - if action in self.FSM_ACTIONS or action in ("update", "partial_update", "destroy"): + if action in self.FSM_ACTIONS or action in ("update", "partial_update", "destroy", "add_related", "remove_related"): return bool(request.user and request.user.is_authenticated) # небезопасные → staff ИЛИ стандартные модельные права (add/change/delete) @@ -79,6 +84,10 @@ class DocumentReadOrModelPerms(AdminOrActionOrModelPerms): if action in self.FSM_ACTIONS: return user.has_perm("documents.change_document", obj) + # add_related/remove_related → проверяем django-rules change_document + if action in ("add_related", "remove_related"): + return user.has_perm("documents.change_document", obj) + # PUT/PATCH → проверяем django-rules change_document if request.method in ("PUT", "PATCH"): return user.has_perm("documents.change_document", obj) diff --git a/backend/tests/api/v1/documents/test_documents_api.py b/backend/tests/api/v1/documents/test_documents_api.py index 27be1967..8b08d2c8 100644 --- a/backend/tests/api/v1/documents/test_documents_api.py +++ b/backend/tests/api/v1/documents/test_documents_api.py @@ -5,10 +5,12 @@ from __future__ import annotations Тесты CRUD API документов (/api/v1/documents/ + экшен acknowledge) по чек-листу. Политика доступа (актуальная): -- Создавать/редактировать/удалять — только админы и обладатели модельных прав. +- Создавать — все аутентифицированные пользователи (документы создаются в draft). +- Редактировать/удалять — только создатель, админы или обладатели модельных прав. +- Approve/Publish — только пользователи с правом change_document (django-rules). - Читать/скачивать/ознакомливаться — только аутентифицированные, и только документы, - предназначенные им (или с sent_to_all=True). Получатели не имеют доступ к документам, - которые им не предназначались. + предназначенные им (или с sent_to_all=True). Обычные пользователи видят только PUBLISHED + документы (кроме своих собственных). Все функции и классы снабжены докстрингами (Google-style). """ @@ -215,8 +217,9 @@ class TestAuthAndPermissions: ): """Обычный user без модельных прав: - list → 200 (пустой список, если нет доступных документов); - - CRUD → 403; - - detail/ack чужого документа с sent_to_all=False → 403. + - create → 201 (разрешено создание в draft); + - update/delete чужого документа → 403/404; + - detail/ack чужого документа с sent_to_all=False → 403/404. """ u = make_user("u@example.com") client = auth_client(u) @@ -225,13 +228,18 @@ class TestAuthAndPermissions: # list — 200 (даже без прав), но результаты ограничены доступными документами assert client.get(list_url).status_code == 200 - # POST (multipart) — запрещено + # POST (multipart) — теперь разрешено всем аутентифицированным resp = client.post( list_url, - {"title": "T", "file": _file(), "sent_to_all": True}, + {"title": "My Document", "file": _file(), "sent_to_all": True}, format="multipart", ) - assert resp.status_code == 403 + assert resp.status_code in (200, 201), f"Expected 200/201, got {resp.status_code}: {resp.content}" + # Проверяем что документ создан в статусе draft + doc_id = resp.json()["id"] + doc = Document.objects.get(id=doc_id) + assert doc.status == Document.Status.DRAFT, f"Expected DRAFT status, got {doc.status}" + assert doc.uploaded_by == u # object not intended for user author = make_user("a@example.com") @@ -462,6 +470,39 @@ class TestCreate: assert active.id in ids assert inactive.id not in ids + def test_regular_user_can_create_document_in_draft( + self, auth_client, make_user, api_urls + ): + """Обычный пользователь без прав может создать документ, и он создается в draft.""" + regular_user = make_user("regular@example.com", staff=False) + client = auth_client(regular_user) + + # Создаем документ + r = client.post( + api_urls["list"], + { + "title": "User Created Document", + "description": "Created by regular user", + "file": _file(name="user_doc.pdf"), + "sent_to_all": True, + }, + format="multipart", + ) + assert r.status_code in (200, 201), f"Expected 200/201, got {r.status_code}: {r.content}" + + body = r.json() + assert body["title"] == "User Created Document" + assert body["sent_to_all"] is True + + # Проверяем что документ создан в статусе DRAFT + doc = Document.objects.get(pk=body["id"]) + assert doc.status == Document.Status.DRAFT + assert doc.uploaded_by == regular_user + + # Проверяем что обычный пользователь видит свой документ в любом статусе + detail_r = client.get(api_urls["detail"](doc.pk)) + assert detail_r.status_code == 200 + # -------------------- C. Чтение (GET) -------------------- class TestRead: