add Friendrequests
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
j3d1 2023-06-22 11:44:28 +02:00
parent b1a221a8e8
commit b6f1da1580
5 changed files with 407 additions and 4 deletions

View file

@ -102,6 +102,9 @@ class ToolshedUser(AbstractUser):
private_key = SigningKey(self.private_key.encode(), encoder=HexEncoder) private_key = SigningKey(self.private_key.encode(), encoder=HexEncoder)
return private_key.sign(message.encode('utf-8'), encoder=HexEncoder).signature.decode('utf-8') return private_key.sign(message.encode('utf-8'), encoder=HexEncoder).signature.decode('utf-8')
def public_key(self):
return self.public_identity.public_key
class FriendRequestOutgoing(models.Model): class FriendRequestOutgoing(models.Model):
secret = models.CharField(max_length=255) secret = models.CharField(max_length=255)

View file

@ -1,3 +1,5 @@
from nacl.exceptions import BadSignatureError
from nacl.signing import VerifyKey
from rest_framework import authentication from rest_framework import authentication
from authentication.models import KnownIdentity, ToolshedUser from authentication.models import KnownIdentity, ToolshedUser
@ -48,6 +50,28 @@ def verify_request(request, raw_request_body):
return username, domain, signed_data, signature_bytes_hex return username, domain, signed_data, signature_bytes_hex
def verify_incoming_friend_request(request, raw_request_body):
try:
username, domain, signed_data, signature_bytes_hex = verify_request(request, raw_request_body)
except ValueError:
return False
try:
befriender = request.data['befriender']
befriender_key = request.data['befriender_key']
except KeyError:
return False
if username + "@" + domain != befriender:
return False
if len(befriender_key) != 64:
return False
verify_key = VerifyKey(bytes.fromhex(befriender_key))
try:
verify_key.verify(signed_data.encode('utf-8'), bytes.fromhex(signature_bytes_hex))
return True
except BadSignatureError:
return False
def authenticate_request_against_known_identities(request, raw_request_body): def authenticate_request_against_known_identities(request, raw_request_body):
try: try:
username, domain, signed_data, signature_bytes_hex = verify_request(request, raw_request_body) username, domain, signed_data, signature_bytes_hex = verify_request(request, raw_request_body)

View file

