Projet

Général

Profil

0003-auth_saml-genericize-related-object-code-53442.patch

Valentin Deniaud, 21 septembre 2022 12:47

Télécharger (24,5 ko)

Voir les différences:

Subject: [PATCH 03/10] auth_saml: genericize related object code (#53442)

 src/authentic2/apps/authenticators/models.py  |  2 +
 .../journal_event_types.py                    | 36 ++++++++-------
 ..._samlattributelookup_setattributeaction.py |  4 ++
 src/authentic2_auth_saml/models.py            | 44 ++++++++++++++++---
 .../authenticator_detail.html                 | 22 ++++------
 .../related_object_list.html                  |  6 +--
 src/authentic2_auth_saml/urls.py              |  6 +--
 src/authentic2_auth_saml/views.py             | 34 ++++++--------
 tests/test_manager_authenticators.py          |  6 +--
 tests/test_manager_journal.py                 | 12 ++---
 10 files changed, 99 insertions(+), 73 deletions(-)
src/authentic2/apps/authenticators/models.py
74 74
    authenticators = AuthenticatorManager()
75 75

  
76 76
    type = ''
77
    related_models = []
78
    related_object_form_class = None
77 79
    manager_view_template_name = 'authentic2/authenticators/authenticator_detail.html'
78 80
    unique = False
79 81
    protected = False
src/authentic2_auth_saml/journal_event_types.py
16 16

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

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

  
22
from .models import SAMLAuthenticator
23 23

  
24

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

  
32 31

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

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

  
48 48

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

  
53 53
    @classmethod
54 54
    def record(cls, *, user, session, form):
......
61 61

  
62 62
    @classmethod
63 63
    def get_message(cls, event, context):
64
        (authenticator,) = event.get_typed_references(SAMLAuthenticator)
64
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
65
        authenticator = authenticator or event.get_data('authenticator_name')
65 66
        related_object = event.get_data('related_object')
66 67
        new = event.get_data('new') or {}
67 68
        edited_attributes = ', '.join(new) or ''
......
79 80
            )
80 81

  
81 82

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

  
86 87
    @classmethod
87 88
    def get_message(cls, event, context):
88
        (authenticator,) = event.get_typed_references(SAMLAuthenticator)
89
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
90
        authenticator = authenticator or event.get_data('authenticator_name')
89 91
        related_object = event.get_data('related_object')
90 92
        if context != authenticator:
91 93
            return _('deletion of object "{related_object}" in authenticator "{authenticator}"').format(
src/authentic2_auth_saml/migrations/0005_addroleaction_renameattributeaction_samlattributelookup_setattributeaction.py
39 39
            ],
40 40
            options={
41 41
                'verbose_name': 'Set an attribute',
42
                'verbose_name_plural': 'Set attributes',
42 43
                'default_related_name': 'set_attribute_actions',
43 44
            },
44 45
        ),
......
63 64
            ],
64 65
            options={
65 66
                'verbose_name': 'Attribute lookup',
67
                'verbose_name_plural': 'Lookup by attributes',
66 68
                'default_related_name': 'attribute_lookups',
67 69
            },
68 70
        ),
......
86 88
            ],
87 89
            options={
88 90
                'verbose_name': 'Rename an attribute',
91
                'verbose_name_plural': 'Rename attributes',
89 92
                'default_related_name': 'rename_attribute_actions',
90 93
            },
91 94
        ),
......
126 129
            ],
127 130
            options={
128 131
                'verbose_name': 'Add a role',
132
                'verbose_name_plural': 'Add roles',
129 133
                'default_related_name': 'add_role_actions',
130 134
            },
131 135
        ),
src/authentic2_auth_saml/models.py
170 170
            (_('Advanced'), SAMLAuthenticatorAdvancedForm),
171 171
        ]
172 172

  
173
    @property
174
    def related_object_form_class(self):
175
        from .forms import SAMLRelatedObjectForm
176

  
177
        return SAMLRelatedObjectForm
178

  
179
    @property
180
    def related_models(self):
181
        return {
182
            SAMLAttributeLookup: self.attribute_lookups.all(),
183
            SetAttributeAction: self.set_attribute_actions.all(),
184
            AddRoleAction: self.add_role_actions.all(),
185
        }
186

  
173 187
    def clean(self):
174 188
        if not (self.metadata or self.metadata_path or self.metadata_url):
175 189
            raise ValidationError(_('One of the metadata fields must be filled.'))
