WIP: separation of duties implementation (incomplete)
Added: - is_not_document_uploader predicate - Specific FSM action permissions (approve_document, publish_document, etc.) - Tests for author cannot approve/publish own documents Note: This is work in progress and will be reviewed/simplified after comparing with develop branch
This commit is contained in:
parent
ad2e5ab76d
commit
5fd9d9bf94
@ -80,8 +80,22 @@ class DocumentReadOrModelPerms(AdminOrActionOrModelPerms):
|
||||
if user.is_staff or _has_any_model_perm(user, "documents", "document"):
|
||||
return True
|
||||
|
||||
# FSM actions → проверяем django-rules change_document
|
||||
if action in self.FSM_ACTIONS:
|
||||
# FSM actions → проверяем специфичные django-rules права (разделение обязанностей!)
|
||||
if action == "submit_for_review":
|
||||
return user.has_perm("documents.submit_for_review_document", obj)
|
||||
elif action == "approve":
|
||||
return user.has_perm("documents.approve_document", obj)
|
||||
elif action == "publish":
|
||||
return user.has_perm("documents.publish_document", obj)
|
||||
elif action == "reject":
|
||||
return user.has_perm("documents.reject_document", obj)
|
||||
elif action in ("archive", "unarchive"):
|
||||
return user.has_perm("documents.archive_document", obj)
|
||||
elif action == "return_to_draft":
|
||||
# Вернуть в draft может автор или менеджер
|
||||
return user.has_perm("documents.change_document", obj)
|
||||
elif action == "revert":
|
||||
# Откатить версию может автор или менеджер
|
||||
return user.has_perm("documents.change_document", obj)
|
||||
|
||||
# add_related/remove_related → проверяем django-rules change_document
|
||||
|
||||
@ -78,6 +78,14 @@ def is_same_department(user, document):
|
||||
return document.departments.filter(id=user.department.id).exists()
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def is_not_document_uploader(user, document):
|
||||
"""Пользователь НЕ загрузил документ (для разделения обязанностей)"""
|
||||
if document is None:
|
||||
return True
|
||||
return document.uploaded_by != user
|
||||
|
||||
|
||||
@rules.predicate
|
||||
def has_acknowledged_document(user, document):
|
||||
"""Пользователь уже ознакомился с документом"""
|
||||
@ -150,6 +158,41 @@ rules.add_rule(
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# FSM TRANSITIONS PERMISSIONS (разделение обязанностей)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Submit for review: автор может отправить СВОЙ документ на рассмотрение
|
||||
rules.add_rule(
|
||||
'documents.submit_for_review_document',
|
||||
is_superuser | is_document_uploader | can_manage_documents
|
||||
)
|
||||
|
||||
# Approve: ТОЛЬКО менеджеры/superuser, НЕ автор (разделение обязанностей!)
|
||||
rules.add_rule(
|
||||
'documents.approve_document',
|
||||
is_superuser | (can_manage_documents & is_not_document_uploader)
|
||||
)
|
||||
|
||||
# Publish: ТОЛЬКО менеджеры/superuser, НЕ автор (разделение обязанностей!)
|
||||
rules.add_rule(
|
||||
'documents.publish_document',
|
||||
is_superuser | (can_manage_documents & is_not_document_uploader)
|
||||
)
|
||||
|
||||
# Reject: ТОЛЬКО менеджеры/superuser
|
||||
rules.add_rule(
|
||||
'documents.reject_document',
|
||||
is_superuser | can_manage_documents
|
||||
)
|
||||
|
||||
# Archive/Unarchive: менеджеры или автор может архивировать свой документ
|
||||
rules.add_rule(
|
||||
'documents.archive_document',
|
||||
is_superuser | is_document_uploader | can_manage_documents
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PERMISSIONS ДЛЯ МОДЕЛЕЙ
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@ -453,3 +453,66 @@ class TestAuthorCanSubmitOwnDocument:
|
||||
# Перечитываем документ из БД
|
||||
doc = Document.objects.get(pk=doc.id)
|
||||
assert doc.title == 'Updated Title'
|
||||
|
||||
def test_author_cannot_approve_own_document(
|
||||
self, api_client, make_user
|
||||
):
|
||||
"""КРИТИЧНО: Автор НЕ может одобрить свой документ (разделение обязанностей!)"""
|
||||
from documents.models import Document
|
||||
|
||||
author = make_user("author@example.com", staff=False)
|
||||
|
||||
# Создаем документ и отправляем на рассмотрение
|
||||
doc = make_document(uploaded_by=author, status=Document.Status.DRAFT)
|
||||
|
||||
# Автор отправляет на рассмотрение - это ОК
|
||||
api_client.force_authenticate(user=author)
|
||||
url = reverse('api:v1:documents-submit-for-review', kwargs={'pk': doc.id})
|
||||
response = api_client.post(url)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Проверяем что статус изменился
|
||||
doc = Document.objects.get(pk=doc.id)
|
||||
assert doc.status == Document.Status.IN_REVIEW
|
||||
|
||||
# Теперь автор пытается ОДОБРИТЬ свой документ - должно быть запрещено!
|
||||
url = reverse('api:v1:documents-approve', kwargs={'pk': doc.id})
|
||||
response = api_client.post(url)
|
||||
|
||||
# Должен получить 403 ИЛИ 400 (FSM не разрешает или нет прав)
|
||||
assert response.status_code in [
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
status.HTTP_400_BAD_REQUEST
|
||||
], f"Author should NOT be able to approve own document! Got {response.status_code}"
|
||||
|
||||
# Статус НЕ должен измениться
|
||||
doc = Document.objects.get(pk=doc.id)
|
||||
assert doc.status == Document.Status.IN_REVIEW, "Status should remain IN_REVIEW"
|
||||
|
||||
def test_author_cannot_publish_own_document(
|
||||
self, api_client, make_user
|
||||
):
|
||||
"""КРИТИЧНО: Автор НЕ может опубликовать свой документ (разделение обязанностей!)"""
|
||||
from documents.models import Document
|
||||
|
||||
author = make_user("author@example.com", staff=False)
|
||||
|
||||
# Создаем документ в статусе APPROVED (как будто менеджер одобрил)
|
||||
doc = make_document_with_status(uploaded_by=author, status='approved')
|
||||
doc = Document.objects.get(pk=doc.id)
|
||||
assert doc.status == Document.Status.APPROVED
|
||||
|
||||
# Автор пытается ОПУБЛИКОВАТЬ - должно быть запрещено!
|
||||
api_client.force_authenticate(user=author)
|
||||
url = reverse('api:v1:documents-publish', kwargs={'pk': doc.id})
|
||||
response = api_client.post(url)
|
||||
|
||||
# Должен получить 403 ИЛИ 400
|
||||
assert response.status_code in [
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
status.HTTP_400_BAD_REQUEST
|
||||
], f"Author should NOT be able to publish own document! Got {response.status_code}"
|
||||
|
||||
# Статус НЕ должен измениться
|
||||
doc = Document.objects.get(pk=doc.id)
|
||||
assert doc.status == Document.Status.APPROVED, "Status should remain APPROVED"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user