hide instance actor from users

The Instance Actor is required for signing http GET requests but is not a "user" and should not be otherwise interacted with.

- hides instance actor profile page, returning a 404
- excludes instance actor from search results and suggestions including in Getting Started
- replaces link to user profile in user admin page with a brief message box
- replaces panel in user admin page that allows for user to be suspended or removed with a message explaining why that is a very bad idea

fixes #3119
This commit is contained in:
Hugh Rundle 2023-11-20 12:17:52 +11:00
parent 06568aab88
commit f011f2bce9
No known key found for this signature in database
GPG key ID: A7E35779918253F9
8 changed files with 115 additions and 69 deletions

View file

@ -396,7 +396,7 @@ def resolve_remote_id(
def get_representative():
"""Get or create an actor representing the instance
to sign requests to 'secure mastodon' servers"""
to sign outgoing HTTP GET requests"""
username = f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}"
email = "bookwyrm@localhost"
try:

View file

@ -8,6 +8,7 @@ from opentelemetry import trace
from bookwyrm import models
from bookwyrm.redis_store import RedisStore, r
from bookwyrm.settings import INSTANCE_ACTOR_USERNAME
from bookwyrm.tasks import app, SUGGESTED_USERS
from bookwyrm.telemetry import open_telemetry
@ -98,9 +99,15 @@ class SuggestedUsers(RedisStore):
for (pk, score) in values
]
# annotate users with mutuals and shared book counts
users = models.User.objects.filter(
is_active=True, bookwyrm_user=True, id__in=[pk for (pk, _) in values]
).annotate(mutuals=Case(*annotations, output_field=IntegerField(), default=0))
users = (
models.User.objects.filter(
is_active=True, bookwyrm_user=True, id__in=[pk for (pk, _) in values]
)
.annotate(
mutuals=Case(*annotations, output_field=IntegerField(), default=0)
)
.exclude(localname=INSTANCE_ACTOR_USERNAME)
)
if local:
users = users.filter(local=True)
return users.order_by("-mutuals")[:5]

View file

@ -1,6 +1,7 @@
{% load i18n %}
{% load markdown %}
{% load humanize %}
{% load utilities %}
<div class="block columns">
<div class="column is-flex is-flex-direction-column">
@ -13,7 +14,17 @@
</div>
{% endif %}
{% if user.localname|is_instance_admin %}
<div class="message is-warning">
<div class="message-body">
{% trans "This account is the instance actor for signing HTTP requests." %}
</div>
</div>
{% else %}
<p class="mt-2"><a href="{{ user.local_path }}">{% trans "View user profile" %}</a></p>
{% endif %}
{% url 'settings-user' user.id as url %}
{% if not request.path == url %}
<p class="mt-2"><a href="{{ url }}">{% trans "Go to user admin" %}</a></p>

View file

@ -1,4 +1,5 @@
{% load i18n %}
{% load utilities %}
<div class="block content">
{% if not user.is_active and user.deactivation_reason == "self_deletion" or user.deactivation_reason == "moderator_deletion" %}
<div class="notification is-danger">
@ -7,77 +8,90 @@
{% else %}
<h3>{% trans "User Actions" %}</h3>
<div class="box">
<div class="is-flex">
{% if user.is_active %}
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
</p>
{% endif %}
{% if user.localname|is_instance_admin %}
<div class="box">
<div class="message is-warning">
<div class="message-header">
<p>{% trans "This is the instance admin actor" %}</p>
</div>
<div class="message-body">
<p>{% trans "You must not delete or disable this account as it is critical to the functioning of your server. This actor signs outgoing GET requests to smooth interaction with secure ActivityPub servers." %}</p>
<p>{% trans "This account is not discoverable by ordinary users and does not have a profile page." %}</p>
</div>
</div>
</div>
{% else %}
<div class="box">
<div class="is-flex">
{% if user.is_active %}
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
</p>
{% endif %}
{% if not user.is_active and user.deactivation_reason == "pending" %}
<form name="activate" method="post" action="{% url 'settings-activate-user' user.id %}" class="mr-1">
{% csrf_token %}
<button type="submit" class="button is-success is-light">{% trans "Activate user" %}</button>
</form>
{% endif %}
{% if user.is_active or user.deactivation_reason == "pending" %}
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id report.id %}" class="mr-1">
{% csrf_token %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
</form>
{% else %}
<form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id report.id %}" class="mr-1">
{% csrf_token %}
<button class="button">{% trans "Un-suspend user" %}</button>
</form>
{% if not user.is_active and user.deactivation_reason == "pending" %}
<form name="activate" method="post" action="{% url 'settings-activate-user' user.id %}" class="mr-1">
{% csrf_token %}
<button type="submit" class="button is-success is-light">{% trans "Activate user" %}</button>
</form>
{% endif %}
{% if user.is_active or user.deactivation_reason == "pending" %}
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id report.id %}" class="mr-1">
{% csrf_token %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
</form>
{% else %}
<form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id report.id %}" class="mr-1">
{% csrf_token %}
<button class="button">{% trans "Un-suspend user" %}</button>
</form>
{% endif %}
{% if user.local %}
<div>
{% trans "Permanently delete user" as button_text %}
{% include "snippets/toggle/open_button.html" with controls_text="delete_user" text=button_text class="is-danger is-light" %}
</div>
{% endif %}
</div>
{% if user.local %}
<div>
{% include "settings/users/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %}
</div>
{% endif %}
{% if user.local %}
<div>
{% trans "Permanently delete user" as button_text %}
{% include "snippets/toggle/open_button.html" with controls_text="delete_user" text=button_text class="is-danger is-light" %}
<form name="permission" method="post" action="{% url 'settings-user' user.id report.id %}">
{% csrf_token %}
<label class="label" for="id_user_group">{% trans "Access level:" %}</label>
{% if group_form.non_field_errors %}
{{ group_form.non_field_errors }}
{% endif %}
{% with group=user.groups.first %}
<div class="select">
<select name="groups" id="id_user_group" aria-describedby="desc_user_group">
{% for value, name in group_form.fields.groups.choices %}
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>
{{ name|title }}
</option>
{% endfor %}
<option value="" {% if not group %}selected{% endif %}>
User
</option>
</select>
</div>
{% include 'snippets/form_errors.html' with errors_list=group_form.groups.errors id="desc_user_group" %}
{% endwith %}
<button class="button">
{% trans "Save" %}
</button>
</form>
</div>
{% endif %}
</div>
{% if user.local %}
<div>
{% include "settings/users/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %}
</div>
{% endif %}
{% if user.local %}
<div>
<form name="permission" method="post" action="{% url 'settings-user' user.id report.id %}">
{% csrf_token %}
<label class="label" for="id_user_group">{% trans "Access level:" %}</label>
{% if group_form.non_field_errors %}
{{ group_form.non_field_errors }}
{% endif %}
{% with group=user.groups.first %}
<div class="select">
<select name="groups" id="id_user_group" aria-describedby="desc_user_group">
{% for value, name in group_form.fields.groups.choices %}
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>
{{ name|title }}
</option>
{% endfor %}
<option value="" {% if not group %}selected{% endif %}>
User
</option>
</select>
</div>
{% include 'snippets/form_errors.html' with errors_list=group_form.groups.errors id="desc_user_group" %}
{% endwith %}
<button class="button">
{% trans "Save" %}
</button>
</form>
</div>
{% endif %}
</div>
{% endif %}
</div>

View file

@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _
from django.templatetags.static import static
from bookwyrm.models import User
from bookwyrm.settings import INSTANCE_ACTOR_USERNAME
register = template.Library()
@ -125,3 +126,9 @@ def id_to_username(user_id):
value = f"{name}@{domain}"
return value
@register.filter(name="is_instance_admin")
def is_instance_admin(localname):
"""Returns a boolean indicating whether the user is the instance admin account"""
return localname == INSTANCE_ACTOR_USERNAME

View file

@ -11,6 +11,7 @@ from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import book_search, forms, models
from bookwyrm.settings import INSTANCE_ACTOR_USERNAME
from bookwyrm.suggested_users import suggested_users
from .preferences.edit_user import save_user_form
@ -108,6 +109,7 @@ class GetStartedUsers(View):
.exclude(
id=request.user.id,
)
.exclude(localname=INSTANCE_ACTOR_USERNAME)
.order_by("-similarity")[:5]
)
data = {"no_results": not user_results}

View file

@ -13,7 +13,7 @@ from csp.decorators import csp_update
from bookwyrm import models
from bookwyrm.connectors import connector_manager
from bookwyrm.book_search import search, format_search_result
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.settings import PAGE_LENGTH, INSTANCE_ACTOR_USERNAME
from bookwyrm.utils import regex
from .helpers import is_api_request
from .helpers import handle_remote_webfinger
@ -113,6 +113,7 @@ def user_search(request):
.filter(
similarity__gt=0.5,
)
.exclude(localname=INSTANCE_ACTOR_USERNAME)
.order_by("-similarity")
)

View file

@ -11,7 +11,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.settings import PAGE_LENGTH, INSTANCE_ACTOR_USERNAME
from .helpers import get_user_from_username, is_api_request
@ -31,6 +31,10 @@ class User(View):
return ActivitypubResponse(user.to_activity())
# otherwise we're at a UI view
# if it's not an API request, never show the instance actor profile page
if user.localname == INSTANCE_ACTOR_USERNAME:
raise Http404()
shelf_preview = []
# only show shelves that should be visible