mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-07 02:07:53 +02:00
✨(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>
This commit is contained in:
@@ -18,6 +18,7 @@ and this project adheres to
|
||||
- ♿️(frontend) structure correctly 5xx error alerts #2128
|
||||
- ♿️(frontend) make doc search result labels uniquely identifiable #2212
|
||||
- ⬆️(backend) upgrade docspec to v3.0.x and adapt converter API #2220
|
||||
- ✨(backend) make forward auth request uri header configurable #2241
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ These are the environment variables you can set for the `impress-backend` contai
|
||||
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
|
||||
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
|
||||
| MEDIA_BASE_URL | | |
|
||||
| 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 |
|
||||
| NO_WEBSOCKET_CACHE_TIMEOUT | Cache used to store current editor session key when only users without websocket are editing a document | 120 |
|
||||
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
|
||||
|
||||
@@ -1752,10 +1752,13 @@ class DocumentViewSet(
|
||||
|
||||
def _auth_get_original_url(self, request):
|
||||
"""
|
||||
Extracts and parses the original URL from the "HTTP_X_ORIGINAL_URL" header.
|
||||
Extracts and parses the original URL from the configured parameter header.
|
||||
Raises PermissionDenied if the header is missing.
|
||||
|
||||
The original url is passed by nginx in the "HTTP_X_ORIGINAL_URL" header.
|
||||
The original url is passed by reverse proxy in the header specified by the
|
||||
MEDIA_AUTH_ORIGINAL_URL_HEADER setting.
|
||||
|
||||
For nginx (the default) this is set to HTTP_X_ORIGINAL_URL.
|
||||
See corresponding ingress configuration in Helm chart and read about the
|
||||
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
|
||||
is configured to do this.
|
||||
@@ -1766,9 +1769,14 @@ class DocumentViewSet(
|
||||
reasons.
|
||||
"""
|
||||
# Extract the original URL from the request header
|
||||
original_url = request.META.get("HTTP_X_ORIGINAL_URL")
|
||||
original_url = request.META.get(settings.MEDIA_AUTH_ORIGINAL_URL_HEADER)
|
||||
if not original_url:
|
||||
logger.debug("Missing HTTP_X_ORIGINAL_URL header in subrequest")
|
||||
logger.debug(
|
||||
"Missing %s header in subrequest. "
|
||||
"Maybe you need to set MEDIA_AUTH_ORIGINAL_URL_HEADER correctly for your ingress"
|
||||
" proxy.",
|
||||
settings.MEDIA_AUTH_ORIGINAL_URL_HEADER,
|
||||
)
|
||||
raise drf.exceptions.PermissionDenied()
|
||||
|
||||
logger.debug("Original url: '%s'", original_url)
|
||||
|
||||
@@ -6,7 +6,6 @@ from io import BytesIO
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -37,7 +36,7 @@ def test_api_documents_media_auth_unkown_document():
|
||||
assert models.Document.objects.exists() is False
|
||||
|
||||
|
||||
def test_api_documents_media_auth_anonymous_public():
|
||||
def test_api_documents_media_auth_anonymous_public(settings):
|
||||
"""Anonymous users should be able to retrieve attachments linked to a public document"""
|
||||
document_id = uuid4()
|
||||
filename = f"{uuid4()!s}.jpg"
|
||||
@@ -139,7 +138,7 @@ def test_api_documents_media_auth_anonymous_authenticated_or_restricted(reach):
|
||||
assert "Authorization" not in response
|
||||
|
||||
|
||||
def test_api_documents_media_auth_anonymous_attachments():
|
||||
def test_api_documents_media_auth_anonymous_attachments(settings):
|
||||
"""
|
||||
Declaring a media key as original attachment on a document to which
|
||||
a user has access should give them access to the attachment file
|
||||
@@ -202,7 +201,9 @@ def test_api_documents_media_auth_anonymous_attachments():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||
def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
|
||||
def test_api_documents_media_auth_authenticated_public_or_authenticated(
|
||||
reach, settings
|
||||
):
|
||||
"""
|
||||
Authenticated users who are not related to a document should be able to retrieve
|
||||
attachments related to a document with public or authenticated link reach.
|
||||
@@ -284,7 +285,7 @@ def test_api_documents_media_auth_authenticated_restricted():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||
def test_api_documents_media_auth_related(via, mock_user_teams, settings):
|
||||
"""
|
||||
Users who have a specific access to a document, whatever the role, should be able to
|
||||
retrieve related attachments.
|
||||
@@ -368,7 +369,7 @@ def test_api_documents_media_auth_not_ready_status():
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_api_documents_media_auth_missing_status_metadata():
|
||||
def test_api_documents_media_auth_missing_status_metadata(settings):
|
||||
"""Attachments without status metadata should be considered as ready"""
|
||||
document_id = uuid4()
|
||||
filename = f"{uuid4()!s}.jpg"
|
||||
@@ -412,3 +413,51 @@ def test_api_documents_media_auth_missing_status_metadata():
|
||||
timeout=1,
|
||||
)
|
||||
assert response.content.decode("utf-8") == "my prose"
|
||||
|
||||
|
||||
def test_api_documents_media_auth_anonymous_public_custom_origin_header(settings):
|
||||
"""Changing the setting MEDIA_AUTH_ORIGINAL_URL_HEADER to match other header should work"""
|
||||
settings.MEDIA_AUTH_ORIGINAL_URL_HEADER = "HTTP_X_FORWARDED_URI"
|
||||
document_id = uuid4()
|
||||
filename = f"{uuid4()!s}.jpg"
|
||||
key = f"{document_id!s}/attachments/{filename:s}"
|
||||
default_storage.connection.meta.client.put_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=key,
|
||||
Body=BytesIO(b"my prose"),
|
||||
ContentType="text/plain",
|
||||
Metadata={"status": DocumentAttachmentStatus.READY},
|
||||
)
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
|
||||
|
||||
original_url = f"http://localhost/media/{key:s}"
|
||||
now = timezone.now()
|
||||
with freeze_time(now):
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_FORWARDED_URI=original_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
authorization = response["Authorization"]
|
||||
assert "AWS4-HMAC-SHA256 Credential=" in authorization
|
||||
assert (
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
response = requests.get(
|
||||
file_url,
|
||||
headers={
|
||||
"authorization": authorization,
|
||||
"x-amz-date": response["x-amz-date"],
|
||||
"x-amz-content-sha256": response["x-amz-content-sha256"],
|
||||
"Host": f"{s3_url.hostname:s}:{s3_url.port:d}",
|
||||
},
|
||||
timeout=1,
|
||||
)
|
||||
assert response.content.decode("utf-8") == "my prose"
|
||||
|
||||
@@ -130,6 +130,12 @@ class Base(Configuration):
|
||||
default=50, environ_name="SEARCH_INDEXER_QUERY_LIMIT", environ_prefix=None
|
||||
)
|
||||
|
||||
MEDIA_AUTH_ORIGINAL_URL_HEADER = values.Value(
|
||||
default="HTTP_X_ORIGINAL_URL",
|
||||
environ_name="MEDIA_AUTH_ORIGINAL_URL_HEADER",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.path.join(DATA_DIR, "static")
|
||||
|
||||
Reference in New Issue
Block a user