Projet

Général

Profil

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

Valentin Deniaud, 22 août 2022 14:44

Télécharger (12,2 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             | 24 ++++++++-
 src/authentic2_auth_saml/models.py            |  9 ++--
 tests/test_manager_authenticators.py          | 16 ++++--
 tests/test_manager_journal.py                 |  2 +-
 8 files changed, 132 insertions(+), 30 deletions(-)
src/authentic2/apps/authenticators/journal_event_types.py
44 44
    label = _('authenticator edit')
45 45

  
46 46
    @classmethod
47
    def record(cls, *, user, session, form):
48
        super().record(user=user, session=session, references=[form.instance], data=form_to_old_new(form))
47
    def record(cls, *, user, session, forms):
48
        data = {k: v for form in forms for k, v in form_to_old_new(form).items()}
49
        super().record(user=user, session=session, references=[forms[0].instance], data=data)
49 50

  
50 51
    @classmethod
51 52
    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.as_p }}
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
            'verify_ssl_certificate',
31
            'transient_federation_attribute',
32
            'realm',
33
            'username_template',
34
            'name_id_policy_format',
35
            'name_id_policy_allow_create',
36
            'force_authn',
37
            'add_authnrequest_next_url_extension',
38
            'group_attribute',
39
            'create_group',
40
            'error_url',
41
            'error_redirect_after_timeout',
42
            'authn_classref',
43
            'attribute_mapping',
44
        )
45

  
46

  
47
class SAMLAuthenticatorAdvancedForm(forms.ModelForm):
48
    class Meta:
49
        model = SAMLAuthenticator
50
        fields = SAMLAuthenticatorForm.Meta.exclude
29 51

  
30 52

  
31 53
class RoleChoiceField(forms.ModelChoiceField):
src/authentic2_auth_saml/models.py
165 165
        return settings
166 166

  
167 167
    @property
168
    def manager_form_class(self):
169
        from .forms import SAMLAuthenticatorForm
168
    def manager_form_classes(self):
169
        from .forms import SAMLAuthenticatorAdvancedForm, SAMLAuthenticatorForm
170 170

  
171
        return SAMLAuthenticatorForm
171
        return [
172
            (_('General'), SAMLAuthenticatorForm),
173
            (_('Advanced'), SAMLAuthenticatorAdvancedForm),
174
        ]
172 175

  
173 176
    def clean(self):
174 177
        if not (self.metadata or self.metadata_path or self.metadata_url):
tests/test_manager_authenticators.py
261 261
    assert 'configuration is not complete' in resp.text
262 262

  
263 263
    resp = resp.click('Edit')
264
    assert resp.pyquery('button#tab-general').attr('class') == 'pk-tabs--button-marker'
265
    assert not resp.pyquery('button#tab-advanced').attr('class')
266

  
264 267
    resp = resp.form.submit()
265 268
    assert 'One of the metadata fields must be filled.' in resp.text
266 269

  
267 270
    resp.form['metadata_path'] = '/var/lib/authentic2/metadata.xml'
268
    resp.form['attribute_mapping'] = '[{"attribute": "email", "saml_attribute": "mail", "mandatory": false}]'
269 271
    resp = resp.form.submit().follow()
270

  
271 272
    assert 'Metadata file path: /var/lib/authentic2/metadata.xml' in resp.text
272 273

  
274
    resp = resp.click('Enable').follow()
275
    assert 'Authenticator has been enabled.' in resp.text
276

  
277
    resp = resp.click('Edit')
278
    resp.form['attribute_mapping'] = '[{"attribute": "email", "saml_attribute": "mail", "mandatory": false}]'
279
    resp = resp.form.submit().follow()
280

  
273 281
    authenticator.refresh_from_db()
274 282
    assert authenticator.attribute_mapping == [
275 283
        {"attribute": "email", "saml_attribute": "mail", "mandatory": False}
276 284
    ]
277 285

  
278
    resp = resp.click('Enable').follow()
279
    assert 'Authenticator has been enabled.' in resp.text
286
    resp = resp.click('Edit')
287
    assert resp.pyquery('button#tab-advanced').attr('class') == 'pk-tabs--button-marker'
280 288

  
281 289

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