Add Accept entity and ActivityPub follow auto-Accept flow

When receiving an ActivityPub Follow, send back an Accept activity
automatically. Due to application hook needed to fetch sending
user private key (for signing), this is only available if Django
is installed since currently application hooks exist only for
Django configuration.

Django applications should include a new configuration item
"get_private_key_function" which points to a function which takes
a user identifier (fid, handle or guid) and returns a private key
in RSA object format.
merge-requests/143/head
Jason Robinson 2019-03-17 03:17:10 +02:00
rodzic d8a067a691
commit 353ae9ff9d
8 zmienionych plików z 70 dodań i 2 usunięć

Wyświetl plik

@ -118,6 +118,7 @@ Some settings need to be set in Django settings. An example is below:
FEDERATION = {
"base_url": "https://myserver.domain.tld,
"get_object_function": "myproject.utils.get_object",
"get_private_key_function": "myproject.utils.get_private_key",
"get_profile_function": "myproject.utils.get_profile",
"nodeinfo2_function": "myproject.utils.get_nodeinfo2_data",
"process_payload_function": "myproject.utils.process_payload",
@ -126,6 +127,7 @@ Some settings need to be set in Django settings. An example is below:
* ``base_url`` is the base URL of the server, ie protocol://domain.tld.
* ``get_object_function`` should be the full path to a function that will return the object matching the ActivityPub ID for the request object passed to this function.
* ``get_private_key_function`` should be the full path to a function that will accept a federation ID (url, handle or guid) and return the private key of the user (as an RSA object). Required for example to sign outbound messages in some cases.
* ``get_profile_function`` should be the full path to a function that should return a ``Profile`` entity. The function should take the following parameters: ``handle``, ``guid`` and ``request``. It should look up a profile with one or more of the provided parameters.
* ``nodeinfo2_function`` (optional) function that returns data for generating a `NodeInfo2 document <https://github.com/jaywink/nodeinfo2>`_. Once configured the path ``/.well-known/x-nodeinfo2`` will automatically generate a NodeInfo2 document. The function should return a ``dict`` corresponding to the NodeInfo2 schema, with the following minimum items:

Wyświetl plik

@ -1,3 +1,5 @@
import logging
import uuid
from typing import Dict
from federation.entities.activitypub.constants import (
@ -5,13 +7,57 @@ from federation.entities.activitypub.constants import (
CONTEXT_LD_SIGNATURES)
from federation.entities.activitypub.enums import ActorType, ObjectType, ActivityType
from federation.entities.activitypub.mixins import ActivitypubEntityMixin
from federation.entities.base import Profile, Post, Follow
from federation.entities.base import Profile, Post, Follow, Accept
from federation.outbound import handle_send
from federation.types import UserType
from federation.utils.text import with_slash
logger = logging.getLogger("federation")
class ActivitypubAccept(ActivitypubEntityMixin, Accept):
_type = ActivityType.ACCEPT.value
def to_as2(self) -> Dict:
as2 = {
"@context": CONTEXTS_DEFAULT,
"id": self.activity_id,
"type": self._type,
"actor": self.actor_id,
"object": self.target_id,
}
return as2
class ActivitypubFollow(ActivitypubEntityMixin, Follow):
_type = ActivityType.FOLLOW.value
def post_receive(self, attributes: Dict) -> None:
"""
Post receive hook - send back follow ack.
"""
try:
from federation.utils.django import get_function_from_config
except ImportError:
logger.warning("ActivitypubFollow.post_receive - Unable to send automatic Accept back, only supported on "
"Django currently")
return
get_private_key_function = get_function_from_config("get_private_key_function")
key = get_private_key_function(self.target_id)
if not key:
logger.warning("ActivitypubFollow.post_receive - Failed to send automatic Accept back: could not find "
"profile to sign it with")
return
accept = ActivitypubAccept(
activity_id=f"{self.target_id}#accept-{uuid.uuid4()}",
actor_id=self.target_id,
target_id=self.activity_id,
)
try:
handle_send(accept, UserType(id=self.target_id, private_key=key), recipients=[self.actor_id])
except Exception:
logger.exception("ActivitypubFollow.post_receive - Failed to send Accept back")
def to_as2(self) -> Dict:
as2 = {
"@context": CONTEXTS_DEFAULT,

Wyświetl plik

@ -8,6 +8,7 @@ class EnumBase(Enum):
class ActivityType(EnumBase):
ACCEPT = "Accept"
CREATE = "Create"
DELETE = "Delete"
FOLLOW = "Follow"

Wyświetl plik

@ -23,7 +23,12 @@ def element_to_objects(
return []
transformed = transform_attributes(payload, cls)
entities.append(cls(**transformed))
entity = cls(**transformed)
if hasattr(entity, "post_receive"):
entity.post_receive(transformed)
entities.append(entity)
return entities

Wyświetl plik

@ -5,6 +5,11 @@ from federation.entities.mixins import (
EntityTypeMixin, ProviderDisplayNameMixin)
class Accept(CreatedAtMixin, TargetIDMixin):
"""An acceptance message for some target."""
pass
class Image(PublicMixin, OptionalRawContentMixin, CreatedAtMixin):
"""Reflects a single image, possibly linked to another object."""
remote_path = ""

Wyświetl plik

@ -5,6 +5,7 @@ INSTALLED_APPS = tuple()
FEDERATION = {
"base_url": "https://example.com",
"get_object_function": "federation.tests.django.utils.get_object_function",
"get_private_key_function": "federation.tests.django.utils.get_private_key",
"get_profile_function": "federation.tests.django.utils.get_profile",
"process_payload_function": "federation.tests.django.utils.process_payload",
"search_path": "/search?q=",

Wyświetl plik

@ -1,4 +1,7 @@
from Crypto.PublicKey.RSA import RsaKey
from federation.entities.base import Profile
from federation.tests.fixtures.keys import get_dummy_private_key
def dummy_profile():
@ -16,6 +19,10 @@ def get_object_function(object_id):
return dummy_profile()
def get_private_key(identifier: str) -> RsaKey:
return get_dummy_private_key()
def get_profile(handle=None, request=None):
return dummy_profile()

Wyświetl plik

@ -19,6 +19,7 @@ def get_configuration():
}
configuration.update(settings.FEDERATION)
if not all([
"get_private_key_function" in configuration,
"get_profile_function" in configuration,
"base_url" in configuration,
]):