From 901ca17d16fbb5985521103f2fdda28c18071c9d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 13:01:40 -0700 Subject: [PATCH 01/14] Adds site setting to disable federation --- bookwyrm/forms/admin.py | 7 +++ .../0210_sitesettings_disable_federation.py | 18 +++++++ bookwyrm/models/site.py | 2 + .../settings/federation/settings.html | 48 +++++++++++++++++++ bookwyrm/templates/settings/layout.html | 11 ++++- bookwyrm/urls.py | 5 ++ bookwyrm/views/__init__.py | 1 + bookwyrm/views/admin/federation_settings.py | 36 ++++++++++++++ 8 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 bookwyrm/migrations/0210_sitesettings_disable_federation.py create mode 100644 bookwyrm/templates/settings/federation/settings.html create mode 100644 bookwyrm/views/admin/federation_settings.py diff --git a/bookwyrm/forms/admin.py b/bookwyrm/forms/admin.py index 72f50ccb87..c246acb746 100644 --- a/bookwyrm/forms/admin.py +++ b/bookwyrm/forms/admin.py @@ -177,6 +177,13 @@ class Meta: exclude = ["remote_id"] +class FederationSettings(CustomForm): + class Meta: + model = models.SiteSettings + fields = [ + "disable_federation", + ] + class AutoModRuleForm(CustomForm): class Meta: model = models.AutoMod diff --git a/bookwyrm/migrations/0210_sitesettings_disable_federation.py b/bookwyrm/migrations/0210_sitesettings_disable_federation.py new file mode 100644 index 0000000000..b4630bc286 --- /dev/null +++ b/bookwyrm/migrations/0210_sitesettings_disable_federation.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.15 on 2024-08-27 19:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0209_user_show_ratings"), + ] + + operations = [ + migrations.AddField( + model_name="sitesettings", + name="disable_federation", + field=models.BooleanField(default=False), + ), + ] diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index 6c2a73422b..15af25c1cd 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -83,6 +83,7 @@ class SiteSettings(SiteModel): invite_question_text = models.CharField( max_length=255, blank=True, default="What is your favourite book?" ) + # images logo = models.ImageField(upload_to="logos/", null=True, blank=True) logo_small = models.ImageField(upload_to="logos/", null=True, blank=True) @@ -103,6 +104,7 @@ class SiteSettings(SiteModel): import_limit_reset = models.IntegerField(default=0) user_exports_enabled = models.BooleanField(default=False) user_import_time_limit = models.IntegerField(default=48) + disable_federation = models.BooleanField(default=False) field_tracker = FieldTracker(fields=["name", "instance_tagline", "logo"]) diff --git a/bookwyrm/templates/settings/federation/settings.html b/bookwyrm/templates/settings/federation/settings.html new file mode 100644 index 0000000000..79c78bd497 --- /dev/null +++ b/bookwyrm/templates/settings/federation/settings.html @@ -0,0 +1,48 @@ +{% extends 'settings/layout.html' %} +{% load i18n %} +{% block title %}{% trans "Federation Settings" %}{% endblock %} + +{% block header %}{% trans "Federated Settings" %}{% endblock %} + +{% block panel %} +{% if success %} +
+ + + {% trans "Settings saved" %} + +
+{% endif %} + +{% if form.errors %} +
+ + + {% trans "Unable to save settings" %} + +
+{% endif %} + +
+ {% csrf_token %} +
+
+ +
+
+
+ +
+
+ + +{% endblock %} + diff --git a/bookwyrm/templates/settings/layout.html b/bookwyrm/templates/settings/layout.html index 70c7ef0f47..79abea1917 100644 --- a/bookwyrm/templates/settings/layout.html +++ b/bookwyrm/templates/settings/layout.html @@ -41,12 +41,19 @@ {% url 'settings-invites' as alt_url %} {% trans "Invites" %} - {% if perms.bookwyrm.control_federation %} + + {% endif %} + {% if perms.bookwyrm.control_federation %} + + {% endif %} {% if perms.bookwyrm.moderate_user %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index cd75eb0c02..61b8e05bbe 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -162,6 +162,11 @@ views.ActivateUserAdmin.as_view(), name="settings-activate-user", ), + re_path( + r"^settings/federation-settings/?$", + views.FederationSettings.as_view(), + name="settings-federation-settings", + ), re_path( r"^settings/federation/(?P(federated|blocked))?/?$", views.Federation.as_view(), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index ebc851847a..28cf56792b 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -10,6 +10,7 @@ from .admin.federation import Federation, FederatedServer from .admin.federation import AddFederatedServer, ImportServerBlocklist from .admin.federation import block_server, unblock_server, refresh_server +from .admin.federation_settings import FederationSettings from .admin.email_blocklist import EmailBlocklist from .admin.email_config import EmailConfig from .admin.imports import ( diff --git a/bookwyrm/views/admin/federation_settings.py b/bookwyrm/views/admin/federation_settings.py new file mode 100644 index 0000000000..127f85a120 --- /dev/null +++ b/bookwyrm/views/admin/federation_settings.py @@ -0,0 +1,36 @@ +""" big picture settings about how the instance shares data """ +from django.contrib.auth.decorators import login_required, permission_required +from django.template.response import TemplateResponse +from django.utils.decorators import method_decorator +from django.views import View + +from bookwyrm import forms, models + + +# pylint: disable= no-self-use +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required("bookwyrm.control_federation", raise_exception=True), + name="dispatch", +) +class FederationSettings(View): + """what servers do we federate with""" + + def get(self, request): + """show the current settings""" + site = models.SiteSettings.objects.get() + data = { + "form": forms.FederationSettings(instance=site), + } + return TemplateResponse(request, "settings/federation/settings.html", data) + + def post(self, request): + """Update federation settings""" + site = models.SiteSettings.objects.get() + form = forms.FederationSettings(request.POST, instance=site) + if not form.is_valid(): + data = {"form": form} + return TemplateResponse(request, "settings/federation/settings.html", data) + form.save(request) + data = {"form": forms.FederationSettings(instance=site), "success": True} + return TemplateResponse(request, "settings/federation/settings.html", data) From 48d564701834fed7789cbb97bd5a6e706bdf5c68 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 13:06:42 -0700 Subject: [PATCH 02/14] Don't use bookwyrm connectors when federation is disabled --- bookwyrm/connectors/bookwyrm_connector.py | 8 ++++++++ bookwyrm/connectors/connector_manager.py | 6 +++++- bookwyrm/forms/admin.py | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/bookwyrm/connectors/bookwyrm_connector.py b/bookwyrm/connectors/bookwyrm_connector.py index 4064f4b4c5..5040041a6d 100644 --- a/bookwyrm/connectors/bookwyrm_connector.py +++ b/bookwyrm/connectors/bookwyrm_connector.py @@ -5,11 +5,19 @@ from bookwyrm import activitypub, models from bookwyrm.book_search import SearchResult from .abstract_connector import AbstractMinimalConnector +from .connector_manager import ConnectorException class Connector(AbstractMinimalConnector): """this is basically just for search""" + def __init__(self, identifier: str): + if models.SiteSettings.objects.get().disable_federation: + raise ConnectorException( + "Federation is disabled, cannot load BookWyrm connector", identifier + ) + super().__init__(identifier) + def get_or_create_book(self, remote_id: str) -> models.Edition: return activitypub.resolve_remote_id(remote_id, model=models.Edition) diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index ad68af1dc8..e5a2399d3a 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -111,7 +111,11 @@ def first_search_result( def get_connectors() -> Iterator[abstract_connector.AbstractConnector]: """load all connectors""" - for info in models.Connector.objects.filter(active=True).order_by("priority").all(): + queryset = models.Connector.objects.filter(active=True) + if models.SiteSettings.objects.get().disable_federation: + queryset = queryset.exclude(connector_file="bookwyrm_connector") + + for info in queryset.order_by("priority").all(): yield load_connector(info) diff --git a/bookwyrm/forms/admin.py b/bookwyrm/forms/admin.py index c246acb746..a2b35a3745 100644 --- a/bookwyrm/forms/admin.py +++ b/bookwyrm/forms/admin.py @@ -184,6 +184,7 @@ class Meta: "disable_federation", ] + class AutoModRuleForm(CustomForm): class Meta: model = models.AutoMod From 005f9fc0797431e17b5f693d4b4d7575ef2a0cdd Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 13:31:44 -0700 Subject: [PATCH 03/14] Adds decorator for views that require federation to be enabled --- bookwyrm/decorators.py | 13 +++++++++++++ bookwyrm/models/activitypub_mixin.py | 3 +++ bookwyrm/models/site.py | 6 ++++++ bookwyrm/views/follow.py | 5 +++++ bookwyrm/views/inbox.py | 2 ++ bookwyrm/views/wellknown.py | 8 ++++++++ 6 files changed, 37 insertions(+) create mode 100644 bookwyrm/decorators.py diff --git a/bookwyrm/decorators.py b/bookwyrm/decorators.py new file mode 100644 index 0000000000..56eef8fda2 --- /dev/null +++ b/bookwyrm/decorators.py @@ -0,0 +1,13 @@ +""" Custom view decorators """ +from functools import wraps +from bookwyrm.models.site import SiteSettings + + +def require_federation(function): + """Ensure that federation is allowed before proceeding with this view""" + + @wraps(function) + def wrap(request, *args, **kwargs): # pylint: disable=unused-argument + SiteSettings.objects.get().raise_federation_disabled() + + return wrap diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index 54ad115119..7758ecc4fe 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -129,6 +129,9 @@ def find_existing(cls, data): def broadcast(self, activity, sender, software=None, queue=BROADCAST): """send out an activity""" + site_model = apps.get_model("bookwyrm.SiteSettings", require_ready=True) + site_model.objects.get().raise_federation_disabled() + broadcast_task.apply_async( args=( sender.id, diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index 15af25c1cd..09f67419a4 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -12,6 +12,7 @@ from model_utils import FieldTracker from bookwyrm.connectors.abstract_connector import get_data +from bookwyrm.connectors import ConnectorException from bookwyrm.preview_images import generate_site_preview_image_task from bookwyrm.settings import BASE_URL, ENABLE_PREVIEW_IMAGES, STATIC_FULL_URL from bookwyrm.settings import RELEASE_API @@ -133,6 +134,11 @@ def favicon_url(self): """helper to build the logo url""" return self.get_url("favicon", "images/favicon.png") + def raise_federation_disabled(self): + """Don't connect to the outside world""" + if self.disable_federation: + raise ConnectorException("Federation is disabled") + def get_url(self, field, default_path): """get a media url or a default static path""" uploaded = getattr(self, field, None) diff --git a/bookwyrm/views/follow.py b/bookwyrm/views/follow.py index dcb1c695cd..0ff0001f60 100644 --- a/bookwyrm/views/follow.py +++ b/bookwyrm/views/follow.py @@ -9,6 +9,7 @@ from django.views.decorators.http import require_POST from bookwyrm import models +from bookwyrm.decorators import require_federation from bookwyrm.models.relationship import clear_cache from .helpers import ( get_user_from_username, @@ -131,6 +132,7 @@ def delete_follow_request(request): return redirect(f"/user/{request.user.localname}") +@require_federation def ostatus_follow_request(request): """prepare an outgoing remote follow request""" uri = urllib.parse.unquote(request.GET.get("acct")) @@ -169,6 +171,7 @@ def ostatus_follow_request(request): @login_required +@require_federation def ostatus_follow_success(request): """display success message for remote follow""" user = get_user_from_username(request.user, request.GET.get("following")) @@ -176,6 +179,7 @@ def ostatus_follow_success(request): return TemplateResponse(request, "ostatus/success.html", data) +@require_federation def remote_follow_page(request): """display remote follow page""" user = get_user_from_username(request.user, request.GET.get("user")) @@ -184,6 +188,7 @@ def remote_follow_page(request): @require_POST +@require_federation def remote_follow(request): """direct user to follow from remote account using ostatus subscribe protocol""" remote_user = request.POST.get("remote_user") diff --git a/bookwyrm/views/inbox.py b/bookwyrm/views/inbox.py index 4b95469d95..4a1f954d65 100644 --- a/bookwyrm/views/inbox.py +++ b/bookwyrm/views/inbox.py @@ -13,6 +13,7 @@ from django.views.decorators.csrf import csrf_exempt from bookwyrm import activitypub, models +from bookwyrm.decorators import require_federation from bookwyrm.tasks import app, INBOX from bookwyrm.signatures import Signature from bookwyrm.utils import regex @@ -21,6 +22,7 @@ @method_decorator(csrf_exempt, name="dispatch") +@method_decorator(require_federation, name="dispatch") # pylint: disable=no-self-use class Inbox(View): """requests sent by outside servers""" diff --git a/bookwyrm/views/wellknown.py b/bookwyrm/views/wellknown.py index e640c1c72d..4e3d6e4d7e 100644 --- a/bookwyrm/views/wellknown.py +++ b/bookwyrm/views/wellknown.py @@ -9,10 +9,12 @@ from django.views.decorators.http import require_GET from bookwyrm import models +from bookwyrm.decorators import require_federation from bookwyrm.settings import BASE_URL, DOMAIN, VERSION, LANGUAGE_CODE @require_GET +@require_federation def webfinger(request): """allow other servers to ask about a user""" resource = request.GET.get("resource") @@ -42,6 +44,7 @@ def webfinger(request): @require_GET +@require_federation def nodeinfo_pointer(_): """direct servers to nodeinfo""" return JsonResponse( @@ -57,6 +60,7 @@ def nodeinfo_pointer(_): @require_GET +@require_federation def nodeinfo(_): """basic info about the server""" status_count = models.Status.objects.filter(user__local=True, deleted=False).count() @@ -92,6 +96,7 @@ def nodeinfo(_): @require_GET +@require_federation def instance_info(_): """let's talk about your cool unique instance""" user_count = models.User.objects.filter(is_active=True, local=True).count() @@ -121,6 +126,7 @@ def instance_info(_): @require_GET +@require_federation def peers(_): """list of federated servers this instance connects with""" names = models.FederatedServer.objects.filter(status="federated").values_list( @@ -130,12 +136,14 @@ def peers(_): @require_GET +@require_federation def host_meta(request): """meta of the host""" return TemplateResponse(request, "host_meta.xml", {"DOMAIN": DOMAIN}) @require_GET +@require_federation def opensearch(request): """Open Search xml spec""" site = models.SiteSettings.get() From a5ae9c86a85e86cc81301da803b0155be2e6fe35 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 27 Aug 2024 14:01:31 -0700 Subject: [PATCH 04/14] Adds help text to federation settings page --- bookwyrm/forms/admin.py | 6 ++++++ bookwyrm/templates/settings/federation/settings.html | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/bookwyrm/forms/admin.py b/bookwyrm/forms/admin.py index a2b35a3745..7fa6218585 100644 --- a/bookwyrm/forms/admin.py +++ b/bookwyrm/forms/admin.py @@ -184,6 +184,12 @@ class Meta: "disable_federation", ] + widgets = { + "disable_federation": forms.CheckboxInput( + attrs={"aria-describedby": "desc_disable_federation"} + ), + } + class AutoModRuleForm(CustomForm): class Meta: diff --git a/bookwyrm/templates/settings/federation/settings.html b/bookwyrm/templates/settings/federation/settings.html index 79c78bd497..665bc29528 100644 --- a/bookwyrm/templates/settings/federation/settings.html +++ b/bookwyrm/templates/settings/federation/settings.html @@ -30,12 +30,15 @@ method="POST" > {% csrf_token %} -
+
+ + {% trans "Prevents your instance from interacting with other federated services. Existing data from other instances will still be present." %} +