......
199 213
        return views.profile(request, *args, **kwargs)
200 214

  
201 215

  
202
class SAMLRelatedObjectBase(models.Model):
216
class AuthenticatorRelatedObjectBase(models.Model):
203 217
    authenticator = models.ForeignKey(SAMLAuthenticator, on_delete=models.CASCADE)
204 218

  
205 219
    class Meta:
......
208 222
    def get_journal_text(self):
209 223
        return '%s (%s)' % (self._meta.verbose_name, self.pk)
210 224

  
211
    def get_user_field_display(self):
212
        from authentic2.forms.widgets import SelectAttributeWidget
225
    @property
226
    def model_name(self):
227
        return self._meta.model_name
213 228

  
214
        return SelectAttributeWidget.get_options().get(self.user_field, self.user_field)
229
    @property
230
    def verbose_name_plural(self):
231
        return self._meta.verbose_name_plural
215 232

  
216 233

  
217
class SAMLAttributeLookup(SAMLRelatedObjectBase):
234
class SAMLAttributeLookup(AuthenticatorRelatedObjectBase):
218 235
    user_field = models.CharField(_('User field'), max_length=256)
219 236
    saml_attribute = models.CharField(_('SAML attribute'), max_length=1024)
220 237
    ignore_case = models.BooleanField(_('Ignore case'), default=False)
......
222 239
    class Meta:
223 240
        default_related_name = 'attribute_lookups'
224 241
        verbose_name = _('Attribute lookup')
242
        verbose_name_plural = _('Lookup by attributes')
225 243

  
226 244
    def __str__(self):
227 245
        label = _('"%(saml_attribute)s" (from "%(user_field)s")') % {
......
239 257
            'ignore-case': self.ignore_case,
240 258
        }
241 259

  
260
    def get_user_field_display(self):
261
        from authentic2.forms.widgets import SelectAttributeWidget
262

  
263
        return SelectAttributeWidget.get_options().get(self.user_field, self.user_field)
264

  
242 265

  
243
class SetAttributeAction(SAMLRelatedObjectBase):
266
class SetAttributeAction(AuthenticatorRelatedObjectBase):
244 267
    user_field = models.CharField(_('User field'), max_length=256)
245 268
    saml_attribute = models.CharField(_('SAML attribute name'), max_length=1024)
246 269
    mandatory = models.BooleanField(_('Mandatory'), default=False, help_text=_('Deny login if action fails.'))
......
248 271
    class Meta:
249 272
        default_related_name = 'set_attribute_actions'
250 273
        verbose_name = _('Set an attribute')
274
        verbose_name_plural = _('Set attributes')
251 275

  
252 276
    def __str__(self):
253 277
        label = _('"%(attribute)s" from "%(saml_attribute)s"') % {
......
258 282
            label = '%s (%s)' % (label, _('mandatory'))
259 283
        return label
260 284

  
285
    def get_user_field_display(self):
286
        from authentic2.forms.widgets import SelectAttributeWidget
287

  
288
        return SelectAttributeWidget.get_options().get(self.user_field, self.user_field)
289

  
261 290

  
262
class AddRoleAction(SAMLRelatedObjectBase):
291
class AddRoleAction(AuthenticatorRelatedObjectBase):
263 292
    role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.CASCADE)
264 293
    condition = models.CharField(_('Condition (unused)'), editable=False, max_length=256, blank=True)
265 294
    mandatory = models.BooleanField(_('Mandatory (unused)'), editable=False, default=False)
......
267 296
    class Meta:
268 297
        default_related_name = 'add_role_actions'
269 298
        verbose_name = _('Add a role')
299
        verbose_name_plural = _('Add roles')
270 300

  
271 301
    def __str__(self):
272 302
        return label_from_role(self.role)
src/authentic2_auth_saml/templates/authentic2_auth_saml/authenticator_detail.html
12 12
{% endblock %}
13 13

  
14 14
{% block extra-tab-buttons %}
15
  <button aria-controls="panel-samlattributelookup" aria-selected="false" id="tab-samlattributelookup" role="tab" tabindex="-1">{% trans "Lookup by attributes" %}</button>
16
  <button aria-controls="panel-setattributeaction" aria-selected="false" id="tab-setattributeaction" role="tab" tabindex="-1">{% trans "Set attributes" %}</button>
