♻️(backend) refactor media_auth and collaboration_auth for flexibility

These 2 actions had factorized code but a few iterations lead to
spaghetti code where factorized code includes "if" clauses.

Refactor abstractions so that code factorization really works.
This commit is contained in:
Samuel Paccoud - DINUM
2024-12-27 17:19:16 +01:00
committed by Manuel Raynaud
parent 710bbf512c
commit 54f9b3963e

View File

@@ -1088,10 +1088,10 @@ class DocumentViewSet(
status=drf.status.HTTP_201_CREATED, status=drf.status.HTTP_201_CREATED,
) )
def _authorize_subrequest(self, request, pattern): def _auth_get_original_url(self, request):
""" """
Shared method to authorize access based on the original URL of an Nginx subrequest Extracts and parses the original URL from the "HTTP_X_ORIGINAL_URL" header.
and user permissions. Returns a dictionary of URL parameters if authorized. 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 nginx in the "HTTP_X_ORIGINAL_URL" header.
See corresponding ingress configuration in Helm chart and read about the See corresponding ingress configuration in Helm chart and read about the
@@ -1102,14 +1102,6 @@ class DocumentViewSet(
to let this request go through (by returning a 200 code) or if we block it (by returning to let this request go through (by returning a 200 code) or if we block it (by returning
a 403 error). Note that we return 403 errors without any further details for security a 403 error). Note that we return 403 errors without any further details for security
reasons. reasons.
Parameters:
- pattern: The regex pattern to extract identifiers from the URL.
Returns:
- A dictionary of URL parameters if the request is authorized.
Raises:
- PermissionDenied if authorization fails.
""" """
# Extract the original URL from the request header # Extract the original URL from the request header
original_url = request.META.get("HTTP_X_ORIGINAL_URL") original_url = request.META.get("HTTP_X_ORIGINAL_URL")
@@ -1117,52 +1109,32 @@ class DocumentViewSet(
logger.debug("Missing HTTP_X_ORIGINAL_URL header in subrequest") logger.debug("Missing HTTP_X_ORIGINAL_URL header in subrequest")
raise drf.exceptions.PermissionDenied() raise drf.exceptions.PermissionDenied()
parsed_url = urlparse(original_url) logger.debug("Original url: '%s'", original_url)
match = pattern.search(parsed_url.path) return urlparse(original_url)
# If the path does not match the pattern, try to extract the parameters from the query
if not match:
match = pattern.search(parsed_url.query)
if not match:
logger.debug(
"Subrequest URL '%s' did not match pattern '%s'",
parsed_url.path,
pattern,
)
raise drf.exceptions.PermissionDenied()
def _auth_get_url_params(self, pattern, fragment):
"""
Extracts URL parameters from the given fragment using the specified regex pattern.
Raises PermissionDenied if parameters cannot be extracted.
"""
match = pattern.search(fragment)
try: try:
url_params = match.groupdict() return match.groupdict()
except (ValueError, AttributeError) as exc: except (ValueError, AttributeError) as exc:
logger.debug("Failed to extract parameters from subrequest URL: %s", exc) logger.debug("Failed to extract parameters from subrequest URL: %s", exc)
raise drf.exceptions.PermissionDenied() from exc raise drf.exceptions.PermissionDenied() from exc
pk = url_params.get("pk") def _auth_get_document(self, pk):
if not pk: """
logger.debug("Document ID (pk) not found in URL parameters: %s", url_params) Retrieves the document corresponding to the given primary key (pk).
raise drf.exceptions.PermissionDenied() Raises PermissionDenied if the document is not found.
"""
# Fetch the document and check if the user has access
try: try:
document = models.Document.objects.get(pk=pk) return models.Document.objects.get(pk=pk)
except models.Document.DoesNotExist as exc: except models.Document.DoesNotExist as exc:
logger.debug("Document with ID '%s' does not exist", pk) logger.debug("Document with ID '%s' does not exist", pk)
raise drf.exceptions.PermissionDenied() from exc raise drf.exceptions.PermissionDenied() from exc
user_abilities = document.get_abilities(request.user)
if not user_abilities.get(self.action, False):
logger.debug(
"User '%s' lacks permission for document '%s'", request.user, pk
)
raise drf.exceptions.PermissionDenied()
logger.debug(
"Subrequest authorization successful. Extracted parameters: %s", url_params
)
return url_params, user_abilities, request.user.id
@drf.decorators.action(detail=False, methods=["get"], url_path="media-auth") @drf.decorators.action(detail=False, methods=["get"], url_path="media-auth")
def media_auth(self, request, *args, **kwargs): def media_auth(self, request, *args, **kwargs):
""" """
@@ -1174,13 +1146,24 @@ class DocumentViewSet(
annotation. The request will then be proxied to the object storage backend who will annotation. The request will then be proxied to the object storage backend who will
respond with the file after checking the signature included in headers. respond with the file after checking the signature included in headers.
""" """
url_params, _, _ = self._authorize_subrequest( parsed_url = self._auth_get_original_url(request)
request, MEDIA_STORAGE_URL_PATTERN url_params = self._auth_get_url_params(
enums.MEDIA_STORAGE_URL_PATTERN, parsed_url.path
) )
pk, key = url_params.values() document = self._auth_get_document(url_params["pk"])
if not document.get_abilities(request.user).get(self.action, False):
logger.debug(
"User '%s' lacks permission for document '%s'",
request.user,
document.pk,
)
raise drf.exceptions.PermissionDenied()
# Generate S3 authorization headers using the extracted URL parameters # Generate S3 authorization headers using the extracted URL parameters
request = utils.generate_s3_authorization_headers(f"{pk:s}/{key:s}") request = utils.generate_s3_authorization_headers(
f"{url_params['pk']:s}/{url_params['key']:s}"
)
return drf.response.Response("authorized", headers=request.headers, status=200) return drf.response.Response("authorized", headers=request.headers, status=200)
@@ -1190,18 +1173,34 @@ class DocumentViewSet(
This view is used by an Nginx subrequest to control access to a document's This view is used by an Nginx subrequest to control access to a document's
collaboration server. collaboration server.
""" """
_, user_abilities, user_id = self._authorize_subrequest( parsed_url = self._auth_get_original_url(request)
request, COLLABORATION_WS_URL_PATTERN url_params = self._auth_get_url_params(
enums.COLLABORATION_WS_URL_PATTERN, parsed_url.query
) )
can_edit = user_abilities["partial_update"] document = self._auth_get_document(url_params["pk"])
abilities = document.get_abilities(request.user)
if not abilities.get(self.action, False):
logger.debug(
"User '%s' lacks permission for document '%s'",
request.user,
document.pk,
)
raise drf.exceptions.PermissionDenied()
if not settings.COLLABORATION_SERVER_SECRET:
logger.debug("Collaboration server secret is not defined")
raise drf.exceptions.PermissionDenied()
# Add the collaboration server secret token to the headers # Add the collaboration server secret token to the headers
headers = { headers = {
"Authorization": settings.COLLABORATION_SERVER_SECRET, "Authorization": settings.COLLABORATION_SERVER_SECRET,
"X-Can-Edit": str(can_edit), "X-Can-Edit": str(abilities["partial_update"]),
"X-User-Id": str(user_id),
} }
if request.user.is_authenticated:
headers["X-User-Id"] = str(request.user.id)
return drf.response.Response("authorized", headers=headers, status=200) return drf.response.Response("authorized", headers=headers, status=200)
@drf.decorators.action( @drf.decorators.action(