Compare commits

..

1 Commits

Author SHA1 Message Date
Manuel Raynaud
4e1a9b6a27 📝(contributing) add signed commits paragraph
Add a paragraph to inform that only signed commits are accepted now.
2025-04-14 18:14:41 +02:00
5 changed files with 21 additions and 177 deletions

View File

@@ -12,10 +12,6 @@ and this project adheres to
- 🚩 add homepage feature flag #861
## Fixed
- 🔒️(back) don't allow an owner to change or delete other owner accesses
## [3.1.0] - 2025-04-07

View File

@@ -48,6 +48,10 @@ All commit messages must adhere to the following format:
Implemented login and signup features, and integrated OAuth2 for social login.
```
## Signing commits
Only signed commits are accepted. They can be signed using a SSH or GPG key. Github documentation about signing commits contains all the information you need : https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#about-commit-signature-verification
## Changelog Update
Please add a line to the changelog describing your development. The changelog entry should include a brief summary of the changes, this helps in tracking changes effectively and keeping everyone informed. We usually include the title of the pull request, followed by the pull request ID to finish the log entry. The changelog line should be less than 80 characters in total.

View File

@@ -1113,17 +1113,11 @@ class DocumentAccess(BaseAccess):
"""
roles = self._get_roles(self.document, user)
is_owner_or_admin = bool(set(roles).intersection(set(PRIVILEGED_ROLES)))
if self.role == RoleChoices.OWNER:
# An owner can only delete its own access if other owners exist
can_delete = (
self.user
and self.user.id == user.id
RoleChoices.OWNER in roles
and self.document.accesses.filter(role=RoleChoices.OWNER).count() > 1
)
# An owner can only update its own access to a non-owner role
# and only if other owners exist
set_role_to = (
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
if can_delete

View File

@@ -1,7 +1,6 @@
"""
Test document accesses API endpoints for users in impress's core app.
"""
# pylint: disable=too-many-lines
import random
from uuid import uuid4
@@ -625,18 +624,14 @@ def test_api_document_accesses_update_administrator_to_owner(
@pytest.mark.parametrize("via", VIA)
@pytest.mark.parametrize(
"role", [role for role in models.RoleChoices if role != models.RoleChoices.OWNER]
)
def test_api_document_accesses_update_owner(
via,
role,
mock_user_teams,
mock_reset_connections, # pylint: disable=redefined-outer-name
):
"""
A user who is an owner in a document should be allowed to update
a user access for this document for all roles except owner.
a user access for this document whatever the role.
"""
user = factories.UserFactory(with_owned_document=True)
@@ -653,7 +648,9 @@ def test_api_document_accesses_update_owner(
)
factories.UserFactory()
access = factories.UserDocumentAccessFactory(document=document, role=role)
access = factories.UserDocumentAccessFactory(
document=document,
)
old_values = serializers.DocumentAccessSerializer(instance=access).data
new_values = {
@@ -732,30 +729,11 @@ def test_api_document_accesses_update_owner_self(
access.refresh_from_db()
assert access.role == "owner"
# Add another owner and it should now work only for user, not for team
# Add another owner and it should now work
factories.UserDocumentAccessFactory(document=document, role="owner")
if via == USER:
factories.UserDocumentAccessFactory(document=document, role="owner")
user_id = str(access.user_id) if via == USER else None
with mock_reset_connections(document.id, user_id):
response = client.put(
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
data={
**old_values,
"role": new_role,
"user_id": old_values.get("user", {}).get("id")
if old_values.get("user") is not None
else None,
},
format="json",
)
assert response.status_code == 200
access.refresh_from_db()
assert access.role == new_role
else:
user_id = str(access.user_id) if via == USER else None
with mock_reset_connections(document.id, user_id):
response = client.put(
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
data={
@@ -767,41 +745,13 @@ def test_api_document_accesses_update_owner_self(
},
format="json",
)
assert response.status_code == 403
assert response.status_code == 200
access.refresh_from_db()
assert access.role == new_role
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_update_owner_from_other_owner(via, mock_user_teams):
"""
A user who is an owner in a document should not be allowed to update
access for another user who is also an owner in the document.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="owner"
)
other_owner = factories.UserFactory()
access = factories.UserDocumentAccessFactory(
document=document, user=other_owner, role="owner"
)
old_values = serializers.DocumentAccessSerializer(instance=access).data
response = client.put(
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
data={**old_values, "role": models.RoleChoices.ADMIN},
format="json",
)
assert response.status_code == 403
# Delete
def test_api_document_accesses_delete_anonymous():
@@ -948,18 +898,14 @@ def test_api_document_accesses_delete_administrator_on_owners(via, mock_user_tea
@pytest.mark.parametrize("via", VIA)
@pytest.mark.parametrize(
"role", [role for role in models.RoleChoices if role != models.RoleChoices.OWNER]
)
def test_api_document_accesses_delete_owners(
via,
role,
mock_user_teams,
mock_reset_connections, # pylint: disable=redefined-outer-name
):
"""
Users should be able to delete the document access of another user
for a document of which they are owner but can not delete other owners.
for a document of which they are owner.
"""
user = factories.UserFactory()
@@ -975,7 +921,7 @@ def test_api_document_accesses_delete_owners(
document=document, team="lasuite", role="owner"
)
access = factories.UserDocumentAccessFactory(document=document, role=role)
access = factories.UserDocumentAccessFactory(document=document)
assert models.DocumentAccess.objects.count() == 2
assert models.DocumentAccess.objects.filter(user=access.user).exists()
@@ -989,79 +935,6 @@ def test_api_document_accesses_delete_owners(
assert models.DocumentAccess.objects.count() == 1
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_delete_other_owners(via, mock_user_teams):
"""
A user should not be able to delete the document access of another owner.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="owner"
)
access = factories.UserDocumentAccessFactory(document=document, role="owner")
assert models.DocumentAccess.objects.count() == 2
assert models.DocumentAccess.objects.filter(user=access.user).exists()
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 403
assert models.DocumentAccess.objects.count() == 2
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_delete_self_owner_if_other_owner_exists(
via,
mock_reset_connections, # pylint: disable=redefined-outer-name
):
"""
A user should be able to delete their own ownership access if there is another owner in the
document.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
access = None
if via == USER:
access = factories.UserDocumentAccessFactory(
document=document, user=user, role="owner"
)
elif via == TEAM:
pytest.skip("Implement when team ownership is implemented")
factories.UserDocumentAccessFactory(document=document, role="owner")
assert (
models.DocumentAccess.objects.filter(document=document, role="owner").count()
== 2
)
with mock_reset_connections(document.id, str(access.user_id)):
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 204
assert (
models.DocumentAccess.objects.filter(document=document, role="owner").count()
== 1
)
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_delete_owners_last_owner(via, mock_user_teams):
"""
@@ -1084,16 +957,10 @@ def test_api_document_accesses_delete_owners_last_owner(via, mock_user_teams):
document=document, team="lasuite", role="owner"
)
assert (
models.DocumentAccess.objects.filter(document=document, role="owner").count()
== 1
)
assert models.DocumentAccess.objects.count() == 2
response = client.delete(
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 403
assert (
models.DocumentAccess.objects.filter(document=document, role="owner").count()
== 1
)
assert models.DocumentAccess.objects.count() == 2

View File

@@ -150,23 +150,6 @@ def test_models_document_access_get_abilities_for_owner_of_owner():
document=access.document, role="owner"
).user
abilities = access.get_abilities(user)
assert abilities == {
"destroy": False,
"retrieve": True,
"update": False,
"partial_update": False,
"set_role_to": [],
}
def test_models_document_access_get_abilities_for_owner_of_self_with_other_owner():
"""
Check abilities of self access for the owner of a document when there is at least one other
owner left.
"""
access = factories.UserDocumentAccessFactory(role="owner")
factories.UserDocumentAccessFactory(document=access.document, role="owner")
abilities = access.get_abilities(access.user)
assert abilities == {
"destroy": True,
"retrieve": True,