Projet

Général

Profil

0007-auth_saml-add-views-to-configure-related-objects-670.patch

Valentin Deniaud, 16 août 2022 14:12

Télécharger (29,1 ko)

Voir les différences:

Subject: [PATCH 7/7] auth_saml: add views to configure related objects
 (#67025)

 src/authentic2/apps/authenticators/models.py  |  1 +
 .../authenticators/authenticator_detail.html  | 24 ++++-
 src/authentic2/apps/authenticators/views.py   |  3 +-
 src/authentic2_auth_saml/forms.py             |  9 ++
 .../journal_event_types.py                    | 93 +++++++++++++++++++
 src/authentic2_auth_saml/models.py            | 68 +++++++++++---
 .../authenticator_detail.html                 | 27 ++++++
 .../related_object_list.html                  | 11 +++
 src/authentic2_auth_saml/urls.py              | 27 ++++++
 src/authentic2_auth_saml/views.py             | 91 +++++++++++++++++-
 tests/test_manager_authenticators.py          | 77 +++++++++++++++
 tests/test_manager_journal.py                 | 43 +++++++++
 12 files changed, 455 insertions(+), 19 deletions(-)
 create mode 100644 src/authentic2_auth_saml/journal_event_types.py
 create mode 100644 src/authentic2_auth_saml/templates/authentic2_auth_saml/authenticator_detail.html
 create mode 100644 src/authentic2_auth_saml/templates/authentic2_auth_saml/related_object_list.html
src/authentic2/apps/authenticators/models.py
75 75

  
76 76
    type = ''
77 77
    manager_form_class = None
78
    manager_view_template_name = 'authentic2/authenticators/authenticator_detail.html'
78 79
    unique = False
79 80
    protected = False
80 81
    description_fields = ['show_condition']
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_detail.html
31 31
  </div>
32 32
  {% endif %}
33 33

  
34
  <div class='placeholder'>
35
    {% for line in object.get_full_description %}
36
      <p>{{ line }}</p>
37
    {% endfor %}
34
  <div class='section authenticator-detail'>
35
    <div class='pk-tabs'>
36
      <div class="pk-tabs--tab-list" role="tablist">
37
        <button aria-controls="panel-description" aria-selected="true" id="tab-description" role="tab" tabindex="0">{% trans "Description" %}</button>
38
        {% block extra-tab-buttons %}
39
        {% endblock %}
40
      </div>
41

  
42
      <div class="pk-tabs--container">
43
        <div aria-labelledby="tab-description" id="panel-description" role="tabpanel" tabindex="0">
44
          <ul>
45
            {% for line in object.get_full_description %}
46
              <li>{{ line }}</li>
47
            {% endfor %}
48
          </ul>
49
        </div>
50
        {% block extra-tab-list %}
51
        {% endblock %}
52
      </div>
53
    </div>
38 54
  </div>
39 55
{% endblock %}
src/authentic2/apps/authenticators/views.py
64 64

  
65 65

  
66 66
class AuthenticatorDetailView(AuthenticatorsMixin, DetailView):
67
    template_name = 'authentic2/authenticators/authenticator_detail.html'
67
    def get_template_names(self):
68
        return self.object.manager_view_template_name
68 69

  
69 70
    @property
70 71
    def title(self):
src/authentic2_auth_saml/forms.py
16 16

  
17 17
from django import forms
18 18

  
19
from authentic2.a2_rbac.models import Role
20

  
19 21
from .models import SAMLAuthenticator
20 22

  
21 23

  
......
23 25
    class Meta:
24 26
        model = SAMLAuthenticator
25 27
        exclude = ('ou',)
28

  
29

  
30
class RelatedObjectForm(forms.ModelForm):
31
    def __init__(self, *args, **kwargs):
32
        super().__init__(*args, **kwargs)
33
        if 'role' in self.fields:
34
            self.fields['role'].queryset = Role.objects.exclude(slug__startswith='_')
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.journal.models import EventTypeDefinition
20
from authentic2.apps.journal.utils import form_to_old_new
21

  
22
from .models import SAMLAuthenticator
23

  
24

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

  
32

  
33
class SAMLAuthenticatorRelatedObjectCreation(SAMLAuthenticatorEventsMixin):
34
    name = 'authenticator.saml.related_object.creation'
35
    label = _('SAML authenticator related object creation')
36

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

  
48

  
49
class SAMLAuthenticatorRelatedObjectEdit(SAMLAuthenticatorEventsMixin):
50
    name = 'authenticator.saml.related_object.edit'
51
    label = _('SAML 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(SAMLAuthenticator)
65
        related_object = event.get_data('related_object')
66
        new = event.get_data('new') or {}
67
        edited_attributes = ', '.join(new) or ''
68
        if context != authenticator:
69
            return _('edit {related_object} in authenticator "{authenticator}" ({change})').format(
70
                related_object=related_object,
71
                authenticator=authenticator,
72
                change=edited_attributes,
73
            )
74
        else:
75
            return _('edit {related_object} ({change})').format(
76
                related_object=related_object, change=edited_attributes
77
            )
78

  
79

  
80
class SAMLAuthenticatorRelatedObjectDeletion(SAMLAuthenticatorEventsMixin):
81
    name = 'authenticator.saml.related_object.deletion'
82
    label = _('SAML authenticator related object deletion')
83

  
84
    @classmethod
85
    def get_message(cls, event, context):
86
        (authenticator,) = event.get_typed_references(SAMLAuthenticator)
87
        related_object = event.get_data('related_object')
88
        if context != authenticator:
89
            return _('deletion of {related_object} in authenticator "{authenticator}"').format(
90
                related_object=related_object, authenticator=authenticator
91
            )
92
        else:
93
            return _('deletion of %s') % related_object
src/authentic2_auth_saml/models.py
21 21

  
22 22
from authentic2.a2_rbac.models import Role
23 23
from authentic2.apps.authenticators.models import BaseAuthenticator
24
from authentic2.utils.evaluate import condition_validator
24 25
from authentic2.utils.misc import redirect_to_login
25 26

  
26
from . import views
27

  
28 27

  
29 28
class SAMLAuthenticator(BaseAuthenticator):
30 29
    metadata_url = models.URLField(_('Metadata URL'), max_length=300, blank=True)
......
147 146

  
148 147
    type = 'saml'
149 148
    how = ['saml']
149
    manager_view_template_name = 'authentic2_auth_saml/authenticator_detail.html'
150 150
    description_fields = [
151 151
        'show_condition',
152 152
        'metadata_url',
......
195 195
        )
196 196

  
197 197
    def login(self, request, *args, **kwargs):
198
        from . import views
199

  
198 200
        return views.login(request, self, *args, **kwargs)
199 201

  
200 202
    def profile(self, request, *args, **kwargs):
203
        from . import views
204

  
201 205
        return views.profile(request, *args, **kwargs)
202 206

  
203 207

  
204
class SAMLAttributeLookup(models.Model):
205
    authenticator = models.ForeignKey(
206
        SAMLAuthenticator, on_delete=models.CASCADE, related_name='attribute_lookups'
207
    )
208
class SAMLRelatedObjectBase(models.Model):
209
    authenticator = models.ForeignKey(SAMLAuthenticator, on_delete=models.CASCADE)
210

  
211
    def __repr__(self):
212
        return '%s (%s)' % (self._meta.object_name, self.pk)
213

  
214
    class Meta:
215
        abstract = True
216

  
217

  
218
class SAMLAttributeLookup(SAMLRelatedObjectBase):
208 219
    user_field = models.CharField(_('User field'), max_length=32)
209 220
    saml_attribute = models.CharField(_('SAML attribute'), max_length=128)
210 221
    ignore_case = models.BooleanField(_('Ignore case'), default=False)
211 222

  
212 223
    class Meta:
224
        default_related_name = 'attribute_lookups'
213 225
        verbose_name = _('Attribute lookup')
214 226

  
227
    def __str__(self):
228
        label = _('"%(saml_attribute)s" (from "%(user_field)s")') % {
229
            'saml_attribute': self.saml_attribute,
230
            'user_field': self.user_field,
231
        }
232
        if self.ignore_case:
233
            label = '%s, %s' % (label, _('case insensitive'))
234
        return label
235

  
215 236
    def as_dict(self):
216 237
        return {
217 238
            'user_field': self.user_field,
......
219 240
            'ignore-case': self.ignore_case,
220 241
        }
221 242

  
222
class RenameAttributeAction(models.Model):
223
    authenticator = models.ForeignKey(SAMLAuthenticator, on_delete=models.CASCADE)
243

  
244
class RenameAttributeAction(SAMLRelatedObjectBase):
224 245
    from_name = models.CharField(_('From'), max_length=128)
225 246
    to_name = models.CharField(_('To'), max_length=32)
226 247

  
......
228 249
        default_related_name = 'rename_attribute_actions'
229 250
        verbose_name = _('Rename an attribute')
230 251

  
252
    def __str__(self):
253
        return '%s → %s' % (self.from_name, self.to_name)
231 254

  
232
class SetAttributeAction(models.Model):
233
    authenticator = models.ForeignKey(SAMLAuthenticator, on_delete=models.CASCADE)
255
    def __repr__(self):
256
        return '%s (%s)' % (self._meta.object_name, self.pk)
257

  
258

  
259
class SetAttributeAction(SAMLRelatedObjectBase):
234 260
    attribute = models.CharField(_('User attribute name'), max_length=32)
235 261
    saml_attribute = models.CharField(_('SAML attribute name'), max_length=128)
236 262
    mandatory = models.BooleanField(_('Mandatory'), default=False, help_text=_('Deny login if action fails.'))
......
239 265
        default_related_name = 'set_attribute_actions'
240 266
        verbose_name = _('Set an attribute')
241 267

  
268
    def __str__(self):
269
        label = _('"%(attribute)s" from "%(saml_attribute)s"') % {
270
            'attribute': self.attribute,
271
            'saml_attribute': self.saml_attribute,
272
        }
273
        if self.mandatory:
274
            label = '%s (%s)' % (label, _('mandatory'))
275
        return label
276

  
242 277

  
243
class AddRoleAction(models.Model):
244
    authenticator = models.ForeignKey(SAMLAuthenticator, on_delete=models.CASCADE)
278
class AddRoleAction(SAMLRelatedObjectBase):
245 279
    role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.CASCADE)
246
    condition = models.CharField(_('Condition'), max_length=256, blank=True)
280
    condition = models.CharField(_('Condition'), max_length=256, blank=True, validators=[condition_validator])
247 281
    mandatory = models.BooleanField(_('Mandatory'), default=False, help_text=_('Deny login if action fails.'))
248 282

  
249 283
    class Meta:
250 284
        default_related_name = 'add_role_actions'
251 285
        verbose_name = _('Add a role')
286

  
287
    def __str__(self):
288
        label = str(self.role)
289
        if self.condition:
290
            label = '%s, %s' % (label, _('with condition'))
291
        if self.mandatory:
292
            label = '%s (%s)' % (label, _('mandatory'))
293
        return label
src/authentic2_auth_saml/templates/authentic2_auth_saml/authenticator_detail.html
1
{% extends 'authentic2/authenticators/authenticator_detail.html' %}
2
{% load i18n %}
3

  
4
{% block extra-tab-buttons %}
5
  <button aria-controls="panel-samlattributelookup" aria-selected="false" id="tab-samlattributelookup" role="tab" tabindex="-1">{% trans "Lookup by attributes" %}</button>
6
  <button aria-controls="panel-renameattributeaction" aria-selected="false" id="tab-renameattributeaction" role="tab" tabindex="-1">{% trans "Rename attributes" %}</button>
7
  <button aria-controls="panel-setattributeaction" aria-selected="false" id="tab-setattributeaction" role="tab" tabindex="-1">{% trans "Set attributes" %}</button>
8
  <button aria-controls="panel-addroleaction" aria-selected="false" id="tab-addroleaction" role="tab" tabindex="-1">{% trans "Add roles" %}</button>
9
{% endblock %}
10

  
11
{% block extra-tab-list %}
12
  <div aria-labelledby="tab-samlattributelookup" hidden="" id="panel-samlattributelookup" role="tabpanel" tabindex="0">
13
    {% include 'authentic2_auth_saml/related_object_list.html' with object_list=object.attribute_lookups.all model_name='samlattributelookup' %}
14
  </div>
15

  
16
  <div aria-labelledby="tab-renameattributeaction" hidden="" id="panel-renameattributeaction" role="tabpanel" tabindex="0">
17
    {% include 'authentic2_auth_saml/related_object_list.html' with object_list=object.rename_attribute_actions.all model_name='renameattributeaction' %}
18
  </div>
19

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

  
24
  <div aria-labelledby="tab-addroleaction" hidden="" id="panel-addroleaction" role="tabpanel" tabindex="0">
25
    {% include 'authentic2_auth_saml/related_object_list.html' with object_list=object.add_role_actions.all model_name='addroleaction' %}
26
  </div>
27
{% endblock %}
src/authentic2_auth_saml/templates/authentic2_auth_saml/related_object_list.html
1
{% load i18n %}
2

  
3
<ul class="objects-list single-links">
4
  {% for related_object in object_list %}
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>
8
    </li>
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>
11
</ul>
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
18 24

  
19 25
urlpatterns = [
20 26
    url(r'^accounts/saml/', include('mellon.urls'), kwargs={'template_base': 'authentic2/base.html'})
21 27
]
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-saml-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-saml-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-saml-delete-related-object',
46
        ),
