Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
jovuit
funkwhale
Commits
253f026d
Commit
253f026d
authored
Jan 30, 2019
by
Eliot Berriot
Browse files
System actor
parent
8963218b
Changes
18
Hide whitespace changes
Inline
Side-by-side
api/config/settings/common.py
View file @
253f026d
...
...
@@ -94,6 +94,9 @@ FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool(
)
# XXX: deprecated, see #186
FEDERATION_ACTOR_FETCH_DELAY
=
env
.
int
(
"FEDERATION_ACTOR_FETCH_DELAY"
,
default
=
60
*
12
)
FEDERATION_SERVICE_ACTOR_USERNAME
=
env
(
"FEDERATION_SERVICE_ACTOR_USERNAME"
,
default
=
"service"
)
ALLOWED_HOSTS
=
env
.
list
(
"DJANGO_ALLOWED_HOSTS"
,
default
=
[])
+
[
FUNKWHALE_HOSTNAME
]
# APP CONFIGURATION
...
...
api/funkwhale_api/common/utils.py
View file @
253f026d
...
...
@@ -147,3 +147,24 @@ def order_for_search(qs, field):
this function will order the given qs based on the length of the given field
"""
return
qs
.
annotate
(
__size
=
models
.
functions
.
Length
(
field
)).
order_by
(
"__size"
)
def
recursive_getattr
(
obj
,
key
,
permissive
=
False
):
"""
Given a dictionary such as {'user': {'name': 'Bob'}} and
a dotted string such as user.name, returns 'Bob'.
If the value is not present, returns None
"""
v
=
obj
for
k
in
key
.
split
(
"."
):
try
:
v
=
v
.
get
(
k
)
except
(
TypeError
,
AttributeError
):
if
not
permissive
:
raise
return
if
v
is
None
:
return
return
v
api/funkwhale_api/federation/activity.py
View file @
253f026d
...
...
@@ -9,6 +9,8 @@ from django.db.models import Q
from
funkwhale_api.common
import
channels
from
funkwhale_api.common
import
utils
as
funkwhale_utils
recursive_getattr
=
funkwhale_utils
.
recursive_getattr
logger
=
logging
.
getLogger
(
__name__
)
PUBLIC_ADDRESS
=
"https://www.w3.org/ns/activitystreams#Public"
...
...
@@ -89,9 +91,9 @@ def should_reject(id, actor_id=None, payload={}):
media_types
=
[
"Audio"
,
"Artist"
,
"Album"
,
"Track"
,
"Library"
,
"Image"
]
relevant_values
=
[
recursive_get
t
attr
(
payload
,
"type"
,
permissive
=
True
),
recursive_get
t
attr
(
payload
,
"object.type"
,
permissive
=
True
),
recursive_get
t
attr
(
payload
,
"target.type"
,
permissive
=
True
),
recursive_getattr
(
payload
,
"type"
,
permissive
=
True
),
recursive_getattr
(
payload
,
"object.type"
,
permissive
=
True
),
recursive_getattr
(
payload
,
"target.type"
,
permissive
=
True
),
]
# if one of the payload types match our internal media types, then
# we apply policies that reject media
...
...
@@ -343,7 +345,7 @@ class OutboxRouter(Router):
return
activities
def
recursive_get
t
attr
(
obj
,
key
,
permissive
=
False
):
def
recursive_getattr
(
obj
,
key
,
permissive
=
False
):
"""
Given a dictionary such as {'user': {'name': 'Bob'}} and
a dotted string such as user.name, returns 'Bob'.
...
...
@@ -366,7 +368,7 @@ def recursive_gettattr(obj, key, permissive=False):
def
match_route
(
route
,
payload
):
for
key
,
value
in
route
.
items
():
payload_value
=
recursive_get
t
attr
(
payload
,
key
)
payload_value
=
recursive_getattr
(
payload
,
key
)
if
payload_value
!=
value
:
return
False
...
...
api/funkwhale_api/federation/actors.py
View file @
253f026d
...
...
@@ -5,8 +5,9 @@ from django.conf import settings
from
django.utils
import
timezone
from
funkwhale_api.common
import
preferences
,
session
from
funkwhale_api.users
import
models
as
users_models
from
.
import
models
,
serializers
from
.
import
keys
,
models
,
serializers
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -28,7 +29,7 @@ def get_actor_data(actor_url):
def
get_actor
(
fid
,
skip_cache
=
False
):
if
not
skip_cache
:
try
:
actor
=
models
.
Actor
.
objects
.
get
(
fid
=
fid
)
actor
=
models
.
Actor
.
objects
.
select_related
().
get
(
fid
=
fid
)
except
models
.
Actor
.
DoesNotExist
:
actor
=
None
fetch_delta
=
datetime
.
timedelta
(
...
...
@@ -42,3 +43,23 @@ def get_actor(fid, skip_cache=False):
serializer
.
is_valid
(
raise_exception
=
True
)
return
serializer
.
save
(
last_fetch_date
=
timezone
.
now
())
def
get_service_actor
():
name
,
domain
=
(
settings
.
FEDERATION_SERVICE_ACTOR_USERNAME
,
settings
.
FEDERATION_HOSTNAME
,
)
try
:
return
models
.
Actor
.
objects
.
select_related
().
get
(
preferred_username
=
name
,
domain__name
=
domain
)
except
models
.
Actor
.
DoesNotExist
:
pass
args
=
users_models
.
get_actor_data
(
name
)
private
,
public
=
keys
.
get_key_pair
()
args
[
"private_key"
]
=
private
.
decode
(
"utf-8"
)
args
[
"public_key"
]
=
public
.
decode
(
"utf-8"
)
args
[
"type"
]
=
"Service"
return
models
.
Actor
.
objects
.
create
(
**
args
)
api/funkwhale_api/federation/authentication.py
View file @
253f026d
import
cryptography
import
logging
import
datetime
from
django.contrib.auth.models
import
AnonymousUser
from
rest_framework
import
authentication
,
exceptions
as
rest_exceptions
from
django.utils
import
timezone
from
rest_framework
import
authentication
,
exceptions
as
rest_exceptions
from
funkwhale_api.moderation
import
models
as
moderation_models
from
.
import
actors
,
exceptions
,
keys
,
signing
,
utils
from
.
import
actors
,
exceptions
,
keys
,
signing
,
tasks
,
utils
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -57,6 +59,15 @@ class SignatureAuthentication(authentication.BaseAuthentication):
actor
=
actors
.
get_actor
(
actor_url
,
skip_cache
=
True
)
signing
.
verify_django
(
request
,
actor
.
public_key
.
encode
(
"utf-8"
))
# we trigger a nodeinfo update on the actor's domain, if needed
fetch_delay
=
24
*
3600
now
=
timezone
.
now
()
last_fetch
=
actor
.
domain
.
nodeinfo_fetch_date
if
not
last_fetch
or
(
last_fetch
<
(
now
-
datetime
.
timedelta
(
seconds
=
fetch_delay
))
):
tasks
.
update_domain_nodeinfo
(
domain_name
=
actor
.
domain
.
name
)
actor
.
domain
.
refresh_from_db
()
return
actor
def
authenticate
(
self
,
request
):
...
...
api/funkwhale_api/federation/factories.py
View file @
253f026d
...
...
@@ -69,6 +69,7 @@ def create_user(actor):
@
registry
.
register
class
DomainFactory
(
NoUpdateOnCreate
,
factory
.
django
.
DjangoModelFactory
):
name
=
factory
.
Faker
(
"domain_name"
)
nodeinfo_fetch_date
=
factory
.
LazyFunction
(
lambda
:
timezone
.
now
())
class
Meta
:
model
=
"federation.Domain"
...
...
api/funkwhale_api/federation/migrations/0017_auto_20190130_0926.py
0 → 100644
View file @
253f026d
# Generated by Django 2.1.5 on 2019-01-30 09:26
import
django.contrib.postgres.fields.jsonb
from
django.db
import
migrations
,
models
import
django.db.models.deletion
import
funkwhale_api.common.validators
import
funkwhale_api.federation.models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'federation'
,
'0016_auto_20181227_1605'
),
]
operations
=
[
migrations
.
RemoveField
(
model_name
=
'actor'
,
name
=
'old_domain'
,
),
migrations
.
AddField
(
model_name
=
'domain'
,
name
=
'service_actor'
,
field
=
models
.
ForeignKey
(
blank
=
True
,
null
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
SET_NULL
,
related_name
=
'managed_domains'
,
to
=
'federation.Actor'
),
),
migrations
.
AlterField
(
model_name
=
'domain'
,
name
=
'name'
,
field
=
models
.
CharField
(
max_length
=
255
,
primary_key
=
True
,
serialize
=
False
,
validators
=
[
funkwhale_api
.
common
.
validators
.
DomainValidator
()]),
),
migrations
.
AlterField
(
model_name
=
'domain'
,
name
=
'nodeinfo'
,
field
=
django
.
contrib
.
postgres
.
fields
.
jsonb
.
JSONField
(
blank
=
True
,
default
=
funkwhale_api
.
federation
.
models
.
empty_dict
,
max_length
=
50000
),
),
]
api/funkwhale_api/federation/models.py
View file @
253f026d
...
...
@@ -46,7 +46,9 @@ class FederationMixin(models.Model):
class
ActorQuerySet
(
models
.
QuerySet
):
def
local
(
self
,
include
=
True
):
return
self
.
exclude
(
user__isnull
=
include
)
if
include
:
return
self
.
filter
(
domain__name
=
settings
.
FEDERATION_HOSTNAME
)
return
self
.
exclude
(
domain__name
=
settings
.
FEDERATION_HOSTNAME
)
def
with_current_usage
(
self
):
qs
=
self
...
...
@@ -92,7 +94,13 @@ class Domain(models.Model):
creation_date
=
models
.
DateTimeField
(
default
=
timezone
.
now
)
nodeinfo_fetch_date
=
models
.
DateTimeField
(
default
=
None
,
null
=
True
,
blank
=
True
)
nodeinfo
=
JSONField
(
default
=
empty_dict
,
max_length
=
50000
,
blank
=
True
)
service_actor
=
models
.
ForeignKey
(
"Actor"
,
related_name
=
"managed_domains"
,
on_delete
=
models
.
SET_NULL
,
null
=
True
,
blank
=
True
,
)
objects
=
DomainQuerySet
.
as_manager
()
def
__str__
(
self
):
...
...
api/funkwhale_api/federation/tasks.py
View file @
253f026d
...
...
@@ -11,6 +11,7 @@ from requests.exceptions import RequestException
from
funkwhale_api.common
import
preferences
from
funkwhale_api.common
import
session
from
funkwhale_api.common
import
utils
as
common_utils
from
funkwhale_api.music
import
models
as
music_models
from
funkwhale_api.taskapp
import
celery
...
...
@@ -18,6 +19,7 @@ from . import keys
from
.
import
models
,
signing
from
.
import
serializers
from
.
import
routes
from
.
import
utils
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -184,9 +186,27 @@ def update_domain_nodeinfo(domain):
nodeinfo
=
{
"status"
:
"ok"
,
"payload"
:
fetch_nodeinfo
(
domain
.
name
)}
except
(
requests
.
RequestException
,
serializers
.
serializers
.
ValidationError
)
as
e
:
nodeinfo
=
{
"status"
:
"error"
,
"error"
:
str
(
e
)}
service_actor_id
=
common_utils
.
recursive_getattr
(
nodeinfo
,
"payload.metadata.actorId"
,
permissive
=
True
)
try
:
domain
.
service_actor
=
(
utils
.
retrieve_ap_object
(
service_actor_id
,
queryset
=
models
.
Actor
,
serializer_class
=
serializers
.
ActorSerializer
,
)
if
service_actor_id
else
None
)
except
(
serializers
.
serializers
.
ValidationError
,
RequestException
)
as
e
:
logger
.
warning
(
"Cannot fetch system actor for domain %s: %s"
,
domain
.
name
,
str
(
e
)
)
domain
.
nodeinfo_fetch_date
=
now
domain
.
nodeinfo
=
nodeinfo
domain
.
save
(
update_fields
=
[
"nodeinfo"
,
"nodeinfo_fetch_date"
])
domain
.
save
(
update_fields
=
[
"nodeinfo"
,
"nodeinfo_fetch_date"
,
"service_actor"
])
def
delete_qs
(
qs
):
...
...
api/funkwhale_api/instance/nodeinfo.py
View file @
253f026d
...
...
@@ -2,6 +2,7 @@ import memoize.djangocache
import
funkwhale_api
from
funkwhale_api.common
import
preferences
from
funkwhale_api.federation
import
actors
from
.
import
stats
...
...
@@ -19,6 +20,7 @@ def get():
"openRegistrations"
:
preferences
.
get
(
"users__registration_enabled"
),
"usage"
:
{
"users"
:
{
"total"
:
0
,
"activeHalfyear"
:
0
,
"activeMonth"
:
0
}},
"metadata"
:
{
"actorId"
:
actors
.
get_service_actor
().
fid
,
"private"
:
preferences
.
get
(
"instance__nodeinfo_private"
),
"shortDescription"
:
preferences
.
get
(
"instance__short_description"
),
"longDescription"
:
preferences
.
get
(
"instance__long_description"
),
...
...
api/funkwhale_api/users/models.py
View file @
253f026d
...
...
@@ -245,41 +245,52 @@ class Invitation(models.Model):
return
super
().
save
(
**
kwargs
)
def
get_actor_data
(
user
):
username
=
federation_utils
.
slugify_username
(
user
.
username
)
def
get_actor_data
(
user
name
):
slugified_
username
=
federation_utils
.
slugify_username
(
username
)
return
{
"preferred_username"
:
username
,
"preferred_username"
:
slugified_
username
,
"domain"
:
federation_models
.
Domain
.
objects
.
get_or_create
(
name
=
settings
.
FEDERATION_HOSTNAME
)[
0
],
"type"
:
"Person"
,
"name"
:
user
.
username
,
"name"
:
username
,
"manually_approves_followers"
:
False
,
"fid"
:
federation_utils
.
full_url
(
reverse
(
"federation:actors-detail"
,
kwargs
=
{
"preferred_username"
:
username
})
reverse
(
"federation:actors-detail"
,
kwargs
=
{
"preferred_username"
:
slugified_username
},
)
),
"shared_inbox_url"
:
federation_models
.
get_shared_inbox_url
(),
"inbox_url"
:
federation_utils
.
full_url
(
reverse
(
"federation:actors-inbox"
,
kwargs
=
{
"preferred_username"
:
username
})
reverse
(
"federation:actors-inbox"
,
kwargs
=
{
"preferred_username"
:
slugified_username
},
)
),
"outbox_url"
:
federation_utils
.
full_url
(
reverse
(
"federation:actors-outbox"
,
kwargs
=
{
"preferred_username"
:
username
})
reverse
(
"federation:actors-outbox"
,
kwargs
=
{
"preferred_username"
:
slugified_username
},
)
),
"followers_url"
:
federation_utils
.
full_url
(
reverse
(
"federation:actors-followers"
,
kwargs
=
{
"preferred_username"
:
username
}
"federation:actors-followers"
,
kwargs
=
{
"preferred_username"
:
slugified_username
},
)
),
"following_url"
:
federation_utils
.
full_url
(
reverse
(
"federation:actors-following"
,
kwargs
=
{
"preferred_username"
:
username
}
"federation:actors-following"
,
kwargs
=
{
"preferred_username"
:
slugified_username
},
)
),
}
def
create_actor
(
user
):
args
=
get_actor_data
(
user
)
args
=
get_actor_data
(
user
.
username
)
private
,
public
=
keys
.
get_key_pair
()
args
[
"private_key"
]
=
private
.
decode
(
"utf-8"
)
args
[
"public_key"
]
=
public
.
decode
(
"utf-8"
)
...
...
api/tests/federation/test_actors.py
View file @
253f026d
...
...
@@ -46,3 +46,15 @@ def test_get_actor_refresh(factories, preferences, mocker):
assert
new_actor
==
actor
assert
new_actor
.
last_fetch_date
>
actor
.
last_fetch_date
assert
new_actor
.
preferred_username
==
"New me"
def
test_get_service_actor
(
db
,
settings
):
settings
.
FEDERATION_HOSTNAME
=
"test.hello"
settings
.
FEDERATION_SERVICE_ACTOR_USERNAME
=
"bob"
actor
=
actors
.
get_service_actor
()
assert
actor
.
preferred_username
==
"bob"
assert
actor
.
domain
.
name
==
"test.hello"
assert
actor
.
private_key
is
not
None
assert
actor
.
type
==
"Service"
assert
actor
.
public_key
is
not
None
api/tests/federation/test_authentication.py
View file @
253f026d
...
...
@@ -5,6 +5,7 @@ from funkwhale_api.federation import authentication, exceptions, keys
def
test_authenticate
(
factories
,
mocker
,
api_request
):
private
,
public
=
keys
.
get_key_pair
()
factories
[
"federation.Domain"
](
name
=
"test.federation"
,
nodeinfo_fetch_date
=
None
)
actor_url
=
"https://test.federation/actor"
mocker
.
patch
(
"funkwhale_api.federation.actors.get_actor_data"
,
...
...
@@ -22,6 +23,10 @@ def test_authenticate(factories, mocker, api_request):
},
},
)
update_domain_nodeinfo
=
mocker
.
patch
(
"funkwhale_api.federation.tasks.update_domain_nodeinfo"
)
signed_request
=
factories
[
"federation.SignedRequest"
](
auth__key
=
private
,
auth__key_id
=
actor_url
+
"#main-key"
,
auth__headers
=
[
"date"
]
)
...
...
@@ -40,6 +45,7 @@ def test_authenticate(factories, mocker, api_request):
assert
user
.
is_anonymous
is
True
assert
actor
.
public_key
==
public
.
decode
(
"utf-8"
)
assert
actor
.
fid
==
actor_url
update_domain_nodeinfo
.
assert_called_once_with
(
domain_name
=
"test.federation"
)
def
test_authenticate_skips_blocked_domain
(
factories
,
api_request
):
...
...
api/tests/federation/test_tasks.py
View file @
253f026d
...
...
@@ -161,22 +161,32 @@ def test_fetch_nodeinfo(factories, r_mock, now):
def
test_update_domain_nodeinfo
(
factories
,
mocker
,
now
):
domain
=
factories
[
"federation.Domain"
]()
mocker
.
patch
.
object
(
tasks
,
"fetch_nodeinfo"
,
return_value
=
{
"hello"
:
"world"
})
domain
=
factories
[
"federation.Domain"
](
nodeinfo_fetch_date
=
None
)
actor
=
factories
[
"federation.Actor"
](
fid
=
"https://actor.id"
)
mocker
.
patch
.
object
(
tasks
,
"fetch_nodeinfo"
,
return_value
=
{
"hello"
:
"world"
,
"metadata"
:
{
"actorId"
:
"https://actor.id"
}},
)
assert
domain
.
nodeinfo
==
{}
assert
domain
.
nodeinfo_fetch_date
is
None
assert
domain
.
service_actor
is
None
tasks
.
update_domain_nodeinfo
(
domain_name
=
domain
.
name
)
domain
.
refresh_from_db
()
assert
domain
.
nodeinfo_fetch_date
==
now
assert
domain
.
nodeinfo
==
{
"status"
:
"ok"
,
"payload"
:
{
"hello"
:
"world"
}}
assert
domain
.
nodeinfo
==
{
"status"
:
"ok"
,
"payload"
:
{
"hello"
:
"world"
,
"metadata"
:
{
"actorId"
:
"https://actor.id"
}},
}
assert
domain
.
service_actor
==
actor
def
test_update_domain_nodeinfo_error
(
factories
,
r_mock
,
now
):
domain
=
factories
[
"federation.Domain"
]()
domain
=
factories
[
"federation.Domain"
](
nodeinfo_fetch_date
=
None
)
wellknown_url
=
"https://{}/.well-known/nodeinfo"
.
format
(
domain
.
name
)
r_mock
.
get
(
wellknown_url
,
status_code
=
500
)
...
...
api/tests/federation/test_views.py
View file @
253f026d
...
...
@@ -2,7 +2,7 @@ import pytest
from
django.core.paginator
import
Paginator
from
django.urls
import
reverse
from
funkwhale_api.federation
import
serializers
,
webfinger
from
funkwhale_api.federation
import
actors
,
serializers
,
webfinger
def
test_wellknown_webfinger_validates_resource
(
db
,
api_client
,
settings
,
mocker
):
...
...
@@ -54,6 +54,19 @@ def test_local_actor_detail(factories, api_client):
assert
response
.
data
==
serializer
.
data
def
test_service_actor_detail
(
factories
,
api_client
):
actor
=
actors
.
get_service_actor
()
url
=
reverse
(
"federation:actors-detail"
,
kwargs
=
{
"preferred_username"
:
actor
.
preferred_username
},
)
serializer
=
serializers
.
ActorSerializer
(
actor
)
response
=
api_client
.
get
(
url
)
assert
response
.
status_code
==
200
assert
response
.
data
==
serializer
.
data
def
test_local_actor_inbox_post_requires_auth
(
factories
,
api_client
):
user
=
factories
[
"users.User"
](
with_actor
=
True
)
url
=
reverse
(
...
...
api/tests/instance/test_nodeinfo.py
View file @
253f026d
import
funkwhale_api
from
funkwhale_api.instance
import
nodeinfo
from
funkwhale_api.federation
import
actors
def
test_nodeinfo_dump
(
preferences
,
mocker
):
...
...
@@ -23,6 +24,7 @@ def test_nodeinfo_dump(preferences, mocker):
"openRegistrations"
:
preferences
[
"users__registration_enabled"
],
"usage"
:
{
"users"
:
{
"total"
:
1
,
"activeHalfyear"
:
12
,
"activeMonth"
:
13
}},
"metadata"
:
{
"actorId"
:
actors
.
get_service_actor
().
fid
,
"private"
:
preferences
[
"instance__nodeinfo_private"
],
"shortDescription"
:
preferences
[
"instance__short_description"
],
"longDescription"
:
preferences
[
"instance__long_description"
],
...
...
@@ -60,6 +62,7 @@ def test_nodeinfo_dump_stats_disabled(preferences, mocker):
"openRegistrations"
:
preferences
[
"users__registration_enabled"
],
"usage"
:
{
"users"
:
{
"total"
:
0
,
"activeHalfyear"
:
0
,
"activeMonth"
:
0
}},
"metadata"
:
{
"actorId"
:
actors
.
get_service_actor
().
fid
,
"private"
:
preferences
[
"instance__nodeinfo_private"
],
"shortDescription"
:
preferences
[
"instance__short_description"
],
"longDescription"
:
preferences
[
"instance__long_description"
],
...
...
api/tests/manage/test_serializers.py
View file @
253f026d
...
...
@@ -40,7 +40,7 @@ def test_user_update_permission(factories):
def
test_manage_domain_serializer
(
factories
,
now
):
domain
=
factories
[
"federation.Domain"
]()
domain
=
factories
[
"federation.Domain"
](
nodeinfo_fetch_date
=
None
)
setattr
(
domain
,
"actors_count"
,
42
)
setattr
(
domain
,
"outbox_activities_count"
,
23
)
expected
=
{
...
...
changes/changelog.d/system-actor.enhancement
0 → 100644
View file @
253f026d
Expose an instance-level actor (service@domain) in nodeinfo endpoint (#689)
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment