create AP users on inbox delivery for an indirect or nonexistent Web user

for #512
circle-datastore-transactions
Ryan Barrett 2023-05-31 13:17:17 -07:00
rodzic 086c6d032c
commit 28eabd07a3
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
4 zmienionych plików z 82 dodań i 22 usunięć

Wyświetl plik

@ -48,7 +48,10 @@ def default_signature_user():
class ActivityPub(User, Protocol):
"""ActivityPub protocol class."""
"""ActivityPub protocol class.
Key id is AP/AS2 actor id URL. (*Not* fediverse/WebFinger @-@ handle!)
"""
LABEL = 'activitypub'
@classmethod
@ -566,14 +569,20 @@ def inbox(protocol=None, domain=None):
error(f"Couldn't parse body as non-empty JSON mapping: {body}", exc_info=True)
type = activity.get('type')
actor_id = as1.get_object(activity, 'actor').get('id')
actor = as1.get_object(activity, 'actor')
actor_id = actor.get('id')
logger.info(f'Got {type} from {actor_id}: {json_dumps(activity, indent=2)}')
# load user
# TODO(#512) parameterize on protocol, move to Protocol
if protocol and domain:
g.user = PROTOCOLS[protocol].get_by_id(domain)
if not g.user:
error(f'{protocol} user {domain} not found', status=404)
g.user = PROTOCOLS[protocol].get_by_id(domain) # receiving user
if (not g.user or not g.user.direct) and actor_id:
# this is a deliberate interaction with an indirect receiving user;
# create a local AP User for the sending user
actor_obj = ActivityPub.load(actor_id)
ActivityPub.get_or_create(actor_id, direct=True,
actor_as2=as2.from_as1(actor_obj.as1))
ActivityPub.verify_signature(activity)

Wyświetl plik

