Projet

Général

Profil

0001-authenticators-add-new-app-53902.patch

Valentin Deniaud, 17 mai 2022 17:21

Télécharger (33,5 ko)

Voir les différences:

Subject: [PATCH 1/3] authenticators: add new app (#53902)

 .../apps/authenticators/__init__.py           |   0
 src/authentic2/apps/authenticators/forms.py   |  37 ++++++
 .../apps/authenticators/manager_urls.py       |  75 ++++++++++++
 .../authenticators/migrations/0001_initial.py |  34 ++++++
 .../authenticators/migrations/__init__.py     |   0
 src/authentic2/apps/authenticators/models.py  | 108 ++++++++++++++++++
 src/authentic2/apps/authenticators/query.py   |  23 ++++
 .../authenticator_add_form.html               |  18 +++
 .../authenticators/authenticator_common.html  |  11 ++
 .../authenticator_delete_form.html            |  19 +++
 .../authenticators/authenticator_detail.html  |  30 +++++
 .../authenticator_edit_form.html              |  19 +++
 .../authenticators/authenticators.html        |  24 ++++
 src/authentic2/apps/authenticators/views.py   | 105 +++++++++++++++++
 src/authentic2/authenticators.py              |   3 +
 src/authentic2/forms/mixins.py                |  21 ++++
 src/authentic2/manager/forms.py               |  22 +---
 .../authentic2/manager/homepage.html          |   1 +
 src/authentic2/manager/urls.py                |   3 +
 src/authentic2/settings.py                    |   1 +
 src/authentic2/utils/misc.py                  |   7 +-
 src/authentic2/views.py                       |   4 +-
 tests/test_manager_authenticators.py          |  29 +++++
 23 files changed, 570 insertions(+), 24 deletions(-)
 create mode 100644 src/authentic2/apps/authenticators/__init__.py
 create mode 100644 src/authentic2/apps/authenticators/forms.py
 create mode 100644 src/authentic2/apps/authenticators/manager_urls.py
 create mode 100644 src/authentic2/apps/authenticators/migrations/0001_initial.py
 create mode 100644 src/authentic2/apps/authenticators/migrations/__init__.py
 create mode 100644 src/authentic2/apps/authenticators/models.py
 create mode 100644 src/authentic2/apps/authenticators/query.py
 create mode 100644 src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_add_form.html
 create mode 100644 src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_common.html
 create mode 100644 src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_delete_form.html
 create mode 100644 src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_detail.html
 create mode 100644 src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_edit_form.html
 create mode 100644 src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticators.html
 create mode 100644 src/authentic2/apps/authenticators/views.py
 create mode 100644 tests/test_manager_authenticators.py
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
from django.conf import settings
4
from django.db import migrations, models
5
import django.db.models.deletion
6
import uuid
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    initial = True
12

  
13
    dependencies = [
14
        migrations.swappable_dependency(settings.RBAC_OU_MODEL),
15
    ]
16

  
17
    operations = [
18
        migrations.CreateModel(
19
            name='BaseAuthenticator',
20
            fields=[
21
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22
                ('uuid', models.CharField(default=uuid.uuid4, editable=False, max_length=255, unique=True)),
23
                ('name', models.CharField(max_length=128, verbose_name='Name')),
24
                ('slug', models.SlugField(unique=True)),
25
                ('order', models.IntegerField(default=0, verbose_name='Order')),
26
                ('enabled', models.BooleanField(default=False, editable=False)),
27
                ('show_condition', models.CharField(blank=True, help_text='Django template controlling authenticator display. For example, "\'backoffice\' in login_hint or remotre_addr == \'1.2.3.4\'" would hide the authenticator from normal users except if they come from the specified IP address. Available variables include service_ou_slug, service_slug, remote_addr, login_hint and headers.', max_length=128, verbose_name='Show condition')),
28
                ('ou', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.RBAC_OU_MODEL, verbose_name='organizational unit')),
29
            ],
30
            options={
31
                'ordering': ('-enabled', 'name', 'slug', 'ou'),
32
            },
33
        ),
34
    ]
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
-