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