17
  <button aria-controls="panel-addroleaction" aria-selected="false" id="tab-addroleaction" role="tab" tabindex="-1">{% trans "Add roles" %}</button>
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 18
{% endblock %}
19 19

  
20 20
{% block extra-tab-list %}
21
  <div aria-labelledby="tab-samlattributelookup" hidden="" id="panel-samlattributelookup" role="tabpanel" tabindex="0">
22
    {% include 'authentic2_auth_saml/related_object_list.html' with object_list=object.attribute_lookups.all model_name='samlattributelookup' %}
23
  </div>
24

  
25
  <div aria-labelledby="tab-setattributeaction" hidden="" id="panel-setattributeaction" role="tabpanel" tabindex="0">
26
    {% include 'authentic2_auth_saml/related_object_list.html' with object_list=object.set_attribute_actions.all model_name='setattributeaction' %}
27
  </div>
28

  
29
  <div aria-labelledby="tab-addroleaction" hidden="" id="panel-addroleaction" role="tabpanel" tabindex="0">
30
    {% include 'authentic2_auth_saml/related_object_list.html' with object_list=object.add_role_actions.all model_name='addroleaction' %}
31
  </div>
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 %}
32 26
{% endblock %}
src/authentic2_auth_saml/templates/authentic2_auth_saml/related_object_list.html
3 3
<ul class="objects-list single-links">
4 4
  {% for related_object in object_list %}
5 5
    <li>
6
      <a rel="popup" href="{% url 'a2-manager-saml-edit-related-object' authenticator_pk=object.pk model_name=model_name pk=related_object.pk %}">{{ related_object }}</a>
7
      <a rel="popup" class="delete" href="{% url 'a2-manager-saml-delete-related-object' authenticator_pk=object.pk model_name=model_name pk=related_object.pk %}">{% trans "Remove" %}</a>
6
      <a rel="popup" href="{% url 'a2-manager-authenticators-edit-related-object' authenticator_pk=object.pk model_name=model_name pk=related_object.pk %}">{{ related_object }}</a>
7
      <a rel="popup" class="delete" href="{% url 'a2-manager-authenticators-delete-related-object' authenticator_pk=object.pk model_name=model_name pk=related_object.pk %}">{% trans "Remove" %}</a>
8 8
    </li>
9 9
  {% endfor %}
10
  <li><a class="add" rel="popup" href="{% url 'a2-manager-saml-add-related-object' authenticator_pk=object.pk model_name=model_name %}">{% trans 'Add' %}</a></li>
10
  <li><a class="add" rel="popup" href="{% url 'a2-manager-authenticators-add-related-object' authenticator_pk=object.pk model_name=model_name %}">{% trans 'Add' %}</a></li>
11 11
</ul>
src/authentic2_auth_saml/urls.py
32 32
        path(
33 33
            'authenticators/<int:authenticator_pk>/<slug:model_name>/add/',
34 34
            views.add_related_object,
35
            name='a2-manager-saml-add-related-object',
35
            name='a2-manager-authenticators-add-related-object',
36 36
        ),
37 37
        path(
38 38
            'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/edit/',
39 39
            views.edit_related_object,
40
            name='a2-manager-saml-edit-related-object',
40
            name='a2-manager-authenticators-edit-related-object',
41 41
        ),
42 42
        path(
43 43
            'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/delete/',
44 44
            views.delete_related_object,
45
            name='a2-manager-saml-delete-related-object',
45
            name='a2-manager-authenticators-delete-related-object',
46 46
        ),
47 47
    ],
48 48
)
src/authentic2_auth_saml/views.py
7 7
from django.views.generic import CreateView, DeleteView, UpdateView
8 8
from mellon.utils import get_idp
9 9

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

  
13
from .forms import SAMLRelatedObjectForm
14
from .models import AddRoleAction, SAMLAttributeLookup, SAMLAuthenticator, SetAttributeAction
15

  
16 14

  
17 15
def login(request, authenticator, *args, **kwargs):
18 16
    context = kwargs.pop('context', {}).copy()
......
45 43
    return render_to_string('authentic2_auth_saml/profile.html', context, request=request)
46 44

  
47 45

  
48
class SAMLAuthenticatorMixin(MediaMixin, TitleMixin):
49
    allowed_models = (SAMLAttributeLookup, SetAttributeAction, AddRoleAction)
50

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

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

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

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

  
64 62
    def get_form_kwargs(self):
65 63
        kwargs = super().get_form_kwargs()
......
79 77
        return self.model._meta.verbose_name
