Projet

Général

Profil

0005-auth_saml-move-related-object-code-to-authenticators.patch

Valentin Deniaud, 21 septembre 2022 12:47

Télécharger (23,8 ko)

Voir les différences:

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
src/authentic2/apps/authenticators/journal_event_types.py
104 104
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
105 105
        authenticator = authenticator or event.get_data('authenticator_name')
106 106
        return _('deletion of authenticator "%s"') % authenticator
107

  
108

  
109
class AuthenticatorRelatedObjectEvents(AuthenticatorEvents):
110
    @classmethod
111
    def record(cls, *, user, session, related_object, data=None):
112
        data = data or {}
113
        data.update({'related_object': related_object.get_journal_text()})
114
        super().record(user=user, session=session, authenticator=related_object.authenticator, data=data)
115

  
116

  
117
class AuthenticatorRelatedObjectCreation(AuthenticatorRelatedObjectEvents):
118
    name = 'authenticator.related_object.creation'
119
    label = _('Authenticator related object creation')
120

  
121
    @classmethod
122
    def get_message(cls, event, context):
123
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
124
        authenticator = authenticator or event.get_data('authenticator_name')
125
        related_object = event.get_data('related_object')
126
        if context != authenticator:
127
            return _('creation of object "{related_object}" in authenticator "{authenticator}"').format(
128
                related_object=related_object, authenticator=authenticator
129
            )
130
        else:
131
            return _('creation of object "%s"') % related_object
132

  
133

  
134
class AuthenticatorRelatedObjectEdit(AuthenticatorRelatedObjectEvents):
135
    name = 'authenticator.related_object.edit'
136
    label = _('Authenticator related object edit')
137

  
138
    @classmethod
139
    def record(cls, *, user, session, form):
140
        super().record(
141
            user=user,
142
            session=session,
143
            related_object=form.instance,
144
            data=form_to_old_new(form),
145
        )
146

  
147
    @classmethod
148
    def get_message(cls, event, context):
149
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
150
        authenticator = authenticator or event.get_data('authenticator_name')
151
        related_object = event.get_data('related_object')
152
        new = event.get_data('new') or {}
153
        edited_attributes = ', '.join(new) or ''
154
        if context != authenticator:
155
            return _(
156
                'edit of object "{related_object}" in authenticator "{authenticator}" ({change})'
157
            ).format(
158
                related_object=related_object,
159
                authenticator=authenticator,
160
                change=edited_attributes,
161
            )
162
        else:
163
            return _('edit of object "{related_object}" ({change})').format(
164
                related_object=related_object, change=edited_attributes
165
            )
166

  
167

  
168
class AuthenticatorRelatedObjectDeletion(AuthenticatorRelatedObjectEvents):
169
    name = 'authenticator.related_object.deletion'
170
    label = _('Authenticator related object deletion')
171

  
172
    @classmethod
173
    def get_message(cls, event, context):
174
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
175
        authenticator = authenticator or event.get_data('authenticator_name')
176
        related_object = event.get_data('related_object')
177
        if context != authenticator:
178
            return _('deletion of object "{related_object}" in authenticator "{authenticator}"').format(
179
                related_object=related_object, authenticator=authenticator
180
            )
181
        else:
182
            return _('deletion of object "%s"') % related_object
src/authentic2/apps/authenticators/manager_urls.py
81 81
            views.order,
82 82
            name='a2-manager-authenticators-order',
83 83
        ),
84
        path(
85
            'authenticators/<int:authenticator_pk>/<slug:model_name>/add/',
86
            views.add_related_object,
87
            name='a2-manager-authenticators-add-related-object',
88
        ),
89
        path(
90
            'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/edit/',
91
            views.edit_related_object,
92
            name='a2-manager-authenticators-edit-related-object',
93
        ),
94
        path(
95
            'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/delete/',
96
            views.delete_related_object,
97
            name='a2-manager-authenticators-delete-related-object',
98
        ),
