Compare commits

...

3 commits

9 changed files with 125 additions and 64 deletions

View file

@ -1,8 +1,8 @@
from djeveric.emails import ConfirmationEmail from djeveric.emails import ConfirmationEmail
class ConfidantConfirmationEmail(ConfirmationEmail): class TrustBridgeConfirmationEmail(ConfirmationEmail):
subject = "TODO" subject = "TODO"
def get_message(self, context): def get_body(self, context: dict[str]) -> str:
return '"token": "{token}", "uid": "{uid}", "rtid": "{rtid}", "rid": "{rid}"'.format(**context) return "{token}".format(**context)

View file

@ -1,3 +1,4 @@
from django.conf import settings
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.models import PermissionsMixin from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.validators import UnicodeUsernameValidator from django.contrib.auth.validators import UnicodeUsernameValidator
@ -5,8 +6,11 @@ from django.core.mail import send_mail
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from djeveric.fields import ConfirmationField
from djeveric.models import ConfirmableModelMixin
from userausfall import ldap from userausfall import ldap
from userausfall.emails import TrustBridgeConfirmationEmail
class MissingUserAttribute(Exception): class MissingUserAttribute(Exception):
@ -97,14 +101,18 @@ class User(PermissionsMixin, AbstractBaseUser):
raise PasswordMismatch("The given password does not match the user's password.") raise PasswordMismatch("The given password does not match the user's password.")
return ldap.create_account(self.username, raw_password) return ldap.create_account(self.username, raw_password)
def get_or_create_trust_bridge(self): def get_primary_email(self):
try: """Returns the primary email address for this user."""
return self.trust_bridge return f"{self.username}@{settings.USERAUSFALL['PRIMARY_EMAIL_DOMAIN']}"
except TrustBridge.DoesNotExist:
return TrustBridge.objects.create(trust_taker=self)
class TrustBridge(models.Model): class TrustBridge(ConfirmableModelMixin, models.Model):
is_trusted = models.BooleanField(default=False) is_trusted = ConfirmationField(email_class=TrustBridgeConfirmationEmail)
trust_giver = models.ForeignKey("User", on_delete=models.SET_NULL, null=True) trust_giver = models.ForeignKey("User", on_delete=models.SET_NULL, null=True)
trust_taker = models.OneToOneField("User", on_delete=models.CASCADE, related_name="trust_bridge") trust_taker = models.OneToOneField("User", on_delete=models.CASCADE, related_name="trust_bridge")
def get_confirmation_email_recipient(self) -> str:
return self.trust_giver.get_primary_email()
def _has_confirmation_recipient(self):
return self.trust_giver is not None

View file

@ -1,10 +1,26 @@
from rest_framework import serializers from rest_framework import serializers
from userausfall.models import TrustBridge from userausfall.models import TrustBridge, User
from userausfall.views import get_authenticated_user
class TrustBridgeSerializer(serializers.ModelSerializer): class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ["username"]
# the UniqueValidator for username prevents successful validation for existing users
extra_kwargs = {"username": {"validators": []}}
class TrustBridgeSerializer(serializers.HyperlinkedModelSerializer):
trust_giver = UserSerializer()
class Meta: class Meta:
model = TrustBridge model = TrustBridge
fields = ["is_trusted", "trust_giver"] fields = ["is_trusted", "trust_giver"]
read_only_fields = ["is_trusted"] read_only_fields = ["is_trusted"]
def create(self, validated_data):
user = get_authenticated_user(self.context["request"])
trust_giver, _ = User.objects.get_or_create(username=validated_data["trust_giver"]["username"])
return TrustBridge.objects.create(trust_taker=user, trust_giver=trust_giver)

View file

@ -2,7 +2,29 @@ from rest_framework import status
from userausfall.models import User from userausfall.models import User
from userausfall.rest_api.tests.userausfall import UserausfallAPITestCase from userausfall.rest_api.tests.userausfall import UserausfallAPITestCase
from userausfall.tests import UserMixin
class UserMixin:
user: User
password: str
username: str
def create_user(self):
self.username = f"test{User.objects.count()}"
self.password = "test12345"
self.user = User.objects.create_user(self.username, self.password)
return self.user
def ensure_user_exists(self):
if not hasattr(self, "user"):
self.create_user()
def authenticate_user(self):
self.ensure_user_exists()
if hasattr(self.client, "force_authentication"):
self.client.force_authenticate(user=self.user)
else:
self.client.force_login(user=self.user)
class UserTestCase(UserMixin, UserausfallAPITestCase): class UserTestCase(UserMixin, UserausfallAPITestCase):

View file

