0001-authenticators-add-new-app-53902.patch
src/authentic2/apps/authenticators/forms.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 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 import forms |
|
18 | ||
19 |
from authentic2.forms.mixins import SlugMixin |
|
20 | ||
21 |
from .models import BaseAuthenticator |
|
22 | ||
23 | ||
24 |
class AuthenticatorAddForm(SlugMixin, forms.ModelForm): |
|
25 |
field_order = ('authenticator', 'name', 'ou') |
|
26 |
authenticators = {x.type: x for x in BaseAuthenticator.__subclasses__()} |
|
27 | ||
28 |
authenticator = forms.ChoiceField(choices=[(k, v._meta.verbose_name) for k, v in authenticators.items()]) |
|
29 | ||
30 |
class Meta: |
|
31 |
model = BaseAuthenticator |
|
32 |
fields = ('name', 'ou') |
|
33 | ||
34 |
def save(self): |
|
35 |
Authenticator = self.authenticators[self.cleaned_data['authenticator']] |
|
36 |
self.instance = Authenticator(name=self.cleaned_data['name'], ou=self.cleaned_data['ou']) |
|
37 |
return super().save() |
src/authentic2/apps/authenticators/manager_urls.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.contrib.auth.decorators import user_passes_test |
|
18 |
from django.core.exceptions import PermissionDenied |
|
19 |
from django.urls import path |
|
20 |
from django.utils.functional import lazy |
|
21 | ||
22 |
from authentic2.decorators import required |
|
23 |
from authentic2.utils import misc as utils_misc |
|
24 | ||
25 |
from . import views |
|
26 | ||
27 | ||
28 |
def superuser_required(function, login_url): |
|
29 |
def check_superuser(user): |
|
30 |
if user and user.is_superuser: |
|
31 |
return True |
|
32 |
if user and not user.is_anonymous: |
|
33 |
raise PermissionDenied() |
|
34 |
return False |
|
35 | ||
36 |
actual_decorator = user_passes_test(check_superuser, login_url=login_url) |
|
37 |
return actual_decorator(function) |
|
38 | ||
39 | ||
40 |
def superuser_login_required(func): |
|
41 |
return superuser_required(func, login_url=lazy(utils_misc.get_manager_login_url, str)()) |
|
42 | ||
43 | ||
44 |
urlpatterns = required( |
|
45 |
superuser_login_required, |
|
46 |
[ |
|
47 |
# Authenticators |
|
48 |
path('authenticators/', views.authenticators, name='a2-manager-authenticators'), |
|
49 |
path( |
|
50 |
'authenticators/add/', |
|
51 |
views.add, |
|
52 |
name='a2-manager-authenticator-add', |
|
53 |
), |
|
54 |
path( |
|
55 |
'authenticators/<int:pk>/detail/', |
|
56 |
views.detail, |
|
57 |
name='a2-manager-authenticator-detail', |
|
58 |
), |
|
59 |
path( |
|
60 |
'authenticators/<int:pk>/edit/', |
|
61 |
views.edit, |
|
62 |
name='a2-manager-authenticator-edit', |
|
63 |
), |
|
64 |
path( |
|
65 |
'authenticators/<int:pk>/delete/', |
|
66 |
views.delete, |
|
67 |
name='a2-manager-authenticator-delete', |
|
68 |
), |
|
69 |
path( |
|
70 |
'authenticators/<int:pk>/toggle/', |
|
71 |
views.toggle, |
|
72 |
name='a2-manager-authenticator-toggle', |
|
73 |
), |
|
74 |
], |
|
75 |
) |
src/authentic2/apps/authenticators/migrations/0001_initial.py | ||
---|---|---|
1 |
# Generated by Django 2.2.26 on 2022-05-17 14:24 |
|
2 | ||
3 |
import uuid |
|
4 | ||
5 |
import django.db.models.deletion |
|
6 |
from django.conf import settings |
|
7 |
from django.db import migrations, models |
|
8 | ||
9 | ||
10 |
class Migration(migrations.Migration): |
|
11 | ||
12 |
initial = True |
|
13 | ||
14 |
dependencies = [ |
|
15 |
migrations.swappable_dependency(settings.RBAC_OU_MODEL), |
|
16 |
] |
|
17 | ||
18 |
operations = [ |
|
19 |
migrations.CreateModel( |
|
20 |
name='BaseAuthenticator', |
|
21 |
fields=[ |
|
22 |
( |
|
23 |
'id', |
|
24 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
25 |
), |
|
26 |
('uuid', models.CharField(default=uuid.uuid4, editable=False, max_length=255, unique=True)), |
|
27 |
('name', models.CharField(max_length=128, verbose_name='Name')), |
|
28 |
('slug', models.SlugField(unique=True)), |
|
29 |
('order', models.IntegerField(default=0, verbose_name='Order')), |
|
30 |
('enabled', models.BooleanField(default=False, editable=False)), |
|
31 |
( |
|
32 |
'show_condition', |
|
33 |
models.CharField( |
|
34 |
blank=True, |
|
35 |
help_text=( |
|
36 |
'Django template controlling authenticator display. For example, "\'backoffice\' ' |
|
37 |
'in login_hint or remotre_addr == \'1.2.3.4\'" would hide the authenticator from ' |
|
38 |
'normal users except if they come from the specified IP address. Available ' |
|
39 |
'variables include service_ou_slug, service_slug, remote_addr, login_hint and ' |
|
40 |
'headers.' |
|
41 |
), |
|
42 |
max_length=128, |
|
43 |
verbose_name='Show condition', |
|
44 |
), |
|
45 |
), |
|
46 |
( |
|
47 |
'ou', |
|
48 |
models.ForeignKey( |
|
49 |
null=True, |
|
50 |
on_delete=django.db.models.deletion.CASCADE, |
|
51 |
to=settings.RBAC_OU_MODEL, |
|
52 |
verbose_name='organizational unit', |
|
53 |
), |
|
54 |
), |
|
55 |
], |
|
56 |
options={ |
|
57 |
'ordering': ('-enabled', 'name', 'slug', 'ou'), |
|
58 |
}, |
|
59 |
), |
|
60 |
] |
src/authentic2/apps/authenticators/models.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 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 |
import datetime |
|
18 |
import logging |
|
19 |
import uuid |
|
20 | ||
21 |
from django.db import models |
|
22 |
from django.shortcuts import render, reverse |
|
23 |
from django.utils.formats import date_format |
|
24 |
from django.utils.translation import ugettext_lazy as _ |
|
25 | ||
26 |
from authentic2.utils.evaluate import evaluate_condition |
|
27 | ||
28 |
from .query import AuthenticatorManager |
|
29 | ||
30 |
logger = logging.getLogger(__name__) |
|
31 | ||
32 | ||
33 |
class BaseAuthenticator(models.Model): |
|
34 |
uuid = models.CharField(max_length=255, unique=True, default=uuid.uuid4, editable=False) |
|
35 |
name = models.CharField(_('Name'), max_length=128) |
|
36 |
slug = models.SlugField(unique=True) |
|
37 |
ou = models.ForeignKey( |
|
38 |
verbose_name=_('organizational unit'), |
|
39 |
to='a2_rbac.OrganizationalUnit', |
|
40 |
null=True, |
|
41 |
blank=False, |
|
42 |
on_delete=models.CASCADE, |
|
43 |
) |
|
44 |
order = models.IntegerField(_('Order'), default=0) |
|
45 |
enabled = models.BooleanField(default=False, editable=False) |
|
46 |
show_condition = models.CharField( |
|
47 |
_('Show condition'), |
|
48 |
max_length=128, |
|
49 |
blank=True, |
|
50 |
help_text=_( |
|
51 |
'Django template controlling authenticator display. For example, "\'backoffice\' in ' |
|
52 |
'login_hint or remotre_addr == \'1.2.3.4\'" would hide the authenticator from normal users ' |
|
53 |
'except if they come from the specified IP address. Available variables include ' |
|
54 |
'service_ou_slug, service_slug, remote_addr, login_hint and headers.' |
|
55 |
), |
|
56 |
) |
|
57 | ||
58 |
objects = models.Manager() |
|
59 |
authenticators = AuthenticatorManager() |
|
60 | ||
61 |
type = '' |
|
62 |
manager_form_class = None |
|
63 |
description_fields = ['show_condition'] |
|
64 | ||
65 |
class Meta: |
|
66 |
ordering = ('-enabled', 'name', 'slug', 'ou') |
|
67 | ||
68 |
def __str__(self): |
|
69 |
if self.name: |
|
70 |
return '%s - %s' % (self._meta.verbose_name, self.name) |
|
71 |
return str(self._meta.verbose_name) |
|
72 | ||
73 |
def get_identifier(self): |
|
74 |
return '%s_%s' % (self.type, self.pk) |
|
75 | ||
76 |
def get_absolute_url(self): |
|
77 |
return reverse('a2-manager-authenticator-detail', kwargs={'pk': self.pk}) |
|
78 | ||
79 |
def get_short_description(self): |
|
80 |
return '' |
|
81 | ||
82 |
def get_full_description(self): |
|
83 |
for field in self.description_fields: |
|
84 |
value = getattr(self, field) |
|
85 |
if not value: |
|
86 |
continue |
|
87 | ||
88 |
if isinstance(value, datetime.datetime): |
|
89 |
value = date_format(value, 'DATETIME_FORMAT') |
|
90 | ||
91 |
yield _('%(field)s: %(value)s') % { |
|
92 |
'field': self._meta.get_field(field).verbose_name.capitalize(), |
|
93 |
'value': value, |
|
94 |
} |
|
95 | ||
96 |
@property |
|
97 |
def priority(self): |
|
98 |
return self.order |
|
99 | ||
100 |
def shown(self, ctx=()): |
|
101 |
if not self.show_condition: |
|
102 |
return True |
|
103 |
ctx = dict(ctx, id=self.slug) |
|
104 |
try: |
|
105 |
return evaluate_condition(self.show_condition, ctx, on_raise=True) |
|
106 |
except Exception as e: |
|
107 |
logger.error(e) |
|
108 |
return False |
src/authentic2/apps/authenticators/query.py | ||
---|---|---|
1 |
from django.db import models |
|
2 |
from django.db.models.query import ModelIterable |
|
3 | ||
4 | ||
5 |
class AuthenticatorIterable(ModelIterable): |
|
6 |
def __iter__(self): |
|
7 |
for obj in ModelIterable(self.queryset): |
|
8 |
yield next(getattr(obj, field) for field in self.queryset.subclasses if hasattr(obj, field)) |
|
9 | ||
10 | ||
11 |
class AuthenticatorQuerySet(models.QuerySet): |
|
12 |
def __init__(self, *args, **kwargs): |
|
13 |
super().__init__(*args, **kwargs) |
|
14 |
self.subclasses = [ |
|
15 |
field.name for field in self.model._meta.get_fields() if isinstance(field, models.OneToOneRel) |
|
16 |
] |
|
17 |
self._iterable_class = AuthenticatorIterable |
|
18 | ||
19 | ||
20 |
class AuthenticatorManager(models.Manager): |
|
21 |
def get_queryset(self): |
|
22 |
qs = AuthenticatorQuerySet(self.model, using=self._db) |
|
23 |
return qs.select_related(*qs.subclasses) |
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_add_form.html | ||
---|---|---|
1 |
{% extends "authentic2/authenticators/authenticator_common.html" %} |
|
2 |
{% load gadjo i18n %} |
|
3 | ||
4 |
{% block breadcrumb %} |
|
5 |
{{ block.super }} |
|
6 |
<a href="#"></a> |
|
7 |
{% endblock %} |
|
8 | ||
9 |
{% block content %} |
|
10 |
<form method="post" enctype="multipart/form-data"> |
|
11 |
{% csrf_token %} |
|
12 |
{{ form|with_template }} |
|
13 |
<div class="buttons"> |
|
14 |
<button>{% trans "Add" %}</button> |
|
15 |
<a class="cancel" href="{% url 'a2-manager-authenticators' %}">{% trans 'Cancel' %}</a> |
|
16 |
</div> |
|
17 |
</form> |
|
18 |
{% endblock %} |
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_common.html | ||
---|---|---|
1 |
{% extends "authentic2/manager/base.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block page-title %}{{ block.super }} - {% if object %}{{ object }}{% else %}{% trans "Authenticators" %}{% endif %}{% endblock %} |
|
5 | ||
6 |
{% block title %}{{ block.super }} - {% trans "Authenticators" %}{% endblock %} |
|
7 | ||
8 |
{% block breadcrumb %} |
|
9 |
{{ block.super }} |
|
10 |
<a href="{% url 'a2-manager-authenticators' %}">{% trans "Authenticators" %}</a> |
|
11 |
{% endblock %} |
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_delete_form.html | ||
---|---|---|
1 |
{% extends "authentic2/authenticators/authenticator_common.html" %} |
|
2 |
{% load i18n gadjo %} |
|
3 | ||
4 |
{% block breadcrumb %} |
|
5 |
{{ block.super }} |
|
6 |
<a href="{% url 'a2-manager-authenticators' %}">{% trans "Authenticators" %}</a> |
|
7 |
<a href="#"></a> |
|
8 |
{% endblock %} |
|
9 | ||
10 |
{% block content %} |
|
11 |
<form method="post" enctype="multipart/form-data"> |
|
12 |
{% csrf_token %} |
|
13 |
<p>{% blocktrans %}Do you want to delete "{{ object }}" ?{% endblocktrans %}</p> |
|
14 |
<div class="buttons"> |
|
15 |
<button class="delete-button">{% trans "Delete" %}</button> |
|
16 |
<a class="cancel" href="{% url 'a2-manager-authenticator-detail' pk=object.pk %}">{% trans 'Cancel' %}</a> |
|
17 |
</div> |
|
18 |
</form> |
|
19 |
{% endblock %} |
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_detail.html | ||
---|---|---|
1 |
{% extends "authentic2/authenticators/authenticator_common.html" %} |
|
2 |
{% load i18n gadjo %} |
|
3 | ||
4 |
{% block appbar %} |
|
5 |
{{ block.super }} |
|
6 |
<span class="actions"> |
|
7 |
<a class="extra-actions-menu-opener"></a> |
|
8 | ||
9 |
<a href="{% url 'a2-manager-authenticator-toggle' pk=object.pk %}">{{ object.enabled|yesno:_("Disable,Enable") }}</a> |
|
10 |
<a href="{% url 'a2-manager-authenticator-edit' pk=object.pk %}">{% trans "Edit" %}</a> |
|
11 |
<ul class="extra-actions-menu"> |
|
12 |
<li><a rel="popup" href="{% url 'a2-manager-authenticator-delete' pk=object.pk %}">{% trans "Delete" %}</a></li> |
|
13 |
</ul> |
|
14 |
</span> |
|
15 |
{% endblock %} |
|
16 | ||
17 |
{% block breadcrumb %} |
|
18 |
{{ block.super }} |
|
19 |
<a href="#"></a> |
|
20 |
{% endblock %} |
|
21 | ||
22 |
{% block content %} |
|
23 |
<div class='placeholder'> |
|
24 |
{% for line in object.get_full_description %} |
|
25 |
<p>{{ line }}</p> |
|
26 |
{% empty %} |
|
27 |
<p>{% trans 'Click "Edit" to change configuration.' %}</p> |
|
28 |
{% endfor %} |
|
29 |
</div> |
|
30 |
{% endblock %} |
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_edit_form.html | ||
---|---|---|
1 |
{% extends "authentic2/authenticators/authenticator_common.html" %} |
|
2 |
{% load i18n gadjo %} |
|
3 | ||
4 |
{% block breadcrumb %} |
|
5 |
{{ block.super }} |
|
6 |
<a href="{% url 'a2-manager-authenticator-detail' pk=object.pk %}">{{ object }}</a> |
|
7 |
<a href="#"></a> |
|
8 |
{% endblock %} |
|
9 | ||
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> |
|
19 |
{% endblock %} |
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticators.html | ||
---|---|---|
1 |
{% extends "authentic2/authenticators/authenticator_common.html" %} |
|
2 |
{% load i18n gadjo %} |
|
3 | ||
4 |
{% block appbar %} |
|
5 |
{{ block.super }} |
|
6 |
<span class="actions"> |
|
7 |
<a href="{% url 'a2-manager-authenticator-add' %}" rel="popup">{% trans "Add new authenticator" %}</a> |
|
8 |
</span> |
|
9 |
{% endblock %} |
|
10 | ||
11 |
{% block main %} |
|
12 |
{% for authenticator in object_list %} |
|
13 |
<div class="section {% if not authenticator.enabled %}disabled{% endif %}"> |
|
14 |
<h3>{{ authenticator }} |
|
15 |
<a class="button" href="{% url 'a2-manager-authenticator-detail' pk=authenticator.pk %}">{% trans "Configure" %}</a> |
|
16 |
</h3> |
|
17 |
{% if authenticator.enabled and authenticator.get_short_description %} |
|
18 |
<div> |
|
19 |
<p>{{ authenticator.get_short_description }}</p> |
|
20 |
</div> |
|
21 |
{% endif %} |
|
22 |
</div> |
|
23 |
{% endfor %} |
|
24 |
{% endblock %} |
src/authentic2/apps/authenticators/views.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.contrib import messages |
|
18 |
from django.http import HttpResponseRedirect |
|
19 |
from django.urls import reverse_lazy |
|
20 |
from django.utils.translation import ugettext as _ |
|
21 |
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView |
|
22 |
from django.views.generic.list import ListView |
|
23 | ||
24 |
from authentic2.apps.authenticators import forms |
|
25 |
from authentic2.apps.authenticators.models import BaseAuthenticator |
|
26 |
from authentic2.manager.views import MediaMixin, TitleMixin |
|
27 | ||
28 | ||
29 |
class AuthenticatorsMixin(MediaMixin, TitleMixin): |
|
30 |
def get_queryset(self): |
|
31 |
return self.model.authenticators.all() |
|
32 | ||
33 | ||
34 |
class AuthenticatorsView(AuthenticatorsMixin, ListView): |
|
35 |
template_name = 'authentic2/authenticators/authenticators.html' |
|
36 |
model = BaseAuthenticator |
|
37 |
title = _('Authenticators') |
|
38 | ||
39 | ||
40 |
authenticators = AuthenticatorsView.as_view() |
|
41 | ||
42 | ||
43 |
class AuthenticatorAddView(AuthenticatorsMixin, CreateView): |
|
44 |
template_name = 'authentic2/authenticators/authenticator_add_form.html' |
|
45 |
title = _('New authenticator') |
|
46 |
form_class = forms.AuthenticatorAddForm |
|
47 | ||
48 | ||
49 |
add = AuthenticatorAddView.as_view() |
|
50 | ||
51 | ||
52 |
class AuthenticatorDetailView(AuthenticatorsMixin, DetailView): |
|
53 |
template_name = 'authentic2/authenticators/authenticator_detail.html' |
|
54 |
model = BaseAuthenticator |
|
55 | ||
56 |
@property |
|
57 |
def title(self): |
|
58 |
return str(self.object) |
|
59 | ||
60 | ||
61 |
detail = AuthenticatorDetailView.as_view() |
|
62 | ||
63 | ||
64 |
class AuthenticatorEditView(AuthenticatorsMixin, UpdateView): |
|
65 |
template_name = 'authentic2/authenticators/authenticator_edit_form.html' |
|
66 |
title = _('Edit authenticator') |
|
67 |
model = BaseAuthenticator |
|
68 | ||
69 |
def get_form_class(self): |
|
70 |
return self.object.manager_form_class |
|
71 | ||
72 | ||
73 |
edit = AuthenticatorEditView.as_view() |
|
74 | ||
75 | ||
76 |
class AuthenticatorDeleteView(AuthenticatorsMixin, DeleteView): |
|
77 |
template_name = 'authentic2/authenticators/authenticator_delete_form.html' |
|
78 |
title = _('Delete authenticator') |
|
79 |
model = BaseAuthenticator |
|
80 |
success_url = reverse_lazy('a2-manager-authenticators') |
|
81 | ||
82 | ||
83 |
delete = AuthenticatorDeleteView.as_view() |
|
84 | ||
85 | ||
86 |
class AuthenticatorToggleView(DetailView): |
|
87 |
model = BaseAuthenticator |
|
88 | ||
89 |
def get(self, request, *args, **kwargs): |
|
90 |
authenticator = self.get_object() |
|
91 | ||
92 |
if authenticator.enabled: |
|
93 |
authenticator.enabled = False |
|
94 |
authenticator.save() |
|
95 |
message = _('Authenticator has been disabled.') |
|
96 |
else: |
|
97 |
authenticator.enabled = True |
|
98 |
authenticator.save() |
|
99 |
message = _('Authenticator has been enabled.') |
|
100 | ||
101 |
messages.info(self.request, message) |
|
102 |
return HttpResponseRedirect(authenticator.get_absolute_url()) |
|
103 | ||
104 | ||
105 |
toggle = AuthenticatorToggleView.as_view() |
src/authentic2/authenticators.py | ||
---|---|---|
59 | 59 |
logger.error(e) |
60 | 60 |
return False |
61 | 61 | |
62 |
def get_identifier(self): |
|
63 |
return self.id |
|
64 | ||
62 | 65 | |
63 | 66 |
class LoginPasswordAuthenticator(BaseAuthenticator): |
64 | 67 |
id = 'password' |
src/authentic2/forms/mixins.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import hashlib |
|
17 | 18 |
from collections import OrderedDict |
18 | 19 | |
19 | 20 |
from django import forms |
21 |
from django.utils.text import slugify |
|
20 | 22 |
from django.utils.translation import ugettext as _ |
21 | 23 | |
22 | 24 | |
... | ... | |
76 | 78 | |
77 | 79 |
def is_field_locked(self, name): |
78 | 80 |
raise NotImplementedError |
81 | ||
82 | ||
83 |
class SlugMixin(forms.ModelForm): |
|
84 |
def save(self, commit=True): |
|
85 |
instance = self.instance |
|
86 |
if not instance.slug: |
|
87 |
instance.slug = slugify(str(instance.name)).lstrip('_') |
|
88 |
qs = instance.__class__.objects.all() |
|
89 |
if instance.pk: |
|
90 |
qs = qs.exclude(pk=instance.pk) |
|
91 |
new_slug = instance.slug |
|
92 |
i = 1 |
|
93 |
while qs.filter(slug=new_slug).exists(): |
|
94 |
new_slug = '%s-%d' % (instance.slug, i) |
|
95 |
i += 1 |
|
96 |
instance.slug = new_slug |
|
97 |
if len(instance.slug) > 256: |
|
98 |
instance.slug = instance.slug[:252] + hashlib.md5(instance.slug).hexdigest()[:4] |
|
99 |
return super().save(commit=commit) |
src/authentic2/manager/forms.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 |
import csv |
18 |
import hashlib |
|
19 | 18 |
import json |
20 | 19 |
import logging |
21 | 20 |
import smtplib |
... | ... | |
27 | 26 |
from django.contrib.contenttypes.models import ContentType |
28 | 27 |
from django.core.exceptions import ValidationError |
29 | 28 |
from django.urls import reverse |
30 |
from django.utils.text import slugify |
|
31 | 29 |
from django.utils.translation import pgettext, ugettext |
32 | 30 |
from django.utils.translation import ugettext_lazy as _ |
33 | 31 |
from django_select2.forms import HeavySelect2Widget |
... | ... | |
35 | 33 |
from authentic2.a2_rbac.models import OrganizationalUnit, Permission, Role |
36 | 34 |
from authentic2.a2_rbac.utils import generate_slug, get_default_ou |
37 | 35 |
from authentic2.forms.fields import CheckPasswordField, NewPasswordField, ValidatedEmailField |
36 |
from authentic2.forms.mixins import SlugMixin |
|
38 | 37 |
from authentic2.forms.profile import BaseUserForm |
39 | 38 |
from authentic2.models import PasswordReset |
40 | 39 |
from authentic2.passwords import generate_password |
... | ... | |
65 | 64 |
super().__init__(*args, **kwargs) |
66 | 65 | |
67 | 66 | |
68 |
class SlugMixin(forms.ModelForm): |
|
69 |
def save(self, commit=True): |
|
70 |
instance = self.instance |
|
71 |
if not instance.slug: |
|
72 |
instance.slug = slugify(str(instance.name)).lstrip('_') |
|
73 |
qs = instance.__class__.objects.all() |
|
74 |
if instance.pk: |
|
75 |
qs = qs.exclude(pk=instance.pk) |
|
76 |
new_slug = instance.slug |
|
77 |
i = 1 |
|
78 |
while qs.filter(slug=new_slug).exists(): |
|
79 |
new_slug = '%s-%d' % (instance.slug, i) |
|
80 |
i += 1 |
|
81 |
instance.slug = new_slug |
|
82 |
if len(instance.slug) > 256: |
|
83 |
instance.slug = instance.slug[:252] + hashlib.md5(instance.slug).hexdigest()[:4] |
|
84 |
return super().save(commit=commit) |
|
85 | ||
86 | ||
87 | 67 |
class PrefixFormMixin: |
88 | 68 |
def __init__(self, *args, **kwargs): |
89 | 69 |
kwargs['prefix'] = self.__class__.prefix |
src/authentic2/manager/templates/authentic2/manager/homepage.html | ||
---|---|---|
20 | 20 |
{% if user.is_superuser %} |
21 | 21 |
<li><a href="{% url 'a2-manager-tech-info' %}">{% trans 'Technical information' %}</a></li> |
22 | 22 |
{% endif %} |
23 |
<li><a href="{% url 'a2-manager-authenticators' %}">{% trans 'Authenticators' %}</a></li> |
|
23 | 24 |
</ul> |
24 | 25 |
</span> |
25 | 26 |
{% endif %} |
src/authentic2/manager/urls.py | ||
---|---|---|
20 | 20 |
from django.utils.functional import lazy |
21 | 21 |
from django.views.i18n import JavaScriptCatalog |
22 | 22 | |
23 |
from authentic2.apps.authenticators.manager_urls import urlpatterns as authenticator_urlpatterns |
|
23 | 24 |
from authentic2.utils import misc as utils_misc |
24 | 25 | |
25 | 26 |
from ..decorators import required |
... | ... | |
200 | 201 |
], |
201 | 202 |
) |
202 | 203 | |
204 |
urlpatterns += authenticator_urlpatterns |
|
205 | ||
203 | 206 |
urlpatterns += [ |
204 | 207 |
url( |
205 | 208 |
r'^jsi18n/$', |
src/authentic2/settings.py | ||
---|---|---|
143 | 143 |
'authentic2.attribute_aggregator', |
144 | 144 |
'authentic2.disco_service', |
145 | 145 |
'authentic2.manager', |
146 |
'authentic2.apps.authenticators', |
|
146 | 147 |
'authentic2.apps.journal', |
147 | 148 |
'authentic2.backends', |
148 | 149 |
'authentic2', |
src/authentic2/utils/misc.py | ||
---|---|---|
163 | 163 |
def get_backends(setting_name='IDP_BACKENDS'): |
164 | 164 |
'''Return the list of enabled cleaned backends.''' |
165 | 165 |
backends = [] |
166 |
if setting_name == 'AUTH_FRONTENDS': |
|
167 |
from authentic2.apps.authenticators.models import BaseAuthenticator |
|
168 | ||
169 |
backends = list(BaseAuthenticator.authenticators.filter(enabled=True)) |
|
170 | ||
166 | 171 |
for backend_path in getattr(app_settings, setting_name): |
167 | 172 |
kwargs = {} |
168 | 173 |
if not isinstance(backend_path, str): |
... | ... | |
214 | 219 |
if hasattr(response, 'context_data') and response.context_data: |
215 | 220 |
extra_css_class = response.context_data.get('block-extra-css-class', '') |
216 | 221 |
return { |
217 |
'id': authenticator.id,
|
|
222 |
'id': authenticator.get_identifier(),
|
|
218 | 223 |
'name': authenticator.name, |
219 | 224 |
'content': content, |
220 | 225 |
'response': response, |
src/authentic2/views.py | ||
---|---|---|
380 | 380 |
continue |
381 | 381 |
# Legacy API |
382 | 382 |
if not hasattr(authenticator, 'login'): |
383 |
fid = authenticator.id
|
|
383 |
fid = authenticator.get_identifier()
|
|
384 | 384 |
name = authenticator.name |
385 | 385 |
form_class = authenticator.form() |
386 | 386 |
submit_name = 'submit-%s' % fid |
... | ... | |
514 | 514 | |
515 | 515 |
if request.method == "POST": |
516 | 516 |
for frontend in frontends: |
517 |
if 'submit-%s' % frontend.id in request.POST:
|
|
517 |
if 'submit-%s' % frontend.get_identifier() in request.POST:
|
|
518 | 518 |
form = frontend.form()(data=request.POST) |
519 | 519 |
if form.is_valid(): |
520 | 520 |
return frontend.post(request, form, None, '/profile') |
tests/test_manager_authenticators.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 .utils import login, logout |
|
18 | ||
19 | ||
20 |
def test_authenticators_authorization(app, simple_user, superuser): |
|
21 |
resp = login(app, simple_user) |
|
22 |
app.get('/manage/authenticators/', status=403) |
|
23 | ||
24 |
logout(app) |
|
25 |
resp = login(app, superuser, path='/manage/') |
|
26 |
assert 'Authenticators' in resp.text |
|
27 | ||
28 |
resp = resp.click('Authenticators') |
|
29 |
assert 'Authenticators' in resp.text |
|
0 |
- |