From 6892977141ee1c5c65f422e3fd42a29da19840e1 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Tue, 20 Sep 2022 12:08:19 +0200 Subject: [PATCH 05/10] auth_saml: move related object code to authenticators app (#53442) --- .../authenticators/journal_event_types.py | 76 +++++++++++++++ .../apps/authenticators/manager_urls.py | 15 +++ src/authentic2/apps/authenticators/models.py | 18 ++++ .../authenticators/authenticator_detail.html | 8 ++ .../authenticators}/related_object_list.html | 0 src/authentic2/apps/authenticators/views.py | 74 +++++++++++++- .../journal_event_types.py | 97 ------------------- src/authentic2_auth_saml/models.py | 20 +--- .../authenticator_detail.html | 14 --- src/authentic2_auth_saml/urls.py | 27 ------ src/authentic2_auth_saml/views.py | 79 +-------------- 11 files changed, 192 insertions(+), 236 deletions(-) rename src/{authentic2_auth_saml/templates/authentic2_auth_saml => authentic2/apps/authenticators/templates/authentic2/authenticators}/related_object_list.html (100%) delete mode 100644 src/authentic2_auth_saml/journal_event_types.py diff --git a/src/authentic2/apps/authenticators/journal_event_types.py b/src/authentic2/apps/authenticators/journal_event_types.py index a2d9ea34d..f5877500f 100644 --- a/src/authentic2/apps/authenticators/journal_event_types.py +++ b/src/authentic2/apps/authenticators/journal_event_types.py @@ -104,3 +104,79 @@ class AuthenticatorDeletion(AuthenticatorEvents): (authenticator,) = event.get_typed_references(BaseAuthenticator) authenticator = authenticator or event.get_data('authenticator_name') return _('deletion of authenticator "%s"') % authenticator + + +class AuthenticatorRelatedObjectEvents(AuthenticatorEvents): + @classmethod + def record(cls, *, user, session, related_object, data=None): + data = data or {} + data.update({'related_object': related_object.get_journal_text()}) + super().record(user=user, session=session, authenticator=related_object.authenticator, data=data) + + +class AuthenticatorRelatedObjectCreation(AuthenticatorRelatedObjectEvents): + name = 'authenticator.related_object.creation' + label = _('Authenticator related object creation') + + @classmethod + def get_message(cls, event, context): + (authenticator,) = event.get_typed_references(BaseAuthenticator) + authenticator = authenticator or event.get_data('authenticator_name') + related_object = event.get_data('related_object') + if context != authenticator: + return _('creation of object "{related_object}" in authenticator "{authenticator}"').format( + related_object=related_object, authenticator=authenticator + ) + else: + return _('creation of object "%s"') % related_object + + +class AuthenticatorRelatedObjectEdit(AuthenticatorRelatedObjectEvents): + name = 'authenticator.related_object.edit' + label = _('Authenticator related object edit') + + @classmethod + def record(cls, *, user, session, form): + super().record( + user=user, + session=session, + related_object=form.instance, + data=form_to_old_new(form), + ) + + @classmethod + def get_message(cls, event, context): + (authenticator,) = event.get_typed_references(BaseAuthenticator) + authenticator = authenticator or event.get_data('authenticator_name') + related_object = event.get_data('related_object') + new = event.get_data('new') or {} + edited_attributes = ', '.join(new) or '' + if context != authenticator: + return _( + 'edit of object "{related_object}" in authenticator "{authenticator}" ({change})' + ).format( + related_object=related_object, + authenticator=authenticator, + change=edited_attributes, + ) + else: + return _('edit of object "{related_object}" ({change})').format( + related_object=related_object, change=edited_attributes + ) + + +class AuthenticatorRelatedObjectDeletion(AuthenticatorRelatedObjectEvents): + name = 'authenticator.related_object.deletion' + label = _('Authenticator related object deletion') + + @classmethod + def get_message(cls, event, context): + (authenticator,) = event.get_typed_references(BaseAuthenticator) + authenticator = authenticator or event.get_data('authenticator_name') + related_object = event.get_data('related_object') + if context != authenticator: + return _('deletion of object "{related_object}" in authenticator "{authenticator}"').format( + related_object=related_object, authenticator=authenticator + ) + else: + return _('deletion of object "%s"') % related_object diff --git a/src/authentic2/apps/authenticators/manager_urls.py b/src/authentic2/apps/authenticators/manager_urls.py index 664bf05c4..c0352f538 100644 --- a/src/authentic2/apps/authenticators/manager_urls.py +++ b/src/authentic2/apps/authenticators/manager_urls.py @@ -81,5 +81,20 @@ urlpatterns = required( views.order, name='a2-manager-authenticators-order', ), + path( + 'authenticators///add/', + views.add_related_object, + name='a2-manager-authenticators-add-related-object', + ), + path( + 'authenticators////edit/', + views.edit_related_object, + name='a2-manager-authenticators-edit-related-object', + ), + path( + 'authenticators////delete/', + views.delete_related_object, + name='a2-manager-authenticators-delete-related-object', + ), ], ) diff --git a/src/authentic2/apps/authenticators/models.py b/src/authentic2/apps/authenticators/models.py index 6bedc7554..f89d26acd 100644 --- a/src/authentic2/apps/authenticators/models.py +++ b/src/authentic2/apps/authenticators/models.py @@ -141,6 +141,24 @@ class BaseAuthenticator(models.Model): return True +class AuthenticatorRelatedObjectBase(models.Model): + authenticator = models.ForeignKey(BaseAuthenticator, on_delete=models.CASCADE) + + class Meta: + abstract = True + + def get_journal_text(self): + return '%s (%s)' % (self._meta.verbose_name, self.pk) + + @property + def model_name(self): + return self._meta.model_name + + @property + def verbose_name_plural(self): + return self._meta.verbose_name_plural + + class LoginPasswordAuthenticator(BaseAuthenticator): remember_me = models.PositiveIntegerField( _('Remember me duration'), diff --git a/src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_detail.html b/src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_detail.html index a0c8cd7c6..8214ae708 100644 --- a/src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_detail.html +++ b/src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_detail.html @@ -35,6 +35,9 @@
+ {% for model in object.related_models %} + + {% endfor %} {% block extra-tab-buttons %} {% endblock %}
@@ -47,6 +50,11 @@ {% endfor %}
+ {% for model, objects in object.related_models.items %} + + {% endfor %} {% block extra-tab-list %} {% endblock %} diff --git a/src/authentic2_auth_saml/templates/authentic2_auth_saml/related_object_list.html b/src/authentic2/apps/authenticators/templates/authentic2/authenticators/related_object_list.html similarity index 100% rename from src/authentic2_auth_saml/templates/authentic2_auth_saml/related_object_list.html rename to src/authentic2/apps/authenticators/templates/authentic2/authenticators/related_object_list.html diff --git a/src/authentic2/apps/authenticators/views.py b/src/authentic2/apps/authenticators/views.py index d54a3d294..bc0b57bfb 100644 --- a/src/authentic2/apps/authenticators/views.py +++ b/src/authentic2/apps/authenticators/views.py @@ -14,9 +14,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.apps import apps from django.contrib import messages from django.core.exceptions import PermissionDenied -from django.http import HttpResponseRedirect +from django.forms.models import modelform_factory +from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.urls import reverse, reverse_lazy from django.utils.functional import cached_property @@ -215,3 +217,73 @@ class AuthenticatorsOrderView(AuthenticatorsMixin, FormView): order = AuthenticatorsOrderView.as_view() + + +class AuthenticatorRelatedObjectMixin(MediaMixin, TitleMixin): + def dispatch(self, request, *args, **kwargs): + self.authenticator = get_object_or_404( + BaseAuthenticator.authenticators.all(), pk=kwargs.get('authenticator_pk') + ) + + model_name = kwargs.get('model_name') + if model_name not in (x._meta.model_name for x in self.authenticator.related_models): + raise Http404() + self.model = apps.get_model(self.authenticator._meta.app_label, model_name) + + return super().dispatch(request, *args, **kwargs) + + def get_form_class(self): + return modelform_factory(self.model, self.authenticator.related_object_form_class) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + if not kwargs.get('instance'): + kwargs['instance'] = self.model() + kwargs['instance'].authenticator = self.authenticator + return kwargs + + def get_success_url(self): + return ( + reverse('a2-manager-authenticator-detail', kwargs={'pk': self.authenticator.pk}) + + '#open:%s' % self.model._meta.model_name + ) + + @property + def title(self): + return self.model._meta.verbose_name + + +class RelatedObjectAddView(AuthenticatorRelatedObjectMixin, CreateView): + template_name = 'authentic2/manager/form.html' + + def form_valid(self, form): + resp = super().form_valid(form) + self.request.journal.record('authenticator.related_object.creation', related_object=form.instance) + return resp + + +add_related_object = RelatedObjectAddView.as_view() + + +class RelatedObjectEditView(AuthenticatorRelatedObjectMixin, UpdateView): + template_name = 'authentic2/manager/form.html' + + def form_valid(self, form): + resp = super().form_valid(form) + self.request.journal.record('authenticator.related_object.edit', form=form) + return resp + + +edit_related_object = RelatedObjectEditView.as_view() + + +class RelatedObjectDeleteView(AuthenticatorRelatedObjectMixin, DeleteView): + template_name = 'authentic2/authenticators/authenticator_delete_form.html' + title = '' + + def delete(self, *args, **kwargs): + self.request.journal.record('authenticator.related_object.deletion', related_object=self.get_object()) + return super().delete(*args, **kwargs) + + +delete_related_object = RelatedObjectDeleteView.as_view() diff --git a/src/authentic2_auth_saml/journal_event_types.py b/src/authentic2_auth_saml/journal_event_types.py deleted file mode 100644 index 689560e29..000000000 --- a/src/authentic2_auth_saml/journal_event_types.py +++ /dev/null @@ -1,97 +0,0 @@ -# authentic2 - versatile identity manager -# Copyright (C) 2010-2022 Entr'ouvert -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from django.utils.translation import gettext_lazy as _ - -from authentic2.apps.authenticators.journal_event_types import AuthenticatorEvents -from authentic2.apps.authenticators.models import BaseAuthenticator -from authentic2.apps.journal.utils import form_to_old_new - - -class AuthenticatorRelatedObjectEvents(AuthenticatorEvents): - @classmethod - def record(cls, *, user, session, related_object, data=None): - data = data or {} - data.update({'related_object': related_object.get_journal_text()}) - super().record(user=user, session=session, authenticator=related_object.authenticator, data=data) - - -class AuthenticatorRelatedObjectCreation(AuthenticatorRelatedObjectEvents): - name = 'authenticator.related_object.creation' - label = _('Authenticator related object creation') - - @classmethod - def get_message(cls, event, context): - (authenticator,) = event.get_typed_references(BaseAuthenticator) - authenticator = authenticator or event.get_data('authenticator_name') - related_object = event.get_data('related_object') - if context != authenticator: - return _('creation of object "{related_object}" in authenticator "{authenticator}"').format( - related_object=related_object, authenticator=authenticator - ) - else: - return _('creation of object "%s"') % related_object - - -class AuthenticatorRelatedObjectEdit(AuthenticatorRelatedObjectEvents): - name = 'authenticator.related_object.edit' - label = _('Authenticator related object edit') - - @classmethod - def record(cls, *, user, session, form): - super().record( - user=user, - session=session, - related_object=form.instance, - data=form_to_old_new(form), - ) - - @classmethod - def get_message(cls, event, context): - (authenticator,) = event.get_typed_references(BaseAuthenticator) - authenticator = authenticator or event.get_data('authenticator_name') - related_object = event.get_data('related_object') - new = event.get_data('new') or {} - edited_attributes = ', '.join(new) or '' - if context != authenticator: - return _( - 'edit of object "{related_object}" in authenticator "{authenticator}" ({change})' - ).format( - related_object=related_object, - authenticator=authenticator, - change=edited_attributes, - ) - else: - return _('edit of object "{related_object}" ({change})').format( - related_object=related_object, change=edited_attributes - ) - - -class AuthenticatorRelatedObjectDeletion(AuthenticatorRelatedObjectEvents): - name = 'authenticator.related_object.deletion' - label = _('Authenticator related object deletion') - - @classmethod - def get_message(cls, event, context): - (authenticator,) = event.get_typed_references(BaseAuthenticator) - authenticator = authenticator or event.get_data('authenticator_name') - related_object = event.get_data('related_object') - if context != authenticator: - return _('deletion of object "{related_object}" in authenticator "{authenticator}"').format( - related_object=related_object, authenticator=authenticator - ) - else: - return _('deletion of object "%s"') % related_object diff --git a/src/authentic2_auth_saml/models.py b/src/authentic2_auth_saml/models.py index 7121c0b6c..de276fe87 100644 --- a/src/authentic2_auth_saml/models.py +++ b/src/authentic2_auth_saml/models.py @@ -21,7 +21,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from authentic2.a2_rbac.models import Role -from authentic2.apps.authenticators.models import BaseAuthenticator +from authentic2.apps.authenticators.models import AuthenticatorRelatedObjectBase, BaseAuthenticator from authentic2.manager.utils import label_from_role from authentic2.utils.misc import redirect_to_login @@ -213,24 +213,6 @@ class SAMLAuthenticator(BaseAuthenticator): return views.profile(request, *args, **kwargs) -class AuthenticatorRelatedObjectBase(models.Model): - authenticator = models.ForeignKey(BaseAuthenticator, on_delete=models.CASCADE) - - class Meta: - abstract = True - - def get_journal_text(self): - return '%s (%s)' % (self._meta.verbose_name, self.pk) - - @property - def model_name(self): - return self._meta.model_name - - @property - def verbose_name_plural(self): - return self._meta.verbose_name_plural - - class SAMLAttributeLookup(AuthenticatorRelatedObjectBase): user_field = models.CharField(_('User field'), max_length=256) saml_attribute = models.CharField(_('SAML attribute'), max_length=1024) diff --git a/src/authentic2_auth_saml/templates/authentic2_auth_saml/authenticator_detail.html b/src/authentic2_auth_saml/templates/authentic2_auth_saml/authenticator_detail.html index 797224758..0bd10bfd6 100644 --- a/src/authentic2_auth_saml/templates/authentic2_auth_saml/authenticator_detail.html +++ b/src/authentic2_auth_saml/templates/authentic2_auth_saml/authenticator_detail.html @@ -10,17 +10,3 @@ {{ block.super }} {% endblock %} - -{% block extra-tab-buttons %} - {% for model in object.related_models %} - - {% endfor %} -{% endblock %} - -{% block extra-tab-list %} - {% for model, objects in object.related_models.items %} - - {% endfor %} -{% endblock %} diff --git a/src/authentic2_auth_saml/urls.py b/src/authentic2_auth_saml/urls.py index c9269f6c9..8c2f7b85d 100644 --- a/src/authentic2_auth_saml/urls.py +++ b/src/authentic2_auth_saml/urls.py @@ -15,34 +15,7 @@ # along with this program. If not, see . from django.conf.urls import include, url -from django.urls import path - -from authentic2.apps.authenticators.manager_urls import superuser_login_required -from authentic2.decorators import required - -from . import views urlpatterns = [ url(r'^accounts/saml/', include('mellon.urls'), kwargs={'template_base': 'authentic2/base.html'}) ] - -urlpatterns += required( - superuser_login_required, - [ - path( - 'authenticators///add/', - views.add_related_object, - name='a2-manager-authenticators-add-related-object', - ), - path( - 'authenticators////edit/', - views.edit_related_object, - name='a2-manager-authenticators-edit-related-object', - ), - path( - 'authenticators////delete/', - views.delete_related_object, - name='a2-manager-authenticators-delete-related-object', - ), - ], -) diff --git a/src/authentic2_auth_saml/views.py b/src/authentic2_auth_saml/views.py index 7b0362cd5..1cb44cf3d 100644 --- a/src/authentic2_auth_saml/views.py +++ b/src/authentic2_auth_saml/views.py @@ -1,14 +1,7 @@ -from django.apps import apps -from django.forms.models import modelform_factory -from django.http import Http404 -from django.shortcuts import get_object_or_404, render +from django.shortcuts import render from django.template.loader import render_to_string -from django.urls import reverse -from django.views.generic import CreateView, DeleteView, UpdateView from mellon.utils import get_idp -from authentic2.apps.authenticators.models import BaseAuthenticator -from authentic2.manager.views import MediaMixin, TitleMixin from authentic2.utils.misc import redirect_to_login @@ -41,73 +34,3 @@ def profile(request, *args, **kwargs): user_saml_identifier.idp = get_idp(user_saml_identifier.issuer.entity_id) context['user_saml_identifiers'] = user_saml_identifiers return render_to_string('authentic2_auth_saml/profile.html', context, request=request) - - -class AuthenticatorRelatedObjectMixin(MediaMixin, TitleMixin): - def dispatch(self, request, *args, **kwargs): - self.authenticator = get_object_or_404( - BaseAuthenticator.authenticators.all(), pk=kwargs.get('authenticator_pk') - ) - - model_name = kwargs.get('model_name') - if model_name not in (x._meta.model_name for x in self.authenticator.related_models): - raise Http404() - self.model = apps.get_model(self.authenticator._meta.app_label, model_name) - - return super().dispatch(request, *args, **kwargs) - - def get_form_class(self): - return modelform_factory(self.model, self.authenticator.related_object_form_class) - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - if not kwargs.get('instance'): - kwargs['instance'] = self.model() - kwargs['instance'].authenticator = self.authenticator - return kwargs - - def get_success_url(self): - return ( - reverse('a2-manager-authenticator-detail', kwargs={'pk': self.authenticator.pk}) - + '#open:%s' % self.model._meta.model_name - ) - - @property - def title(self): - return self.model._meta.verbose_name - - -class RelatedObjectAddView(AuthenticatorRelatedObjectMixin, CreateView): - template_name = 'authentic2/manager/form.html' - - def form_valid(self, form): - resp = super().form_valid(form) - self.request.journal.record('authenticator.related_object.creation', related_object=form.instance) - return resp - - -add_related_object = RelatedObjectAddView.as_view() - - -class RelatedObjectEditView(AuthenticatorRelatedObjectMixin, UpdateView): - template_name = 'authentic2/manager/form.html' - - def form_valid(self, form): - resp = super().form_valid(form) - self.request.journal.record('authenticator.related_object.edit', form=form) - return resp - - -edit_related_object = RelatedObjectEditView.as_view() - - -class RelatedObjectDeleteView(AuthenticatorRelatedObjectMixin, DeleteView): - template_name = 'authentic2/authenticators/authenticator_delete_form.html' - title = '' - - def delete(self, *args, **kwargs): - self.request.journal.record('authenticator.related_object.deletion', related_object=self.get_object()) - return super().delete(*args, **kwargs) - - -delete_related_object = RelatedObjectDeleteView.as_view() -- 2.30.2