diff --git a/ephios/plugins/federation/templates/federation/event_detail.html b/ephios/plugins/federation/templates/federation/event_detail.html index c8c3d710a..e3353cd59 100644 --- a/ephios/plugins/federation/templates/federation/event_detail.html +++ b/ephios/plugins/federation/templates/federation/event_detail.html @@ -5,6 +5,6 @@ {% block messages %}
{{ invite.url }} | {% translate "Show invite code" %} | @@ -57,7 +57,7 @@
{{ host.name }} | {{ host.url }} | diff --git a/ephios/plugins/federation/views/api.py b/ephios/plugins/federation/views/api.py index 174122115..13269f941 100644 --- a/ephios/plugins/federation/views/api.py +++ b/ephios/plugins/federation/views/api.py @@ -2,6 +2,7 @@ import requests from django.conf import settings +from django.core.exceptions import MultipleObjectsReturned from django.shortcuts import redirect from django.urls import reverse from django.views import View @@ -21,6 +22,10 @@ class RedeemInviteCodeView(CreateAPIView): + """ + API view that accepts an InviteCode and creates a FederatedGuest (to start sharing events with that instance). + """ + serializer_class = FederatedGuestCreateSerializer queryset = FederatedGuest.objects.all() authentication_classes = [] @@ -28,6 +33,10 @@ class RedeemInviteCodeView(CreateAPIView): class SharedEventListView(ListAPIView): + """ + API view that lists all events that are shared with the instance corresponding to the access token. + """ + serializer_class = SharedEventSerializer permission_classes = [TokenHasScope] required_scopes = [] @@ -42,20 +51,26 @@ def get_queryset(self): class FederationOAuthView(View): + """ + View that handles the OAuth2 flow for federated users from another instance. + """ + def get(self, request, *args, **kwargs): try: self.guest = ( - FederatedGuest.objects.get(pk=self.request.session["guest"]) - if "guest" in self.request.session.keys() + FederatedGuest.objects.get(pk=self.request.session["federation_guest_pk"]) + if "federation_guest_pk" in self.request.session.keys() else FederatedGuest.objects.get(url=self.request.GET["referrer"]) ) - except (KeyError, FederatedGuest.DoesNotExist) as exc: + except (KeyError, FederatedGuest.DoesNotExist, MultipleObjectsReturned) as exc: raise PermissionDenied from exc if "error" in request.GET.keys(): return redirect(urljoin(self.guest.url, reverse("federation:external_event_list"))) elif "code" in request.GET.keys(): self._oauth_callback() - return redirect("federation:event_detail", pk=self.request.session.pop("event")) + return redirect( + "federation:event_detail", pk=self.request.session.pop("federation_event") + ) else: return redirect(self._get_authorization_url()) @@ -68,8 +83,8 @@ def _get_authorization_url(self): ) verifier = oauth_client.create_code_verifier(64) self.request.session["code_verifier"] = verifier - self.request.session["event"] = self.kwargs["pk"] - self.request.session["guest"] = self.guest.pk + self.request.session["federation_event"] = self.kwargs["pk"] + self.request.session["federation_guest_pk"] = self.guest.pk challenge = oauth_client.create_code_challenge(verifier, "S256") authorization_url, _ = oauth.authorization_url( urljoin(self.guest.url, "api/oauth/authorize/"), @@ -89,7 +104,7 @@ def _oauth_callback(self): client_secret=self.guest.client_secret, code_verifier=self.request.session["code_verifier"], ) - self.request.session["access_token"] = token["access_token"] + self.request.session["federation_access_token"] = token["access_token"] self.request.session.set_expiry(token["expires_in"]) user_data = requests.get( urljoin(self.guest.url, "api/users/me/"), @@ -101,27 +116,31 @@ def _oauth_callback(self): federated_instance=self.guest, email=user_data.json()["email"] ) except FederatedUser.DoesNotExist: - user = FederatedUser.objects.create( - federated_instance=self.guest, - email=user_data.json()["email"], - first_name=user_data.json()["first_name"], - last_name=user_data.json()["last_name"], - date_of_birth=user_data.json()["date_of_birth"], - ) - for qualification in user_data.json()["qualifications"]: - try: - # Note that we assign the qualification on the host instance without further checks. - # This may lead to incorrect qualifications if the inclusions for the qualification - # are defined differently on the guest instance. We are accepting this as it should - # not happen with the pre-defined qualifications as we are displaying a warning if - # the user adapt these and custom qualifications will have different uuids anyway. - user.qualifications.add(Qualification.objects.get(uuid=qualification["uuid"])) - except Qualification.DoesNotExist: - for included_qualification in qualification["includes"]: - try: - user.qualifications.add( - Qualification.objects.get(uuid=included_qualification["uuid"]) - ) - except Qualification.DoesNotExist: - continue - self.request.session["federated_user"] = user.pk + user = self._create_user(user_data) + self.request.session["federation_user"] = user.pk + + def _create_user(self, user_data): + user = FederatedUser.objects.create( + federated_instance=self.guest, + email=user_data.json()["email"], + first_name=user_data.json()["first_name"], + last_name=user_data.json()["last_name"], + date_of_birth=user_data.json()["date_of_birth"], + ) + for qualification in user_data.json()["qualifications"]: + try: + # Note that we assign the qualification on the host instance without further checks. + # This may lead to incorrect qualifications if the inclusions for the qualification + # are defined differently on the guest instance. We are accepting this as it should + # not happen with the pre-defined qualifications as we are displaying a warning if + # the user adapt these and custom qualifications will have different uuids anyway. + user.qualifications.add(Qualification.objects.get(uuid=qualification["uuid"])) + except Qualification.DoesNotExist: + for included_qualification in qualification["includes"]: + try: + user.qualifications.add( + Qualification.objects.get(uuid=included_qualification["uuid"]) + ) + except Qualification.DoesNotExist: + continue + return user diff --git a/ephios/plugins/federation/views/frontend.py b/ephios/plugins/federation/views/frontend.py index 0e874ea2d..75c53c93e 100644 --- a/ephios/plugins/federation/views/frontend.py +++ b/ephios/plugins/federation/views/frontend.py @@ -27,6 +27,10 @@ class ExternalEventListView(LoginRequiredMixin, TemplateView): + """ + View that lists all events that are shared with this instance. + """ + template_name = "federation/external_event_list.html" def get_context_data(self, **kwargs): @@ -56,55 +60,74 @@ def get_context_data(self, **kwargs): class CheckFederatedAccessTokenMixin: def dispatch(self, request, *args, **kwargs): - if "access_token" not in request.session.keys(): + if "federation_access_token" not in request.session.keys(): return FederationOAuthView.as_view()(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs) def get_context_data(self, object): context = super().get_context_data() - context["guest"] = FederatedGuest.objects.get(pk=self.request.session["guest"]) + context["federation_guest"] = FederatedGuest.objects.get( + pk=self.request.session["federation_guest_pk"] + ) return context class FederatedEventDetailView(CheckFederatedAccessTokenMixin, DetailView): + """ + View that displays a shared event to a federated user from another instance + """ + model = Event template_name = "federation/event_detail.html" def get_object(self, queryset=None): obj = super().get_object(queryset) try: - guest = self.request.session["guest"] + guest = self.request.session["federation_guest_pk"] FederatedEventShare.objects.get( event=obj, shared_with__in=[FederatedGuest.objects.get(pk=guest)], ) except (KeyError, FederatedEventShare.DoesNotExist) as exc: + self.request.session.flush() raise PermissionDenied from exc return obj class FederatedUserShiftActionView(CheckFederatedAccessTokenMixin, BaseShiftActionView): + """ + View that allows a federated user from another instanceto sign up for a shift + """ + def get_participant(self): try: return FederatedUser.objects.get( - pk=self.request.session["federated_user"] + pk=self.request.session["federation_user"] ).as_participant() except FederatedUser.DoesNotExist as e: raise PermissionDenied from e class FederationSettingsView(StaffRequiredMixin, TemplateView): + """ + View that displays the federation settings page where new instances can be connected + """ + template_name = "federation/federation_settings.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["guests"] = FederatedGuest.objects.all() - context["hosts"] = FederatedHost.objects.all() - context["invites"] = InviteCode.objects.all() + context["federation_guests"] = FederatedGuest.objects.all() + context["federation_hosts"] = FederatedHost.objects.all() + context["federation_invites"] = InviteCode.objects.all() return context class CreateInviteCodeView(StaffRequiredMixin, CreateView): + """ + View that allows staff users to create new invite codes (to share events with another instance) + """ + model = InviteCode form_class = InviteCodeForm success_url = reverse_lazy("federation:settings") @@ -114,6 +137,10 @@ def get_success_url(self): class InviteCodeRevealView(StaffRequiredMixin, TemplateView): + """ + View that displays an invite code to a staff user + """ + template_name = "federation/invitecode_reveal.html" def get(self, request, *args, **kwargs): @@ -126,6 +153,10 @@ def get(self, request, *args, **kwargs): class RedeemInviteCodeView(StaffRequiredMixin, FormView): + """ + View that allows staff users to redeem an invite code (to receive events from another instance) + """ + form_class = RedeemInviteCodeForm template_name = "federation/redeem_invite_code.html" @@ -146,6 +177,10 @@ def form_valid(self, form): class FederatedGuestDeleteView(StaffRequiredMixin, SuccessMessageMixin, DeleteView): + """ + View that allows staff users to remove a guest instance (to stop sharing events with another instance) + """ + model = FederatedGuest success_url = reverse_lazy("federation:settings") success_message = _("You are no longer sharing events with this instance.") @@ -158,6 +193,10 @@ def form_valid(self, form): class FederatedHostDeleteView(StaffRequiredMixin, SuccessMessageMixin, DeleteView): + """ + View that allows staff users to remove a host instance (to stop receiving events from another instance) + """ + model = FederatedHost success_url = reverse_lazy("federation:settings") success_message = _("You are no longer receiving events from this instance.")