47
    ],
48
)
src/authentic2_auth_saml/views.py
1
from django.shortcuts import render
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
2 5
from django.template.loader import render_to_string
6
from django.urls import reverse
7
from django.views.generic import CreateView, DeleteView, UpdateView
3 8
from mellon.utils import get_idp
4 9

  
10
from authentic2.manager.views import MediaMixin, TitleMixin
5 11
from authentic2.utils.misc import redirect_to_login
6 12

  
13
from .forms import RelatedObjectForm
14
from .models import (
15
    AddRoleAction,
16
    RenameAttributeAction,
17
    SAMLAttributeLookup,
18
    SAMLAuthenticator,
19
    SetAttributeAction,
20
)
21

  
7 22

  
8 23
def login(request, authenticator, *args, **kwargs):
9 24
    context = kwargs.pop('context', {}).copy()
......
34 49
        user_saml_identifier.idp = get_idp(user_saml_identifier.issuer.entity_id)
35 50
    context['user_saml_identifiers'] = user_saml_identifiers
36 51
    return render_to_string('authentic2_auth_saml/profile.html', context, request=request)
52

  
53

  
54
class SAMLAuthenticatorMixin(MediaMixin, TitleMixin):
55
    allowed_models = (SAMLAttributeLookup, RenameAttributeAction, SetAttributeAction, AddRoleAction)
