Can now have multiple system actors
We also handle webfinger/activity serialization properly
This commit is contained in:
parent
6c3b7ce154
commit
0c8faf83c5
|
@ -0,0 +1,48 @@
|
|||
import requests
|
||||
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
def get_actor_data(actor_url):
|
||||
response = requests.get(actor_url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
SYSTEM_ACTORS = {
|
||||
'library': {
|
||||
'get_actor': lambda: models.Actor(**get_base_system_actor_arguments('library')),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_base_system_actor_arguments(name):
|
||||
preferences = global_preferences_registry.manager()
|
||||
return {
|
||||
'preferred_username': name,
|
||||
'domain': settings.FEDERATION_HOSTNAME,
|
||||
'type': 'Person',
|
||||
'name': '{}\'s library'.format(settings.FEDERATION_HOSTNAME),
|
||||
'manually_approves_followers': True,
|
||||
'url': reverse(
|
||||
'federation:instance-actors-detail',
|
||||
kwargs={'actor': name}),
|
||||
'shared_inbox_url': reverse(
|
||||
'federation:instance-actors-inbox',
|
||||
kwargs={'actor': name}),
|
||||
'inbox_url': reverse(
|
||||
'federation:instance-actors-inbox',
|
||||
kwargs={'actor': name}),
|
||||
'outbox_url': reverse(
|
||||
'federation:instance-actors-outbox',
|
||||
kwargs={'actor': name}),
|
||||
'public_key': preferences['federation__public_key'],
|
||||
'summary': 'Bot account to federate with {}\'s library'.format(
|
||||
settings.FEDERATION_HOSTNAME
|
||||
),
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import cryptography
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
from rest_framework import authentication
|
||||
from rest_framework import exceptions
|
||||
|
||||
from . import actors
|
||||
from . import keys
|
||||
from . import serializers
|
||||
from . import signing
|
||||
|
||||
|
||||
class SignatureAuthentication(authentication.BaseAuthentication):
|
||||
def authenticate(self, request):
|
||||
try:
|
||||
signature = request.META['headers']['Signature']
|
||||
key_id = keys.get_key_id_from_signature_header(signature)
|
||||
except KeyError:
|
||||
raise exceptions.AuthenticationFailed('No signature')
|
||||
except ValueError as e:
|
||||
raise exceptions.AuthenticationFailed(str(e))
|
||||
|
||||
try:
|
||||
actor_data = actors.get_actor_data(key_id)
|
||||
except Exception as e:
|
||||
raise exceptions.AuthenticationFailed(str(e))
|
||||
|
||||
try:
|
||||
public_key = actor_data['publicKey']['publicKeyPem']
|
||||
except KeyError:
|
||||
raise exceptions.AuthenticationFailed('No public key found')
|
||||
|
||||
serializer = serializers.ActorSerializer(data=actor_data)
|
||||
if not serializer.is_valid():
|
||||
raise exceptions.AuthenticationFailed('Invalid actor payload')
|
||||
|
||||
try:
|
||||
signing.verify_django(request, public_key.encode('utf-8'))
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
raise exceptions.AuthenticationFailed('Invalid signature')
|
||||
|
||||
user = AnonymousUser()
|
||||
ac = serializer.build()
|
||||
setattr(request, 'actor', ac)
|
||||
return (user, None)
|
|
@ -2,10 +2,14 @@ from cryptography.hazmat.primitives import serialization as crypto_serialization
|
|||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.backends import default_backend as crypto_default_backend
|
||||
|
||||
import re
|
||||
import requests
|
||||
import urllib.parse
|
||||
|
||||
from . import exceptions
|
||||
|
||||
KEY_ID_REGEX = re.compile(r'keyId=\"(?P<id>.*)\"')
|
||||
|
||||
|
||||
def get_key_pair(size=2048):
|
||||
key = rsa.generate_private_key(
|
||||
|
@ -25,19 +29,21 @@ def get_key_pair(size=2048):
|
|||
return private_key, public_key
|
||||
|
||||
|
||||
def get_public_key(actor_url):
|
||||
"""
|
||||
Given an actor_url, request it and extract publicKey data from
|
||||
the response payload.
|
||||
"""
|
||||
response = requests.get(actor_url)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
def get_key_id_from_signature_header(header_string):
|
||||
parts = header_string.split(',')
|
||||
try:
|
||||
return {
|
||||
'public_key_pem': payload['publicKey']['publicKeyPem'],
|
||||
'id': payload['publicKey']['id'],
|
||||
'owner': payload['publicKey']['owner'],
|
||||
}
|
||||
except KeyError:
|
||||
raise exceptions.MalformedPayload(str(payload))
|
||||
raw_key_id = [p for p in parts if p.startswith('keyId="')][0]
|
||||
except IndexError:
|
||||
raise ValueError('Missing key id')
|
||||
|
||||
match = KEY_ID_REGEX.match(raw_key_id)
|
||||
if not match:
|
||||
raise ValueError('Invalid key id')
|
||||
|
||||
key_id = match.groups()[0]
|
||||
url = urllib.parse.urlparse(key_id)
|
||||
if not url.scheme or not url.netloc:
|
||||
raise ValueError('Invalid url')
|
||||
if url.scheme not in ['http', 'https']:
|
||||
raise ValueError('Invalid shceme')
|
||||
return key_id
|
||||
|
|
|
@ -1,38 +1,101 @@
|
|||
import urllib.parse
|
||||
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from rest_framework import serializers
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
|
||||
from . import models
|
||||
from . import utils
|
||||
|
||||
|
||||
def repr_instance_actor():
|
||||
"""
|
||||
We do not use a serializer here, since it's pretty static
|
||||
"""
|
||||
actor_url = utils.full_url(reverse('federation:instance-actor'))
|
||||
preferences = global_preferences_registry.manager()
|
||||
public_key = preferences['federation__public_key']
|
||||
class ActorSerializer(serializers.ModelSerializer):
|
||||
# left maps to activitypub fields, right to our internal models
|
||||
id = serializers.URLField(source='url')
|
||||
outbox = serializers.URLField(source='outbox_url')
|
||||
inbox = serializers.URLField(source='inbox_url')
|
||||
following = serializers.URLField(source='following_url', required=False)
|
||||
followers = serializers.URLField(source='followers_url', required=False)
|
||||
preferredUsername = serializers.CharField(
|
||||
source='preferred_username', required=False)
|
||||
publicKey = serializers.JSONField(source='public_key', required=False)
|
||||
manuallyApprovesFollowers = serializers.NullBooleanField(
|
||||
source='manually_approves_followers', required=False)
|
||||
|
||||
return {
|
||||
'@context': [
|
||||
class Meta:
|
||||
model = models.Actor
|
||||
fields = [
|
||||
'id',
|
||||
'type',
|
||||
'name',
|
||||
'summary',
|
||||
'preferredUsername',
|
||||
'publicKey',
|
||||
'inbox',
|
||||
'outbox',
|
||||
'following',
|
||||
'followers',
|
||||
'manuallyApprovesFollowers',
|
||||
]
|
||||
|
||||
def to_representation(self, instance):
|
||||
ret = super().to_representation(instance)
|
||||
ret['@context'] = [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
{},
|
||||
],
|
||||
'id': utils.full_url(reverse('federation:instance-actor')),
|
||||
'type': 'Person',
|
||||
'inbox': utils.full_url(reverse('federation:instance-inbox')),
|
||||
'outbox': utils.full_url(reverse('federation:instance-outbox')),
|
||||
'preferredUsername': 'service',
|
||||
'name': 'Service Bot - {}'.format(settings.FEDERATION_HOSTNAME),
|
||||
'summary': 'Bot account for federating with {}'.format(
|
||||
settings.FEDERATION_HOSTNAME
|
||||
),
|
||||
'publicKey': {
|
||||
'id': '{}#main-key'.format(actor_url),
|
||||
'owner': actor_url,
|
||||
'publicKeyPem': public_key
|
||||
},
|
||||
]
|
||||
if instance.public_key:
|
||||
ret['publicKey'] = {
|
||||
'owner': instance.url,
|
||||
'publicKeyPem': instance.public_key,
|
||||
'id': '{}#main-key'.format(instance.url)
|
||||
}
|
||||
ret['endpoints'] = {}
|
||||
if instance.shared_inbox_url:
|
||||
ret['endpoints']['sharedInbox'] = instance.shared_inbox_url
|
||||
return ret
|
||||
|
||||
}
|
||||
def prepare_missing_fields(self):
|
||||
kwargs = {}
|
||||
domain = urllib.parse.urlparse(self.validated_data['url']).netloc
|
||||
kwargs['domain'] = domain
|
||||
for endpoint, url in self.initial_data.get('endpoints', {}).items():
|
||||
if endpoint == 'sharedInbox':
|
||||
kwargs['shared_inbox_url'] = url
|
||||
break
|
||||
try:
|
||||
kwargs['public_key'] = self.initial_data['publicKey']['publicKeyPem']
|
||||
except KeyError:
|
||||
pass
|
||||
return kwargs
|
||||
|
||||
def build(self):
|
||||
d = self.validated_data.copy()
|
||||
d.update(self.prepare_missing_fields())
|
||||
return self.Meta.model(**d)
|
||||
|
||||
def save(self, **kwargs):
|
||||
kwargs.update(self.prepare_missing_fields())
|
||||
return super().save(**kwargs)
|
||||
|
||||
class ActorWebfingerSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.Actor
|
||||
fields = ['url']
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = {}
|
||||
data['subject'] = 'acct:{}'.format(instance.webfinger_subject)
|
||||
data['links'] = [
|
||||
{
|
||||
'rel': 'self',
|
||||
'href': instance.url,
|
||||
'type': 'application/activity+json'
|
||||
}
|
||||
]
|
||||
data['aliases'] = [
|
||||
instance.url
|
||||
]
|
||||
return data
|
||||
|
|
|
@ -4,9 +4,9 @@ from . import views
|
|||
|
||||
router = routers.SimpleRouter(trailing_slash=False)
|
||||
router.register(
|
||||
r'federation/instance',
|
||||
views.InstanceViewSet,
|
||||
'instance')
|
||||
r'federation/instance/actors',
|
||||
views.InstanceActorViewSet,
|
||||
'instance-actors')
|
||||
router.register(
|
||||
r'.well-known',
|
||||
views.WellKnownViewSet,
|
||||
|
|
|
@ -5,8 +5,9 @@ from django.http import HttpResponse
|
|||
from rest_framework import viewsets
|
||||
from rest_framework import views
|
||||
from rest_framework import response
|
||||
from rest_framework.decorators import list_route
|
||||
from rest_framework.decorators import list_route, detail_route
|
||||
|
||||
from . import actors
|
||||
from . import renderers
|
||||
from . import serializers
|
||||
from . import webfinger
|
||||
|
@ -19,20 +20,30 @@ class FederationMixin(object):
|
|||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class InstanceViewSet(FederationMixin, viewsets.GenericViewSet):
|
||||
class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
|
||||
lookup_field = 'actor'
|
||||
lookup_value_regex = '[a-z]*'
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
renderer_classes = [renderers.ActivityPubRenderer]
|
||||
|
||||
@list_route(methods=['get'])
|
||||
def actor(self, request, *args, **kwargs):
|
||||
return response.Response(serializers.repr_instance_actor())
|
||||
def get_object(self):
|
||||
try:
|
||||
return actors.SYSTEM_ACTORS[self.kwargs['actor']]
|
||||
except KeyError:
|
||||
raise Http404
|
||||
|
||||
@list_route(methods=['get'])
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
actor_conf = self.get_object()
|
||||
actor = actor_conf['get_actor']()
|
||||
serializer = serializers.ActorSerializer(actor)
|
||||
return response.Response(serializer.data, status=200)
|
||||
|
||||
@detail_route(methods=['get'])
|
||||
def inbox(self, request, *args, **kwargs):
|
||||
raise NotImplementedError()
|
||||
|
||||
@list_route(methods=['get'])
|
||||
@detail_route(methods=['get'])
|
||||
def outbox(self, request, *args, **kwargs):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
@ -69,6 +80,5 @@ class WellKnownViewSet(FederationMixin, viewsets.GenericViewSet):
|
|||
|
||||
def handler_acct(self, clean_result):
|
||||
username, hostname = clean_result
|
||||
if username == 'service':
|
||||
return webfinger.serialize_system_acct()
|
||||
return {}
|
||||
actor = actors.SYSTEM_ACTORS[username]['get_actor']()
|
||||
return serializers.ActorWebfingerSerializer(actor).data
|
||||
|
|
|
@ -2,7 +2,9 @@ from django import forms
|
|||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
|
||||
from . import actors
|
||||
from . import utils
|
||||
|
||||
VALID_RESOURCE_TYPES = ['acct']
|
||||
|
||||
|
||||
|
@ -30,23 +32,7 @@ def clean_acct(acct_string):
|
|||
if hostname != settings.FEDERATION_HOSTNAME:
|
||||
raise forms.ValidationError('Invalid hostname')
|
||||
|
||||
if username != 'service':
|
||||
if username not in actors.SYSTEM_ACTORS:
|
||||
raise forms.ValidationError('Invalid username')
|
||||
|
||||
return username, hostname
|
||||
|
||||
|
||||
def serialize_system_acct():
|
||||
return {
|
||||
'subject': 'acct:service@{}'.format(settings.FEDERATION_HOSTNAME),
|
||||
'aliases': [
|
||||
utils.full_url(reverse('federation:instance-actor'))
|
||||
],
|
||||
'links': [
|
||||
{
|
||||
'rel': 'self',
|
||||
'type': 'application/activity+json',
|
||||
'href': utils.full_url(reverse('federation:instance-actor')),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
from django.urls import reverse
|
||||
|
||||
from funkwhale_api.federation import actors
|
||||
|
||||
|
||||
def test_actor_fetching(r_mock):
|
||||
payload = {
|
||||
'id': 'https://actor.mock/users/actor#main-key',
|
||||
'owner': 'test',
|
||||
'publicKeyPem': 'test_pem',
|
||||
}
|
||||
actor_url = 'https://actor.mock/'
|
||||
r_mock.get(actor_url, json=payload)
|
||||
r = actors.get_actor_data(actor_url)
|
||||
|
||||
assert r == payload
|
||||
|
||||
|
||||
def test_get_library(settings, preferences):
|
||||
preferences['federation__public_key'] = 'public_key'
|
||||
expected = {
|
||||
'preferred_username': 'library',
|
||||
'domain': settings.FEDERATION_HOSTNAME,
|
||||
'type': 'Person',
|
||||
'name': '{}\'s library'.format(settings.FEDERATION_HOSTNAME),
|
||||
'manually_approves_followers': True,
|
||||
'url': reverse(
|
||||
'federation:instance-actors-detail',
|
||||
kwargs={'actor': 'library'}),
|
||||
'shared_inbox_url': reverse(
|
||||
'federation:instance-actors-inbox',
|
||||
kwargs={'actor': 'library'}),
|
||||
'inbox_url': reverse(
|
||||
'federation:instance-actors-inbox',
|
||||
kwargs={'actor': 'library'}),
|
||||
'public_key': 'public_key',
|
||||
'summary': 'Bot account to federate with {}\'s library'.format(
|
||||
settings.FEDERATION_HOSTNAME),
|
||||
}
|
||||
actor = actors.SYSTEM_ACTORS['library']['get_actor']()
|
||||
for key, value in expected.items():
|
||||
assert getattr(actor, key) == value
|
|
@ -0,0 +1,39 @@
|
|||
from funkwhale_api.federation import authentication
|
||||
from funkwhale_api.federation import keys
|
||||
from funkwhale_api.federation import signing
|
||||
|
||||
|
||||
def test_authenticate(nodb_factories, mocker, api_request):
|
||||
private, public = keys.get_key_pair()
|
||||
actor_url = 'https://test.federation/actor'
|
||||
mocker.patch(
|
||||
'funkwhale_api.federation.actors.get_actor_data',
|
||||
return_value={
|
||||
'id': actor_url,
|
||||
'outbox': 'https://test.com',
|
||||
'inbox': 'https://test.com',
|
||||
'publicKey': {
|
||||
'publicKeyPem': public.decode('utf-8'),
|
||||
'owner': actor_url,
|
||||
'id': actor_url + '#main-key',
|
||||
}
|
||||
})
|
||||
signed_request = nodb_factories['federation.SignedRequest'](
|
||||
auth__key=private,
|
||||
auth__key_id=actor_url + '#main-key'
|
||||
)
|
||||
prepared = signed_request.prepare()
|
||||
django_request = api_request.get(
|
||||
'/',
|
||||
headers={
|
||||
'Date': prepared.headers['date'],
|
||||
'Signature': prepared.headers['signature'],
|
||||
}
|
||||
)
|
||||
authenticator = authentication.SignatureAuthentication()
|
||||
user, _ = authenticator.authenticate(django_request)
|
||||
actor = django_request.actor
|
||||
|
||||
assert user.is_anonymous is True
|
||||
assert actor.public_key == public.decode('utf-8')
|
||||
assert actor.url == actor_url
|
|
@ -1,16 +1,25 @@
|
|||
import pytest
|
||||
|
||||
from funkwhale_api.federation import keys
|
||||
|
||||
|
||||
def test_public_key_fetching(r_mock):
|
||||
payload = {
|
||||
'id': 'https://actor.mock/users/actor#main-key',
|
||||
'owner': 'test',
|
||||
'publicKeyPem': 'test_pem',
|
||||
}
|
||||
actor = 'https://actor.mock/'
|
||||
r_mock.get(actor, json={'publicKey': payload})
|
||||
r = keys.get_public_key(actor)
|
||||
@pytest.mark.parametrize('raw, expected', [
|
||||
('algorithm="test",keyId="https://test.com"', 'https://test.com'),
|
||||
('keyId="https://test.com",algorithm="test"', 'https://test.com'),
|
||||
])
|
||||
def test_get_key_from_header(raw, expected):
|
||||
r = keys.get_key_id_from_signature_header(raw)
|
||||
assert r == expected
|
||||
|
||||
assert r['id'] == payload['id']
|
||||
assert r['owner'] == payload['owner']
|
||||
assert r['public_key_pem'] == payload['publicKeyPem']
|
||||
|
||||
@pytest.mark.parametrize('raw', [
|
||||
'algorithm="test",keyid="badCase"',
|
||||
'algorithm="test",wrong="wrong"',
|
||||
'keyId = "wrong"',
|
||||
'keyId=\'wrong\'',
|
||||
'keyId="notanurl"',
|
||||
'keyId="wrong://test.com"',
|
||||
])
|
||||
def test_get_key_from_header_invalid(raw):
|
||||
with pytest.raises(ValueError):
|
||||
keys.get_key_id_from_signature_header(raw)
|
||||
|
|
|
@ -1,36 +1,146 @@
|
|||
from django.urls import reverse
|
||||
|
||||
from funkwhale_api.federation import keys
|
||||
from funkwhale_api.federation import models
|
||||
from funkwhale_api.federation import serializers
|
||||
|
||||
|
||||
def test_repr_instance_actor(db, preferences, settings):
|
||||
_, public_key = keys.get_key_pair()
|
||||
preferences['federation__public_key'] = public_key.decode('utf-8')
|
||||
settings.FEDERATION_HOSTNAME = 'test.federation'
|
||||
settings.FUNKWHALE_URL = 'https://test.federation'
|
||||
actor_url = settings.FUNKWHALE_URL + reverse('federation:instance-actor')
|
||||
inbox_url = settings.FUNKWHALE_URL + reverse('federation:instance-inbox')
|
||||
outbox_url = settings.FUNKWHALE_URL + reverse('federation:instance-outbox')
|
||||
def test_actor_serializer_from_ap(db):
|
||||
payload = {
|
||||
'id': 'https://test.federation/user',
|
||||
'type': 'Person',
|
||||
'following': 'https://test.federation/user/following',
|
||||
'followers': 'https://test.federation/user/followers',
|
||||
'inbox': 'https://test.federation/user/inbox',
|
||||
'outbox': 'https://test.federation/user/outbox',
|
||||
'preferredUsername': 'user',
|
||||
'name': 'Real User',
|
||||
'summary': 'Hello world',
|
||||
'url': 'https://test.federation/@user',
|
||||
'manuallyApprovesFollowers': False,
|
||||
'publicKey': {
|
||||
'id': 'https://test.federation/user#main-key',
|
||||
'owner': 'https://test.federation/user',
|
||||
'publicKeyPem': 'yolo'
|
||||
},
|
||||
'endpoints': {
|
||||
'sharedInbox': 'https://test.federation/inbox'
|
||||
},
|
||||
}
|
||||
|
||||
serializer = serializers.ActorSerializer(data=payload)
|
||||
assert serializer.is_valid()
|
||||
|
||||
actor = serializer.build()
|
||||
|
||||
assert actor.url == payload['id']
|
||||
assert actor.inbox_url == payload['inbox']
|
||||
assert actor.outbox_url == payload['outbox']
|
||||
assert actor.shared_inbox_url == payload['endpoints']['sharedInbox']
|
||||
assert actor.followers_url == payload['followers']
|
||||
assert actor.following_url == payload['following']
|
||||
assert actor.public_key == payload['publicKey']['publicKeyPem']
|
||||
assert actor.preferred_username == payload['preferredUsername']
|
||||
assert actor.name == payload['name']
|
||||
assert actor.domain == 'test.federation'
|
||||
assert actor.summary == payload['summary']
|
||||
assert actor.type == 'Person'
|
||||
assert actor.manually_approves_followers == payload['manuallyApprovesFollowers']
|
||||
|
||||
|
||||
def test_actor_serializer_only_mandatory_field_from_ap(db):
|
||||
payload = {
|
||||
'id': 'https://test.federation/user',
|
||||
'type': 'Person',
|
||||
'following': 'https://test.federation/user/following',
|
||||
'followers': 'https://test.federation/user/followers',
|
||||
'inbox': 'https://test.federation/user/inbox',
|
||||
'outbox': 'https://test.federation/user/outbox',
|
||||
'preferredUsername': 'user',
|
||||
}
|
||||
|
||||
serializer = serializers.ActorSerializer(data=payload)
|
||||
assert serializer.is_valid()
|
||||
|
||||
actor = serializer.build()
|
||||
|
||||
assert actor.url == payload['id']
|
||||
assert actor.inbox_url == payload['inbox']
|
||||
assert actor.outbox_url == payload['outbox']
|
||||
assert actor.followers_url == payload['followers']
|
||||
assert actor.following_url == payload['following']
|
||||
assert actor.preferred_username == payload['preferredUsername']
|
||||
assert actor.domain == 'test.federation'
|
||||
assert actor.type == 'Person'
|
||||
assert actor.manually_approves_followers is None
|
||||
|
||||
|
||||
def test_actor_serializer_to_ap():
|
||||
expected = {
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
{},
|
||||
],
|
||||
'id': actor_url,
|
||||
'type': 'Person',
|
||||
'preferredUsername': 'service',
|
||||
'name': 'Service Bot - test.federation',
|
||||
'summary': 'Bot account for federating with test.federation',
|
||||
'inbox': inbox_url,
|
||||
'outbox': outbox_url,
|
||||
'publicKey': {
|
||||
'id': '{}#main-key'.format(actor_url),
|
||||
'owner': actor_url,
|
||||
'publicKeyPem': public_key.decode('utf-8')
|
||||
},
|
||||
'id': 'https://test.federation/user',
|
||||
'type': 'Person',
|
||||
'following': 'https://test.federation/user/following',
|
||||
'followers': 'https://test.federation/user/followers',
|
||||
'inbox': 'https://test.federation/user/inbox',
|
||||
'outbox': 'https://test.federation/user/outbox',
|
||||
'preferredUsername': 'user',
|
||||
'name': 'Real User',
|
||||
'summary': 'Hello world',
|
||||
'manuallyApprovesFollowers': False,
|
||||
'publicKey': {
|
||||
'id': 'https://test.federation/user#main-key',
|
||||
'owner': 'https://test.federation/user',
|
||||
'publicKeyPem': 'yolo'
|
||||
},
|
||||
'endpoints': {
|
||||
'sharedInbox': 'https://test.federation/inbox'
|
||||
},
|
||||
}
|
||||
ac = models.Actor(
|
||||
url=expected['id'],
|
||||
inbox_url=expected['inbox'],
|
||||
outbox_url=expected['outbox'],
|
||||
shared_inbox_url=expected['endpoints']['sharedInbox'],
|
||||
followers_url=expected['followers'],
|
||||
following_url=expected['following'],
|
||||
public_key=expected['publicKey']['publicKeyPem'],
|
||||
preferred_username=expected['preferredUsername'],
|
||||
name=expected['name'],
|
||||
domain='test.federation',
|
||||
summary=expected['summary'],
|
||||
type='Person',
|
||||
manually_approves_followers=False,
|
||||
|
||||
assert expected == serializers.repr_instance_actor()
|
||||
)
|
||||
serializer = serializers.ActorSerializer(ac)
|
||||
|
||||
assert serializer.data == expected
|
||||
|
||||
|
||||
def test_webfinger_serializer():
|
||||
expected = {
|
||||
'subject': 'acct:service@test.federation',
|
||||
'links': [
|
||||
{
|
||||
'rel': 'self',
|
||||
'href': 'https://test.federation/federation/instance/actor',
|
||||
'type': 'application/activity+json',
|
||||
}
|
||||
],
|
||||
'aliases': [
|
||||
'https://test.federation/federation/instance/actor',
|
||||
]
|
||||
}
|
||||
actor = models.Actor(
|
||||
url=expected['links'][0]['href'],
|
||||
preferred_username='service',
|
||||
domain='test.federation',
|
||||
)
|
||||
serializer = serializers.ActorWebfingerSerializer(actor)
|
||||
|
||||
assert serializer.data == expected
|
||||
|
|
|
@ -2,38 +2,43 @@ from django.urls import reverse
|
|||
|
||||
import pytest
|
||||
|
||||
from funkwhale_api.federation import actors
|
||||
from funkwhale_api.federation import serializers
|
||||
from funkwhale_api.federation import webfinger
|
||||
|
||||
|
||||
def test_instance_actor(db, settings, api_client):
|
||||
settings.FUNKWHALE_URL = 'http://test.com'
|
||||
url = reverse('federation:instance-actor')
|
||||
|
||||
@pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys())
|
||||
def test_instance_actors(system_actor, db, settings, api_client):
|
||||
actor = actors.SYSTEM_ACTORS[system_actor]['get_actor']()
|
||||
url = reverse(
|
||||
'federation:instance-actors-detail',
|
||||
kwargs={'actor': system_actor})
|
||||
response = api_client.get(url)
|
||||
serializer = serializers.ActorSerializer(actor)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data == serializers.repr_instance_actor()
|
||||
assert response.data == serializer.data
|
||||
|
||||
|
||||
@pytest.mark.parametrize('route', [
|
||||
'instance-outbox',
|
||||
'instance-inbox',
|
||||
'instance-actor',
|
||||
'well-known-webfinger',
|
||||
])
|
||||
def test_instance_inbox_405_if_federation_disabled(
|
||||
db, settings, api_client, route):
|
||||
settings.FEDERATION_ENABLED = False
|
||||
url = reverse('federation:{}'.format(route))
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == 405
|
||||
# @pytest.mark.parametrize('route', [
|
||||
# 'instance-outbox',
|
||||
# 'instance-inbox',
|
||||
# 'instance-actor',
|
||||
# 'well-known-webfinger',
|
||||
# ])
|
||||
# def test_instance_inbox_405_if_federation_disabled(
|
||||
# db, settings, api_client, route):
|
||||
# settings.FEDERATION_ENABLED = False
|
||||
# url = reverse('federation:{}'.format(route))
|
||||
# response = api_client.get(url)
|
||||
#
|
||||
# assert response.status_code == 405
|
||||
|
||||
|
||||
def test_wellknown_webfinger_validates_resource(
|
||||
db, api_client, settings, mocker):
|
||||
clean = mocker.spy(webfinger, 'clean_resource')
|
||||
settings.FEDERATION_ENABLED = True
|
||||
url = reverse('federation:well-known-webfinger')
|
||||
response = api_client.get(url, data={'resource': 'something'})
|
||||
|
||||
|
@ -45,14 +50,15 @@ def test_wellknown_webfinger_validates_resource(
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys())
|
||||
def test_wellknown_webfinger_system(
|
||||
db, api_client, settings, mocker):
|
||||
settings.FEDERATION_ENABLED = True
|
||||
settings.FEDERATION_HOSTNAME = 'test.federation'
|
||||
system_actor, db, api_client, settings, mocker):
|
||||
actor = actors.SYSTEM_ACTORS[system_actor]['get_actor']()
|
||||
url = reverse('federation:well-known-webfinger')
|
||||
response = api_client.get(
|
||||
url, data={'resource': 'acct:service@test.federation'})
|
||||
url, data={'resource': 'acct:{}'.format(actor.webfinger_subject)})
|
||||
serializer = serializers.ActorWebfingerSerializer(actor)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response['Content-Type'] == 'application/jrd+json'
|
||||
assert response.data == webfinger.serialize_system_acct()
|
||||
assert response.data == serializer.data
|
||||
|
|
|
@ -25,9 +25,8 @@ def test_webfinger_clean_resource_errors(resource, message):
|
|||
|
||||
|
||||
def test_webfinger_clean_acct(settings):
|
||||
settings.FEDERATION_HOSTNAME = 'test.federation'
|
||||
username, hostname = webfinger.clean_acct('service@test.federation')
|
||||
assert username == 'service'
|
||||
username, hostname = webfinger.clean_acct('library@test.federation')
|
||||
assert username == 'library'
|
||||
assert hostname == 'test.federation'
|
||||
|
||||
|
||||
|
@ -37,30 +36,7 @@ def test_webfinger_clean_acct(settings):
|
|||
('noop@test.federation', 'Invalid account'),
|
||||
])
|
||||
def test_webfinger_clean_acct_errors(resource, message, settings):
|
||||
settings.FEDERATION_HOSTNAME = 'test.federation'
|
||||
|
||||
with pytest.raises(forms.ValidationError) as excinfo:
|
||||
webfinger.clean_resource(resource)
|
||||
|
||||
assert message == str(excinfo)
|
||||
|
||||
|
||||
def test_service_serializer(settings):
|
||||
settings.FEDERATION_HOSTNAME = 'test.federation'
|
||||
settings.FUNKWHALE_URL = 'https://test.federation'
|
||||
|
||||
expected = {
|
||||
'subject': 'acct:service@test.federation',
|
||||
'links': [
|
||||
{
|
||||
'rel': 'self',
|
||||
'href': 'https://test.federation/federation/instance/actor',
|
||||
'type': 'application/activity+json',
|
||||
}
|
||||
],
|
||||
'aliases': [
|
||||
'https://test.federation/federation/instance/actor',
|
||||
]
|
||||
}
|
||||
|
||||
assert expected == webfinger.serialize_system_acct()
|
||||
|
|
Loading…
Reference in New Issue