0007-auth_saml-add-views-to-configure-related-objects-670.patch
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 |
- |