56

  
57
    def dispatch(self, request, *args, **kwargs):
58
        self.authenticator = get_object_or_404(SAMLAuthenticator, pk=kwargs.get('authenticator_pk'))
59

  
60
        model_name = kwargs.get('model_name')
61
        if model_name not in (x._meta.model_name for x in self.allowed_models):
62
            raise Http404()
63
        self.model = apps.get_model('authentic2_auth_saml', model_name)
64

  
65
        return super().dispatch(request, *args, **kwargs)
66

  
67
    def get_form_class(self):
68
        return modelform_factory(self.model, exclude=('authenticator',), form=RelatedObjectForm)
69

  
70
    def get_form_kwargs(self):
71
        kwargs = super().get_form_kwargs()
72
        if not kwargs.get('instance'):
73
            kwargs['instance'] = self.model()
74
        kwargs['instance'].authenticator = self.authenticator
75
        return kwargs
76

  
77
    def get_success_url(self):
78
        return (
79
            reverse('a2-manager-authenticator-detail', kwargs={'pk': self.authenticator.pk})
80
            + '#open:%s' % self.model._meta.model_name
81
        )
82

  
83
    @property
84
    def title(self):
85
        return self.model._meta.verbose_name
86

  
87

  
88
class RelatedObjectAddView(SAMLAuthenticatorMixin, CreateView):
89
    template_name = 'authentic2/manager/form.html'