@ -1,34 +1,65 @@
from django.core import mail
from rest_framework import status from rest_framework import status
from userausfall.rest_api.tests import UserausfallAPITestCase from userausfall.models import TrustBridge
from userausfall.tests import UserMixin from userausfall.rest_api.tests import UserausfallAPITestCase, UserMixin
class TrustBridgeTestCase(UserMixin, UserausfallAPITestCase): class TrustBridgeTestCase(UserMixin, UserausfallAPITestCase):
def test_retrieve_trust_bridge(self): def create_trust_bridge(self):
""" self.trust_giver = self.create_user()
Retrieve the trust bridge information of a user without an ldap account. self.create_user()
""" self.trust_bridge = TrustBridge.objects.create(trust_taker=self.user, trust_giver=self.trust_giver)
url = "/trust-bridge/" return self.trust_bridge
def test_create_trust_bridge(self):
"""Create a trust bridge for the current user."""
url = "/trust-bridges/"
trust_giver = self.create_user()
self.create_user()
self.authenticate_user() self.authenticate_user()
response = self.client.get(self.get_api_url(url)) response = self.client.post(
self.get_api_url(url),
{
"trust_giver": {
"username": trust_giver.username,
},
},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(TrustBridge.objects.count(), 1)
self.assertEqual(TrustBridge.objects.get().trust_giver, trust_giver)
def test_retrieve_trust_bridge(self):
"""Retrieve the trust bridge information of a user without an ldap account."""
url = "/trust-bridges/{pk}/"
self.create_trust_bridge()
self.authenticate_user()
response = self.client.get(self.get_api_url(url, pk=self.trust_bridge.pk))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
response.data, response.data,
{ {
"is_trusted": False, "is_trusted": False,
"trust_giver": None, "trust_giver": {
"username": self.trust_giver.username,
},
}, },
) )
def test_update_trust_bridge(self): def test_send_confirmation_email_on_creation(self):
""" """When setting a trust giver a confirmation email is sent."""
Update the trust giver of the user's trust bridge. self.create_trust_bridge()
""" self.assertEqual(len(mail.outbox), 1)
url = "/trust-bridge/" self.assertIn(self.user.trust_bridge.get_confirmation_token(), mail.outbox[0].body)
other_user = self.create_user()
self.create_user() def test_confirm_trust_bridge(self):
self.authenticate_user() """The trust giver may confirm the trust bridge."""
response = self.client.put(self.get_api_url(url), {"trust_giver": other_user.pk}) url = "/trust-bridges/{pk}/confirm/"
self.assertEqual(response.status_code, status.HTTP_200_OK) self.create_trust_bridge()
self.assertEqual(self.user.trust_bridge.trust_giver, other_user) response = self.client.post(
self.get_api_url(url, pk=self.trust_bridge.pk), {"token": self.trust_bridge.get_confirmation_token()}
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.trust_bridge.refresh_from_db()
self.assertTrue(self.trust_bridge.is_trusted)

View file

@ -2,9 +2,10 @@ from django.urls import include, path
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from rest_framework import routers from rest_framework import routers
from userausfall.rest_api.views import TrustBridgeView from userausfall.rest_api.views import TrustBridgeViewSet
router = routers.SimpleRouter() router = routers.SimpleRouter()
router.register(r"trust-bridges", TrustBridgeViewSet, "trust-bridge")
urlpatterns = [ urlpatterns = [
path("", include(router.urls)), path("", include(router.urls)),
@ -12,5 +13,4 @@ urlpatterns = [
path("schema/", SpectacularAPIView.as_view(), name="schema"), path("schema/", SpectacularAPIView.as_view(), name="schema"),
path("schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), path("schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
path("schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), path("schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
path("trust-bridge/", TrustBridgeView.as_view()),
] ]

View file

@ -1,17 +1,21 @@
from rest_framework import generics, status, viewsets from djeveric.views import ConfirmModelMixin
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from userausfall.models import MissingUserAttribute, PasswordMismatch, User from userausfall.models import MissingUserAttribute, PasswordMismatch, TrustBridge, User
from userausfall.rest_api.serializers import TrustBridgeSerializer from userausfall.rest_api.serializers import TrustBridgeSerializer
from userausfall.views import get_authenticated_user from userausfall.views import get_authenticated_user
class TrustBridgeView(generics.RetrieveUpdateAPIView): class TrustBridgeViewSet(
ConfirmModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
queryset = TrustBridge.objects
serializer_class = TrustBridgeSerializer serializer_class = TrustBridgeSerializer
def get_object(self): def get_basic_queryset(self):
return get_authenticated_user(self.request).get_or_create_trust_bridge() return self.queryset.filter(trust_taker=get_authenticated_user(self.request))
class UserViewSet(viewsets.GenericViewSet): class UserViewSet(viewsets.GenericViewSet):

View file

@ -200,3 +200,7 @@ USERAUSFALL_LDAP = {
"ADMIN_USER_DN": "cn=admin,dc=local", "ADMIN_USER_DN": "cn=admin,dc=local",
"ADMIN_USER_PASSWORD": os.environ.get("USERAUSFALL_LDAP_PASSWORD"), "ADMIN_USER_PASSWORD": os.environ.get("USERAUSFALL_LDAP_PASSWORD"),
} }
USERAUSFALL = {
"PRIMARY_EMAIL_DOMAIN": "systemausfall.org",
}

View file

@ -1,24 +0,0 @@
from userausfall.models import User
class UserMixin:
user: User
password: str
username: str
def create_user(self):
self.username = f"test{User.objects.count()}"
self.password = "test12345"
self.user = User.objects.create_user(self.username, self.password)
return self.user
def ensure_user_exists(self):
if not hasattr(self, "user"):
self.create_user()
def authenticate_user(self):
self.ensure_user_exists()
if hasattr(self.client, "force_authentication"):
self.client.force_authenticate(user=self.user)
else:
self.client.force_login(user=self.user)