diff --git a/judge/admin/profile.py b/judge/admin/profile.py index d9119011c2..59e60e9a29 100644 --- a/judge/admin/profile.py +++ b/judge/admin/profile.py @@ -6,7 +6,7 @@ from reversion.admin import VersionAdmin from django_ace import AceWidget -from judge.models import Profile +from judge.models import Profile, WebAuthnCredential from judge.utils.views import NoBatchDeleteMixin from judge.widgets import AdminMartorWidget, AdminSelect2Widget @@ -44,6 +44,15 @@ def queryset(self, request, queryset): return queryset.filter(timezone=self.value()) +class WebAuthnInline(admin.TabularInline): + model = WebAuthnCredential + readonly_fields = ('cred_id', 'public_key', 'counter') + extra = 0 + + def has_add_permission(self, request): + return False + + class ProfileAdmin(NoBatchDeleteMixin, VersionAdmin): fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme', 'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'notes', 'is_totp_enabled', 'user_script', @@ -58,6 +67,7 @@ class ProfileAdmin(NoBatchDeleteMixin, VersionAdmin): actions_on_top = True actions_on_bottom = True form = ProfileForm + inlines = [WebAuthnInline] def get_queryset(self, request): return super(ProfileAdmin, self).get_queryset(request).select_related('user') diff --git a/judge/models/profile.py b/judge/models/profile.py index 2873b2ddaa..1afed6fa74 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -263,6 +263,13 @@ def webauthn_user(self): rp_id=settings.WEBAUTHN_RP_ID, ) + def __str__(self): + return f'WebAuthn credential: {self.name}' + + class Meta: + verbose_name = _('WebAuthn credential') + verbose_name_plural = _('WebAuthn credentials') + class OrganizationRequest(models.Model): user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='requests', on_delete=models.CASCADE) diff --git a/judge/signals.py b/judge/signals.py index 82a3acc9ac..797b9c0f39 100644 --- a/judge/signals.py +++ b/judge/signals.py @@ -10,7 +10,7 @@ from .caching import finished_submission from .models import BlogPost, Comment, Contest, ContestSubmission, EFFECTIVE_MATH_ENGINES, Judge, Language, License, \ - MiscConfig, Organization, Problem, Profile, Submission + MiscConfig, Organization, Problem, Profile, Submission, WebAuthnCredential def get_pdf_path(basename): @@ -56,6 +56,14 @@ def profile_update(sender, instance, **kwargs): for org_id in instance.organizations.values_list('id', flat=True)]) +@receiver(post_delete, sender=WebAuthnCredential) +def webauthn_delete(sender, instance, **kwargs): + profile = instance.user + if profile.webauthn_credentials.count() == 0: + profile.is_webauthn_enabled = False + profile.save(update_fields=['is_webauthn_enabled']) + + @receiver(post_save, sender=Contest) def contest_update(sender, instance, **kwargs): if hasattr(instance, '_updating_stats_only'): diff --git a/judge/views/two_factor.py b/judge/views/two_factor.py index d8c52858b8..42e48539f2 100644 --- a/judge/views/two_factor.py +++ b/judge/views/two_factor.py @@ -221,10 +221,6 @@ def post(self, request, *args, **kwargs): return HttpResponseBadRequest(_('Staff may not disable 2FA')) credential.delete() - if count <= 1: - request.profile.is_webauthn_enabled = False - request.profile.save(update_fields=['is_webauthn_enabled']) - return HttpResponse()