90

  
91
    def form_valid(self, form):
92
        resp = super().form_valid(form)
93
        self.request.journal.record(
94
            'authenticator.saml.related_object.creation', related_object=form.instance
95
        )
96
        return resp
97

  
98

  
99
add_related_object = RelatedObjectAddView.as_view()
100

  
101

  
102
class RelatedObjectEditView(SAMLAuthenticatorMixin, UpdateView):
103
    template_name = 'authentic2/manager/form.html'
104

  
105
    def form_valid(self, form):
106
        resp = super().form_valid(form)
107
        self.request.journal.record('authenticator.saml.related_object.edit', form=form)
108
        return resp
109

  
110

  
111
edit_related_object = RelatedObjectEditView.as_view()
112

  
113

  
114
class RelatedObjectDeleteView(SAMLAuthenticatorMixin, DeleteView):
115
    template_name = 'authentic2/authenticators/authenticator_delete_form.html'
116
    title = ''
117

  
118
    def delete(self, *args, **kwargs):
119
        self.request.journal.record(
120
            'authenticator.saml.related_object.deletion', related_object=self.get_object()
121
        )
122
        return super().delete(*args, **kwargs)
123

  
124

  
125
delete_related_object = RelatedObjectDeleteView.as_view()
tests/test_manager_authenticators.py
16 16

  
17 17
import pytest
18 18
from django import VERSION as DJ_VERSION
19
from django.utils.html import escape
19 20

  
20 21
from authentic2.a2_rbac.utils import get_default_ou
21 22
from authentic2.apps.authenticators.models import BaseAuthenticator, LoginPasswordAuthenticator
......
279 280
    assert 'Authenticator has been enabled.' in resp.text
