auth_saml: move related object code to authenticators app (#53442)

This commit is contained in:
Valentin Deniaud 2022-09-20 12:08:19 +02:00
parent 700a5bb196
commit b24fad1bd2
11 changed files with 192 additions and 236 deletions

View File

@ -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

View File

@ -81,5 +81,20 @@ urlpatterns = required(
views.order,
name='a2-manager-authenticators-order',
),
path(
'authenticators/<int:authenticator_pk>/<slug:model_name>/add/',
views.add_related_object,
name='a2-manager-authenticators-add-related-object',
),
path(
'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/edit/',
views.edit_related_object,
name='a2-manager-authenticators-edit-related-object',
),
path(
'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/delete/',
views.delete_related_object,
name='a2-manager-authenticators-delete-related-object',
),
],
)

View File

@ -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'),

View File

@ -35,6 +35,9 @@
<div class='pk-tabs'>
<div class="pk-tabs--tab-list" role="tablist">
<button aria-controls="panel-description" aria-selected="true" id="tab-description" role="tab" tabindex="0">{% trans "Description" %}</button>
{% for model in object.related_models %}
<button aria-controls="panel-{{ model.model_name }}" aria-selected="false" id="tab-{{ model.model_name }}" role="tab" tabindex="-1">{{ model.verbose_name_plural }}</button>
{% endfor %}
{% block extra-tab-buttons %}
{% endblock %}
</div>
@ -47,6 +50,11 @@
{% endfor %}
</ul>
</div>
{% for model, objects in object.related_models.items %}
<div aria-labelledby="tab-{{ model.model_name }}" hidden="" id="panel-{{ model.model_name }}" role="tabpanel" tabindex="0">
{% include 'authentic2/authenticators/related_object_list.html' with object_list=objects model_name=model.model_name %}
</div>
{% endfor %}
{% block extra-tab-list %}
{% endblock %}
</div>

View File

@ -14,9 +14,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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()

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -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)

View File

@ -10,17 +10,3 @@
{{ block.super }}
{% endblock %}
{% block extra-tab-buttons %}
{% for model in object.related_models %}
<button aria-controls="panel-{{ model.model_name }}" aria-selected="false" id="tab-{{ model.model_name }}" role="tab" tabindex="-1">{{ model.verbose_name_plural }}</button>
{% endfor %}
{% endblock %}
{% block extra-tab-list %}
{% for model, objects in object.related_models.items %}
<div aria-labelledby="tab-{{ model.model_name }}" hidden="" id="panel-{{ model.model_name }}" role="tabpanel" tabindex="0">
{% include 'authentic2_auth_saml/related_object_list.html' with object_list=objects model_name=model.model_name %}
</div>
{% endfor %}
{% endblock %}

View File

@ -15,34 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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/<int:authenticator_pk>/<slug:model_name>/add/',
views.add_related_object,
name='a2-manager-authenticators-add-related-object',
),
path(
'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/edit/',
views.edit_related_object,
name='a2-manager-authenticators-edit-related-object',
),
path(
'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/delete/',
views.delete_related_object,
name='a2-manager-authenticators-delete-related-object',
),
],
)

View File

@ -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()