Projet

Général

Profil

0002-authenticators-allow-splitting-configuration-form-67.patch

Valentin Deniaud, 13 septembre 2022 15:23

Télécharger (13,1 ko)

Voir les différences:

Subject: [PATCH 2/2] authenticators: allow splitting configuration form
 (#67875)

 .../authenticators/journal_event_types.py     |  5 +-
 src/authentic2/apps/authenticators/models.py  | 13 +++--
 .../authenticator_edit_form.html              | 43 +++++++++++++---
 src/authentic2/apps/authenticators/views.py   | 50 ++++++++++++++++---
 src/authentic2_auth_saml/forms.py             | 32 +++++++++++-
 src/authentic2_auth_saml/models.py            |  9 ++--
 tests/test_manager_authenticators.py          | 33 ++++++++++--
 tests/test_manager_journal.py                 |  2 +-
 8 files changed, 157 insertions(+), 30 deletions(-)
src/authentic2/apps/authenticators/journal_event_types.py
49 49
    label = _('authenticator edit')
50 50

  
51 51
    @classmethod
52
    def record(cls, *, user, session, form):
53
        super().record(user=user, session=session, authenticator=form.instance, data=form_to_old_new(form))
52
    def record(cls, *, user, session, forms):
53
        data = {k: v for form in forms for k, v in form_to_old_new(form).items()}
54
        super().record(user=user, session=session, authenticator=forms[0].instance, data=data)
54 55

  
55 56
    @classmethod
56 57
    def get_message(cls, event, context):
src/authentic2/apps/authenticators/models.py
89 89
            return '%s - %s' % (self._meta.verbose_name, self.name)
90 90
        return str(self._meta.verbose_name)
91 91

  
92
    @property
93
    def manager_form_classes(self):
94
        return [(_('General'), self.manager_form_class)]
95

  
92 96
    def get_identifier(self):
93 97
        return self.type if self.unique else '%s_%s' % (self.type, self.slug)
94 98

  
......
128 132
            return False
129 133

  
130 134
    def has_valid_configuration(self):
131
        try:
132
            self.full_clean(exclude=getattr(self.manager_form_class._meta, 'exclude', None))
133
        except ValidationError:
134
            return False
135
        for _, form_class in self.manager_form_classes:
136
            try:
137
                self.full_clean(exclude=getattr(form_class._meta, 'exclude', None))
138
            except ValidationError:
139
                return False
135 140
        return True
136 141

  
137 142

  
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_edit_form.html
8 8
{% endblock %}
9 9

  
10 10
{% block content %}
11
  <form method="post" enctype="multipart/form-data">
12
    {% csrf_token %}
13
    {{ form|with_template }}
14
    <div class="buttons">
15
      <button>{% trans "Save" %}</button>
16
      <a class="cancel" href="{% url 'a2-manager-authenticator-detail' pk=object.pk %}">{% trans 'Cancel' %}</a>
17
    </div>
18
  </form>
11
  <div class="section pk-tabs">
12
    {% if forms|length > 1 %}
13
      <div class="pk-tabs--tab-list" role="tablist" aria-label="{% trans "Cell Properties" %}">
14
        {% for form in forms %}
15
          <button role="tab"
16
            aria-selected="{{ forloop.first|yesno:"true,false" }}"
17
            aria-controls="panel-{{ form.tab_slug }}"
18
            id="tab-{{ form.tab_slug }}"
19
            tabindex="{{ forloop.first|yesno:"0,-1" }}"
20
            {% if form.is_not_default %}class="pk-tabs--button-marker"{% endif %}>{{ form.tab_name }}</button>
21
        {% endfor %}
22
      </div>
23
    {% endif %}
24

  
25

  
26
    <form method="post" {% if forms|length > 1 %}class="pk-tabs--container"{% endif %} enctype="multipart/form-data">
27
      {% csrf_token %}
28
      {% if forms|length > 1 %}
29
        {% for form in forms %}
30
          <div id="panel-{{ form.tab_slug }}"
31
            role="tabpanel" tabindex="0" {% if not forloop.first %}hidden{% endif %}
32
            data-tab-slug="{{ form.tab_slug }}"
33
            aria-labelledby="tab-{{ form.tab_slug }}">
34
            {{ form|with_template }}
35
          </div>
36
        {% endfor %}
37
      {% else %}
38
        {{ forms.0|with_template }}
39
      {% endif %}
40
      <div class="buttons">
41
        <button>{% trans "Save" %}</button>
42
        <a class="cancel" href="{% url 'a2-manager-authenticator-detail' pk=object.pk %}">{% trans 'Cancel' %}</a>
43
      </div>
44
    </form>
45
  </div>
19 46
{% endblock %}
src/authentic2/apps/authenticators/views.py
20 20
from django.shortcuts import get_object_or_404
21 21
from django.urls import reverse, reverse_lazy
22 22
from django.utils.functional import cached_property
23
from django.utils.text import slugify
23 24
from django.utils.translation import ugettext as _
24 25
from django.views.generic import CreateView, DeleteView, DetailView, FormView, UpdateView
25 26
from django.views.generic.list import ListView
......
75 76
detail = AuthenticatorDetailView.as_view()
76 77

  
77 78

  
78
class AuthenticatorEditView(AuthenticatorsMixin, UpdateView):
79
def build_tab_is_not_default(form):
80
    for field_name, field in form.fields.items():
81
        if field.initial is not None:
82
            initial_value = field.initial() if callable(field.initial) else field.initial
83
            if initial_value != form.initial.get(field_name):
84
                return True
85
        else:
86
            if bool(form.initial.get(field_name)):
87
                return True
88
    return False
89

  
90

  
91
class MultipleFormsUpdateView(UpdateView):
92
    def get_context_data(self, **kwargs):
93
        kwargs['object'] = self.object
94
        kwargs['forms'] = kwargs.get('forms') or self.get_forms()
95
        return kwargs
96

  
97

  
98
class AuthenticatorEditView(AuthenticatorsMixin, MultipleFormsUpdateView):
79 99
    template_name = 'authentic2/authenticators/authenticator_edit_form.html'
80 100
    title = _('Edit authenticator')
81 101

  
82
    def get_form_class(self):
83
        return self.object.manager_form_class
84

  
85
    def form_valid(self, form):
86
        self.request.journal.record('authenticator.edit', form=form)
87
        return super().form_valid(form)
102
    def get_forms(self):
103
        forms = []
104
        for label, form_class in self.object.manager_form_classes:
105
            form = form_class(**self.get_form_kwargs())
106
            form.tab_name = label
107
            form.tab_slug = slugify(label)
108
            form.is_not_default = build_tab_is_not_default(form)
109
            forms.append(form)
110
        return forms
111

  
112
    def post(self, request, *args, **kwargs):
113
        self.object = self.get_object()
114
        forms = self.get_forms()
115

  
116
        all_valid = all(form.is_valid() for form in forms)
117
        if all_valid:
118
            for form in forms:
119
                form.save()
120
            self.request.journal.record('authenticator.edit', forms=forms)
121
            return HttpResponseRedirect(self.get_success_url())
122

  
123
        return self.render_to_response(self.get_context_data(forms=forms))
88 124

  
89 125

  
90 126
edit = AuthenticatorEditView.as_view()
src/authentic2_auth_saml/forms.py
25 25
class SAMLAuthenticatorForm(forms.ModelForm):
26 26
    class Meta:
27 27
        model = SAMLAuthenticator
28
        exclude = ('ou',)
28
        exclude = (
29
            'ou',
30
            'metadata_cache_time',
31
            'metadata_http_timeout',
32
            'verify_ssl_certificate',
33
            'transient_federation_attribute',
34
            'realm',
35
            'username_template',
36
            'name_id_policy_format',
37
            'name_id_policy_allow_create',
38
            'force_authn',
39
            'add_authnrequest_next_url_extension',
40
            'group_attribute',
41
            'create_group',
42
            'error_url',
43
            'error_redirect_after_timeout',
44
            'authn_classref',
45
            'attribute_mapping',
46
        )
47

  
48

  
49
class SAMLAuthenticatorAdvancedForm(forms.ModelForm):
50
    class Meta:
51
        model = SAMLAuthenticator
52
        fields = SAMLAuthenticatorForm.Meta.exclude
53

  
54
    def __init__(self, *args, **kwargs):
55
        super().__init__(*args, **kwargs)
56
        if not self.instance.metadata_url:
57
            del self.fields['metadata_cache_time']
58
            del self.fields['metadata_http_timeout']
29 59

  
30 60

  
31 61
class RoleChoiceField(forms.ModelChoiceField):
src/authentic2_auth_saml/models.py
161 161
        return settings
162 162

  
163 163
    @property
164
    def manager_form_class(self):
165
        from .forms import SAMLAuthenticatorForm
164
    def manager_form_classes(self):
165
        from .forms import SAMLAuthenticatorAdvancedForm, SAMLAuthenticatorForm
166 166

  
167
        return SAMLAuthenticatorForm
167
        return [
168
            (_('General'), SAMLAuthenticatorForm),
169
            (_('Advanced'), SAMLAuthenticatorAdvancedForm),
170
        ]
168 171

  
169 172
    def clean(self):
170 173
        if not (self.metadata or self.metadata_path or self.metadata_url):
tests/test_manager_authenticators.py
291 291
    assert 'configuration is not complete' in resp.text
292 292

  
293 293
    resp = resp.click('Edit')
294
    assert resp.pyquery('button#tab-general').attr('class') == 'pk-tabs--button-marker'
295
    assert not resp.pyquery('button#tab-advanced').attr('class')
296

  
294 297
    resp = resp.form.submit()
295 298
    assert 'One of the metadata fields must be filled.' in resp.text
296 299

  
297 300
    resp.form['metadata_path'] = '/var/lib/authentic2/metadata.xml'
298
    resp.form['attribute_mapping'] = '[{"attribute": "email", "saml_attribute": "mail", "mandatory": false}]'
299 301
    resp = resp.form.submit().follow()
300

  
301 302
    assert 'Metadata file path: /var/lib/authentic2/metadata.xml' in resp.text
302 303

  
304
    resp = resp.click('Enable').follow()
305
    assert 'Authenticator has been enabled.' in resp.text
306

  
307
    resp = resp.click('Edit')
308
    resp.form['attribute_mapping'] = '[{"attribute": "email", "saml_attribute": "mail", "mandatory": false}]'
309
    resp = resp.form.submit().follow()
310

  
303 311
    authenticator.refresh_from_db()
304 312
    assert authenticator.attribute_mapping == [
305 313
        {"attribute": "email", "saml_attribute": "mail", "mandatory": False}
306 314
    ]
307 315

  
308
    resp = resp.click('Enable').follow()
309
    assert 'Authenticator has been enabled.' in resp.text
316
    resp = resp.click('Edit')
317
    assert resp.pyquery('button#tab-advanced').attr('class') == 'pk-tabs--button-marker'
318

  
319

  
320
def test_authenticators_saml_hide_metadata_url_advanced_fields(app, superuser, ou1, ou2):
321
    authenticator = SAMLAuthenticator.objects.create(slug='idp1')
322

  
323
    resp = login(app, superuser)
324
    resp = app.get('/manage/authenticators/%s/edit/' % authenticator.pk)
325
    assert 'Metadata cache time' not in resp.text
326
    assert 'Metadata HTTP timeout' not in resp.text
327

  
328
    resp.form['metadata_path'] = ''
329
    resp.form['metadata_url'] = 'https://example.com/metadata.xml'
330
    resp = resp.form.submit().follow()
331

  
332
    resp = resp.click('Edit')
333
    assert 'Metadata cache time' in resp.text
334
    assert 'Metadata HTTP timeout' in resp.text
310 335

  
311 336

  
312 337
def test_authenticators_saml_attribute_lookup(app, superuser):
tests/test_manager_journal.py
303 303
    authenticator_edit_form.initial = {'name': 'old'}
304 304
    authenticator_edit_form.changed_data = ['name']
305 305
    authenticator_edit_form.cleaned_data = {'name': 'new'}
306
    make('authenticator.edit', user=agent, session=session2, form=authenticator_edit_form)
306
    make('authenticator.edit', user=agent, session=session2, forms=[authenticator_edit_form])
307 307
    make('authenticator.enable', user=agent, session=session2, authenticator=authenticator)
308 308
    make('authenticator.disable', user=agent, session=session2, authenticator=authenticator)
309 309
    make('authenticator.deletion', user=agent, session=session2, authenticator=authenticator)
310
-