Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c924706aa8 |
@@ -7,15 +7,12 @@ and this project adheres to
|
||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
- 📝(doc) change documentation folder name to "documentation" #876
|
||||
|
||||
## Added
|
||||
|
||||
- 🚩 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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/suitenumerique/docs">
|
||||
<img alt="Docs" src="/docs/assets/docs-logo.png" width="300" />
|
||||
<img alt="Docs" src="/documentation/assets/docs-logo-black-white.png" width="300" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -20,7 +20,7 @@ Welcome to Docs! The open source document editor where your notes can become kno
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<img src="/docs/assets/docs_live_collaboration_light.gif" width="100%" align="center"/>
|
||||
<img src="/documentation/assets/docs_live_collaboration_light.gif" width="100%" align="center"/>
|
||||
|
||||
## Why use Docs ❓
|
||||
|
||||
@@ -193,5 +193,5 @@ We are proud sponsors of [BlockNotejs](https://www.blocknotejs.org/) and [Yjs](h
|
||||
We are always looking for new public partners (we are currently onboarding the Netherlands 🇳🇱🧀), feel free to [reach out](mailto:docs@numerique.gouv.fr) if you are interested in using or contributing to Docs.
|
||||
|
||||
<p align="center">
|
||||
<img src="/docs/assets/europe_opensource.png" width="50%"/>
|
||||
<img src="/documentation/assets/europe_opensource.png" width="50%"/>
|
||||
</p>
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 5.8 MiB After Width: | Height: | Size: 5.8 MiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||