Projet

Général

Profil

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

Valentin Deniaud, 19 avril 2022 18:14

Télécharger (35,5 ko)

Voir les différences:

Subject: [PATCH 2/4] authenticators: add new app (#53902)

 .../apps/authenticators/__init__.py           |   0
 src/authentic2/apps/authenticators/forms.py   |  40 ++++++
 .../apps/authenticators/manager_urls.py       |  75 +++++++++++
 .../authenticators/migrations/0001_initial.py |  82 ++++++++++++
 .../authenticators/migrations/__init__.py     |   0
 src/authentic2/apps/authenticators/models.py  | 120 ++++++++++++++++++
 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                |  24 ++++
 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, 636 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()
38

  
39
    def get_existing_objects_queryset(self):
40
        return BaseAuthenticator.objects.filter(ou=self.cleaned_data['ou'])
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.28 on 2022-04-13 12:09
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
                (
27
                    'uuid',
28
                    models.CharField(
29
                        default=uuid.uuid4,
30
                        max_length=255,
31
                        unique=True,
32
                        editable=False,
33
                    ),
34
                ),
35
                ('name', models.CharField(max_length=128, verbose_name='Name')),
36
                ('slug', models.SlugField()),
37
                ('order', models.IntegerField(default=0, verbose_name='Order')),
38
                ('enabled', models.BooleanField(default=False, editable=False)),
39
                (
40
                    'show_condition',
41
                    models.CharField(
42
                        blank=True,
43
                        help_text=(
44
                            'Django template controlling authenticator display. '
45
                            'For example, "\'backoffice\' in login_hint or remotre_addr '
46
                            '== \'1.2.3.4\'" would hide the authenticator from normal users '
47
                            'except if they come from the specified IP address. Available '
48
                            'variables include service_ou_slug, service_slug, remote_addr, '
49
                            'login_hint and headers.'
50
                        ),
51
                        max_length=128,
52
                        verbose_name='Show condition',
53
                    ),
54
                ),
55
                (
56
                    'ou',
57
                    models.ForeignKey(
58
                        blank=False,
59
                        null=True,
60
                        on_delete=django.db.models.deletion.CASCADE,
61
                        to=settings.RBAC_OU_MODEL,
62
                        verbose_name='organizational unit',
63
                    ),
64
                ),
65
            ],
66
            options={
67
                'ordering': ('-enabled', 'name', 'slug', 'ou'),
68
            },
69
        ),
70
        migrations.AddConstraint(
71
            model_name='baseauthenticator',
72
            constraint=models.UniqueConstraint(
73
                condition=models.Q(ou__isnull=False), fields=('slug', 'ou'), name='unique_slug_with_ou'
74
            ),
75
        ),
76
        migrations.AddConstraint(
77
            model_name='baseauthenticator',
78
            constraint=models.UniqueConstraint(
79
                condition=models.Q(ou__isnull=True), fields=('slug',), name='unique_slug_without_ou'
80
            ),
81
        ),
82
    ]
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()
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
        constraints = [
68
            models.UniqueConstraint(
69
                fields=['slug', 'ou'],
70
                name='unique_slug_with_ou',
71
                condition=models.Q(ou__isnull=False),
72
            ),
73
            models.UniqueConstraint(
74
                fields=['slug'],
75
                name='unique_slug_without_ou',
76
                condition=models.Q(ou__isnull=True),
77
            ),
78
        ]
79

  
80
    def __str__(self):
81
        if self.name:
82
            return '%s - %s' % (self._meta.verbose_name, self.name)
83
        return str(self._meta.verbose_name)
84

  
85
    def get_identifier(self):
86
        return '%s_%s' % (self.type, self.pk)
87

  
88
    def get_absolute_url(self):
89
        return reverse('a2-manager-authenticator-detail', kwargs={'pk': self.pk})
90

  
91
    def get_short_description(self):
92
        return ''
93

  
94
    def get_full_description(self):
95
        for field in self.description_fields:
96
            value = getattr(self, field)
97
            if not value:
98
                continue
99

  
100
            if isinstance(value, datetime.datetime):
101
                value = date_format(value, 'DATETIME_FORMAT')
102

  
103
            yield _('%(field)s: %(value)s') % {
104
                'field': self._meta.get_field(field).verbose_name.capitalize(),
105
                'value': value,
106
            }
107

  
108
    @property
109
    def priority(self):
110
        return self.order
111

  
112
    def shown(self, ctx=()):
113
        if not self.show_condition:
114
            return True
115
        ctx = dict(ctx, id=self.slug)
116
        try:
117
            return evaluate_condition(self.show_condition, ctx, on_raise=True)
118
        except Exception as e:
119
            logger.error(e)
120
            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 = self.get_existing_objects_queryset()
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)
100

  
101
    def get_existing_objects_queryset(self):
102
        return self.instance.__class__.objects.all()
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
19 19
from django.utils.functional import lazy
20 20
from django.views.i18n import JavaScriptCatalog
21 21

  
22
from authentic2.apps.authenticators.manager_urls import urlpatterns as authenticator_urlpatterns
22 23
from authentic2.utils import misc as utils_misc
23 24

  
24 25
from ..decorators import required
......
191 192
    ],
192 193
)
193 194

  
195
urlpatterns += authenticator_urlpatterns
196

  
194 197
urlpatterns += [
195 198
    url(
196 199
        r'^jsi18n/$',
src/authentic2/settings.py
144 144
    'authentic2.attribute_aggregator',
145 145
    'authentic2.disco_service',
146 146
    'authentic2.manager',
147
    'authentic2.apps.authenticators',
147 148
    'authentic2.apps.journal',
148 149
    'authentic2.backends',
149 150
    '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
379 379
            continue
380 380
        # Legacy API
381 381
        if not hasattr(authenticator, 'login'):
382
            fid = authenticator.id
382
            fid = authenticator.get_identifier()
383 383
            name = authenticator.name
384 384
            form_class = authenticator.form()
385 385
            submit_name = 'submit-%s' % fid
......
513 513

  
514 514
        if request.method == "POST":
515 515
            for frontend in frontends:
516
                if 'submit-%s' % frontend.id in request.POST:
516
                if 'submit-%s' % frontend.get_identifier() in request.POST:
517 517
                    form = frontend.form()(data=request.POST)
518 518
                    if form.is_valid():
519 519
                        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
-