Use permissions instead of authentication in API

This commit is contained in:
Tobias Kunze 2025-09-26 13:41:51 +02:00
parent 7cbd1162ff
commit b21148db01
2 changed files with 50 additions and 56 deletions

View file

@ -1,56 +0,0 @@
import base64
from django.conf import settings
from django.http import HttpResponse
from django.utils.translation import gettext as _
class OSBBasicAuthentication:
"""
HTTP Basic Authentication for OSB API endpoints.
Uses environment variables for username/password configuration.
"""
def __init__(self, get_response):
self.get_response = get_response
self.osb_username = getattr(settings, "OSB_USERNAME", None)
self.osb_password = getattr(settings, "OSB_PASSWORD", None)
def __call__(self, request):
# Only apply authentication to OSB API endpoints
if request.path.startswith("/v2/"):
if not self._authenticate_request(request):
return self._authentication_required()
return self.get_response(request)
def _authenticate_request(self, request):
"""
Authenticate the request using HTTP Basic Authentication.
"""
if not self.osb_username or not self.osb_password:
return False
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
if not auth_header.startswith("Basic "):
return False
try:
encoded_credentials = auth_header[6:] # Remove 'Basic ' prefix
decoded_credentials = base64.b64decode(encoded_credentials).decode("utf-8")
username, password = decoded_credentials.split(":", 1)
return username == self.osb_username and password == self.osb_password
except ValueError:
return False
def _authentication_required(self):
"""
Return an HTTP 401 Unauthorized response.
"""
response = HttpResponse(
_("Authentication required"), status=401, content_type="text/plain"
)
response["WWW-Authenticate"] = 'Basic realm="OSB API"'
return response

View file

@ -0,0 +1,50 @@
import base64
from contextlib import suppress
from django.conf import settings
from django.http import HttpResponseForbidden
def get_username_and_password(request): # pragma: no cover
# This method is vendored from assorted DRF bits, so we
# skip it in our test coverage report
auth = request.META.get("HTTP_AUTHORIZATION", b"")
if isinstance(auth, str):
# Work around django test client oddness
auth = auth.encode("iso-8859-1")
auth = auth.split()
if not auth or auth[0].lower() != b"basic":
return False, False
if len(auth) != 2:
return False, False
with suppress(TypeError, ValueError):
try:
auth_decoded = base64.b64decode(auth[1]).decode("utf-8")
except UnicodeDecodeError:
auth_decoded = base64.b64decode(auth[1]).decode("latin-1")
return auth_decoded.split(":", 1)
return False, False
class OSBBasicAuthPermission:
"""
Basic auth for OSB is implemented as a permission class rather than as
an authentication class, because authentication is expected to associate
the request with a Django user. However, the OSB/Exoscale requests do not
relate to a user account, so we treat the auth result as a permission instead.
"""
def dispatch(self, request, *args, **kwargs):
osb_username = getattr(settings, "OSB_USERNAME", None)
osb_password = getattr(settings, "OSB_PASSWORD", None)
if not osb_username or not osb_password:
return False # pragma: no cover
username, password = get_username_and_password(request)
if username == osb_username and password == osb_password:
return super().dispatch(request, *args, **kwargs)
return HttpResponseForbidden()