Skip to content

Commit cc9b831

Browse files
committed
Merge branch 'main' into add-legal-warning
2 parents 2ff748c + 53be6de commit cc9b831

Some content is hidden

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

46 files changed

+871
-568
lines changed

.github/workflows/docker-hub.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,5 @@ jobs:
134134
name: Call argocd github webhook
135135
run: |
136136
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/${{ secrets.DEPLOYMENT_REPO_URL }}"}}'
137-
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}" | awk '{print "X-Hub-Signature: sha1="$2}')
137+
sig=$(echo -n ${data} | openssl dgst -sha256 -hmac "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}" | awk '{print "X-Hub-Signature-256: sha256="$2}')
138138
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" ${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}

CHANGELOG.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,22 @@ and this project adheres to
88

99
## [Unreleased]
1010

11+
## [3.0.0] - 2025-03-28
12+
1113
## Added
1214

1315
- 📄(legal) Require contributors to sign a DCO #779
1416

1517
## Changed
1618

1719
- ♻️(frontend) Integrate UI kit #783
20+
- 🏗️(y-provider) manage auth in y-provider app #804
1821

1922
## Fixed
2023

2124
- 🐛(backend) compute ancestor_links in get_abilities if needed #725
25+
- 🔒️(back) restrict access to document accesses #801
26+
2227

2328
## [2.6.0] - 2025-03-21
2429