280 281

  
281 282

  
283
def test_authenticators_saml_attribute_lookup(app, superuser):
284
    authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
285
    resp = login(app, superuser, path=authenticator.get_absolute_url())
286

  
287
    resp = resp.click('Add', href='samlattributelookup')
288
    resp.form['user_field'] = 'email'
289
    resp.form['saml_attribute'] = 'mail'
290
    resp = resp.form.submit()
291
    assert_event('authenticator.saml.related_object.creation', user=superuser, session=app.session)
292
    assert '#open:samlattributelookup' in resp.location
293

  
294
    resp = resp.follow()
295
    assert escape('"mail" (from "email")') in resp.text
296

  
297
    resp = resp.click('mail')
298
    resp.form['ignore_case'] = True
299
    resp = resp.form.submit().follow()
300
    assert escape('"mail" (from "email"), case insensitive') in resp.text
301
    assert_event('authenticator.saml.related_object.edit', user=superuser, session=app.session)
302

  
303
    resp = resp.click('Remove', href='samlattributelookup')
304
    resp = resp.form.submit().follow()
305
    assert 'mail' not in resp.text
306
    assert_event('authenticator.saml.related_object.deletion', user=superuser, session=app.session)
307

  
308

  
309
def test_authenticators_saml_rename_attribute(app, superuser):
310
    authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
311
    resp = login(app, superuser, path=authenticator.get_absolute_url())
312

  
313
    resp = resp.click('Add', href='renameattributeaction')
314
    resp.form['from_name'] = 'a'
315
    resp.form['to_name'] = 'b'
316
    resp = resp.form.submit().follow()
317
    assert 'a → b' in resp.text
318

  
319

  
320
def test_authenticators_saml_set_attribute(app, superuser):
321
    authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
322
    resp = login(app, superuser, path=authenticator.get_absolute_url())
323

  
324
    resp = resp.click('Add', href='setattributeaction')
325
    resp.form['attribute'] = 'email'
326
    resp.form['saml_attribute'] = 'mail'
327
    resp = resp.form.submit().follow()
328
    assert escape('"email" from "mail"') in resp.text
329

  
330
    resp = resp.click('mail')
331
    resp.form['mandatory'] = True
332
    resp = resp.form.submit().follow()
333
    assert escape('"email" from "mail" (mandatory)') in resp.text
334

  
335

  
336
def test_authenticators_saml_add_role(app, superuser, role_ou1):
337
    authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
338
    resp = login(app, superuser, path=authenticator.get_absolute_url())
339

  
340
    resp = resp.click('Add', href='addroleaction')
341
    assert len(resp.form['role'].options) == 2  # blank choice and role_ou1, no internal roles
342

  
343
    resp.form['role'] = role_ou1.pk
344
    resp = resp.form.submit().follow()
345
    assert 'role_ou1' in resp.text
346

  
347
    resp = resp.click('role_ou1')
