stash
This commit is contained in:
parent
6d2167ac66
commit
4787acd8eb
23 changed files with 1241 additions and 25 deletions
|
|
@ -1,7 +1,7 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from authentication.models import ToolshedUser, KnownIdentity, FriendRequestOutgoing, FriendRequestIncoming, \
|
||||
AccountPreference
|
||||
AccountPreference, Group, GroupInvite, GroupInviteIncoming
|
||||
|
||||
|
||||
class ToolshedUserAdmin(admin.ModelAdmin):
|
||||
|
|
@ -29,8 +29,30 @@ class FriendRequestIncomingAdmin(admin.ModelAdmin):
|
|||
search_fields = ('secret', 'befriender_username', 'befriender_domain', 'befriendee_user', 'befriender_public_key')
|
||||
|
||||
|
||||
class GroupAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'domain', 'get_members')
|
||||
search_fields = ('name', 'domain', 'members__username')
|
||||
|
||||
@admin.display(description='Members')
|
||||
def get_members(self, obj):
|
||||
return ', '.join(str(member) for member in obj.members.all())
|
||||
|
||||
|
||||
class GroupInviteAdmin(admin.ModelAdmin):
|
||||
list_display = ('secret', 'group', 'invitee_username', 'invitee_domain')
|
||||
search_fields = ('secret', 'group__name', 'invitee_username', 'invitee_domain')
|
||||
|
||||
|
||||
class GroupInviteIncomingAdmin(admin.ModelAdmin):
|
||||
list_display = ('secret', 'group_name', 'group_domain', 'inviter_username', 'inviter_domain', 'invitee_user')
|
||||
search_fields = ('secret', 'group_name', 'group_domain', 'inviter_username', 'inviter_domain', 'invitee_user')
|
||||
|
||||
|
||||
admin.site.register(ToolshedUser, ToolshedUserAdmin)
|
||||
admin.site.register(KnownIdentity, KnownIdentityAdmin)
|
||||
admin.site.register(FriendRequestOutgoing, FriendRequestOutgoingAdmin)
|
||||
admin.site.register(FriendRequestIncoming, FriendRequestIncomingAdmin)
|
||||
admin.site.register(AccountPreference, AccountPreferenceAdmin)
|
||||
admin.site.register(Group, GroupAdmin)
|
||||
admin.site.register(GroupInvite, GroupInviteAdmin)
|
||||
admin.site.register(GroupInviteIncoming, GroupInviteIncomingAdmin)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
# Generated by Django 4.2.2 on 2026-08-19 13:44
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0004_alter_accountpreference_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Group',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('domain', models.CharField(max_length=255)),
|
||||
('members', models.ManyToManyField(related_name='member_of_groups', to='authentication.knownidentity')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GroupInviteIncoming',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('secret', models.CharField(max_length=255)),
|
||||
('group_name', models.CharField(max_length=255)),
|
||||
('group_domain', models.CharField(max_length=255)),
|
||||
('inviter_username', models.CharField(max_length=255)),
|
||||
('inviter_domain', models.CharField(max_length=255)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('invitee_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_invites_incoming', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GroupInvite',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('secret', models.CharField(max_length=255)),
|
||||
('invitee_username', models.CharField(max_length=255)),
|
||||
('invitee_domain', models.CharField(max_length=255)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='authentication.group')),
|
||||
],
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='group',
|
||||
index=models.Index(fields=['name', 'domain'], name='group_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='group',
|
||||
unique_together={('name', 'domain')},
|
||||
),
|
||||
]
|
||||
|
|
@ -144,3 +144,42 @@ class FriendRequestIncoming(models.Model):
|
|||
befriender_public_key = models.CharField(max_length=255)
|
||||
befriendee_user = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='friend_requests_incoming')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class Group(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
domain = models.CharField(max_length=255)
|
||||
members = models.ManyToManyField(KnownIdentity, related_name='member_of_groups')
|
||||
|
||||
class Meta:
|
||||
unique_together = ('name', 'domain')
|
||||
indexes = [
|
||||
models.Index(fields=['name', 'domain'], name='group_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"#{self.name}@{self.domain}"
|
||||
|
||||
def is_member(self, identity):
|
||||
return self.members.filter(pk=identity.pk).exists()
|
||||
|
||||
|
||||
class GroupInvite(models.Model):
|
||||
"""A pending invite tracked on the group's own home backend, checked when the invitee's
|
||||
accept request arrives (see GroupInviteIncoming for the mirror record on the invitee's own
|
||||
backend, and docs/design-in-progress/groups-mvp.md for the full invite/accept dance)."""
|
||||
secret = models.CharField(max_length=255)
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE, related_name='invites')
|
||||
invitee_username = models.CharField(max_length=255)
|
||||
invitee_domain = models.CharField(max_length=255)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class GroupInviteIncoming(models.Model):
|
||||
secret = models.CharField(max_length=255)
|
||||
group_name = models.CharField(max_length=255)
|
||||
group_domain = models.CharField(max_length=255)
|
||||
inviter_username = models.CharField(max_length=255)
|
||||
inviter_domain = models.CharField(max_length=255)
|
||||
invitee_user = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='group_invites_incoming')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from authentication.models import ToolshedUser
|
||||
from authentication.models import ToolshedUser, Group
|
||||
|
||||
|
||||
class OwnerSerializer(serializers.ReadOnlyField):
|
||||
|
|
@ -10,3 +10,12 @@ class OwnerSerializer(serializers.ReadOnlyField):
|
|||
|
||||
def to_representation(self, value):
|
||||
return value.username + '@' + value.domain
|
||||
|
||||
|
||||
class GroupOwnerSerializer(serializers.ReadOnlyField):
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = ['name', 'domain']
|
||||
|
||||
def to_representation(self, value):
|
||||
return f"#{value.name}@{value.domain}"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ def split_userhandle_or_throw(userhandle):
|
|||
return username, domain
|
||||
|
||||
|
||||
def split_grouphandle_or_throw(grouphandle):
|
||||
if not grouphandle.startswith('#'):
|
||||
raise ValueError('Group handle must be in the format #name@domain')
|
||||
return split_userhandle_or_throw(grouphandle[1:])
|
||||
|
||||
|
||||
def verify_request(request, raw_request_body):
|
||||
authentication_header = request.META.get('HTTP_AUTHORIZATION')
|
||||
|
||||
|
|
@ -74,6 +80,36 @@ def verify_incoming_friend_request(request, raw_request_body):
|
|||
return False
|
||||
|
||||
|
||||
def verify_incoming_group_invite(request, raw_request_body, handle_field, key_field):
|
||||
"""Self-certifying verifier for the two legs of the group invite/accept dance that land on a
|
||||
backend which doesn't have the caller cached as a KnownIdentity yet (see
|
||||
docs/design-in-progress/groups-mvp.md): the inviter delivering an invite to the invitee's own
|
||||
backend (handle_field='inviter', key_field='inviter_key'), and the invitee accepting on the
|
||||
group's home backend (handle_field='invitee', key_field='invitee_key'). Mirrors
|
||||
verify_incoming_friend_request exactly, just with configurable field names."""
|
||||
try:
|
||||
username, domain, signed_data, signature_bytes_hex = verify_request(request, raw_request_body)
|
||||
except ValueError:
|
||||
return False
|
||||
try:
|
||||
claimed_handle = request.data[handle_field]
|
||||
claimed_key = request.data[key_field]
|
||||
except KeyError:
|
||||
return False
|
||||
if not claimed_handle or not claimed_key:
|
||||
return False
|
||||
if username + "@" + domain != claimed_handle:
|
||||
return False
|
||||
if len(claimed_key) != 64:
|
||||
return False
|
||||
verify_key = VerifyKey(bytes.fromhex(claimed_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):
|
||||
try:
|
||||
username, domain, signed_data, signature_bytes_hex = verify_request(request, raw_request_body)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import json
|
|||
from django.test import TestCase, Client
|
||||
from nacl.encoding import HexEncoder
|
||||
|
||||
from authentication.models import ToolshedUser, KnownIdentity
|
||||
from authentication.models import ToolshedUser, KnownIdentity, Group
|
||||
from hostadmin.models import Domain
|
||||
from nacl.signing import SigningKey
|
||||
|
||||
|
|
@ -86,3 +86,9 @@ class UserTestMixin:
|
|||
domain=self.f['example_com'].name)
|
||||
self.f['ext_user1'] = DummyExternalUser('extuser1', 'external.org')
|
||||
self.f['ext_user2'] = DummyExternalUser('extuser2', 'external.org')
|
||||
|
||||
|
||||
class GroupTestMixin:
|
||||
def prepare_groups(self):
|
||||
self.f['group1'] = Group.objects.create(name='group1', domain=self.f['example_com'].name)
|
||||
self.f['group1'].members.add(self.f['local_user1'].public_identity)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue