Skip to content

Commit 394fbc5

Browse files
✨(backend) make forward auth request uri header configurable
In deployment, Traefik is used, not nginx, as an ingress. Traefik uses `X-Forwarded-Ur`i instead of `X-Original-Url`. This adds a setting which lets users adapt Docs to their ingress proxy of choice The settings name is MEDIA_AUTH_ORIGINAL_URL_HEADER Signed-off-by: Erin Shepherd <erin.shepherd@e43.eu>
1 parent 7df5aba commit 394fbc5

5 files changed

Lines changed: 75 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to
1818
- ♿️(frontend) structure correctly 5xx error alerts #2128
1919
- ♿️(frontend) make doc search result labels uniquely identifiable #2212
2020
- ⬆️(backend) upgrade docspec to v3.0.x and adapt converter API #2220
21+
- ✨(backend) make forward auth request uri header configurable #2241
2122

2223
### Fixed
2324

docs/env.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ These are the environment variables you can set for the `impress-backend` contai
9191
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
9292
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
9393
| MEDIA_BASE_URL | | |
94+
| MEDIA_AUTH_ORIGINAL_URL_HEADER | Parameter containing the original request URL, as seen at the media auth endpoint, in CGI/WSGI form (HTTP_HEADER_NAME_ALL_CAPS_WITH_UNDERSCORES) | HTTP_X_ORIGINAL_URL |
9495
| NO_WEBSOCKET_CACHE_TIMEOUT | Cache used to store current editor session key when only users without websocket are editing a document | 120 |
9596
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
9697
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |

src/backend/core/api/viewsets.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,10 +1752,13 @@ def attachment_upload(self, request, *args, **kwargs):
17521752

17531753
def _auth_get_original_url(self, request):
17541754
"""
1755-
Extracts and parses the original URL from the "HTTP_X_ORIGINAL_URL" header.
1755+
Extracts and parses the original URL from the configured parameter header.
17561756
Raises PermissionDenied if the header is missing.
17571757
1758-
The original url is passed by nginx in the "HTTP_X_ORIGINAL_URL" header.
1758+
The original url is passed by reverse proxy in the header specified by the
1759+
MEDIA_AUTH_ORIGINAL_URL_HEADER setting.
1760+
1761+
For nginx (the default) this is set to HTTP_X_ORIGINAL_URL.
17591762
See corresponding ingress configuration in Helm chart and read about the
17601763
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
17611764
is configured to do this.
@@ -1766,9 +1769,14 @@ def _auth_get_original_url(self, request):
17661769
reasons.
17671770
"""
17681771
# Extract the original URL from the request header
1769-
original_url = request.META.get("HTTP_X_ORIGINAL_URL")
1772+
original_url = request.META.get(settings.MEDIA_AUTH_ORIGINAL_URL_HEADER)
17701773
if not original_url:
1771-
logger.debug("Missing HTTP_X_ORIGINAL_URL header in subrequest")
1774+
logger.debug(
1775+
"Missing %s header in subrequest. "
1776+
"Maybe you need to set MEDIA_AUTH_ORIGINAL_URL_HEADER correctly for your ingress"
1777+
" proxy.",
1778+
settings.MEDIA_AUTH_ORIGINAL_URL_HEADER,
1779+
)
17721780
raise drf.exceptions.PermissionDenied()
17731781

17741782
logger.debug("Original url: '%s'", original_url)

src/backend/core/tests/documents/test_api_documents_media_auth.py

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from urllib.parse import urlparse
77
from uuid import uuid4
88

9-
from django.conf import settings
109
from django.core.files.storage import default_storage
1110
from django.utils import timezone
1211

@@ -37,7 +36,7 @@ def test_api_documents_media_auth_unkown_document():
3736
assert models.Document.objects.exists() is False
3837

3938

40-
def test_api_documents_media_auth_anonymous_public():
39+
def test_api_documents_media_auth_anonymous_public(settings):
4140
"""Anonymous users should be able to retrieve attachments linked to a public document"""
4241
document_id = uuid4()
4342
filename = f"{uuid4()!s}.jpg"
@@ -139,7 +138,7 @@ def test_api_documents_media_auth_anonymous_authenticated_or_restricted(reach):
139138
assert "Authorization" not in response
140139

141140

