0002-authenticators-allow-splitting-configuration-form-67.patch
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 |
- |