Skip to content
Snippets Groups Projects
Verified Commit 6dcde77b authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Avoid fetching Actor object on every request authentication

parent e0fce268
No related branches found
No related tags found
No related merge requests found
...@@ -56,6 +56,10 @@ FEDERATION_COLLECTION_PAGE_SIZE = env.int( ...@@ -56,6 +56,10 @@ FEDERATION_COLLECTION_PAGE_SIZE = env.int(
FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool( FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool(
'FEDERATION_MUSIC_NEEDS_APPROVAL', default=True 'FEDERATION_MUSIC_NEEDS_APPROVAL', default=True
) )
# how much minutes to wait before refetching actor data
# when authenticating
FEDERATION_ACTOR_FETCH_DELAY = env.int(
'FEDERATION_ACTOR_FETCH_DELAY', default=60 * 12)
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS') ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
# APP CONFIGURATION # APP CONFIGURATION
......
import datetime
import logging import logging
import uuid import uuid
import xml import xml
...@@ -49,11 +50,20 @@ def get_actor_data(actor_url): ...@@ -49,11 +50,20 @@ def get_actor_data(actor_url):
def get_actor(actor_url): def get_actor(actor_url):
try:
actor = models.Actor.objects.get(url=actor_url)
except models.Actor.DoesNotExist:
actor = None
fetch_delta = datetime.timedelta(
minutes=settings.FEDERATION_ACTOR_FETCH_DELAY)
if actor and actor.last_fetch_date > timezone.now() - fetch_delta:
# cache is hot, we can return as is
return actor
data = get_actor_data(actor_url) data = get_actor_data(actor_url)
serializer = serializers.ActorSerializer(data=data) serializer = serializers.ActorSerializer(data=data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
return serializer.build() return serializer.save(last_fetch_date=timezone.now())
class SystemActor(object): class SystemActor(object):
......
...@@ -25,28 +25,19 @@ class SignatureAuthentication(authentication.BaseAuthentication): ...@@ -25,28 +25,19 @@ class SignatureAuthentication(authentication.BaseAuthentication):
raise exceptions.AuthenticationFailed(str(e)) raise exceptions.AuthenticationFailed(str(e))
try: try:
actor_data = actors.get_actor_data(key_id) actor = actors.get_actor(key_id.split('#')[0])
except Exception as e: except Exception as e:
raise exceptions.AuthenticationFailed(str(e)) raise exceptions.AuthenticationFailed(str(e))
try: if not actor.public_key:
public_key = actor_data['publicKey']['publicKeyPem']
except KeyError:
raise exceptions.AuthenticationFailed('No public key found') raise exceptions.AuthenticationFailed('No public key found')
serializer = serializers.ActorSerializer(data=actor_data)
if not serializer.is_valid():
raise exceptions.AuthenticationFailed('Invalid actor payload: {}'.format(serializer.errors))
try: try:
signing.verify_django(request, public_key.encode('utf-8')) signing.verify_django(request, actor.public_key.encode('utf-8'))
except cryptography.exceptions.InvalidSignature: except cryptography.exceptions.InvalidSignature:
raise exceptions.AuthenticationFailed('Invalid signature') raise exceptions.AuthenticationFailed('Invalid signature')
try: return actor
return models.Actor.objects.get(url=actor_data['id'])
except models.Actor.DoesNotExist:
return serializer.save()
def authenticate(self, request): def authenticate(self, request):
setattr(request, 'actor', None) setattr(request, 'actor', None)
......
...@@ -103,9 +103,10 @@ class ActorSerializer(serializers.Serializer): ...@@ -103,9 +103,10 @@ class ActorSerializer(serializers.Serializer):
def save(self, **kwargs): def save(self, **kwargs):
d = self.prepare_missing_fields() d = self.prepare_missing_fields()
d.update(kwargs) d.update(kwargs)
return models.Actor.objects.create( return models.Actor.objects.update_or_create(
**d url=d['url'],
) defaults=d,
)[0]
def validate_summary(self, value): def validate_summary(self, value):
if value: if value:
......
...@@ -29,6 +29,42 @@ def test_actor_fetching(r_mock): ...@@ -29,6 +29,42 @@ def test_actor_fetching(r_mock):
assert r == payload assert r == payload
def test_get_actor(factories, r_mock):
actor = factories['federation.Actor'].build()
payload = serializers.ActorSerializer(actor).data
r_mock.get(actor.url, json=payload)
new_actor = actors.get_actor(actor.url)
assert new_actor.pk is not None
assert serializers.ActorSerializer(new_actor).data == payload
def test_get_actor_use_existing(factories, settings, mocker):
settings.FEDERATION_ACTOR_FETCH_DELAY = 60
actor = factories['federation.Actor']()
get_data = mocker.patch('funkwhale_api.federation.actors.get_actor_data')
new_actor = actors.get_actor(actor.url)
assert new_actor == actor
get_data.assert_not_called()
def test_get_actor_refresh(factories, settings, mocker):
settings.FEDERATION_ACTOR_FETCH_DELAY = 0
actor = factories['federation.Actor']()
payload = serializers.ActorSerializer(actor).data
# actor changed their username in the meantime
payload['preferredUsername'] = 'New me'
get_data = mocker.patch(
'funkwhale_api.federation.actors.get_actor_data',
return_value=payload)
new_actor = actors.get_actor(actor.url)
assert new_actor == actor
assert new_actor.last_fetch_date > actor.last_fetch_date
assert new_actor.preferred_username == 'New me'
def test_get_library(db, settings, mocker): def test_get_library(db, settings, mocker):
get_key_pair = mocker.patch( get_key_pair = mocker.patch(
'funkwhale_api.federation.keys.get_key_pair', 'funkwhale_api.federation.keys.get_key_pair',
......
Avoid fetching Actor object on every request authentication
...@@ -3,10 +3,12 @@ ...@@ -3,10 +3,12 @@
<div class="content"> <div class="content">
<div class="header ellipsis"> <div class="header ellipsis">
<router-link <router-link
v-if="library"
:title="displayName" :title="displayName"
:to="{name: 'federation.libraries.detail', params: {id: library.uuid }}"> :to="{name: 'federation.libraries.detail', params: {id: library.uuid }}">
{{ displayName }} {{ displayName }}
</router-link> </router-link>
<span :title="displayName" v-else>{{ displayName }}</span>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
......
...@@ -150,6 +150,7 @@ export default { ...@@ -150,6 +150,7 @@ export default {
methods: { methods: {
fetchData () { fetchData () {
var self = this var self = this
this.scanTrigerred = false
this.isLoading = true this.isLoading = true
let url = 'federation/libraries/' + this.id + '/' let url = 'federation/libraries/' + this.id + '/'
logger.default.debug('Fetching library "' + this.id + '"') logger.default.debug('Fetching library "' + this.id + '"')
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment