diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 85c83d9..ff7931d 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -20,7 +20,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install pylint==3.0.3 pylint-django==2.5.5 black==23.1.0 + pip install pylint==3.0.3 pylint-django==2.5.5 black==24.4.2 - name: Run pylint and black run: | diff --git a/LedenAdministratie/api.py b/LedenAdministratie/api.py index 34b2ac1..4e982d0 100644 --- a/LedenAdministratie/api.py +++ b/LedenAdministratie/api.py @@ -1,6 +1,12 @@ +import hashlib +import hmac +import imghdr + +from django.conf import settings from django.db.models import Q from django.http import HttpResponse, JsonResponse, HttpResponseForbidden from django.utils import timezone +from django.views import View from oauth2_provider.views import ProtectedResourceView, ScopedProtectedResourceView from LedenAdministratie.models import Member @@ -10,19 +16,20 @@ class ApiV1Smoelenboek(ProtectedResourceView): def get(self, request, *args, **kwargs): - large = request.GET.get("large", "0") == "1" + large = request.GET.get("large", "0") members = Member.objects.filter( Q(afmeld_datum__gt=timezone.now()) | Q(afmeld_datum=None) ).order_by("first_name") response = [] + expiry = int((timezone.now() + timezone.timedelta(days=1)).timestamp()) for member in members: - if large: - photo = member.foto - else: - photo = member.thumbnail - if photo is None: - photo = member.foto + # Generate a signed URL for the image + url = request.build_absolute_uri(f"{member.id}/{expiry}/?large={large}") + signature = hmac.new( + settings.SECRET_KEY.encode(), url.encode(), hashlib.sha256 + ).hexdigest() + url += f"&signature={signature}" memberdict = { "id": member.id, @@ -30,13 +37,34 @@ def get(self, request, *args, **kwargs): "first_name": member.first_name, "last_name": member.last_name, "types": ",".join([tmptype.slug for tmptype in member.types.all()]), - "photo": img2base64(photo), + "photo": url, } response.append(memberdict) return JsonResponse(data=response, safe=False) +class ApiV1SmoelenboekSigned(View): + def get(self, request, *args, **kwargs): + # Validate signature and expiry datetime + signature = request.GET.get("signature", "") + url = request.build_absolute_uri().replace(f"&signature={signature}", "") + new_signature = hmac.new( + settings.SECRET_KEY.encode(), url.encode(), hashlib.sha256 + ).hexdigest() + if new_signature != signature: + return HttpResponseForbidden() + if timezone.now().timestamp() > kwargs.get("expiry", 0): + return HttpResponseForbidden() + + # Return the image if all is OK + large = request.GET.get("large", "0") == "1" + member = Member.objects.get(id=kwargs.get("pk")) + photo = member.foto if large else member.thumbnail + content_type = imghdr.what(None, photo) + return HttpResponse(photo, content_type=f"image/{content_type}") + + class ApiV1SmoelenboekUser(ProtectedResourceView): def get(self, request, *args, **kwargs): large = request.GET.get("large", "0") == "1" diff --git a/LedenAdministratie/oidc.py b/LedenAdministratie/oidc.py index 61ecc86..40a901b 100644 --- a/LedenAdministratie/oidc.py +++ b/LedenAdministratie/oidc.py @@ -27,12 +27,14 @@ def get_additional_claims(self) -> dict: "media": lambda request: True, "account_type": lambda request: request.user.member.idp_types(), "days": lambda request: request.user.member.days, - "stripcard": lambda request: { - "count": request.user.member.active_stripcard.count, - "used": request.user.member.active_stripcard.used, - } - if request.user.member.active_stripcard - else None, + "stripcard": lambda request: ( + { + "count": request.user.member.active_stripcard.count, + "used": request.user.member.active_stripcard.used, + } + if request.user.member.active_stripcard + else None + ), } def validate_user( diff --git a/LedenAdministratie/urls.py b/LedenAdministratie/urls.py index 0b9e863..1895443 100644 --- a/LedenAdministratie/urls.py +++ b/LedenAdministratie/urls.py @@ -13,6 +13,7 @@ 1. Add an import: from blog import urls as blog_urls 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) """ + from django.contrib import admin from django.urls import path, include from two_factor.urls import urlpatterns as tf_urls @@ -98,6 +99,11 @@ path("email/log/", views.EmailLogView.as_view(), name="email_log"), path("settings/", views.SettingsView.as_view(), name="settings"), path("api/v1/smoelenboek/", api.ApiV1Smoelenboek.as_view()), + path( + "api/v1/smoelenboek///", + api.ApiV1SmoelenboekSigned.as_view(), + name="smoelenboek_signed", + ), path("api/v1/smoelenboek//", api.ApiV1SmoelenboekUser.as_view()), path("api/v1/member/details", api.ApiV1UserDetails.as_view()), path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")), diff --git a/LedenAdministratie/views.py b/LedenAdministratie/views.py index f1c17d4..5c970be 100644 --- a/LedenAdministratie/views.py +++ b/LedenAdministratie/views.py @@ -233,9 +233,9 @@ def get_context_data(self, **kwargs): if self.kwargs.get("member_id"): context["member"] = Member.objects.get(pk=self.kwargs["member_id"]) else: - context["form"].fields[ - "members" - ].queryset = InvoiceTool.get_members_invoice_type(self.invoice_type) + context["form"].fields["members"].queryset = ( + InvoiceTool.get_members_invoice_type(self.invoice_type) + ) self.lines = self.LinesFormSet( initial=InvoiceTool.get_defaults_for_invoice_type(