@@ -501,8 +506,9 @@ and this project adheres to
501506
- ✨(frontend) Coming Soon page (#67)
502507
- 🚀 Impress, project to manage your documents easily and collaboratively.
503508

504-
[unreleased]: https://github.com/numerique-gouv/impress/compare/v2.6.0...main
505-
[v2.5.0]: https://github.com/numerique-gouv/impress/releases/v2.6.0
509+
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.0.0...main
510+
[v3.0.0]: https://github.com/numerique-gouv/impress/releases/v3.0.0
511+
[v2.6.0]: https://github.com/numerique-gouv/impress/releases/v2.6.0
506512
[v2.5.0]: https://github.com/numerique-gouv/impress/releases/v2.5.0
507513
[v2.4.0]: https://github.com/numerique-gouv/impress/releases/v2.4.0
508514
[v2.3.0]: https://github.com/numerique-gouv/impress/releases/v2.3.0

UPGRADE.md

+12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ the following command inside your docker container:
1616

1717
## [Unreleased]
1818

19+
## [3.0.0] - 2025-03-28
20+
21+
We are not using the nginx auth request anymore to access the collaboration server (`yProvider`)
22+
The authentication is now managed directly from the yProvider server.
23+
You must remove the annotation `nginx.ingress.kubernetes.io/auth-url` from the `ingressCollaborationWS`.
24+
25+
This means as well that the yProvider server must be able to access the Django server.
26+
To do so, you must set the `COLLABORATION_BACKEND_BASE_URL` environment variable to the `yProvider`
27+
service.
28+
29+
## [2.2.0] - 2025-02-10
30+
1931
- AI features are now limited to users who are authenticated. Before this release, even anonymous
2032
users who gained editor access on a document with link reach used to get AI feature.
2133
IF you want anonymous users to keep access on AI features, you must now define the

docker-compose.yml

+4
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,15 @@ services:
185185
context: .
186186
dockerfile: ./src/frontend/servers/y-provider/Dockerfile
187187
target: y-provider
188+
command: ["yarn", "workspace", "server-y-provider", "run", "dev"]
189+
working_dir: /app/frontend
188190
restart: unless-stopped
189191
env_file:
190192
- env.d/development/common
191193
ports:
192194
- "4444:4444"
195+
volumes:
196+
- ./src/frontend/:/app/frontend
193197

194198
kc_postgresql:
195199
image: postgres:14.3

docker/files/etc/nginx/conf.d/default.conf

-48
Original file line numberDiff line numberDiff line change
@@ -4,54 +4,6 @@ server {
44
server_name localhost;
55
charset utf-8;
66

7-
# Proxy auth for collaboration server
8-
location /collaboration/ws/ {
9-
# Collaboration Auth request configuration
10-
auth_request /collaboration-auth;
11-
auth_request_set $authHeader $upstream_http_authorization;
12-
auth_request_set $canEdit $upstream_http_x_can_edit;
13-
auth_request_set $userId $upstream_http_x_user_id;
14-
15-
# Pass specific headers from the auth response
16-
proxy_set_header Authorization $authHeader;
17-
proxy_set_header X-Can-Edit $canEdit;
18-
proxy_set_header X-User-Id $userId;
19-
20-
# Ensure WebSocket upgrade
21-
proxy_http_version 1.1;
22-
proxy_set_header Upgrade $http_upgrade;
23-
proxy_set_header Connection "Upgrade";
24-
25-
# Collaboration server
26-
proxy_pass http://y-provider:4444;
27-
28-
# Set appropriate timeout for WebSocket
29-
proxy_read_timeout 86400;
30-
proxy_send_timeout 86400;
31-
32-
# Preserve original host and additional headers
33-
proxy_set_header Host $host;
34-
}
35-
36-
location /collaboration-auth {
37-
proxy_pass http://app-dev:8000/api/v1.0/documents/collaboration-auth/;
38-
proxy_set_header Host $host;
39-
proxy_set_header X-Real-IP $remote_addr;
40-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
41-
proxy_set_header X-Original-URL $request_uri;
42-
43-
# Prevent the body from being passed
44-
proxy_pass_request_body off;
45-
proxy_set_header Content-Length "";
46-
proxy_set_header X-Original-Method $request_method;
47-
}
48-
49-
location /collaboration/api/ {
50-
# Collaboration server
51-
proxy_pass http://y-provider:4444;
52-
proxy_set_header Host $host;
53-
}
54-
557
# Proxy auth for media
568
location /media/ {
579
# Auth request configuration

env.d/development/common.dist

+3-2
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,11 @@ AI_API_KEY=password
5555
AI_MODEL=llama
5656

5757
# Collaboration
58-
COLLABORATION_API_URL=http://nginx:8083/collaboration/api/
58+
COLLABORATION_API_URL=http://y-provider:4444/collaboration/api/
59+
COLLABORATION_BACKEND_BASE_URL=http://app-dev:8000
5960
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
6061
COLLABORATION_SERVER_SECRET=my-secret
61-
COLLABORATION_WS_URL=ws://localhost:8083/collaboration/ws/
62+
COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
6263

6364
# Frontend
6465
FRONTEND_THEME=default

src/backend/core/api/serializers.py

+31
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ class Meta:
2727
read_only_fields = ["id", "email", "full_name", "short_name"]
2828

2929

30+
class UserLightSerializer(UserSerializer):
31+
"""Serialize users with limited fields."""
32+
33+
id = serializers.SerializerMethodField(read_only=True)
34+
email = serializers.SerializerMethodField(read_only=True)
35+
36+
def get_id(self, _user):
37+
"""Return always None. Here to have the same fields than in UserSerializer."""
38+
return None
39+
40+
def get_email(self, _user):
41+
"""Return always None. Here to have the same fields than in UserSerializer."""
42+
return None
43+
44+
class Meta:
45+
model = models.User
46+
fields = ["id", "email", "full_name", "short_name"]
47+
read_only_fields = ["id", "email", "full_name", "short_name"]
48+
49+
3050
class BaseAccessSerializer(serializers.ModelSerializer):
3151
"""Serialize template accesses."""
3252

@@ -118,6 +138,17 @@ class Meta:
118138
read_only_fields = ["id", "abilities"]
119139

120140

141+
class DocumentAccessLightSerializer(DocumentAccessSerializer):
142+
"""Serialize document accesses with limited fields."""
143+
144+
user = UserLightSerializer(read_only=True)
145+
146+
class Meta:
147+
model = models.DocumentAccess
148+
fields = ["id", "user", "team", "role", "abilities"]
149+
read_only_fields = ["id", "team", "role", "abilities"]
150+
151+
121152
class TemplateAccessSerializer(BaseAccessSerializer):
122153
"""Serialize template accesses."""
123154

src/backend/core/api/viewsets.py

+29-58
Original file line numberDiff line numberDiff line change
@@ -380,18 +380,15 @@ class DocumentViewSet(
380380
9. **Media Auth**: Authorize access to document media.
381381
Example: GET /documents/media-auth/
382382
383-
10. **Collaboration Auth**: Authorize access to the collaboration server for a document.
384-
Example: GET /documents/collaboration-auth/
385-
386-
11. **AI Transform**: Apply a transformation action on a piece of text with AI.
383+
10. **AI Transform**: Apply a transformation action on a piece of text with AI.
387384
Example: POST /documents/{id}/ai-transform/
388385
Expected data:
389386
- text (str): The input text.
390387
- action (str): The transformation type, one of [prompt, correct, rephrase, summarize].
391388
Returns: JSON response with the processed text.
392389
Throttled by: AIDocumentRateThrottle, AIUserRateThrottle.
393390
394-
12. **AI Translate**: Translate a piece of text with AI.
391+
11. **AI Translate**: Translate a piece of text with AI.
395392
Example: POST /documents/{id}/ai-translate/
396393
Expected data:
397394
- text (str): The input text.
@@ -1207,17 +1204,6 @@ def _auth_get_url_params(self, pattern, fragment):
12071204
logger.debug("Failed to extract parameters from subrequest URL: %s", exc)
12081205
raise drf.exceptions.PermissionDenied() from exc
12091206

1210-
def _auth_get_document(self, pk):
1211-
"""
1212-
Retrieves the document corresponding to the given primary key (pk).
1213-
Raises PermissionDenied if the document is not found.
1214-
"""
1215-
try:
1216-
return models.Document.objects.get(pk=pk)
1217-
except models.Document.DoesNotExist as exc:
1218-
logger.debug("Document with ID '%s' does not exist", pk)
1219-
raise drf.exceptions.PermissionDenied() from exc
1220-
12211207
@drf.decorators.action(detail=False, methods=["get"], url_path="media-auth")
12221208
def media_auth(self, request, *args, **kwargs):
12231209
"""
@@ -1265,42 +1251,6 @@ def media_auth(self, request, *args, **kwargs):
12651251

12661252
return drf.response.Response("authorized", headers=request.headers, status=200)
12671253

1268-
@drf.decorators.action(detail=False, methods=["get"], url_path="collaboration-auth")
1269-
def collaboration_auth(self, request, *args, **kwargs):
1270-
"""
1271-
This view is used by an Nginx subrequest to control access to a document's
1272-
collaboration server.
1273-
"""
1274-
parsed_url = self._auth_get_original_url(request)
1275-
url_params = self._auth_get_url_params(
1276-
enums.COLLABORATION_WS_URL_PATTERN, parsed_url.query
1277-
)
1278-
document = self._auth_get_document(url_params["pk"])
1279-
1280-
abilities = document.get_abilities(request.user)
1281-
if not abilities.get(self.action, False):
1282-
logger.debug(
1283-
"User '%s' lacks permission for document '%s'",
1284-
request.user,
1285-
document.pk,
1286-
)
1287-
raise drf.exceptions.PermissionDenied()
1288-
1289-
if not settings.COLLABORATION_SERVER_SECRET:
1290-
logger.debug("Collaboration server secret is not defined")
1291-
raise drf.exceptions.PermissionDenied()
1292-
1293-
# Add the collaboration server secret token to the headers
1294-
headers = {
1295-
"Authorization": settings.COLLABORATION_SERVER_SECRET,
1296-
"X-Can-Edit": str(abilities["partial_update"]),
1297-
}
1298-
1299-
if request.user.is_authenticated:
1300-
headers["X-User-Id"] = str(request.user.id)
1301-
1302-
return drf.response.Response("authorized", headers=headers, status=200)
1303-
13041254
@drf.decorators.action(
13051255
detail=True,
13061256
methods=["post"],
@@ -1420,12 +1370,7 @@ def cors_proxy(self, request, *args, **kwargs):
14201370

14211371
class DocumentAccessViewSet(
14221372
ResourceAccessViewsetMixin,
1423-
drf.mixins.CreateModelMixin,
1424-
drf.mixins.DestroyModelMixin,
1425-
drf.mixins.ListModelMixin,
1426-
drf.mixins.RetrieveModelMixin,
1427-
drf.mixins.UpdateModelMixin,
1428-
viewsets.GenericViewSet,
1373+
viewsets.ModelViewSet,
14291374
):
14301375
"""
14311376
API ViewSet for all interactions with document accesses.
@@ -1457,6 +1402,32 @@ class DocumentAccessViewSet(
14571402
queryset = models.DocumentAccess.objects.select_related("user").all()
14581403
resource_field_name = "document"
14591404
serializer_class = serializers.DocumentAccessSerializer
1405+
is_current_user_owner_or_admin = False
1406+
1407+
def get_queryset(self):
1408+
"""Return the queryset according to the action."""
1409+
queryset = super().get_queryset()
1410+
1411+
if self.action == "list":
1412+
try:
1413+
document = models.Document.objects.get(pk=self.kwargs["resource_id"])
1414+
except models.Document.DoesNotExist:
1415+
return queryset.none()
1416+
1417+
roles = set(document.get_roles(self.request.user))
1418+
is_owner_or_admin = bool(roles.intersection(set(models.PRIVILEGED_ROLES)))
1419+
self.is_current_user_owner_or_admin = is_owner_or_admin
1420+
if not is_owner_or_admin:
1421+
# Return only the document owner access
1422+
queryset = queryset.filter(role__in=models.PRIVILEGED_ROLES)
1423+
1424+
return queryset
1425+
1426+
def get_serializer_class(self):
1427+
if self.action == "list" and not self.is_current_user_owner_or_admin:
1428+
return serializers.DocumentAccessLightSerializer
1429+
1430+
return super().get_serializer_class()
14601431

14611432
def perform_create(self, serializer):
14621433
"""Add a new access to the document and send an email to the new added user."""

src/backend/core/enums.py

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
MEDIA_STORAGE_URL_EXTRACT = re.compile(
2121
f"{settings.MEDIA_URL:s}({UUID_REGEX}/{ATTACHMENTS_FOLDER}/{UUID_REGEX}{FILE_EXT_REGEX})"
2222
)
23-
COLLABORATION_WS_URL_PATTERN = re.compile(rf"(?:^|&)room=(?P<pk>{UUID_REGEX})(?:&|$)")
2423

2524

2625
# In Django's code base, `LANGUAGES` is set by default with all supported languages.

0 commit comments

Comments
 (0)