authenticators: add new app (#53902)
This commit is contained in:
parent
03082ffc11
commit
8532ac64af
|
@ -0,0 +1,37 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2022 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django import forms
|
||||
|
||||
from authentic2.forms.mixins import SlugMixin
|
||||
|
||||
from .models import BaseAuthenticator
|
||||
|
||||
|
||||
class AuthenticatorAddForm(SlugMixin, forms.ModelForm):
|
||||
field_order = ('authenticator', 'name', 'ou')
|
||||
authenticators = {x.type: x for x in BaseAuthenticator.__subclasses__()}
|
||||
|
||||
authenticator = forms.ChoiceField(choices=[(k, v._meta.verbose_name) for k, v in authenticators.items()])
|
||||
|
||||
class Meta:
|
||||
model = BaseAuthenticator
|
||||
fields = ('name', 'ou')
|
||||
|
||||
def save(self):
|
||||
Authenticator = self.authenticators[self.cleaned_data['authenticator']]
|
||||
self.instance = Authenticator(name=self.cleaned_data['name'], ou=self.cleaned_data['ou'])
|
||||
return super().save()
|
|
@ -0,0 +1,75 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2022 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.urls import path
|
||||
from django.utils.functional import lazy
|
||||
|
||||
from authentic2.decorators import required
|
||||
from authentic2.utils import misc as utils_misc
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
def superuser_required(function, login_url):
|
||||
def check_superuser(user):
|
||||
if user and user.is_superuser:
|
||||
return True
|
||||
if user and not user.is_anonymous:
|
||||
raise PermissionDenied()
|
||||
return False
|
||||
|
||||
actual_decorator = user_passes_test(check_superuser, login_url=login_url)
|
||||
return actual_decorator(function)
|
||||
|
||||
|
||||
def superuser_login_required(func):
|
||||
return superuser_required(func, login_url=lazy(utils_misc.get_manager_login_url, str)())
|
||||
|
||||
|
||||
urlpatterns = required(
|
||||
superuser_login_required,
|
||||
[
|
||||
# Authenticators
|
||||
path('authenticators/', views.authenticators, name='a2-manager-authenticators'),
|
||||
path(
|
||||
'authenticators/add/',
|
||||
views.add,
|
||||
name='a2-manager-authenticator-add',
|
||||
),
|
||||
path(
|
||||
'authenticators/<int:pk>/detail/',
|
||||
views.detail,
|
||||
name='a2-manager-authenticator-detail',
|
||||
),
|
||||
path(
|
||||
'authenticators/<int:pk>/edit/',
|
||||
views.edit,
|
||||
name='a2-manager-authenticator-edit',
|
||||
),
|
||||
path(
|
||||
'authenticators/<int:pk>/delete/',
|
||||
views.delete,
|
||||
name='a2-manager-authenticator-delete',
|
||||
),
|
||||
path(
|
||||
'authenticators/<int:pk>/toggle/',
|
||||
views.toggle,
|
||||
name='a2-manager-authenticator-toggle',
|
||||
),
|
||||
],
|
||||
)
|
|
@ -0,0 +1,60 @@
|
|||
# Generated by Django 2.2.26 on 2022-05-17 14:24
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.RBAC_OU_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BaseAuthenticator',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('uuid', models.CharField(default=uuid.uuid4, editable=False, max_length=255, unique=True)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('order', models.IntegerField(default=0, verbose_name='Order')),
|
||||
('enabled', models.BooleanField(default=False, editable=False)),
|
||||
(
|
||||
'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',
|
||||
),
|
||||
),
|
||||
(
|
||||
'ou',
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.RBAC_OU_MODEL,
|
||||
verbose_name='organizational unit',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-enabled', 'name', 'slug', 'ou'),
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,108 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2022 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.shortcuts import render, reverse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from authentic2.utils.evaluate import evaluate_condition
|
||||
|
||||
from .query import AuthenticatorManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseAuthenticator(models.Model):
|
||||
uuid = models.CharField(max_length=255, unique=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(_('Name'), max_length=128)
|
||||
slug = models.SlugField(unique=True)
|
||||
ou = models.ForeignKey(
|
||||
verbose_name=_('organizational unit'),
|
||||
to='a2_rbac.OrganizationalUnit',
|
||||
null=True,
|
||||
blank=False,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
order = models.IntegerField(_('Order'), default=0)
|
||||
enabled = models.BooleanField(default=False, editable=False)
|
||||
show_condition = models.CharField(
|
||||
_('Show condition'),
|
||||
max_length=128,
|
||||
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.'
|
||||
),
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
authenticators = AuthenticatorManager()
|
||||
|
||||
type = ''
|
||||
manager_form_class = None
|
||||
description_fields = ['show_condition']
|
||||
|
||||
class Meta:
|
||||
ordering = ('-enabled', 'name', 'slug', 'ou')
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return '%s - %s' % (self._meta.verbose_name, self.name)
|
||||
return str(self._meta.verbose_name)
|
||||
|
||||
def get_identifier(self):
|
||||
return '%s_%s' % (self.type, self.pk)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('a2-manager-authenticator-detail', kwargs={'pk': self.pk})
|
||||
|
||||
def get_short_description(self):
|
||||
return ''
|
||||
|
||||
def get_full_description(self):
|
||||
for field in self.description_fields:
|
||||
value = getattr(self, field)
|
||||
if not value:
|
||||
continue
|
||||
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = date_format(value, 'DATETIME_FORMAT')
|
||||
|
||||
yield _('%(field)s: %(value)s') % {
|
||||
'field': self._meta.get_field(field).verbose_name.capitalize(),
|
||||
'value': value,
|
||||
}
|
||||
|
||||
@property
|
||||
def priority(self):
|
||||
return self.order
|
||||
|
||||
def shown(self, ctx=()):
|
||||
if not self.show_condition:
|
||||
return True
|
||||
ctx = dict(ctx, id=self.slug)
|
||||
try:
|
||||
return evaluate_condition(self.show_condition, ctx, on_raise=True)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return False
|
|
@ -0,0 +1,23 @@
|
|||
from django.db import models
|
||||
from django.db.models.query import ModelIterable
|
||||
|
||||
|
||||
class AuthenticatorIterable(ModelIterable):
|
||||
def __iter__(self):
|
||||
for obj in ModelIterable(self.queryset):
|
||||
yield next(getattr(obj, field) for field in self.queryset.subclasses if hasattr(obj, field))
|
||||
|
||||
|
||||
class AuthenticatorQuerySet(models.QuerySet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.subclasses = [
|
||||
field.name for field in self.model._meta.get_fields() if isinstance(field, models.OneToOneRel)
|
||||
]
|
||||
self._iterable_class = AuthenticatorIterable
|
||||
|
||||
|
||||
class AuthenticatorManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
qs = AuthenticatorQuerySet(self.model, using=self._db)
|
||||
return qs.select_related(*qs.subclasses)
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "authentic2/authenticators/authenticator_common.html" %}
|
||||
{% load gadjo i18n %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="#"></a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form|with_template }}
|
||||
<div class="buttons">
|
||||
<button>{% trans "Add" %}</button>
|
||||
<a class="cancel" href="{% url 'a2-manager-authenticators' %}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,11 @@
|
|||
{% extends "authentic2/manager/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page-title %}{{ block.super }} - {% if object %}{{ object }}{% else %}{% trans "Authenticators" %}{% endif %}{% endblock %}
|
||||
|
||||
{% block title %}{{ block.super }} - {% trans "Authenticators" %}{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'a2-manager-authenticators' %}">{% trans "Authenticators" %}</a>
|
||||
{% endblock %}
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "authentic2/authenticators/authenticator_common.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'a2-manager-authenticators' %}">{% trans "Authenticators" %}</a>
|
||||
<a href="#"></a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans %}Do you want to delete "{{ object }}" ?{% endblocktrans %}</p>
|
||||
<div class="buttons">
|
||||
<button class="delete-button">{% trans "Delete" %}</button>
|
||||
<a class="cancel" href="{% url 'a2-manager-authenticator-detail' pk=object.pk %}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,30 @@
|
|||
{% extends "authentic2/authenticators/authenticator_common.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block appbar %}
|
||||
{{ block.super }}
|
||||
<span class="actions">
|
||||
<a class="extra-actions-menu-opener"></a>
|
||||
|
||||
<a href="{% url 'a2-manager-authenticator-toggle' pk=object.pk %}">{{ object.enabled|yesno:_("Disable,Enable") }}</a>
|
||||
<a href="{% url 'a2-manager-authenticator-edit' pk=object.pk %}">{% trans "Edit" %}</a>
|
||||
<ul class="extra-actions-menu">
|
||||
<li><a rel="popup" href="{% url 'a2-manager-authenticator-delete' pk=object.pk %}">{% trans "Delete" %}</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="#"></a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class='placeholder'>
|
||||
{% for line in object.get_full_description %}
|
||||
<p>{{ line }}</p>
|
||||
{% empty %}
|
||||
<p>{% trans 'Click "Edit" to change configuration.' %}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "authentic2/authenticators/authenticator_common.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'a2-manager-authenticator-detail' pk=object.pk %}">{{ object }}</a>
|
||||
<a href="#"></a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form|with_template }}
|
||||
<div class="buttons">
|
||||
<button>{% trans "Save" %}</button>
|
||||
<a class="cancel" href="{% url 'a2-manager-authenticator-detail' pk=object.pk %}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,24 @@
|
|||
{% extends "authentic2/authenticators/authenticator_common.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block appbar %}
|
||||
{{ block.super }}
|
||||
<span class="actions">
|
||||
<a href="{% url 'a2-manager-authenticator-add' %}" rel="popup">{% trans "Add new authenticator" %}</a>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% for authenticator in object_list %}
|
||||
<div class="section {% if not authenticator.enabled %}disabled{% endif %}">
|
||||
<h3>{{ authenticator }}
|
||||
<a class="button" href="{% url 'a2-manager-authenticator-detail' pk=authenticator.pk %}">{% trans "Configure" %}</a>
|
||||
</h3>
|
||||
{% if authenticator.enabled and authenticator.get_short_description %}
|
||||
<div>
|
||||
<p>{{ authenticator.get_short_description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,105 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2022 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView
|
||||
from django.views.generic.list import ListView
|
||||
|
||||
from authentic2.apps.authenticators import forms
|
||||
from authentic2.apps.authenticators.models import BaseAuthenticator
|
||||
from authentic2.manager.views import MediaMixin, TitleMixin
|
||||
|
||||
|
||||
class AuthenticatorsMixin(MediaMixin, TitleMixin):
|
||||
def get_queryset(self):
|
||||
return self.model.authenticators.all()
|
||||
|
||||
|
||||
class AuthenticatorsView(AuthenticatorsMixin, ListView):
|
||||
template_name = 'authentic2/authenticators/authenticators.html'
|
||||
model = BaseAuthenticator
|
||||
title = _('Authenticators')
|
||||
|
||||
|
||||
authenticators = AuthenticatorsView.as_view()
|
||||
|
||||
|
||||
class AuthenticatorAddView(AuthenticatorsMixin, CreateView):
|
||||
template_name = 'authentic2/authenticators/authenticator_add_form.html'
|
||||
title = _('New authenticator')
|
||||
form_class = forms.AuthenticatorAddForm
|
||||
|
||||
|
||||
add = AuthenticatorAddView.as_view()
|
||||
|
||||
|
||||
class AuthenticatorDetailView(AuthenticatorsMixin, DetailView):
|
||||
template_name = 'authentic2/authenticators/authenticator_detail.html'
|
||||
model = BaseAuthenticator
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return str(self.object)
|
||||
|
||||
|
||||
detail = AuthenticatorDetailView.as_view()
|
||||
|
||||
|
||||
class AuthenticatorEditView(AuthenticatorsMixin, UpdateView):
|
||||
template_name = 'authentic2/authenticators/authenticator_edit_form.html'
|
||||
title = _('Edit authenticator')
|
||||
model = BaseAuthenticator
|
||||
|
||||
def get_form_class(self):
|
||||
return self.object.manager_form_class
|
||||
|
||||
|
||||
edit = AuthenticatorEditView.as_view()
|
||||
|
||||
|
||||
class AuthenticatorDeleteView(AuthenticatorsMixin, DeleteView):
|
||||
template_name = 'authentic2/authenticators/authenticator_delete_form.html'
|
||||
title = _('Delete authenticator')
|
||||
model = BaseAuthenticator
|
||||
success_url = reverse_lazy('a2-manager-authenticators')
|
||||
|
||||
|
||||
delete = AuthenticatorDeleteView.as_view()
|
||||
|
||||
|
||||
class AuthenticatorToggleView(DetailView):
|
||||
model = BaseAuthenticator
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
authenticator = self.get_object()
|
||||
|
||||
if authenticator.enabled:
|
||||
authenticator.enabled = False
|
||||
authenticator.save()
|
||||
message = _('Authenticator has been disabled.')
|
||||
else:
|
||||
authenticator.enabled = True
|
||||
authenticator.save()
|
||||
message = _('Authenticator has been enabled.')
|
||||
|
||||
messages.info(self.request, message)
|
||||
return HttpResponseRedirect(authenticator.get_absolute_url())
|
||||
|
||||
|
||||
toggle = AuthenticatorToggleView.as_view()
|
|
@ -59,6 +59,9 @@ class BaseAuthenticator:
|
|||
logger.error(e)
|
||||
return False
|
||||
|
||||
def get_identifier(self):
|
||||
return self.id
|
||||
|
||||
|
||||
class LoginPasswordAuthenticator(BaseAuthenticator):
|
||||
id = 'password'
|
||||
|
|
|
@ -14,9 +14,11 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import hashlib
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
|
@ -76,3 +78,22 @@ class LockedFieldFormMixin:
|
|||
|
||||
def is_field_locked(self, name):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SlugMixin(forms.ModelForm):
|
||||
def save(self, commit=True):
|
||||
instance = self.instance
|
||||
if not instance.slug:
|
||||
instance.slug = slugify(str(instance.name)).lstrip('_')
|
||||
qs = instance.__class__.objects.all()
|
||||
if instance.pk:
|
||||
qs = qs.exclude(pk=instance.pk)
|
||||
new_slug = instance.slug
|
||||
i = 1
|
||||
while qs.filter(slug=new_slug).exists():
|
||||
new_slug = '%s-%d' % (instance.slug, i)
|
||||
i += 1
|
||||
instance.slug = new_slug
|
||||
if len(instance.slug) > 256:
|
||||
instance.slug = instance.slug[:252] + hashlib.md5(instance.slug).hexdigest()[:4]
|
||||
return super().save(commit=commit)
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import smtplib
|
||||
|
@ -27,7 +26,6 @@ from django.contrib.auth import get_user_model
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import pgettext, ugettext
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_select2.forms import HeavySelect2Widget
|
||||
|
@ -35,6 +33,7 @@ from django_select2.forms import HeavySelect2Widget
|
|||
from authentic2.a2_rbac.models import OrganizationalUnit, Permission, Role
|
||||
from authentic2.a2_rbac.utils import generate_slug, get_default_ou
|
||||
from authentic2.forms.fields import CheckPasswordField, NewPasswordField, ValidatedEmailField
|
||||
from authentic2.forms.mixins import SlugMixin
|
||||
from authentic2.forms.profile import BaseUserForm
|
||||
from authentic2.models import PasswordReset
|
||||
from authentic2.passwords import generate_password
|
||||
|
@ -65,25 +64,6 @@ class FormWithRequest(forms.Form):
|
|||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class SlugMixin(forms.ModelForm):
|
||||
def save(self, commit=True):
|
||||
instance = self.instance
|
||||
if not instance.slug:
|
||||
instance.slug = slugify(str(instance.name)).lstrip('_')
|
||||
qs = instance.__class__.objects.all()
|
||||
if instance.pk:
|
||||
qs = qs.exclude(pk=instance.pk)
|
||||
new_slug = instance.slug
|
||||
i = 1
|
||||
while qs.filter(slug=new_slug).exists():
|
||||
new_slug = '%s-%d' % (instance.slug, i)
|
||||
i += 1
|
||||
instance.slug = new_slug
|
||||
if len(instance.slug) > 256:
|
||||
instance.slug = instance.slug[:252] + hashlib.md5(instance.slug).hexdigest()[:4]
|
||||
return super().save(commit=commit)
|
||||
|
||||
|
||||
class PrefixFormMixin:
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['prefix'] = self.__class__.prefix
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
{% if user.is_superuser %}
|
||||
<li><a href="{% url 'a2-manager-tech-info' %}">{% trans 'Technical information' %}</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'a2-manager-authenticators' %}">{% trans 'Authenticators' %}</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
|
|
@ -20,6 +20,7 @@ from django.conf.urls import url
|
|||
from django.utils.functional import lazy
|
||||
from django.views.i18n import JavaScriptCatalog
|
||||
|
||||
from authentic2.apps.authenticators.manager_urls import urlpatterns as authenticator_urlpatterns
|
||||
from authentic2.utils import misc as utils_misc
|
||||
|
||||
from ..decorators import required
|
||||
|
@ -200,6 +201,8 @@ urlpatterns = required(
|
|||
],
|
||||
)
|
||||
|
||||
urlpatterns += authenticator_urlpatterns
|
||||
|
||||
urlpatterns += [
|
||||
url(
|
||||
r'^jsi18n/$',
|
||||
|
|
|
@ -143,6 +143,7 @@ INSTALLED_APPS = (
|
|||
'authentic2.attribute_aggregator',
|
||||
'authentic2.disco_service',
|
||||
'authentic2.manager',
|
||||
'authentic2.apps.authenticators',
|
||||
'authentic2.apps.journal',
|
||||
'authentic2.backends',
|
||||
'authentic2',
|
||||
|
|
|
@ -163,6 +163,11 @@ def load_backend(path, kwargs):
|
|||
def get_backends(setting_name='IDP_BACKENDS'):
|
||||
'''Return the list of enabled cleaned backends.'''
|
||||
backends = []
|
||||
if setting_name == 'AUTH_FRONTENDS':
|
||||
from authentic2.apps.authenticators.models import BaseAuthenticator
|
||||
|
||||
backends = list(BaseAuthenticator.authenticators.filter(enabled=True))
|
||||
|
||||
for backend_path in getattr(app_settings, setting_name):
|
||||
kwargs = {}
|
||||
if not isinstance(backend_path, str):
|
||||
|
@ -214,7 +219,7 @@ def get_authenticator_method(authenticator, method, parameters):
|
|||
if hasattr(response, 'context_data') and response.context_data:
|
||||
extra_css_class = response.context_data.get('block-extra-css-class', '')
|
||||
return {
|
||||
'id': authenticator.id,
|
||||
'id': authenticator.get_identifier(),
|
||||
'name': authenticator.name,
|
||||
'content': content,
|
||||
'response': response,
|
||||
|
|
|
@ -380,7 +380,7 @@ def login(request, template_name='authentic2/login.html', redirect_field_name=RE
|
|||
continue
|
||||
# Legacy API
|
||||
if not hasattr(authenticator, 'login'):
|
||||
fid = authenticator.id
|
||||
fid = authenticator.get_identifier()
|
||||
name = authenticator.name
|
||||
form_class = authenticator.form()
|
||||
submit_name = 'submit-%s' % fid
|
||||
|
@ -514,7 +514,7 @@ class ProfileView(HomeURLMixin, cbv.TemplateNamesMixin, TemplateView):
|
|||
|
||||
if request.method == "POST":
|
||||
for frontend in frontends:
|
||||
if 'submit-%s' % frontend.id in request.POST:
|
||||
if 'submit-%s' % frontend.get_identifier() in request.POST:
|
||||
form = frontend.form()(data=request.POST)
|
||||
if form.is_valid():
|
||||
return frontend.post(request, form, None, '/profile')
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# authentic2 - versatile identity manager
|
||||
# Copyright (C) 2010-2022 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .utils import login, logout
|
||||
|
||||
|
||||
def test_authenticators_authorization(app, simple_user, superuser):
|
||||
resp = login(app, simple_user)
|
||||
app.get('/manage/authenticators/', status=403)
|
||||
|
||||
logout(app)
|
||||
resp = login(app, superuser, path='/manage/')
|
||||
assert 'Authenticators' in resp.text
|
||||
|
||||
resp = resp.click('Authenticators')
|
||||
assert 'Authenticators' in resp.text
|
Loading…
Reference in New Issue