@ -1,11 +1,18 @@
import secrets
from django.urls import path from django.urls import path
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.generics import get_object_or_404
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin from rest_framework.viewsets import ViewSetMixin
from authentication.signature_auth import SignatureAuthentication from authentication.models import KnownIdentity, FriendRequestIncoming, FriendRequestOutgoing, ToolshedUser
from toolshed.serializers import FriendSerializer from authentication.signature_auth import verify_incoming_friend_request, split_userhandle_or_throw, \
authenticate_request_against_local_users, SignatureAuthentication
from toolshed.serializers import FriendSerializer, FriendRequestSerializer
class Friends(APIView, ViewSetMixin): class Friends(APIView, ViewSetMixin):
@ -18,7 +25,102 @@ class Friends(APIView, ViewSetMixin):
serializer = FriendSerializer(friends, many=True) serializer = FriendSerializer(friends, many=True)
return Response(serializer.data) return Response(serializer.data)
def post(self, request, format=None): # /api/friends/
# only for local users
try:
user = request.user
incoming_request = FriendRequestIncoming.objects.get(
pk=request.data.get('friend_request_id'),
secret=request.data.get('secret'))
befriender, _ = KnownIdentity.objects.get_or_create(
username=incoming_request.befriender_username,
domain=incoming_request.befriender_domain,
public_key=incoming_request.befriender_public_key
)
befriender.save()
user.user.get().friends.add(befriender)
user.user.get().save()
incoming_request.delete()
return Response(status=status.HTTP_201_CREATED, data={'status': 'accepted'})
except FriendRequestIncoming.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND, data={'status': 'not found'})
class FriendsRequests(APIView, ViewSetMixin):
def get(self, request, format=None): # /api/friendrequests/
raw_request = request.body.decode('utf-8')
if user := authenticate_request_against_local_users(request, raw_request):
friends_requests = user.friend_requests_incoming.all()
serializer = FriendRequestSerializer(friends_requests, many=True)
return Response(serializer.data)
else:
return Response(status=status.HTTP_401_UNAUTHORIZED, data={'status': 'unauthorized'})
def post(self, request, format=None): # /api/friendrequests/
raw_request = request.body.decode('utf-8')
if 'befriender' not in request.data or 'befriendee' not in request.data:
return Response(status=status.HTTP_400_BAD_REQUEST, data={'status': 'missing parameters'})
befriender_username, befriender_domain = split_userhandle_or_throw(request.data['befriender'])
befriendee_username, befriendee_domain = split_userhandle_or_throw(request.data['befriendee'])
if befriender_domain == befriendee_domain and befriender_username == befriendee_username:
return Response(status=status.HTTP_400_BAD_REQUEST, data={'status': 'cannot befriend yourself'})
if user := authenticate_request_against_local_users(request, raw_request):
secret = secrets.token_hex(64)
befriendee_user = ToolshedUser.objects.filter(username=befriendee_username, domain=befriendee_domain)
if befriendee_user.exists():
FriendRequestIncoming.objects.create(
befriender_username=befriender_username,
befriender_domain=befriender_domain,
befriender_public_key=user.public_identity.public_key,
secret=secret, # request.data['secret'] # TODO ??
befriendee_user=befriendee_user.get(),
)
return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"})
else:
FriendRequestOutgoing.objects.create(
befriender_user=user,
befriendee_username=befriendee_username,
befriendee_domain=befriendee_domain,
secret=secret, # request.data['secret'] # TODO ??
)
return Response(status=status.HTTP_201_CREATED, data={'secret': secret, 'status': "pending"})
elif verify_incoming_friend_request(request, raw_request):
try:
befriendee = ToolshedUser.objects.get(username=befriendee_username, domain=befriendee_domain)
outgoing = FriendRequestOutgoing.objects.filter(
secret=request.data['secret'],
befriender_user=befriendee, # both sides match
befriendee_username=befriender_username,
befriendee_domain=befriender_domain)
if outgoing.exists():
befriender, _ = KnownIdentity.objects.get_or_create(
username=befriender_username,
domain=befriender_domain,
public_key=request.data['befriender_key']
)
befriender.save()
befriendee.friends.add(befriender)
befriendee.save()
outgoing.delete()
return Response(status=status.HTTP_201_CREATED, data={'status': "accepted"})
else:
FriendRequestIncoming.objects.create(
befriender_username=befriender_username,
befriender_domain=befriender_domain,
befriender_public_key=request.data['befriender_key'],
befriendee_user=befriendee,
secret=request.data['secret']
)
return Response(status=status.HTTP_201_CREATED, data={'status': "pending"})
except ToolshedUser.DoesNotExist:
return Response(status=status.HTTP_400_BAD_REQUEST)
except KeyError:
return Response(status=status.HTTP_400_BAD_REQUEST)
else:
return Response(status=status.HTTP_400_BAD_REQUEST)
urlpatterns = [ urlpatterns = [
path('friends/', Friends.as_view(), name='friends'), path('friends/', Friends.as_view(), name='friends'),
path('friendrequests/', FriendsRequests.as_view(), name='friendrequests'),
] ]

View file