80 78

  
81 79

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

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

  
92 88

  
93 89
add_related_object = RelatedObjectAddView.as_view()
94 90

  
95 91

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

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

  
104 100

  
105 101
edit_related_object = RelatedObjectEditView.as_view()
106 102

  
107 103

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

  
112 108
    def delete(self, *args, **kwargs):
113
        self.request.journal.record(
114
            'authenticator.saml.related_object.deletion', related_object=self.get_object()
115
        )
109
        self.request.journal.record('authenticator.related_object.deletion', related_object=self.get_object())
116 110
        return super().delete(*args, **kwargs)
117 111

  
118 112

  
tests/test_manager_authenticators.py
355 355
    resp.form['user_field'].select(text='Email address (email)')
356 356
    resp.form['saml_attribute'] = 'mail'
357 357
    resp = resp.form.submit()
358
    assert_event('authenticator.saml.related_object.creation', user=superuser, session=app.session)
358
    assert_event('authenticator.related_object.creation', user=superuser, session=app.session)
359 359
    assert '#open:samlattributelookup' in resp.location
360 360

  
361 361
    resp = resp.follow()
......
365 365
    resp.form['ignore_case'] = True
366 366
    resp = resp.form.submit().follow()
367 367
    assert escape('"mail" (from "Email address (email)"), case insensitive') in resp.text
368
    assert_event('authenticator.saml.related_object.edit', user=superuser, session=app.session)
368
    assert_event('authenticator.related_object.edit', user=superuser, session=app.session)
369 369

  
370 370
    Attribute.objects.create(kind='string', name='test', label='Test')
371 371
    resp = resp.click('mail')
......
376 376
    resp = resp.click('Remove', href='samlattributelookup')
377 377
    resp = resp.form.submit().follow()
378 378
    assert 'mail' not in resp.text
379
    assert_event('authenticator.saml.related_object.deletion', user=superuser, session=app.session)
379
    assert_event('authenticator.related_object.deletion', user=superuser, session=app.session)
380 380

  
381 381

  
382 382
def test_authenticators_saml_set_attribute(app, superuser):
tests/test_manager_journal.py
308 308
    make('authenticator.disable', user=agent, session=session2, authenticator=authenticator)
309 309
    make('authenticator.deletion', user=agent, session=session2, authenticator=authenticator)
310 310
    make(
311
        'authenticator.saml.related_object.creation',
311
        'authenticator.related_object.creation',
312 312
        user=agent,
313 313
        session=session2,
314 314
        related_object=set_attribute_action,
......
318 318
    action_edit_form.initial = {'from_name': 'old'}
319 319
    action_edit_form.changed_data = ['from_name']
320 320
    action_edit_form.cleaned_data = {'from_name': 'new'}
321
    make('authenticator.saml.related_object.edit', user=agent, session=session2, form=action_edit_form)
321
    make('authenticator.related_object.edit', user=agent, session=session2, form=action_edit_form)
322 322
    make(
323
        'authenticator.saml.related_object.deletion',
323
        'authenticator.related_object.deletion',
324 324
        user=agent,
325 325
        session=session2,
326 326
        related_object=set_attribute_action,
......
730 730
            'message': 'creation of object "Set an attribute (%s)" in authenticator "SAML"'
731 731
            % set_attribute_action.pk,
732 732
            'timestamp': 'Jan. 3, 2020, 8 a.m.',
733
            'type': 'authenticator.saml.related_object.creation',
733
            'type': 'authenticator.related_object.creation',
734 734
            'user': 'agent',
735 735
        },
736 736
        {
737 737
            'message': 'edit of object "Set an attribute (%s)" in authenticator "SAML" (from_name)'
738 738
            % set_attribute_action.pk,
739 739
            'timestamp': 'Jan. 3, 2020, 9 a.m.',
740
            'type': 'authenticator.saml.related_object.edit',
740
            'type': 'authenticator.related_object.edit',
741 741
            'user': 'agent',
742 742
        },
743 743
        {
744 744
            'message': 'deletion of object "Set an attribute (%s)" in authenticator "SAML"'
745 745
            % set_attribute_action.pk,
746 746
            'timestamp': 'Jan. 3, 2020, 10 a.m.',
747
            'type': 'authenticator.saml.related_object.deletion',
747
            'type': 'authenticator.related_object.deletion',
748 748
            'user': 'agent',
749 749
        },
750 750
        {
751
-