348
    resp.form['condition'] = 'abc'
349
    resp.form['mandatory'] = True
350
    resp = resp.form.submit().follow()
351
    assert 'role_ou1, with condition (mandatory)' in resp.text
352

  
353
    resp = resp.click('role_ou1')
354
    resp.form['condition'] = '{'
355
    resp = resp.form.submit()
356
    assert "could not parse expression" in resp.text
357

  
358

  
282 359
def test_authenticators_order(app, superuser):
283 360
    resp = login(app, superuser, path='/manage/authenticators/')
284 361

  
tests/test_manager_journal.py
28 28
from authentic2.custom_user.models import Profile, ProfileType, User
29 29
from authentic2.journal import journal
30 30
from authentic2.models import Service
31
from authentic2_auth_saml.models import RenameAttributeAction, SAMLAuthenticator
31 32

  
32 33
from .utils import login, logout, text_content
33 34

  
......
58 59
    role_agent = Role.objects.create(name="role2", ou=ou)
59 60
    service = Service.objects.create(name="service")
60 61
    authenticator = LoginPasswordAuthenticator.objects.create(slug='test')
62
    saml_authenticator = SAMLAuthenticator.objects.create(slug='saml')
63
    rename_attribute_action = RenameAttributeAction.objects.create(authenticator=saml_authenticator)
61 64

  
62 65
    class EventFactory:
63 66
        date = make_aware(datetime.datetime(2020, 1, 1))
......
301 304
    make('authenticator.enable', user=agent, session=session2, authenticator=authenticator)
302 305
    make('authenticator.disable', user=agent, session=session2, authenticator=authenticator)
303 306
    make('authenticator.deletion', user=agent, session=session2, authenticator=authenticator)
307
    make(
308
        'authenticator.saml.related_object.creation',
309
        user=agent,
310
        session=session2,
311
        related_object=rename_attribute_action,
312
    )
313
    action_edit_form = mock.Mock(spec=['instance', 'initial', 'changed_data', 'cleaned_data'])
314
    action_edit_form.instance = rename_attribute_action
315
    action_edit_form.initial = {'from_name': 'old'}
316
    action_edit_form.changed_data = ['from_name']
317
    action_edit_form.cleaned_data = {'from_name': 'new'}
318
    make('authenticator.saml.related_object.edit', user=agent, session=session2, form=action_edit_form)
319
    make(
320
        'authenticator.saml.related_object.deletion',
321
        user=agent,
322
        session=session2,
323
        related_object=rename_attribute_action,
324
    )
304 325

  
305 326
    # verify we created at least one event for each type
306 327
    assert set(Event.objects.values_list("type__name", flat=True)) == set(_registry)
......
337 358

  
338 359
def test_global_journal(app, superuser, events):
339 360
    response = login(app, user=superuser, path="/manage/")
361
    rename_attribute_action = RenameAttributeAction.objects.get()
340 362

  
341 363
    # remove event about admin login
342 364
    Event.objects.filter(user=superuser).delete()
......
692 714
            'type': 'authenticator.deletion',
693 715
            'user': 'agent',
694 716
        },
717
        {
718
            'message': 'creation of RenameAttributeAction (%s) in authenticator "SAML"'
719
            % rename_attribute_action.pk,
720
            'timestamp': 'Jan. 3, 2020, 8 a.m.',
721
            'type': 'authenticator.saml.related_object.creation',
722
            'user': 'agent',
723
        },
724
        {
725
            'message': 'edit RenameAttributeAction (%s) in authenticator "SAML" (from_name)'
726
            % rename_attribute_action.pk,
727
            'timestamp': 'Jan. 3, 2020, 9 a.m.',
728
            'type': 'authenticator.saml.related_object.edit',
729
            'user': 'agent',
730
        },
731
        {
732
            'message': 'deletion of RenameAttributeAction (%s) in authenticator "SAML"'
733
            % rename_attribute_action.pk,
734
            'timestamp': 'Jan. 3, 2020, 10 a.m.',
735
            'type': 'authenticator.saml.related_object.deletion',
736
            'user': 'agent',
737
        },
695 738
    ]
696 739

  
697 740
    agent_page = response.click('agent', index=1)
698
-