142-
def test_api_documents_media_auth_anonymous_attachments():
141+
def test_api_documents_media_auth_anonymous_attachments(settings):
143142
"""
144143
Declaring a media key as original attachment on a document to which
145144
a user has access should give them access to the attachment file
@@ -202,7 +201,9 @@ def test_api_documents_media_auth_anonymous_attachments():
202201

203202

204203
@pytest.mark.parametrize("reach", ["public", "authenticated"])
205-
def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
204+
def test_api_documents_media_auth_authenticated_public_or_authenticated(
205+
reach, settings
206+
):
206207
"""
207208
Authenticated users who are not related to a document should be able to retrieve
208209
attachments related to a document with public or authenticated link reach.
@@ -284,7 +285,7 @@ def test_api_documents_media_auth_authenticated_restricted():
284285

285286

286287
@pytest.mark.parametrize("via", VIA)
287-
def test_api_documents_media_auth_related(via, mock_user_teams):
288+
def test_api_documents_media_auth_related(via, mock_user_teams, settings):
288289
"""
289290
Users who have a specific access to a document, whatever the role, should be able to
290291
retrieve related attachments.
@@ -368,7 +369,7 @@ def test_api_documents_media_auth_not_ready_status():
368369
assert response.status_code == 403
369370

370371

371-
def test_api_documents_media_auth_missing_status_metadata():
372+
def test_api_documents_media_auth_missing_status_metadata(settings):
372373
"""Attachments without status metadata should be considered as ready"""
373374
document_id = uuid4()
374375
filename = f"{uuid4()!s}.jpg"
@@ -412,3 +413,51 @@ def test_api_documents_media_auth_missing_status_metadata():
412413
timeout=1,
413414
)
414415
assert response.content.decode("utf-8") == "my prose"
416+
417+
418+
def test_api_documents_media_auth_anonymous_public_custom_origin_header(settings):
419+
"""Changing the setting MEDIA_AUTH_ORIGINAL_URL_HEADER to match other header should work"""
420+
settings.MEDIA_AUTH_ORIGINAL_URL_HEADER = "HTTP_X_FORWARDED_URI"
421+
document_id = uuid4()
422+
filename = f"{uuid4()!s}.jpg"
423+
key = f"{document_id!s}/attachments/{filename:s}"
424+
default_storage.connection.meta.client.put_object(
425+
Bucket=default_storage.bucket_name,
426+
Key=key,
427+
Body=BytesIO(b"my prose"),
428+
ContentType="text/plain",
429+
Metadata={"status": DocumentAttachmentStatus.READY},
430+
)
431+
432+
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
433+
434+
original_url = f"http://localhost/media/{key:s}"
435+
now = timezone.now()
436+
with freeze_time(now):
437+
response = APIClient().get(
438+
"/api/v1.0/documents/media-auth/", HTTP_X_FORWARDED_URI=original_url
439+
)
440+
441+
assert response.status_code == 200
442+
443+
authorization = response["Authorization"]
444+
assert "AWS4-HMAC-SHA256 Credential=" in authorization
445+
assert (
446+
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
447+
in authorization
448+
)
449+
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
450+
451+
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
452+
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
453+
response = requests.get(
454+
file_url,
455+
headers={
456+
"authorization": authorization,
457+
"x-amz-date": response["x-amz-date"],
458+
"x-amz-content-sha256": response["x-amz-content-sha256"],
459+
"Host": f"{s3_url.hostname:s}:{s3_url.port:d}",
460+
},
461+
timeout=1,
462+
)
463+
assert response.content.decode("utf-8") == "my prose"

src/backend/impress/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ class Base(Configuration):
130130
default=50, environ_name="SEARCH_INDEXER_QUERY_LIMIT", environ_prefix=None
131131
)
132132

133+
MEDIA_AUTH_ORIGINAL_URL_HEADER = values.Value(
134+
default="HTTP_X_ORIGINAL_URL",
135+
environ_name="MEDIA_AUTH_ORIGINAL_URL_HEADER",
136+
environ_prefix=None,
137+
)
138+
133139
# Static files (CSS, JavaScript, Images)
134140
STATIC_URL = "/static/"
135141
STATIC_ROOT = os.path.join(DATA_DIR, "static")

0 commit comments

Comments
 (0)