diff --git a/website/age/admin.py b/website/age/admin.py index 8c38f3f3..9da28c4f 100644 --- a/website/age/admin.py +++ b/website/age/admin.py @@ -1,3 +1,17 @@ from django.contrib import admin +from django.contrib.admin import register -# Register your models here. +from age.models import AgeRegistration + + +@register(AgeRegistration) +class AgeRegistrationAdmin(admin.ModelAdmin): + """Admin interface for the age registration model.""" + + list_display = ("user", "name", "minimum_age", "created_at") + search_fields = ("user__username",) + ordering = ("created_at",) + + def name(self, instance): + """Name of the user.""" + return instance.user.__str__() diff --git a/website/age/migrations/0001_initial.py b/website/age/migrations/0001_initial.py index 9076ab39..6712d742 100644 --- a/website/age/migrations/0001_initial.py +++ b/website/age/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.4 on 2023-09-18 20:45 +# Generated by Django 4.2.4 on 2023-09-19 07:27 from django.conf import settings from django.db import migrations, models @@ -15,10 +15,12 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="Is18YearsOld", + name="AgeRegistration", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("minimum_age", models.PositiveIntegerField()), ( "user", models.OneToOneField( diff --git a/website/age/models.py b/website/age/models.py index 560a06a0..dadf392e 100644 --- a/website/age/models.py +++ b/website/age/models.py @@ -1,5 +1,3 @@ -import uuid - from django.contrib.auth import get_user_model from django.db import models @@ -7,8 +5,14 @@ User = get_user_model() -class Is18YearsOld(models.Model): - """Class to check whether someone has a legal drinking age.""" +class AgeRegistration(models.Model): + """Class to save an age registration.""" user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="is_18_years_old") created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + minimum_age = models.PositiveIntegerField() + + def __str__(self): + """Convert this object to string.""" + return f"{self.user} ({self.created_at})" diff --git a/website/age/services.py b/website/age/services.py new file mode 100644 index 00000000..5921d8c8 --- /dev/null +++ b/website/age/services.py @@ -0,0 +1,53 @@ +import json + +from django.conf import settings +from django.contrib.auth import get_user_model + +from age import models + +User = get_user_model() + + +def verify_minimum_age(user: User, minimum_age: int = 18) -> bool: + """Verify whether someone has a certain minimum age.""" + try: + age_registration = models.AgeRegistration.objects.get(user=user) + except models.AgeRegistration.DoesNotExist: + return False + + return age_registration.minimum_age >= minimum_age + + +def construct_disclose_tree(): + """Construct the disclose tree.""" + yivi_disclose_tree = [] + yivi_age_disclose_tree = [] + for yivi_key in settings.AGE_VERIFICATION_MINIMUM_AGE_MAPPING.keys(): + yivi_age_disclose_tree.append([yivi_key]) + + yivi_disclose_tree.append(yivi_age_disclose_tree) + if settings.AGE_VERIFICATION_USERNAME_ATTRIBUTE is not None: + yivi_disclose_tree.append([[settings.AGE_VERIFICATION_USERNAME_ATTRIBUTE]]) + return yivi_disclose_tree + + +def get_proven_attributes_from_proof_tree(proof_tree): + """Get the proven attributes from a proof tree.""" + proven_attributes = [] + for attribute_conjuction_clause in proof_tree: + for possibly_proven_attribute in attribute_conjuction_clause: + if possibly_proven_attribute["status"] == "PRESENT": + attribute_id = possibly_proven_attribute["id"] + proven_attributes.append(attribute_id) + return proven_attributes + + +def get_highest_proven_age_from_proven_attributes(proven_attributes: [str]): + """Get the highest minimum age from the proven attributes.""" + highest_age = None + for proven_attribute in proven_attributes: + if proven_attribute in settings.AGE_VERIFICATION_MINIMUM_AGE_MAPPING.keys(): + proven_attribute_minimum_age = settings.AGE_VERIFICATION_MINIMUM_AGE_MAPPING[proven_attribute] + if highest_age is None or highest_age < proven_attribute_minimum_age: + highest_age = proven_attribute_minimum_age + return highest_age diff --git a/website/age/signals.py b/website/age/signals.py index 7d4ed70e..107ce0a2 100644 --- a/website/age/signals.py +++ b/website/age/signals.py @@ -2,23 +2,30 @@ from django.dispatch import receiver from age import models +from age.services import get_proven_attributes_from_proof_tree, get_highest_proven_age_from_proven_attributes from yivi.models import Session from yivi.signals import attributes_verified @receiver(attributes_verified) -def update_is_over_18(sender, **kwargs): +def update_minimum_age_when_proven(sender, **kwargs): + """Update the minimum age of someone when proven by Yivi.""" session: Session = kwargs.get("session") - if session.user is None or models.Is18YearsOld.objects.filter(user=session.user).exists(): + if session.user is None: return attributes = kwargs.get("attributes") - # TODO: How to verify proven attributes with requested attributes? - for attribute_conjuction_clause in attributes: - for attribute_disjunction_clause in attribute_conjuction_clause: - attribute_id = attribute_disjunction_clause["id"] - if ( - attribute_id == settings.AGE_VERIFICATION_DISCLOSE_ATTRIBUTE - and attribute_disjunction_clause["status"] == "PRESENT" - ): - models.Is18YearsOld.objects.create(user=session.user) + proven_attributes = get_proven_attributes_from_proof_tree(attributes) + minimum_proven_age = get_highest_proven_age_from_proven_attributes(proven_attributes) + if minimum_proven_age is None: + return + + try: + age_registration = models.AgeRegistration.objects.get(user=session.user) + except models.AgeRegistration.DoesNotExist: + models.AgeRegistration.objects.create(user=session.user, minimum_age=minimum_proven_age) + return + + if age_registration.minimum_age < minimum_proven_age: + age_registration.minimum_age = minimum_proven_age + age_registration.save() diff --git a/website/age/views.py b/website/age/views.py index 13a3c7ae..dc1e26fc 100644 --- a/website/age/views.py +++ b/website/age/views.py @@ -8,6 +8,7 @@ from django.views.generic import TemplateView from age import models +from age.services import verify_minimum_age, construct_disclose_tree class AgeOverviewView(LoginRequiredMixin, TemplateView): @@ -18,12 +19,12 @@ class AgeOverviewView(LoginRequiredMixin, TemplateView): def get(self, request, **kwargs): """Get Age Overview View.""" - is_18_years_old = models.Is18YearsOld.objects.filter(user=request.user).exists() + is_18_years_old = verify_minimum_age(request.user) rendered_tab = render_to_string( "age/age_overview.html", context={ "is_over_18": is_18_years_old, - "disclose": mark_safe(json.dumps({"disclose": [[[settings.AGE_VERIFICATION_DISCLOSE_ATTRIBUTE]]]})), + "disclose": mark_safe(json.dumps({"disclose": construct_disclose_tree()})), }, ) diff --git a/website/tosti/settings/base.py b/website/tosti/settings/base.py index 72b5be9f..ede9ba5a 100644 --- a/website/tosti/settings/base.py +++ b/website/tosti/settings/base.py @@ -263,5 +263,13 @@ DJANGO_CRON_DELETE_LOGS_OLDER_THAN = 14 -# TODO: We might rather place [[["irma-demo.MijnOverheid.ageLower.over18"]]] here and create a function that can verify the requested attribute combination below with the proven attributes in age/signals.py -AGE_VERIFICATION_DISCLOSE_ATTRIBUTE = "irma-demo.MijnOverheid.ageLower.over18" +AGE_VERIFICATION_MINIMUM_AGE_MAPPING = { + "pbdf.gemeente.personalData.over18": 18, + "pbdf.pbdf.ageLimits.over18": 18, + "pbdf.pilot-amsterdam.passport.over18": 18, + "pbdf.bzkpilot.personalData.over18": 18, + "pbdf.pilot-amsterdam.idcard.over18": 18, + "pbdf.nijmegen.ageLimits.over18": 18, +} +# TODO: Add verification username attribute +AGE_VERIFICATION_USERNAME_ATTRIBUTE = None