@ -1,6 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
from authentication.models import KnownIdentity, ToolshedUser, FriendRequestIncoming
from authentication.models import KnownIdentity, ToolshedUser
from authentication.serializers import OwnerSerializer from authentication.serializers import OwnerSerializer
from files.models import File from files.models import File
from files.serializers import FileSerializer from files.serializers import FileSerializer
@ -18,6 +17,17 @@ class FriendSerializer(serializers.ModelSerializer):
return obj.username + '@' + obj.domain return obj.username + '@' + obj.domain
class FriendRequestSerializer(serializers.ModelSerializer):
befriender = serializers.SerializerMethodField()
class Meta:
model = FriendRequestIncoming
fields = ['befriender', 'befriender_public_key', 'secret', 'id']
def get_befriender(self, obj):
return obj.befriender_username + '@' + obj.befriender_domain
class PropertySerializer(serializers.ModelSerializer): class PropertySerializer(serializers.ModelSerializer):
category = serializers.SlugRelatedField(queryset=Category.objects.all(), slug_field='name') category = serializers.SlugRelatedField(queryset=Category.objects.all(), slug_field='name')

View file

@ -1,4 +1,6 @@
from django.test import Client
from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase
from authentication.models import FriendRequestIncoming, FriendRequestOutgoing
client = SignatureAuthClient() client = SignatureAuthClient()
@ -75,3 +77,265 @@ class FriendApiTestCase(UserTestMixin, ToolshedTestCase):
self.assertEqual(reply.status_code, 200) self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 1) self.assertEqual(len(reply.json()), 1)
self.assertEqual(reply.json()[0]['username'], str(self.f['local_user1'])) self.assertEqual(reply.json()[0]['username'], str(self.f['local_user1']))
# what ~should~ happen:
# 1. user x@A sends a friend request to user y@B
# 1.1. x@A's client sends a POST request to A/api/friendrequests/ with body {from: x@A, to: y@B}
# 1.2. A's backend creates a FriendRequestOutgoing object, containing x@A's identity and y@B's name
# 1.3. x@A's client sends a POST request to B/api/friendrequests/ with body
# {from: x@A, to: y@B, public_key: x@A's public key}
# 1.4. B's backend creates a FriendRequestIncoming object, containing y@B's and x@A's identities
# 2. user y@B accepts the friend request
# 2.1. y@B's client sends a POST request to A/api/friendsrequests/ with body
# {from: x@A, to: y@B, public_key: y@B's public key}
# 2.2. A's backend matches the data to the FriendRequestOutgoing object, deletes both and creates a Friend object,
# containing x@A's and y@B's identities
# 2.3. y@B's client sends a POST request to B/api/friends/ containing the id of the FriendRequestIncoming object
# 2.4. B's backend creates a Friend object, using the identities from the FriendRequestIncoming object
class FriendRequestListTestCase(UserTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
FriendRequestIncoming.objects.create(
befriender_username=self.f['ext_user2'].username, befriender_domain=self.f['ext_user2'].domain,
befriender_public_key=self.f['ext_user2'].public_key(), befriendee_user=self.f['local_user1'],
secret='secret1').save()
def test_friend_request_withouth_auth(self):
reply = Client().get('/api/friendrequests/')
self.assertEqual(reply.status_code, 401)
def test_friend_request_empty(self):
reply = client.get('/api/friendrequests/', self.f['local_user2'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(reply.json(), [])
def test_friend_request_list(self):
reply = client.get('/api/friendrequests/', self.f['local_user1'])
self.assertEqual(reply.status_code, 200)
self.assertEqual(len(reply.json()), 1)
self.assertEqual(reply.json()[0]['befriender'], str(self.f['ext_user2']))
self.assertEqual(reply.json()[0]['befriender_public_key'], self.f['ext_user2'].public_key())
class FriendRequestIncomingTestCase(UserTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
FriendRequestIncoming.objects.create(
befriender_username=self.f['ext_user2'].username, befriender_domain=self.f['ext_user2'].domain,
befriender_public_key=self.f['ext_user2'].public_key(), befriendee_user=self.f['local_user1'],
secret='secret1').save()
def test_post_request(self):
befriender = self.f['ext_user1']
befriendee = self.f['local_user1']
reply = client.post('/api/friendrequests/', befriender, {
'befriender': str(befriender),
'befriender_key': befriender.public_key(),
'befriendee': str(befriendee),
'secret': 'secret2'
})
self.assertEqual(reply.status_code, 201)
self.assertEqual(reply.json()['status'], 'pending')
self.assertEqual(FriendRequestIncoming.objects.count(), 2)
incoming = FriendRequestIncoming.objects.get(befriender_username=befriender.username,
befriender_domain=befriender.domain)
self.assertEqual(incoming.befriendee_user, befriendee)
self.assertEqual(incoming.befriender_public_key, befriender.public_key())
self.assertEqual(incoming.secret, 'secret2')
def test_post_request_local(self):
befriender = self.f['local_user2']
befriendee = self.f['local_user1']
reply = client.post('/api/friendrequests/', befriender, {
'befriender': str(befriender),
'befriendee': str(befriendee),
# 'secret': 'secret2'
})
self.assertEqual(reply.status_code, 201)
self.assertEqual(reply.json()['status'], 'pending')
self.assertEqual(FriendRequestIncoming.objects.count(), 2)
incoming = FriendRequestIncoming.objects.get(befriender_username=befriender.username,
befriender_domain=befriender.domain)
self.assertEqual(incoming.befriendee_user, befriendee)
self.assertEqual(incoming.befriender_public_key, befriender.public_key())
# self.assertEqual(incoming.secret, 'secret2')
def test_post_request_withouth_auth(self):
reply = Client().post('/api/friendrequests/')
self.assertEqual(reply.status_code, 400)
def test_post_request_broken_header(self):
befriender = self.f['ext_user1']
befriendee = self.f['local_user1']
broken_client = SignatureAuthClient(header_prefix='broken ')
reply = broken_client.post('/api/friendrequests/', befriender, {
'befriender': str(befriender),
'befriender_key': befriender.public_key(),
'befriendee': str(befriendee),
'secret': 'secret2'
})
self.assertEqual(reply.status_code, 400)
def test_post_request_missing_key(self):
befriender = self.f['ext_user1']
befriendee = self.f['local_user1']
reply = client.post('/api/friendrequests/', befriender, {
'befriender': str(befriender),
'befriendee': str(befriendee),
'secret': 'secret2'
})
self.assertEqual(reply.status_code, 400)
def test_post_request_breaking_key(self):
befriender = self.f['ext_user1']
befriendee = self.f['local_user1']
reply = client.post('/api/friendrequests/', befriender, {
'befriender': str(befriender),
'befriendee': str(befriendee),
'secret': 'secret2',
'befriender_key': 'broken'
})
self.assertEqual(reply.status_code, 400)
def test_post_request_wrong_befriender(self):
befriender = self.f['ext_user1']
befriendee = self.f['local_user1']
reply = client.post('/api/friendrequests/', befriender, {
'befriender': str(self.f['local_user2']),
'befriender_key': befriender.public_key(),
'befriendee': str(befriendee),
'secret': 'secret2'
})
self.assertEqual(reply.status_code, 400)
def test_post_request_bad_signature(self):
befriender = self.f['ext_user1']
befriendee = self.f['local_user1']
bad_signature = SignatureAuthClient(bad_signature=True)
reply = bad_signature.post('/api/friendrequests/', befriender, {
'befriender': str(befriender),
'befriender_key': befriender.public_key(),
'befriendee': str(befriendee),
'secret': 'secret2'
})
self.assertEqual(reply.status_code, 400)
def test_post_request_self(self):
befriender = self.f['local_user1']
befriendee = self.f['local_user1']
reply = client.post('/api/friendrequests/', befriender, {
'befriender': str(befriender),
'befriender_key': befriender.public_key(),
'befriendee': str(befriendee),
'secret': 'secret2'
})
self.assertEqual(reply.status_code, 400)
def test_post_request_befreindee_not_found(self):
befriender = self.f['ext_user1']
befriendee = self.f['local_user1']
reply = client.post('/api/friendrequests/', befriender, {
'befriender': str(befriender),
'befriender_key': befriender.public_key(),
'befriendee': 'nonexistent@' + befriendee.domain,
'secret': 'secret2'
})
self.assertEqual(reply.status_code, 400)
def test_post_request_missing_secret(self):
befriender = self.f['ext_user1']
befriendee = self.f['local_user1']
reply = client.post('/api/friendrequests/', befriender, {
'befriender': str(befriender),
'befriender_key': befriender.public_key(),
'befriendee': str(befriendee)
})
self.assertEqual(reply.status_code, 400)
def test_accept_request(self):
befriender = self.f['ext_user2']
befriendee = self.f['local_user1']
request = FriendRequestIncoming.objects.filter(befriender_username=befriender.username,
befriender_domain=befriender.domain,
befriendee_user=befriendee).first()
reply = client.post('/api/friends/', befriendee, {
'friend_request_id': request.id,
'secret': request.secret
})
self.assertEqual(reply.status_code, 201)
self.assertEqual(reply.json(), {'status': 'accepted'})
def test_accept_request(self):
befriender = self.f['ext_user2']
befriendee = self.f['local_user1']
request = FriendRequestIncoming.objects.filter(befriender_username=befriender.username,
befriender_domain=befriender.domain,
befriendee_user=befriendee).first()
reply = client.post('/api/friends/', befriendee, {
'friend_request_id': request.id,
'secret': request.secret
})
self.assertEqual(reply.status_code, 201)
self.assertEqual(reply.json(), {'status': 'accepted'})
def test_accept_request_not_found(self):
befriender = self.f['ext_user2']
befriendee = self.f['local_user1']
reply = client.post('/api/friends/', befriendee, {
'friend_request_id': 999,
'secret': 'secret1'
})
self.assertEqual(reply.status_code, 404)
class FriendRequestOutgoingTestCase(UserTestMixin, ToolshedTestCase):
def setUp(self):
super().setUp()
self.prepare_users()
FriendRequestOutgoing.objects.create(
befriender_user=self.f['local_user2'],
befriendee_username=self.f['ext_user1'].username,
befriendee_domain=self.f['ext_user1'].domain,
secret='secret3'
).save()
def test_post_outgoing_friend_request(self):
befriender = self.f['local_user1']
befriendee = self.f['ext_user1']
reply = client.post('/api/friendrequests/', befriender, {
'befriender': str(befriender),
'befriendee': str(befriendee),
})
self.assertEqual(reply.status_code, 201)
self.assertTrue('status' in reply.json())
self.assertEqual(reply.json()['status'], 'pending')
self.assertEqual(FriendRequestOutgoing.objects.count(), 2)
outgoing = FriendRequestOutgoing.objects.get(befriender_user=befriender)
self.assertTrue('secret' in reply.json())
self.assertEqual(reply.json()['secret'], outgoing.secret)
self.assertEqual(outgoing.befriendee_username, befriendee.username)
self.assertEqual(outgoing.befriendee_domain, befriendee.domain)
def test_accept_request(self):
befriender = self.f['ext_user1']
befriendee = self.f['local_user2']
reply = client.post('/api/friendrequests/', befriender, {
'befriender': str(befriender),
'befriender_key': befriender.public_key(),
'befriendee': str(befriendee),
'secret': 'secret3'
})
self.assertEqual(reply.status_code, 201)
self.assertEqual(reply.json(), {'status': 'accepted'})
self.assertEqual(FriendRequestIncoming.objects.count(), 0)
self.assertEqual(FriendRequestOutgoing.objects.count(), 0)
self.assertEqual(befriendee.friends.count(), 1)
self.assertEqual(befriendee.friends.first().username, befriender.username)
self.assertEqual(befriendee.friends.first().domain, befriender.domain)