Skip to content

Commit 958f156

Browse files
authored
Merge branch 'main' into compose-bits
2 parents b5e5987 + 58bf507 commit 958f156

File tree

82 files changed

+3006
-1078
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+3006
-1078
lines changed

CHANGELOG.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ and this project adheres to
1111
### Added
1212

1313
- ✨(frontend) add customization for translations #857
14+
- ✨(frontend) Duplicate a doc #1078
1415
- 📝(project) add troubleshoot doc #1066
1516
- 📝(project) add system-requirement doc #1066
1617
- 🔧(front) configure x-frame-options to DENY in nginx conf #1084
1718
- (doc) add documentation to install with compose #855
19+
- ✨(backend) allow to disable checking unsafe mimetype on attachment upload
20+
- ✨Ask for access #1081
1821

1922
### Changed
2023

@@ -26,11 +29,18 @@ and this project adheres to
2629

2730
### Fixed
2831

29-
-🐛(frontend) table of content disappearing #982
30-
-🐛(frontend) fix multiple EmojiPicker #1012
31-
-🐛(frontend) fix meta title #1017
32-
-🔧(git) set LF line endings for all text files #1032
33-
-📝(docs) minor fixes to docs/env.md
32+
- 🐛(frontend) table of content disappearing #982
33+
- 🐛(frontend) fix multiple EmojiPicker #1012
34+
- 🐛(frontend) fix meta title #1017
35+
- 🔧(git) set LF line endings for all text files #1032
36+
- 📝(docs) minor fixes to docs/env.md
37+
- ✨(backend) support `_FILE` environment variables for secrets #912
38+
- ✨(frontend) support `_FILE` environment variables for secrets #912
39+
40+
### Removed
41+
42+
- 🔥(frontend) remove Beta from logo #1095
43+
3444

3545
## [3.3.0] - 2025-05-06
3646

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ frontend-lint: ## run the frontend linter
348348
.PHONY: frontend-lint
349349

350350
run-frontend-development: ## Run the frontend in development mode
351-
@$(COMPOSE) stop frontend frontend-development
351+
@$(COMPOSE) stop frontend-development
352352
cd $(PATH_FRONT_IMPRESS) && yarn dev
353353
.PHONY: run-frontend-development
354354

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/suitenumerique/docs"/>
1212
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/suitenumerique/docs"/>
1313
<a href="https://github.com/suitenumerique/docs/blob/main/LICENSE">
14-
<img alt="GitHub closed issues" src="https://img.shields.io/github/license/suitenumerique/docs"/>
14+
<img alt="MIT License" src="https://img.shields.io/github/license/suitenumerique/docs"/>
1515
</a>
1616
</p>
1717
<p align="center">
@@ -162,15 +162,15 @@ $ make superuser
162162

163163
We'd love to hear your thoughts, and hear about your experiments, so come and say hi on [Matrix](https://matrix.to/#/#docs-official:matrix.org).
164164

165-
## Roadmap
165+
## Roadmap 💡
166166