@ -79,9 +79,9 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
property: p256_key, PEM encoded
https://atproto.com/guides/overview#account-portability
"""
mod = ndb.StringProperty(required=True)
public_exponent = ndb.StringProperty(required=True)
private_exponent = ndb.StringProperty(required=True)
mod = ndb.StringProperty()
public_exponent = ndb.StringProperty()
private_exponent = ndb.StringProperty()
p256_key = ndb.StringProperty()
has_redirects = ndb.BooleanProperty()
redirects_error = ndb.TextProperty()
@ -129,7 +129,8 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
if user:
# override direct if it's set
direct = kwargs.get('direct')
if direct is not None:
if direct is not None and direct != user.direct:
logger.info(f'Setting {user.key} direct={direct}')
user.direct = direct
user.put()
return user
@ -153,6 +154,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
kwargs['p256_key'] = key.export_key(format='PEM')
user = cls(id=id, **kwargs)
logger.info(f'Created new {user}')
user.put()
return user

Wyświetl plik

@ -109,15 +109,21 @@ LIKE = {
}
LIKE_WRAPPED = copy.deepcopy(LIKE)
LIKE_WRAPPED['object'] = 'http://localhost/r/https://user.com/post'
LIKE_WITH_ACTOR = copy.deepcopy(LIKE)
# TODO: use ACTOR instead
LIKE_WITH_ACTOR['actor'] = {
LIKE_ACTOR = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://user.com/actor',
'type': 'Person',
'name': 'Ms. Actor',
'preferredUsername': 'msactor',
'image': {'type': 'Image', 'url': 'https://user.com/pic.jpg'},
'icon': {'type': 'Image', 'url': 'https://user.com/pic.jpg'},
'image': [
{'type': 'Image', 'url': 'https://user.com/thumb.jpg'},
{'type': 'Image', 'url': 'https://user.com/pic.jpg'},
],
}
LIKE_WITH_ACTOR = {
**LIKE,
'actor': LIKE_ACTOR,
}
# repost, should be delivered to followers if object is a fediverse post,
@ -230,8 +236,7 @@ class ActivityPubTest(TestCase):
def setUp(self):
super().setUp()
self.user = self.make_user('user.com',
has_hcard=True, actor_as2=ACTOR)
self.user = self.make_user('user.com', has_hcard=True, actor_as2=ACTOR)
with self.request_context:
self.key_id_obj = Object(id='http://my/key/id', as2={
**ACTOR,
@ -323,9 +328,23 @@ class ActivityPubTest(TestCase):
got = self.client.get('/nope.com')
self.assertEqual(404, got.status_code)
def test_individual_inbox_no_user(self, *mocks):
got = self.post('/nope.com/inbox', json=REPLY)
self.assertEqual(404, got.status_code)
def test_individual_inbox_no_user(self, mock_head, mock_get, mock_post):
self.user.key.delete()
mock_get.side_effect = [self.as2_resp(LIKE_ACTOR)]
reply = {
**REPLY,
'actor': LIKE_ACTOR,
}
got = self._test_inbox_reply(reply, {
'as2': reply,
'type': 'post',
'labels': ['activity', 'notification'],
}, mock_head, mock_get, mock_post)
self.assert_user(ActivityPub, 'https://user.com/actor',
actor_as2=LIKE_ACTOR, direct=True)
def test_inbox_activity_without_id(self, *_):
note = copy.deepcopy(NOTE)
@ -362,7 +381,9 @@ class ActivityPubTest(TestCase):
def _test_inbox_reply(self, reply, expected_props, mock_head, mock_get, mock_post):
mock_head.return_value = requests_response(url='https://user.com/post')
mock_get.return_value = WEBMENTION_DISCOVERY
mock_get.side_effect = (
(list(mock_get.side_effect) if mock_get.side_effect else [])
+ [WEBMENTION_DISCOVERY])
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=reply)
@ -640,6 +661,17 @@ class ActivityPubTest(TestCase):
labels=['notification', 'activity'],
object_ids=[LIKE['object']])
def test_inbox_like_indirect_user_creates_User(self, mock_get, *_):
self.user.direct = False
self.user.put()
mock_get.return_value = self.as2_resp(LIKE_ACTOR)
self.test_inbox_like()
self.assert_user(ActivityPub, 'https://user.com/actor',
actor_as2=LIKE_ACTOR, direct=True)
def test_inbox_follow_accept_with_id(self, *mocks):
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, *mocks)

Wyświetl plik

@ -86,7 +86,7 @@ with ndb_client.context():
models.reset_protocol_properties()
import app
import activitypub
from activitypub import ActivityPub, CONNEG_HEADERS_AS2_HTML
import common
from web import Web
from flask_app import app, cache, init_globals
@ -184,7 +184,7 @@ class TestCase(unittest.TestCase, testutil.Asserts):
return f'com.example.record/{tid}'
def get_as2(self, *args, **kwargs):
kwargs.setdefault('headers', {})['Accept'] = activitypub.CONNEG_HEADERS_AS2_HTML
kwargs.setdefault('headers', {})['Accept'] = CONNEG_HEADERS_AS2_HTML
return self.client.get(*args, **kwargs)
def req(self, url, **kwargs):
@ -202,7 +202,7 @@ class TestCase(unittest.TestCase, testutil.Asserts):
'Host': util.domain_from_link(url, minimize=False),
'Content-Type': 'application/activity+json',
'Digest': ANY,
**activitypub.CONNEG_HEADERS_AS2_HTML,
**CONNEG_HEADERS_AS2_HTML,
**kwargs.pop('headers', {}),
}
return self.req(url, data=None, auth=ANY, headers=headers,
@ -250,6 +250,23 @@ class TestCase(unittest.TestCase, testutil.Asserts):
ignore=['as1', 'created', 'expire',
'object_ids', 'type', 'updated'])
def assert_user(self, cls, id, **props):
got = cls.get_by_id(id)
assert got, id
self.assert_entities_equal(
cls(id=id, **props), got,
ignore=['created', 'mod', 'p256_key', 'private_exponent',
'public_exponent', 'updated'])
if cls != ActivityPub:
assert got.mod
assert got.private_exponent
assert got.public_exponent
# if cls != ATProto:
# assert got.p256_key
def assert_equals(self, expected, actual, msg=None, ignore=(), **kwargs):
return super().assert_equals(
expected, actual, msg=msg, ignore=tuple(ignore) + ('@context',), **kwargs)