84 99
    ],
85 100
)
src/authentic2/apps/authenticators/models.py
141 141
        return True
142 142

  
143 143

  
144
class AuthenticatorRelatedObjectBase(models.Model):
145
    authenticator = models.ForeignKey(BaseAuthenticator, on_delete=models.CASCADE)
146

  
147
    class Meta:
148
        abstract = True
149

  
150
    def get_journal_text(self):
151
        return '%s (%s)' % (self._meta.verbose_name, self.pk)
152

  
153
    @property
154
    def model_name(self):
155
        return self._meta.model_name
156

  
157
    @property
158
    def verbose_name_plural(self):
159
        return self._meta.verbose_name_plural
160

  
161

  
144 162
class LoginPasswordAuthenticator(BaseAuthenticator):
145 163
    remember_me = models.PositiveIntegerField(
146 164
        _('Remember me duration'),
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_detail.html
35 35
    <div class='pk-tabs'>
36 36
      <div class="pk-tabs--tab-list" role="tablist">
37 37
        <button aria-controls="panel-description" aria-selected="true" id="tab-description" role="tab" tabindex="0">{% trans "Description" %}</button>
38
        {% for model in object.related_models %}
39
          <button aria-controls="panel-{{ model.model_name }}" aria-selected="false" id="tab-{{ model.model_name }}" role="tab" tabindex="-1">{{ model.verbose_name_plural }}</button>
40
        {% endfor %}
38 41
        {% block extra-tab-buttons %}
39 42
        {% endblock %}
40 43
      </div>
......
47 50
            {% endfor %}
48 51
          </ul>
49 52
        </div>
53
        {% for model, objects in object.related_models.items %}
54
          <div aria-labelledby="tab-{{ model.model_name }}" hidden="" id="panel-{{ model.model_name }}" role="tabpanel" tabindex="0">
55
            {% include 'authentic2/authenticators/related_object_list.html' with object_list=objects model_name=model.model_name %}
56
          </div>
57
        {% endfor %}
50 58
        {% block extra-tab-list %}
51 59
        {% endblock %}
52 60
      </div>
src/authentic2/apps/authenticators/views.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
from django.apps import apps
17 18
from django.contrib import messages
18 19
from django.core.exceptions import PermissionDenied
19
from django.http import HttpResponseRedirect
20
from django.forms.models import modelform_factory
21
from django.http import Http404, HttpResponseRedirect
20 22
from django.shortcuts import get_object_or_404
21 23
from django.urls import reverse, reverse_lazy
22 24
from django.utils.functional import cached_property
......
215 217

  
216 218

  
217 219
order = AuthenticatorsOrderView.as_view()
220

  
221

  
222
class AuthenticatorRelatedObjectMixin(MediaMixin, TitleMixin):
223
    def dispatch(self, request, *args, **kwargs):
224
        self.authenticator = get_object_or_404(
225
            BaseAuthenticator.authenticators.all(), pk=kwargs.get('authenticator_pk')
226
        )
227

  
228
        model_name = kwargs.get('model_name')
229
        if model_name not in (x._meta.model_name for x in self.authenticator.related_models):
230
            raise Http404()
231
        self.model = apps.get_model(self.authenticator._meta.app_label, model_name)
232

  
233
        return super().dispatch(request, *args, **kwargs)
234

  
235
    def get_form_class(self):
236
        return modelform_factory(self.model, self.authenticator.related_object_form_class)
237

  
238
    def get_form_kwargs(self):
239
        kwargs = super().get_form_kwargs()
240
        if not kwargs.get('instance'):
241
            kwargs['instance'] = self.model()
242
        kwargs['instance'].authenticator = self.authenticator
243
        return kwargs
244

  
245
    def get_success_url(self):
246
        return (
247
            reverse('a2-manager-authenticator-detail', kwargs={'pk': self.authenticator.pk})
248
            + '#open:%s' % self.model._meta.model_name
249
        )
250

  
251
    @property
252
    def title(self):
253
        return self.model._meta.verbose_name
254

  
255

  
256
class RelatedObjectAddView(AuthenticatorRelatedObjectMixin, CreateView):
257
    template_name = 'authentic2/manager/form.html'
258

  
259
    def form_valid(self, form):
260
        resp = super().form_valid(form)
261
        self.request.journal.record('authenticator.related_object.creation', related_object=form.instance)
262
        return resp
263

  
264

  
265
add_related_object = RelatedObjectAddView.as_view()
266

  
267

  
268
class RelatedObjectEditView(AuthenticatorRelatedObjectMixin, UpdateView):
269
    template_name = 'authentic2/manager/form.html'
270

  
271
    def form_valid(self, form):
272
        resp = super().form_valid(form)
273
        self.request.journal.record('authenticator.related_object.edit', form=form)
274
        return resp
275

  
276

  
277
edit_related_object = RelatedObjectEditView.as_view()
278

  
279

  
280
class RelatedObjectDeleteView(AuthenticatorRelatedObjectMixin, DeleteView):
281
    template_name = 'authentic2/authenticators/authenticator_delete_form.html'
282
    title = ''
283

  
284
    def delete(self, *args, **kwargs):
285
        self.request.journal.record('authenticator.related_object.deletion', related_object=self.get_object())
286
        return super().delete(*args, **kwargs)
287

  
288

  
289
delete_related_object = RelatedObjectDeleteView.as_view()
src/authentic2_auth_saml/journal_event_types.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2022 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.utils.translation import gettext_lazy as _
18

  
19
from authentic2.apps.authenticators.journal_event_types import AuthenticatorEvents
20
from authentic2.apps.authenticators.models import BaseAuthenticator
21
from authentic2.apps.journal.utils import form_to_old_new
22

  
23

  
24
class AuthenticatorRelatedObjectEvents(AuthenticatorEvents):
25
    @classmethod
26
    def record(cls, *, user, session, related_object, data=None):
27
        data = data or {}
28
        data.update({'related_object': related_object.get_journal_text()})
29
        super().record(user=user, session=session, authenticator=related_object.authenticator, data=data)
30

  
31

  
32
class AuthenticatorRelatedObjectCreation(AuthenticatorRelatedObjectEvents):
33
    name = 'authenticator.related_object.creation'
34
    label = _('Authenticator related object creation')
35

  
36
    @classmethod
37
    def get_message(cls, event, context):
38
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
39
        authenticator = authenticator or event.get_data('authenticator_name')
40
        related_object = event.get_data('related_object')
41
        if context != authenticator:
42
            return _('creation of object "{related_object}" in authenticator "{authenticator}"').format(
43
                related_object=related_object, authenticator=authenticator
44
            )
45
        else:
46
            return _('creation of object "%s"') % related_object
47

  
48

  
49
class AuthenticatorRelatedObjectEdit(AuthenticatorRelatedObjectEvents):
50
    name = 'authenticator.related_object.edit'
51
    label = _('Authenticator related object edit')
52

  
53
    @classmethod
54
    def record(cls, *, user, session, form):
55
        super().record(
56
            user=user,
57
            session=session,
58
            related_object=form.instance,
59
            data=form_to_old_new(form),
60
        )
61

  
62
    @classmethod
63
    def get_message(cls, event, context):
64
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
65
        authenticator = authenticator or event.get_data('authenticator_name')
66
        related_object = event.get_data('related_object')
67
        new = event.get_data('new') or {}
68
        edited_attributes = ', '.join(new) or ''
69
        if context != authenticator:
70
            return _(
71
                'edit of object "{related_object}" in authenticator "{authenticator}" ({change})'
72
            ).format(
73
                related_object=related_object,
74
                authenticator=authenticator,
75
                change=edited_attributes,
76
            )
77
        else:
78
            return _('edit of object "{related_object}" ({change})').format(
79
                related_object=related_object, change=edited_attributes
80
            )
81

  
82

  
83
class AuthenticatorRelatedObjectDeletion(AuthenticatorRelatedObjectEvents):
84
    name = 'authenticator.related_object.deletion'
85
    label = _('Authenticator related object deletion')
86

  
87
    @classmethod
88
    def get_message(cls, event, context):
89
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
90
        authenticator = authenticator or event.get_data('authenticator_name')
91
        related_object = event.get_data('related_object')
92
        if context != authenticator:
93
            return _('deletion of object "{related_object}" in authenticator "{authenticator}"').format(
94
                related_object=related_object, authenticator=authenticator
95
            )
96
        else:
97
            return _('deletion of object "%s"') % related_object
src/authentic2_auth_saml/models.py
21 21
from django.utils.translation import gettext_lazy as _
22 22

  
23 23
from authentic2.a2_rbac.models import Role
24
from authentic2.apps.authenticators.models import BaseAuthenticator
24
from authentic2.apps.authenticators.models import AuthenticatorRelatedObjectBase, BaseAuthenticator
25 25
from authentic2.manager.utils import label_from_role
26 26
from authentic2.utils.misc import redirect_to_login
27 27

  
......
213 213
        return views.profile(request, *args, **kwargs)
214 214

  
215 215

  
216
class AuthenticatorRelatedObjectBase(models.Model):
217
    authenticator = models.ForeignKey(BaseAuthenticator, on_delete=models.CASCADE)
218

  
219
    class Meta:
220
        abstract = True
221

  
222
    def get_journal_text(self):
223
        return '%s (%s)' % (self._meta.verbose_name, self.pk)
224

  
225
    @property
226
    def model_name(self):
227
        return self._meta.model_name
228

  
229
    @property
230
    def verbose_name_plural(self):
231
        return self._meta.verbose_name_plural
232

  
233

  
234 216
class SAMLAttributeLookup(AuthenticatorRelatedObjectBase):
235 217
    user_field = models.CharField(_('User field'), max_length=256)
236 218
    saml_attribute = models.CharField(_('SAML attribute'), max_length=1024)
src/authentic2_auth_saml/templates/authentic2_auth_saml/authenticator_detail.html
10 10

  
11 11
{{ block.super }}
12 12
{% endblock %}
13

  
14
{% block extra-tab-buttons %}
15
  {% for model in object.related_models %}
16
    <button aria-controls="panel-{{ model.model_name }}" aria-selected="false" id="tab-{{ model.model_name }}" role="tab" tabindex="-1">{{ model.verbose_name_plural }}</button>
17
  {% endfor %}
18
{% endblock %}
19

  
20
{% block extra-tab-list %}
21
  {% for model, objects in object.related_models.items %}
22
    <div aria-labelledby="tab-{{ model.model_name }}" hidden="" id="panel-{{ model.model_name }}" role="tabpanel" tabindex="0">
23
      {% include 'authentic2_auth_saml/related_object_list.html' with object_list=objects model_name=model.model_name %}
24
    </div>
25
  {% endfor %}
26
{% endblock %}
src/authentic2_auth_saml/urls.py
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
from django.conf.urls import include, url
18
from django.urls import path
19

  
20
from authentic2.apps.authenticators.manager_urls import superuser_login_required
21
from authentic2.decorators import required
22

  
23
from . import views
24 18

  
25 19
urlpatterns = [
26 20
    url(r'^accounts/saml/', include('mellon.urls'), kwargs={'template_base': 'authentic2/base.html'})
27 21
]
28

  
29
urlpatterns += required(
30
    superuser_login_required,
31
    [
32
        path(
33
            'authenticators/<int:authenticator_pk>/<slug:model_name>/add/',
34
            views.add_related_object,
35
            name='a2-manager-authenticators-add-related-object',
36
        ),
37
        path(
38
            'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/edit/',
39
            views.edit_related_object,
40
            name='a2-manager-authenticators-edit-related-object',
41
        ),
42
        path(
43
            'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/delete/',
44
            views.delete_related_object,
45
            name='a2-manager-authenticators-delete-related-object',
46
        ),
47
    ],
48
)
src/authentic2_auth_saml/views.py
1
from django.apps import apps
2
from django.forms.models import modelform_factory
3
from django.http import Http404
4
from django.shortcuts import get_object_or_404, render
1
from django.shortcuts import render
5 2
from django.template.loader import render_to_string
6
from django.urls import reverse
7
from django.views.generic import CreateView, DeleteView, UpdateView
8 3
from mellon.utils import get_idp
9 4

  
10
from authentic2.apps.authenticators.models import BaseAuthenticator
11
from authentic2.manager.views import MediaMixin, TitleMixin
12 5
from authentic2.utils.misc import redirect_to_login
13 6

  
14 7

  
......
41 34
        user_saml_identifier.idp = get_idp(user_saml_identifier.issuer.entity_id)
42 35
    context['user_saml_identifiers'] = user_saml_identifiers
43 36
    return render_to_string('authentic2_auth_saml/profile.html', context, request=request)
44

  
45

  
46
class AuthenticatorRelatedObjectMixin(MediaMixin, TitleMixin):
47
    def dispatch(self, request, *args, **kwargs):
48
        self.authenticator = get_object_or_404(
49
            BaseAuthenticator.authenticators.all(), pk=kwargs.get('authenticator_pk')
50
        )
51

  
52
        model_name = kwargs.get('model_name')
53
        if model_name not in (x._meta.model_name for x in self.authenticator.related_models):
54
            raise Http404()
55
        self.model = apps.get_model(self.authenticator._meta.app_label, model_name)
56

  
57
        return super().dispatch(request, *args, **kwargs)
58

  
59
    def get_form_class(self):
60
        return modelform_factory(self.model, self.authenticator.related_object_form_class)
61

  
62
    def get_form_kwargs(self):
63
        kwargs = super().get_form_kwargs()
64
        if not kwargs.get('instance'):
65
            kwargs['instance'] = self.model()
66
        kwargs['instance'].authenticator = self.authenticator
67
        return kwargs
68

  
69
    def get_success_url(self):
70
        return (
71
            reverse('a2-manager-authenticator-detail', kwargs={'pk': self.authenticator.pk})
72
            + '#open:%s' % self.model._meta.model_name
73
        )
74

  
75
    @property
76
    def title(self):
77
        return self.model._meta.verbose_name
78

  
79

  
80
class RelatedObjectAddView(AuthenticatorRelatedObjectMixin, CreateView):
81
    template_name = 'authentic2/manager/form.html'
82

  
83
    def form_valid(self, form):
84
        resp = super().form_valid(form)
85
        self.request.journal.record('authenticator.related_object.creation', related_object=form.instance)
86
        return resp
87

  
88

  
89
add_related_object = RelatedObjectAddView.as_view()
90

  
91

  
92
class RelatedObjectEditView(AuthenticatorRelatedObjectMixin, UpdateView):
93
    template_name = 'authentic2/manager/form.html'
94

  
95
    def form_valid(self, form):
96
        resp = super().form_valid(form)
97
        self.request.journal.record('authenticator.related_object.edit', form=form)
98
        return resp
99

  
100

  
101
edit_related_object = RelatedObjectEditView.as_view()
102

  
103

  
104
class RelatedObjectDeleteView(AuthenticatorRelatedObjectMixin, DeleteView):
105
    template_name = 'authentic2/authenticators/authenticator_delete_form.html'
106
    title = ''
107

  
108
    def delete(self, *args, **kwargs):
109
        self.request.journal.record('authenticator.related_object.deletion', related_object=self.get_object())
110
        return super().delete(*args, **kwargs)
111

  
112

  
113
delete_related_object = RelatedObjectDeleteView.as_view()
114
-