This commit is contained in:
parent
0b92db278b
commit
91cd5c57b3
15 changed files with 251 additions and 17 deletions
|
@ -7,8 +7,10 @@ show_missing = True
|
|||
skip_covered = True
|
||||
omit =
|
||||
*/tests/*
|
||||
*/migrations/*
|
||||
backend/asgi.py
|
||||
backend/wsgi.py
|
||||
backend/settings.py
|
||||
manage.py
|
||||
configure.py
|
||||
configure.py
|
||||
testdata.py
|
|
@ -91,6 +91,10 @@ class ToolshedUser(AbstractUser):
|
|||
def __str__(self):
|
||||
return f"{self.username}@{self.domain}"
|
||||
|
||||
@property
|
||||
def friends(self):
|
||||
return self.public_identity.friends
|
||||
|
||||
def sign(self, message):
|
||||
if type(message) != str:
|
||||
raise TypeError('Message must be a string')
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from rest_framework import authentication
|
||||
from authentication.models import ToolshedUser
|
||||
|
||||
from authentication.models import KnownIdentity, ToolshedUser
|
||||
|
||||
|
||||
def split_userhandle_or_throw(userhandle):
|
||||
|
@ -47,6 +48,21 @@ def verify_request(request, raw_request_body):
|
|||
return username, domain, signed_data, signature_bytes_hex
|
||||
|
||||
|
||||
def authenticate_request_against_known_identities(request, raw_request_body):
|
||||
try:
|
||||
username, domain, signed_data, signature_bytes_hex = verify_request(request, raw_request_body)
|
||||
except ValueError:
|
||||
return None
|
||||
try:
|
||||
author_identity = KnownIdentity.objects.get(username=username, domain=domain)
|
||||
except KnownIdentity.DoesNotExist:
|
||||
return None
|
||||
if author_identity.verify(signed_data, signature_bytes_hex):
|
||||
return author_identity
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def authenticate_request_against_local_users(request, raw_request_body):
|
||||
try:
|
||||
username, domain, signed_data, signature_bytes_hex = verify_request(request, raw_request_body)
|
||||
|
@ -62,6 +78,12 @@ def authenticate_request_against_local_users(request, raw_request_body):
|
|||
return None
|
||||
|
||||
|
||||
class SignatureAuthentication(authentication.BaseAuthentication):
|
||||
def authenticate(self, request):
|
||||
return authenticate_request_against_known_identities(
|
||||
request, request.body.decode('utf-8')), None
|
||||
|
||||
|
||||
class SignatureAuthenticationLocal(authentication.BaseAuthentication):
|
||||
def authenticate(self, request):
|
||||
return authenticate_request_against_local_users(
|
||||
|
|
|
@ -13,10 +13,10 @@ class DummyExternalUser:
|
|||
self.username = username
|
||||
self.domain = domain
|
||||
self.__signing_key = SigningKey.generate()
|
||||
self.public_identity, _ = KnownIdentity.objects.get_or_create(
|
||||
self.public_identity = KnownIdentity.objects.get_or_create(
|
||||
username=username,
|
||||
domain=domain,
|
||||
public_key=self.public_key()) if known else None
|
||||
public_key=self.public_key())[0] if known else None
|
||||
|
||||
def __str__(self):
|
||||
return self.username + '@' + self.domain
|
||||
|
|
|
@ -5,7 +5,7 @@ from nacl.encoding import HexEncoder
|
|||
from nacl.signing import SigningKey
|
||||
|
||||
from authentication.models import ToolshedUser, KnownIdentity
|
||||
from authentication.tests import UserTestCase, SignatureAuthClient
|
||||
from authentication.tests import UserTestCase, SignatureAuthClient, DummyExternalUser
|
||||
from hostadmin.models import Domain
|
||||
|
||||
|
||||
|
@ -307,6 +307,50 @@ class UserApiTestCase(UserTestCase):
|
|||
self.assertEqual(reply.status_code, 403)
|
||||
|
||||
|
||||
class FriendApiTestCase(UserTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.local_user1.friends.add(self.local_user2.public_identity)
|
||||
self.local_user1.friends.add(self.ext_user1.public_identity)
|
||||
self.ext_user1.friends.add(self.local_user1.public_identity)
|
||||
self.anonymous_client = Client(SERVER_NAME='testserver')
|
||||
self.client = SignatureAuthClient()
|
||||
|
||||
def test_friend_local(self):
|
||||
reply = self.client.get('/api/friends/', self.local_user1)
|
||||
self.assertEqual(reply.status_code, 200)
|
||||
|
||||
def test_friend_external(self):
|
||||
reply = self.client.get('/api/friends/', self.ext_user1)
|
||||
self.assertEqual(reply.status_code, 200)
|
||||
|
||||
def test_friend_fail(self):
|
||||
reply = self.anonymous_client.get('/api/friends/')
|
||||
self.assertEqual(reply.status_code, 403)
|
||||
|
||||
def test_friend_fail2(self):
|
||||
target = "/api/friends/"
|
||||
signature = self.local_user1.sign("http://testserver2" + target)
|
||||
header = {'HTTP_AUTHORIZATION': 'Signature ' + str(self.local_user1) + ':' + signature}
|
||||
reply = self.anonymous_client.get(target, **header)
|
||||
self.assertEqual(reply.status_code, 403)
|
||||
|
||||
def test_friend_fail3(self):
|
||||
target = "/api/friends/"
|
||||
unknown_user = DummyExternalUser('extuser3', 'external.org', False)
|
||||
signature = unknown_user.sign("http://testserver" + target)
|
||||
header = {'HTTP_AUTHORIZATION': 'Signature ' + str(unknown_user) + ':' + signature}
|
||||
reply = self.anonymous_client.get(target, **header)
|
||||
self.assertEqual(reply.status_code, 403)
|
||||
|
||||
def test_friend_fail4(self):
|
||||
target = "/api/friends/"
|
||||
signature = self.local_user1.sign("http://testserver" + target)
|
||||
header = {'HTTP_AUTHORIZATION': 'Auth ' + str(self.local_user1) + ':' + signature}
|
||||
reply = self.anonymous_client.get(target, **header)
|
||||
self.assertEqual(reply.status_code, 403)
|
||||
|
||||
|
||||
class LoginApiTestCase(UserTestCase):
|
||||
user = None
|
||||
client = Client(SERVER_NAME='testserver')
|
||||
|
|
|
@ -48,6 +48,7 @@ INSTALLED_APPS = [
|
|||
'drf_yasg',
|
||||
'authentication',
|
||||
'hostadmin',
|
||||
'toolshed',
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
|
@ -148,11 +149,6 @@ USE_TZ = True
|
|||
STATIC_ROOT = 'staticfiles'
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# Extra places for collectstatic to find static files.
|
||||
STATICFILES_DIRS = (
|
||||
BASE_DIR / 'static',
|
||||
)
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
|
||||
|
||||
|
|
|
@ -32,5 +32,6 @@ urlpatterns = [
|
|||
path('djangoadmin/', admin.site.urls),
|
||||
path('auth/', include('authentication.api')),
|
||||
path('admin/', include('hostadmin.api')),
|
||||
path('api/', include('toolshed.api.friend')),
|
||||
path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='api-docs'),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import dotenv
|
||||
|
||||
|
@ -75,15 +76,67 @@ def configure():
|
|||
from django.core.management import call_command
|
||||
call_command('createsuperuser')
|
||||
|
||||
if not os.path.exists('static'):
|
||||
if yesno("No static directory found, do you want to create one?", default=True):
|
||||
os.mkdir('static')
|
||||
|
||||
call_command('collectstatic', '--no-input')
|
||||
|
||||
# if yesno("Do you want to load initial data?"):
|
||||
# call_command('loaddata', 'initial_data.json')
|
||||
|
||||
def reset():
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
|
||||
try:
|
||||
os.remove('db.sqlite3')
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
os.system("git clean -f */migrations")
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
apps = ['authentication', 'authtoken', 'sessions', 'hostadmin', 'toolshed', 'admin']
|
||||
for app in apps:
|
||||
call_command('makemigrations', app)
|
||||
|
||||
for app in apps:
|
||||
call_command('migrate', app)
|
||||
|
||||
|
||||
def testdata():
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
if os.path.exists('testdata.py'):
|
||||
from testdata import create_test_data
|
||||
create_test_data()
|
||||
else:
|
||||
print('No testdata file found')
|
||||
print('Please create a file named testdata.py in the shared_data directory. the function create_test_data() '
|
||||
'will be called automatically and should create all necessary test data.')
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(description='Toolshed Server Configuration')
|
||||
parser.add_argument('--yes', '-y', help='Answer yes to all questions', action='store_true')
|
||||
parser.add_argument('--no', '-n', help='Answer no to all questions', action='store_true')
|
||||
parser.add_argument('cmd', help='Command', default='configure', nargs='?')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.yes and args.no:
|
||||
print('Error: --yes and --no are mutually exclusive')
|
||||
exit(1)
|
||||
|
||||
if args.cmd == 'configure':
|
||||
configure()
|
||||
elif args.cmd == 'reset':
|
||||
reset()
|
||||
elif args.cmd == 'testdata':
|
||||
testdata()
|
||||
else:
|
||||
print('Unknown command: {}'.format(args.cmd))
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
configure()
|
||||
main()
|
||||
|
|
0
backend/toolshed/__init__.py
Normal file
0
backend/toolshed/__init__.py
Normal file
0
backend/toolshed/api/__init__.py
Normal file
0
backend/toolshed/api/__init__.py
Normal file
24
backend/toolshed/api/friend.py
Normal file
24
backend/toolshed/api/friend.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from django.urls import path
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ViewSetMixin
|
||||
|
||||
from authentication.signature_auth import SignatureAuthentication
|
||||
from toolshed.serializers import FriendSerializer
|
||||
|
||||
|
||||
class Friends(APIView, ViewSetMixin):
|
||||
authentication_classes = [SignatureAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, format=None): # /api/friends/ #
|
||||
user = request.user
|
||||
friends = user.friends.all()
|
||||
serializer = FriendSerializer(friends, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('friends/', Friends.as_view(), name='friends'),
|
||||
]
|
0
backend/toolshed/migrations/__init__.py
Normal file
0
backend/toolshed/migrations/__init__.py
Normal file
13
backend/toolshed/serializers.py
Normal file
13
backend/toolshed/serializers.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from rest_framework import serializers
|
||||
from authentication.models import KnownIdentity
|
||||
|
||||
|
||||
class FriendSerializer(serializers.ModelSerializer):
|
||||
username = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = KnownIdentity
|
||||
fields = ['username', 'public_key']
|
||||
|
||||
def get_username(self, obj):
|
||||
return obj.username + '@' + obj.domain
|
0
backend/toolshed/tests/__init__.py
Normal file
0
backend/toolshed/tests/__init__.py
Normal file
75
backend/toolshed/tests/test_friend.py
Normal file
75
backend/toolshed/tests/test_friend.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
from authentication.tests import UserTestCase, SignatureAuthClient
|
||||
|
||||
client = SignatureAuthClient()
|
||||
|
||||
|
||||
class FriendTestCase(UserTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
def test_friendship_iternal(self):
|
||||
self.assertEqual(self.local_user1.friends.count(), 0)
|
||||
self.assertEqual(self.local_user2.friends.count(), 0)
|
||||
self.assertEqual(self.ext_user1.friends.count(), 0)
|
||||
self.assertEqual(self.ext_user2.friends.count(), 0)
|
||||
self.local_user1.friends.add(self.local_user2.public_identity)
|
||||
self.assertEqual(self.local_user1.friends.count(), 1)
|
||||
self.assertEqual(self.local_user2.friends.count(), 1)
|
||||
self.assertEqual(self.ext_user1.friends.count(), 0)
|
||||
self.assertEqual(self.ext_user2.friends.count(), 0)
|
||||
self.assertEqual(self.local_user1.friends.first(), self.local_user2.public_identity)
|
||||
self.assertEqual(self.local_user2.friends.first(), self.local_user1.public_identity)
|
||||
|
||||
def test_friendship_external(self):
|
||||
self.assertEqual(self.local_user1.friends.count(), 0)
|
||||
self.assertEqual(self.local_user2.friends.count(), 0)
|
||||
self.assertEqual(self.ext_user1.friends.count(), 0)
|
||||
self.assertEqual(self.ext_user2.friends.count(), 0)
|
||||
self.local_user1.friends.add(self.ext_user1.public_identity)
|
||||
self.assertEqual(self.local_user1.friends.count(), 1)
|
||||
self.assertEqual(self.local_user2.friends.count(), 0)
|
||||
self.assertEqual(self.ext_user1.friends.count(), 1)
|
||||
self.assertEqual(self.ext_user2.friends.count(), 0)
|
||||
self.assertEqual(self.local_user1.friends.first(), self.ext_user1.public_identity)
|
||||
self.assertEqual(self.ext_user1.friends.first(), self.local_user1.public_identity)
|
||||
|
||||
def test_friend_from_external(self):
|
||||
self.assertEqual(self.local_user1.friends.count(), 0)
|
||||
self.assertEqual(self.local_user2.friends.count(), 0)
|
||||
self.assertEqual(self.ext_user1.friends.count(), 0)
|
||||
self.assertEqual(self.ext_user2.friends.count(), 0)
|
||||
self.ext_user1.friends.add(self.local_user1.public_identity)
|
||||
self.assertEqual(self.local_user1.friends.count(), 1)
|
||||
self.assertEqual(self.local_user2.friends.count(), 0)
|
||||
self.assertEqual(self.ext_user1.friends.count(), 1)
|
||||
self.assertEqual(self.ext_user2.friends.count(), 0)
|
||||
self.assertEqual(self.local_user1.friends.first(), self.ext_user1.public_identity)
|
||||
self.assertEqual(self.ext_user1.friends.first(), self.local_user1.public_identity)
|
||||
|
||||
|
||||
class FriendApiTestCase(UserTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.local_user1.friends.add(self.local_user2.public_identity)
|
||||
self.local_user1.friends.add(self.ext_user1.public_identity)
|
||||
self.ext_user1.friends.add(self.local_user1.public_identity)
|
||||
|
||||
def test_friend_list_internal1(self):
|
||||
reply = client.get('/api/friends/', self.local_user1)
|
||||
self.assertEqual(reply.status_code, 200)
|
||||
self.assertEqual(len(reply.json()), 2)
|
||||
self.assertEqual(reply.json()[0]['username'], str(self.local_user2))
|
||||
self.assertEqual(reply.json()[1]['username'], str(self.ext_user1))
|
||||
|
||||
def test_friend_list_internal2(self):
|
||||
reply = client.get('/api/friends/', self.local_user2)
|
||||
self.assertEqual(reply.status_code, 200)
|
||||
self.assertEqual(len(reply.json()), 1)
|
||||
self.assertEqual(reply.json()[0]['username'], str(self.local_user1))
|
||||
|
||||
def test_friend_list_external(self):
|
||||
reply = client.get('/api/friends/', self.ext_user1)
|
||||
self.assertEqual(reply.status_code, 200)
|
||||
self.assertEqual(len(reply.json()), 1)
|
||||
self.assertEqual(reply.json()[0]['username'], str(self.local_user1))
|
Loading…
Reference in a new issue