Skip to content

Commit 61005ab

Browse files
committed
✨(interop) allow to save an attachment into Drive workspace
Setup Drive resource server query to retrieve and save files from a configured Drive instance through OIDC resource server. suitenumerique/drive#379
1 parent 3aeb15c commit 61005ab

File tree

27 files changed

+1024
-36
lines changed

27 files changed

+1024
-36
lines changed

docs/env.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ The application uses a new environment file structure with `.defaults` and `.loc
156156
| `OIDC_OP_LOGOUT_ENDPOINT` | None | OIDC logout endpoint | Optional |
157157
| `OIDC_USERINFO_ESSENTIAL_CLAIMS` | `[]` | Essential OIDC claims | Optional |
158158
| `OIDC_USERINFO_FULLNAME_FIELDS` | `["first_name", "last_name"]` | Fields to use for full name | Optional |
159+
| `OIDC_STORE_ACCESS_TOKEN` | `False` | Store access token | Optional |
160+
| `OIDC_STORE_REFRESH_TOKEN` | `False` | Store refresh token | Optional |
161+
| `OIDC_STORE_REFRESH_TOKEN_KEY` | `None` | Refresh token encryption key (Must be a valid Fernet key) | Optional |
159162

160163

161164
### OIDC Advanced Settings

src/backend/core/api/openapi.json

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,17 @@
182182
"api_url": {
183183
"type": "string",
184184
"readOnly": true
185+
},
186+
"file_url": {
187+
"type": "string",
188+
"readOnly": true
185189
}
186190
},
187191
"readOnly": true,
188192
"required": [
189193
"sdk_url",
190-
"api_url"
194+
"api_url",
195+
"file_url"
191196
]
192197
},
193198
"SCHEMA_CUSTOM_ATTRIBUTES_USER": {
@@ -1652,6 +1657,76 @@
16521657
}
16531658
}
16541659
},
1660+
"/api/v1.0/interop/drive/": {
1661+
"get": {
1662+
"operationId": "interop_drive_retrieve",
1663+
"description": "Search for files created by the current user.",
1664+
"parameters": [
1665+
{
1666+
"in": "query",
1667+
"name": "title",
1668+
"schema": {
1669+
"type": "string"
1670+
},
1671+
"description": "Search files by title."
1672+
}
1673+
],
1674+
"tags": [
1675+
"interop/drive"
1676+
],
1677+
"security": [
1678+
{
1679+
"cookieAuth": []
1680+
}
1681+
],
1682+
"responses": {
1683+
"200": {
1684+
"description": "No response body"
1685+
}
1686+
}
1687+
},
1688+
"post": {
1689+
"operationId": "interop_drive_create",
1690+
"description": "Create a new file in the main workspace.",
1691+
"tags": [
1692+
"interop"
1693+
],
1694+
"requestBody": {
1695+
"content": {
1696+
"application/json": {
1697+
"schema": {
1698+
"$ref": "#/components/schemas/DriveUploadAttachmentRequest"
1699+
}
1700+
},
1701+
"multipart/form-data": {
1702+
"schema": {
1703+
"$ref": "#/components/schemas/DriveUploadAttachmentRequest"
1704+
}
1705+
}
1706+
},
1707+
"required": true
1708+
},
1709+
"security": [
1710+
{
1711+
"cookieAuth": []
1712+
}
1713+
],
1714+
"responses": {
1715+
"200": {
1716+
"description": "File created successfully"
1717+
},
1718+
"400": {
1719+
"description": "Bad request - attachment_id is required"
1720+
},
1721+
"404": {
1722+
"description": "Attachment not found"
1723+
},
1724+
"500": {
1725+
"description": "Internal server error"
1726+
}
1727+
}
1728+
}
1729+
},
16551730
"/api/v1.0/labels/": {
16561731
"get": {
16571732
"operationId": "labels_list",
@@ -4972,12 +5047,6 @@
49725047
"type": "object",
49735048
"description": "Serialize attachments.",
49745049
"properties": {
4975-
"id": {
4976-
"type": "string",
4977-
"format": "uuid",
4978-
"readOnly": true,
4979-
"description": "primary key for the record as UUID"
4980-
},
49815050
"blobId": {
49825051
"type": "string",
49835052
"format": "uuid",
@@ -5020,7 +5089,6 @@
50205089
"blobId",
50215090
"cid",
50225091
"created_at",
5023-
"id",
50245092
"name",
50255093
"sha256",
50265094
"size",
@@ -5225,6 +5293,19 @@
52255293
"senderId"
52265294
]
52275295
},
5296+
"DriveUploadAttachmentRequest": {
5297+
"type": "object",
5298+
"properties": {
5299+
"attachment_id": {
5300+
"type": "string",
5301+
"format": "uuid",
5302+
"description": "ID of the attachment to upload"
5303+
}
5304+
},
5305+
"required": [
5306+
"attachment_id"
5307+
]
5308+
},
52285309
"FlagEnum": {
52295310
"enum": [
52305311
"unread",

src/backend/core/api/serializers.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,6 @@ def get_sha256(self, obj):
388388
class Meta:
389389
model = models.Attachment
390390
fields = [
391-
"id",
392391
"blobId",
393392
"name",
394393
"size",

src/backend/core/api/viewsets/config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,13 @@ class ConfigView(drf.views.APIView):
5050
"type": "string",
5151
"readOnly": True,
5252
},
53+
"file_url": {
54+
"type": "string",
55+
"readOnly": True,
56+
},
5357
},
5458
"readOnly": True,
55-
"required": ["sdk_url", "api_url"],
59+
"required": ["sdk_url", "api_url", "file_url"],
5660
},
5761
"SCHEMA_CUSTOM_ATTRIBUTES_USER": {
5862
"type": "object",
@@ -107,6 +111,7 @@ def get(self, request):
107111
"DRIVE": {
108112
"sdk_url": f"{base_url}{settings.DRIVE_CONFIG.get('sdk_url')}",
109113
"api_url": f"{base_url}{settings.DRIVE_CONFIG.get('api_url')}",
114+
"file_url": f"{base_url}{settings.DRIVE_CONFIG.get('file_url')}",
110115
}
111116
}
112117
)
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import requests
2+
from django.conf import settings
3+
from django.utils.decorators import method_decorator
4+
5+
from rest_framework import permissions, serializers
6+
from rest_framework.response import Response
7+
from rest_framework.views import APIView
8+
from lasuite.oidc_login.decorators import refresh_oidc_access_token
9+
from drf_spectacular.utils import OpenApiParameter, OpenApiTypes, extend_schema
10+
11+
from core import models
12+
from drf_spectacular.utils import extend_schema, OpenApiResponse, inline_serializer
13+
14+
15+
16+
17+
18+
class DriveAPIView(APIView):
19+
"""
20+
API View to upload an attachment to Drive.
21+
"""
22+
permission_classes = [permissions.IsAuthenticated]
23+
24+
@extend_schema(
25+
tags=["interop/drive"],
26+
parameters=[
27+
OpenApiParameter(
28+
name="title",
29+
type=OpenApiTypes.STR,
30+
location=OpenApiParameter.QUERY,
31+
description="Search files by title.",
32+
required=False,
33+
),
34+
],
35+
)
36+
@method_decorator(refresh_oidc_access_token)
37+
def get(self, request):
38+
"""
39+
Search for files created by the current user.
40+
"""
41+
drive_external_api = f"{settings.DRIVE_CONFIG.get('base_url')}/external_api/v1.0"
42+
access_token = request.session.get('oidc_access_token')
43+
filters = {
44+
"is_creator_me": True,
45+
"type": "file",
46+
}
47+
for key, value in request.query_params.items():
48+
filters.update({key: value})
49+
50+
# Retrieve the main workspace
51+
response = requests.get(
52+
f"{drive_external_api}/items/",
53+
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
54+
)
55+
response.raise_for_status()
56+
data = response.json()
57+
items = data['results']
58+
main_workspace = None
59+
for item in items:
60+
if item['main_workspace']:
61+
main_workspace = item
62+
break
63+
if not main_workspace:
64+
return Response(status=404, data={"error": "No main workspace found"})
65+
66+
# Search for files in the main workspace
67+
response = requests.get(
68+
f"{drive_external_api}/items/{main_workspace['id']}/children/",
69+
params=filters,
70+
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
71+
)
72+
73+
return Response(response.json())
74+
75+
@extend_schema(
76+
description="Create a new file in the main workspace.",
77+
request=inline_serializer(
78+
name="DriveUploadAttachment",
79+
fields={
80+
"attachment_id": serializers.UUIDField(
81+
required=True,
82+
help_text="ID of the attachment to upload",
83+
),
84+
85+
},
86+
),
87+
responses={
88+
200: OpenApiResponse(description="File created successfully"),
89+
400: OpenApiResponse(description="Bad request - attachment_id is required"),
90+
404: OpenApiResponse(description="Attachment not found"),
91+
500: OpenApiResponse(description="Internal server error"),
92+
},
93+
)
94+
@method_decorator(refresh_oidc_access_token)
95+
def post(self, request):
96+
"""
97+
Create a new file in the main workspace.
98+
"""
99+
drive_external_api = f"{settings.DRIVE_CONFIG.get('base_url')}/external_api/v1.0"
100+
101+
# Get the access token from the session
102+
access_token = request.session.get('oidc_access_token')
103+
attachment_id = request.data.get('attachment_id')
104+
if not attachment_id:
105+
return Response(status=400, data={"error": "attachment_id is required"})
106+
107+
# TODO : Make this shit properly
108+
message_id = attachment_id.split("_")[1]
109+
attachment_number = attachment_id.split("_")[2]
110+
message = models.Message.objects.get(id=message_id)
111+
# Parse the raw mime message to get the attachment
112+
parsed_email = message.get_parsed_data()
113+
attachment = parsed_email.get("attachments", [])[int(attachment_number)]
114+
115+
# Get the main workspace
116+
response = requests.get(
117+
f"{drive_external_api}/items/",
118+
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
119+
)
120+
response.raise_for_status()
121+
data = response.json()
122+
items = data['results']
123+
main_workspace = None
124+
for item in items:
125+
if item['main_workspace']:
126+
main_workspace = item
127+
break
128+
129+
if not main_workspace:
130+
return Response(status=404, data={"error": "No main workspace found"})
131+
132+
# Create a new file in the main workspace
133+
response = requests.post(
134+
f"{drive_external_api}/items/{main_workspace['id']}/children/",
135+
json={
136+
"type": "file",
137+
"filename": attachment['name'],
138+
},
139+
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
140+
)
141+
response.raise_for_status()
142+
item = response.json()
143+
policy = item['policy']
144+
145+
# Upload file content using the presigned URL
146+
upload_response = requests.put(
147+
policy,
148+
data=attachment['content'],
149+
headers={
150+
"Content-Type": attachment['type'],
151+
"x-amz-acl": "private"
152+
}
153+
)
154+
upload_response.raise_for_status()
155+
156+
# Tell the Drive API that the upload is ended
157+
response = requests.post(
158+
f"{drive_external_api}/items/{item['id']}/upload-ended/",
159+
json={},
160+
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
161+
)
162+
response.raise_for_status()
163+
164+
return Response(response.json())

src/backend/core/authentication/backends.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ def get_or_create_user(self, access_token, id_token, payload):
7070

7171
# if sub is absent, try matching on email
7272
user = self.get_existing_user(sub, email)
73-
7473
self.create_testdomain()
7574

7675
if user:

0 commit comments

Comments
 (0)