167167
Want to know where the project is headed? [🗺️ Checkout our roadmap](https://github.com/orgs/numerique-gouv/projects/13/views/11)
168168

169-
## Licence 📝
169+
## License 📝
170170

171171
This work is released under the MIT License (see [LICENSE](https://github.com/suitenumerique/docs/blob/main/LICENSE)).
172172

173-
While Docs is a public-driven initiative, our licence choice is an invitation for private sector actors to use, sell and contribute to the project.
173+
While Docs is a public-driven initiative, our license choice is an invitation for private sector actors to use, sell and contribute to the project.
174174

175175
## Contributing 🙌
176176

docs/env.md

Lines changed: 98 additions & 97 deletions
Large diffs are not rendered by default.

src/backend/core/api/serializers.py

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,7 @@ def create(self, validated_data):
408408
language = user.language or language
409409

410410
try:
411-
document_content = YdocConverter().convert_markdown(
412-
validated_data["content"]
413-
)
411+
document_content = YdocConverter().convert(validated_data["content"])
414412
except ConversionError as err:
415413
raise serializers.ValidationError(
416414
{"content": ["Could not convert content"]}
@@ -517,16 +515,17 @@ def validate_file(self, file):
517515
mime = magic.Magic(mime=True)
518516
magic_mime_type = mime.from_buffer(file.read(1024))
519517
file.seek(0) # Reset file pointer to the beginning after reading
518+
self.context["is_unsafe"] = False
519+
if settings.DOCUMENT_ATTACHMENT_CHECK_UNSAFE_MIME_TYPES_ENABLED:
520+
self.context["is_unsafe"] = (
521+
magic_mime_type in settings.DOCUMENT_UNSAFE_MIME_TYPES
522+
)
520523

521-
self.context["is_unsafe"] = (
522-
magic_mime_type in settings.DOCUMENT_UNSAFE_MIME_TYPES
523-
)
524-
525-
extension_mime_type, _ = mimetypes.guess_type(file.name)
524+
extension_mime_type, _ = mimetypes.guess_type(file.name)
526525

527-
# Try guessing a coherent extension from the mimetype
528-
if extension_mime_type != magic_mime_type:
529-
self.context["is_unsafe"] = True
526+
# Try guessing a coherent extension from the mimetype
527+
if extension_mime_type != magic_mime_type:
528+
self.context["is_unsafe"] = True
530529

531530
guessed_ext = mimetypes.guess_extension(magic_mime_type)
532531
# Missing extensions or extensions longer than 5 characters (it's as long as an extension
@@ -664,6 +663,50 @@ def validate_role(self, role):
664663
return role
665664

666665

666+
class RoleSerializer(serializers.Serializer):
667+
"""Serializer validating role choices."""
668+
669+
role = serializers.ChoiceField(
670+
choices=models.RoleChoices.choices, required=False, allow_null=True
671+
)
672+
673+
674+
class DocumentAskForAccessCreateSerializer(serializers.Serializer):
675+
"""Serializer for creating a document ask for access."""
676+
677+
role = serializers.ChoiceField(
678+
choices=models.RoleChoices.choices,
679+
required=False,
680+
default=models.RoleChoices.READER,
681+
)
682+
683+
684+
class DocumentAskForAccessSerializer(serializers.ModelSerializer):
685+
"""Serializer for document ask for access model"""
686+
687+
abilities = serializers.SerializerMethodField(read_only=True)
688+
user = UserSerializer(read_only=True)
689+
690+
class Meta:
691+
model = models.DocumentAskForAccess
692+
fields = [
693+
"id",
694+
"document",
695+
"user",
696+
"role",
697+
"created_at",
698+
"abilities",
699+
]
700+
read_only_fields = ["id", "document", "user", "role", "created_at", "abilities"]
701+
702+
def get_abilities(self, invitation) -> dict:
703+
"""Return abilities of the logged-in user on the instance."""
704+
request = self.context.get("request")
705+
if request:
706+
return invitation.get_abilities(request.user)
707+
return {}
708+
709+
667710
class VersionFilterSerializer(serializers.Serializer):
668711
"""Validate version filters applied to the list endpoint."""
669712

src/backend/core/api/viewsets.py

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import requests
2626
import rest_framework as drf
2727
from botocore.exceptions import ClientError
28+
from csp.constants import NONE
29+
from csp.decorators import csp_update
2830
from lasuite.malware_detection import malware_detection
2931
from rest_framework import filters, status, viewsets
3032
from rest_framework import response as drf_response
@@ -34,6 +36,7 @@
3436
from core import authentication, enums, models
3537
from core.services.ai_services import AIService
3638
from core.services.collaboration_services import CollaborationService
39+
from core.tasks.mail import send_ask_for_access_mail
3740
from core.utils import extract_attachments, filter_descendants
3841

3942
from . import permissions, serializers, utils
@@ -951,6 +954,8 @@ def duplicate(self, request, *args, **kwargs):
951954
)
952955
serializer.is_valid(raise_exception=True)
953956
with_accesses = serializer.validated_data.get("with_accesses", False)
957+
roles = set(document.get_roles(request.user))
958+
is_owner_or_admin = bool(roles.intersection(set(models.PRIVILEGED_ROLES)))
954959

955960
base64_yjs_content = document.content
956961

@@ -982,7 +987,7 @@ def duplicate(self, request, *args, **kwargs):
982987
]
983988

984989
# If accesses should be duplicated, add other users' accesses as per original document
985-
if with_accesses:
990+
if with_accesses and is_owner_or_admin:
986991
original_accesses = models.DocumentAccess.objects.filter(
987992
document=document
988993
).exclude(user=request.user)
@@ -1412,6 +1417,7 @@ def ai_translate(self, request, *args, **kwargs):
14121417
name="",
14131418
url_path="cors-proxy",
14141419
)
1420+
@csp_update({"img-src": [NONE, "data:"]})
14151421
def cors_proxy(self, request, *args, **kwargs):
14161422
"""
14171423
GET /api/v1.0/documents/<resource_id>/cors-proxy
@@ -1452,7 +1458,6 @@ def cors_proxy(self, request, *args, **kwargs):
14521458
content_type=content_type,
14531459
headers={
14541460
"Content-Disposition": "attachment;",
1455-
"Content-Security-Policy": "default-src 'none'; img-src 'none' data:;",
14561461
},
14571462
status=response.status_code,
14581463
)
@@ -1772,6 +1777,83 @@ def perform_create(self, serializer):
17721777
)
17731778

17741779

1780+
class DocumentAskForAccessViewSet(
1781+
drf.mixins.ListModelMixin,
1782+
drf.mixins.RetrieveModelMixin,
1783+
drf.mixins.DestroyModelMixin,
1784+
viewsets.GenericViewSet,
1785+
):
1786+
"""API ViewSet for asking for access to a document."""
1787+
1788+
lookup_field = "id"
1789+
pagination_class = Pagination
1790+
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
1791+
queryset = models.DocumentAskForAccess.objects.all()
1792+
serializer_class = serializers.DocumentAskForAccessSerializer
1793+
_document = None
1794+
1795+
def get_document_or_404(self):
1796+
"""Get the document related to the viewset or raise a 404 error."""
1797+
if self._document is None:
1798+
try:
1799+
self._document = models.Document.objects.get(
1800+
pk=self.kwargs["resource_id"]
1801+
)
1802+
except models.Document.DoesNotExist as e:
1803+
raise drf.exceptions.NotFound("Document not found.") from e
1804+
return self._document
1805+
1806+
def get_queryset(self):
1807+
"""Return the queryset according to the action."""
1808+
document = self.get_document_or_404()
1809+
1810+
queryset = super().get_queryset()
1811+
queryset = queryset.filter(document=document)
1812+
1813+
roles = set(document.get_roles(self.request.user))
1814+
is_owner_or_admin = bool(roles.intersection(set(models.PRIVILEGED_ROLES)))
1815+
if not is_owner_or_admin:
1816+
queryset = queryset.filter(user=self.request.user)
1817+
1818+
return queryset
1819+
1820+
def create(self, request, *args, **kwargs):
1821+
"""Create a document ask for access resource."""
1822+
document = self.get_document_or_404()
1823+
1824+
serializer = serializers.DocumentAskForAccessCreateSerializer(data=request.data)
1825+
serializer.is_valid(raise_exception=True)
1826+
1827+
queryset = self.get_queryset()
1828+
1829+
if queryset.filter(user=request.user).exists():
1830+
return drf.response.Response(
1831+
{"detail": "You already ask to access to this document."},
1832+
status=drf.status.HTTP_400_BAD_REQUEST,
1833+
)
1834+
1835+
ask_for_access = models.DocumentAskForAccess.objects.create(
1836+
document=document,
1837+
user=request.user,
1838+
role=serializer.validated_data["role"],
1839+
)
1840+
1841+
send_ask_for_access_mail.delay(ask_for_access.id)
1842+
1843+
return drf.response.Response(status=drf.status.HTTP_201_CREATED)
1844+
1845+
@drf.decorators.action(detail=True, methods=["post"])
1846+
def accept(self, request, *args, **kwargs):
1847+
"""Accept a document ask for access resource."""
1848+
document_ask_for_access = self.get_object()
1849+
1850+
serializer = serializers.RoleSerializer(data=request.data)
1851+
serializer.is_valid(raise_exception=True)
1852+
1853+
document_ask_for_access.accept(role=serializer.validated_data.get("role"))
1854+
return drf.response.Response(status=drf.status.HTTP_204_NO_CONTENT)
1855+
1856+
17751857
class ConfigView(drf.views.APIView):
17761858
"""API ViewSet for sharing some public settings."""
17771859

src/backend/core/factories.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# ruff: noqa: S311
21
"""
32
Core application factories
43
"""
@@ -183,6 +182,17 @@ class Meta:
183182
role = factory.fuzzy.FuzzyChoice([r[0] for r in models.RoleChoices.choices])
184183

185184

185+
class DocumentAskForAccessFactory(factory.django.DjangoModelFactory):
186+
"""Create fake document ask for access for testing."""
187+
188+
class Meta:
189+
model = models.DocumentAskForAccess
190+
191+
document = factory.SubFactory(DocumentFactory)
192+
user = factory.SubFactory(UserFactory)
193+
role = factory.fuzzy.FuzzyChoice([r[0] for r in models.RoleChoices.choices])
194+
195+
186196
class TemplateFactory(factory.django.DjangoModelFactory):
187197
"""A factory to create templates"""
188198

0 commit comments

Comments
 (0)