Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to

### Added

- ✨(backend) make the forward auth request uri header configurable
- ✨(backend) make the upload ACL configurable to support GCS based storages
- ✨(frontend) show the messages widget button on the homepage
- ✨(frontend) open the messages widget from the help menu
Expand Down
1 change: 1 addition & 0 deletions docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ This document lists all configurable environment variables for the Drive applica
| `LOGGING_LEVEL_LOGGERS_APP` | Logging level for application loggers | `INFO` |
| `LOGGING_LEVEL_LOGGERS_ROOT` | Logging level for root logger | `INFO` |
| `MAX_PAGE_SIZE` | Limit the maximum page size the client may request | `200` |
| `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` |
| `MEDIA_BASE_URL` | Base URL for media files | `None` |
| `OIDC_AUTH_REQUEST_EXTRA_PARAMS` | Extra parameters for OIDC auth requests | `{}` |
| `OIDC_ALLOW_DUPLICATE_EMAILS` | Allow multiple users with same email | `False` |
Expand Down
14 changes: 11 additions & 3 deletions src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1623,7 +1623,10 @@ def _authorize_subrequest(self, request, pattern):
Shared method to authorize access based on the original URL of an Nginx subrequest
and user permissions. Returns a dictionary of URL parameters if authorized.

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.
Expand All @@ -1642,9 +1645,14 @@ def _authorize_subrequest(self, request, pattern):
- PermissionDenied if authorization fails.
"""
# 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()

parsed_url = urlparse(original_url)
Expand Down
51 changes: 45 additions & 6 deletions src/backend/core/tests/items/test_api_items_media_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from io import BytesIO
from urllib.parse import quote, urlparse

from django.conf import settings
from django.core.files.storage import default_storage
from django.utils import timezone

Expand All @@ -21,7 +20,7 @@
pytestmark = pytest.mark.django_db


def test_api_items_media_auth_anonymous_public():
def test_api_items_media_auth_anonymous_public(settings):
"""Anonymous users should be able to retrieve attachments linked to a public item"""
item = factories.ItemFactory(
link_reach="public",
Expand Down Expand Up @@ -78,7 +77,7 @@ def test_api_items_media_auth_anonymous_authenticated_or_restricted(reach):


@pytest.mark.parametrize("reach", ["public", "authenticated"])
def test_api_items_media_auth_authenticated_public_or_authenticated(reach):
def test_api_items_media_auth_authenticated_public_or_authenticated(reach, settings):
"""
Authenticated users who are not related to an item should be able to retrieve
attachments related to an item with public or authenticated link reach.
Expand Down Expand Up @@ -158,7 +157,7 @@ def test_api_items_media_auth_authenticated_restricted():
models.ItemUploadStateChoices.FILE_TOO_LARGE_TO_ANALYZE,
],
)
def test_api_items_media_auth_related(via, mock_user_teams, upload_state):
def test_api_items_media_auth_related(via, mock_user_teams, upload_state, settings):
"""
Users who have a specific access to an item, whatever the role, should be able to
retrieve related attachments if not pending.
Expand Down Expand Up @@ -209,7 +208,7 @@ def test_api_items_media_auth_related(via, mock_user_teams, upload_state):
assert response.content.decode("utf-8") == "my prose"


def test_api_items_media_auth_related_filename_with_spaces():
def test_api_items_media_auth_related_filename_with_spaces(settings):
"""
Users who have a specific access to an item, whatever the role, should be able to
retrieve related attachments.
Expand Down Expand Up @@ -360,7 +359,7 @@ def test_api_items_media_auth_suspicious_item_creator():
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")


def test_api_items_media_auth_filename_with_hash():
def test_api_items_media_auth_filename_with_hash(settings):
"""Files with '#' in their filename should not cause a SignatureDoesNotMatch."""
item = factories.ItemFactory(
link_reach="public",
Expand Down Expand Up @@ -399,3 +398,43 @@ def test_api_items_media_auth_filename_with_hash():
timeout=1,
)
assert response.content.decode("utf-8") == "my prose"


def test_api_items_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"
item = factories.ItemFactory(
link_reach="public",
type=models.ItemTypeChoices.FILE,
update_upload_state=models.ItemUploadStateChoices.READY,
)

default_storage.save(
item.file_key,
BytesIO(b"my prose"),
)
original_url = f"http://localhost/media/{item.file_key:s}"
now = timezone.now()
with freeze_time(now):
response = APIClient().get("/api/v1.0/items/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}/drive-media-storage/{item.file_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"
5 changes: 5 additions & 0 deletions src/backend/drive/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ class Base(Configuration):
MEDIA_URL_PREVIEW = "/media/preview/"
MEDIA_ROOT = os.path.join(DATA_DIR, "media")
MEDIA_BASE_URL = values.Value(None, environ_name="MEDIA_BASE_URL", 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,
)

SITE_ID = 1

Expand Down