use Inheritance for Factors instead of JSONField
This commit is contained in:
parent
292fbecca0
commit
9c2cfd7db4
|
@ -10,41 +10,52 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>{% trans "Factors" %}</h1>
|
<h1>{% trans "Factors" %}</h1>
|
||||||
<span>{% trans "Factors required for a user to successfully authenticate." %}</span>
|
<span>{% trans "Factors required for a user to successfully authenticate." %}</span>
|
||||||
<hr>
|
<hr>
|
||||||
<a href="{% url 'passbook_admin:factor-create' %}" class="btn btn-primary">
|
<div class="dropdown">
|
||||||
{% trans 'Create...' %}
|
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
|
||||||
</a>
|
{% trans 'Create...' %}
|
||||||
<hr>
|
<span class="caret"></span>
|
||||||
<table class="table table-striped table-bordered">
|
</button>
|
||||||
<thead>
|
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
|
||||||
<tr>
|
{% for type, name in types.items %}
|
||||||
<th>{% trans 'Name' %}</th>
|
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'passbook_admin:factor-create' %}?type={{ type }}">{{ name }}</a></li>
|
||||||
<th>{% trans 'Type' %}</th>
|
|
||||||
<th>{% trans 'Order' %}</th>
|
|
||||||
<th>{% trans 'Enabled?' %}</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for factor in object_list %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ factor.name }} ({{ factor.slug }})</td>
|
|
||||||
<td>{{ factor.type }}</td>
|
|
||||||
<td>{{ factor.order }}</td>
|
|
||||||
<td>{{ factor.enabled }}</td>
|
|
||||||
<td>
|
|
||||||
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:factor-update' pk=factor.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
|
||||||
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:factor-delete' pk=factor.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
|
||||||
{% get_links factor as links %}
|
|
||||||
{% for name, href in links.items %}
|
|
||||||
<a class="btn btn-default btn-sm" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</ul>
|
||||||
</tr>
|
</div>
|
||||||
{% endfor %}
|
<hr>
|
||||||
</tbody>
|
<table class="table table-striped table-bordered">
|
||||||
</table>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans 'Name' %}</th>
|
||||||
|
<th>{% trans 'Type' %}</th>
|
||||||
|
<th>{% trans 'Order' %}</th>
|
||||||
|
<th>{% trans 'Enabled?' %}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for factor in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ factor.name }} ({{ factor.slug }})</td>
|
||||||
|
<td>{{ factor.type }}</td>
|
||||||
|
<td>{{ factor.order }}</td>
|
||||||
|
<td>{{ factor.enabled }}</td>
|
||||||
|
<td>
|
||||||
|
<a class="btn btn-default btn-sm"
|
||||||
|
href="{% url 'passbook_admin:factor-update' pk=factor.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||||
|
<a class="btn btn-default btn-sm"
|
||||||
|
href="{% url 'passbook_admin:factor-delete' pk=factor.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||||
|
{% get_links factor as links %}
|
||||||
|
{% for name, href in links.items %}
|
||||||
|
<a class="btn btn-default btn-sm"
|
||||||
|
href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -6,45 +6,49 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>{% trans "Sources" %}</h1>
|
<h1>{% trans "Sources" %}</h1>
|
||||||
<span>{% trans "External Sources which can be used to get Identities into passbook, for example Social Providers like Twiter and GitHub or Enterprise Providers like ADFS and LDAP." %}</span>
|
<span>{% trans "External Sources which can be used to get Identities into passbook, for example Social Providers like Twiter and GitHub or Enterprise Providers like ADFS and LDAP." %}</span>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
|
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
|
||||||
{% trans 'Create...' %}
|
{% trans 'Create...' %}
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
|
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
|
||||||
{% for type, name in types.items %}
|
{% for type, name in types.items %}
|
||||||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'passbook_admin:source-create' %}?type={{ type }}">{{ name }}</a></li>
|
<li role="presentation"><a role="menuitem" tabindex="-1"
|
||||||
{% endfor %}
|
href="{% url 'passbook_admin:source-create' %}?type={{ type }}">{{ name }}</a></li>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<table class="table table-striped table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans 'Name' %}</th>
|
|
||||||
<th>{% trans 'Class' %}</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for source in object_list %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ source.name }}</td>
|
|
||||||
<td>{{ source|fieldtype }}</td>
|
|
||||||
<td>
|
|
||||||
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
|
||||||
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:source-delete' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
|
||||||
{% get_links source as links %}
|
|
||||||
{% for name, href in links %}
|
|
||||||
<a class="btn btn-default btn-sm" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</ul>
|
||||||
</tr>
|
</div>
|
||||||
{% endfor %}
|
<hr>
|
||||||
</tbody>
|
<table class="table table-striped table-bordered">
|
||||||
</table>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans 'Name' %}</th>
|
||||||
|
<th>{% trans 'Class' %}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for source in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ source.name }}</td>
|
||||||
|
<td>{{ source|fieldtype }}</td>
|
||||||
|
<td>
|
||||||
|
<a class="btn btn-default btn-sm"
|
||||||
|
href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||||
|
<a class="btn btn-default btn-sm"
|
||||||
|
href="{% url 'passbook_admin:source-delete' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||||
|
{% get_links source as links %}
|
||||||
|
{% for name, href in links %}
|
||||||
|
<a class="btn btn-default btn-sm"
|
||||||
|
href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
"""passbook Factor administration"""
|
"""passbook Factor administration"""
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
from django.http import Http404
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||||
|
|
||||||
from passbook.admin.mixins import AdminRequiredMixin
|
from passbook.admin.mixins import AdminRequiredMixin
|
||||||
from passbook.core.forms.factors import FactorForm
|
|
||||||
from passbook.core.models import Factor
|
from passbook.core.models import Factor
|
||||||
|
from passbook.lib.utils.reflection import path_to_class
|
||||||
|
|
||||||
|
|
||||||
|
def all_subclasses(cls):
|
||||||
|
"""Recursively return all subclassess of cls"""
|
||||||
|
return set(cls.__subclasses__()).union(
|
||||||
|
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
|
||||||
|
|
||||||
class FactorListView(AdminRequiredMixin, ListView):
|
class FactorListView(AdminRequiredMixin, ListView):
|
||||||
"""Show list of all factors"""
|
"""Show list of all factors"""
|
||||||
|
|
||||||
|
@ -18,17 +24,32 @@ class FactorListView(AdminRequiredMixin, ListView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs['types'] = {
|
kwargs['types'] = {
|
||||||
x.__name__: x._meta.verbose_name for x in Factor.__subclasses__()}
|
x.__name__: x._meta.verbose_name for x in all_subclasses(Factor)}
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().select_subclasses()
|
||||||
|
|
||||||
class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
|
class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
|
||||||
"""Create new Factor"""
|
"""Create new Factor"""
|
||||||
|
|
||||||
template_name = 'generic/create.html'
|
template_name = 'generic/create_inheritance.html'
|
||||||
success_url = reverse_lazy('passbook_admin:factors')
|
success_url = reverse_lazy('passbook_admin:factors')
|
||||||
success_message = _('Successfully created Factor')
|
success_message = _('Successfully created Factor')
|
||||||
form_class = FactorForm
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
kwargs = super().get_context_data(**kwargs)
|
||||||
|
source_type = self.request.GET.get('type')
|
||||||
|
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type)
|
||||||
|
kwargs['type'] = model._meta.verbose_name
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_form_class(self):
|
||||||
|
source_type = self.request.GET.get('type')
|
||||||
|
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type)
|
||||||
|
if not model:
|
||||||
|
raise Http404
|
||||||
|
return path_to_class(model.form)
|
||||||
|
|
||||||
|
|
||||||
class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
|
class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
|
||||||
|
@ -38,8 +59,13 @@ class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
|
||||||
template_name = 'generic/update.html'
|
template_name = 'generic/update.html'
|
||||||
success_url = reverse_lazy('passbook_admin:factors')
|
success_url = reverse_lazy('passbook_admin:factors')
|
||||||
success_message = _('Successfully updated Factor')
|
success_message = _('Successfully updated Factor')
|
||||||
form_class = FactorForm
|
|
||||||
|
|
||||||
|
def get_form_class(self):
|
||||||
|
source_type = self.request.GET.get('type')
|
||||||
|
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type)
|
||||||
|
if not model:
|
||||||
|
raise Http404
|
||||||
|
return path_to_class(model.form)
|
||||||
|
|
||||||
class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||||
"""Delete factor"""
|
"""Delete factor"""
|
||||||
|
@ -48,3 +74,6 @@ class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||||
template_name = 'generic/delete.html'
|
template_name = 'generic/delete.html'
|
||||||
success_url = reverse_lazy('passbook_admin:factors')
|
success_url = reverse_lazy('passbook_admin:factors')
|
||||||
success_message = _('Successfully updated Factor')
|
success_message = _('Successfully updated Factor')
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
|
||||||
|
|
|
@ -4,10 +4,8 @@ from django.views.generic import FormView
|
||||||
|
|
||||||
from passbook.captcha_factor.forms import CaptchaForm
|
from passbook.captcha_factor.forms import CaptchaForm
|
||||||
from passbook.core.auth.factor import AuthenticationFactor
|
from passbook.core.auth.factor import AuthenticationFactor
|
||||||
from passbook.core.auth.factor_manager import MANAGER
|
|
||||||
|
|
||||||
|
|
||||||
@MANAGER.factor()
|
|
||||||
class CaptchaFactor(FormView, AuthenticationFactor):
|
class CaptchaFactor(FormView, AuthenticationFactor):
|
||||||
"""Simple captcha checker, logic is handeled in django-captcha module"""
|
"""Simple captcha checker, logic is handeled in django-captcha module"""
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,25 @@
|
||||||
from captcha.fields import ReCaptchaField
|
from captcha.fields import ReCaptchaField
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
|
from passbook.captcha_factor.models import CaptchaFactor
|
||||||
|
from passbook.core.forms.factors import GENERAL_FIELDS
|
||||||
|
|
||||||
|
|
||||||
class CaptchaForm(forms.Form):
|
class CaptchaForm(forms.Form):
|
||||||
"""passbook captcha factor form"""
|
"""passbook captcha factor form"""
|
||||||
|
|
||||||
captcha = ReCaptchaField()
|
captcha = ReCaptchaField()
|
||||||
|
|
||||||
|
class CaptchaFactorForm(forms.ModelForm):
|
||||||
|
"""Form to edit CaptchaFactor Instance"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = CaptchaFactor
|
||||||
|
fields = GENERAL_FIELDS + ['public_key', 'private_key']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(),
|
||||||
|
'order': forms.NumberInput(),
|
||||||
|
'public_key': forms.TextInput(),
|
||||||
|
'private_key': forms.TextInput(),
|
||||||
|
}
|
||||||
|
|
29
passbook/captcha_factor/migrations/0001_initial.py
Normal file
29
passbook/captcha_factor/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 2.1.7 on 2019-02-24 21:35
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0010_auto_20190224_1016'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CaptchaFactor',
|
||||||
|
fields=[
|
||||||
|
('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')),
|
||||||
|
('public_key', models.TextField()),
|
||||||
|
('private_key', models.TextField()),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Captcha Factor',
|
||||||
|
'verbose_name_plural': 'Captcha Factors',
|
||||||
|
},
|
||||||
|
bases=('passbook_core.factor',),
|
||||||
|
),
|
||||||
|
]
|
0
passbook/captcha_factor/migrations/__init__.py
Normal file
0
passbook/captcha_factor/migrations/__init__.py
Normal file
23
passbook/captcha_factor/models.py
Normal file
23
passbook/captcha_factor/models.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
"""passbook captcha factor"""
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from passbook.core.models import Factor
|
||||||
|
|
||||||
|
|
||||||
|
class CaptchaFactor(Factor):
|
||||||
|
"""Captcha Factor instance"""
|
||||||
|
|
||||||
|
public_key = models.TextField()
|
||||||
|
private_key = models.TextField()
|
||||||
|
|
||||||
|
type = 'passbook.captcha_factor.factor.CaptchaFactor'
|
||||||
|
form = 'passbook.captcha_factor.forms.CaptchaFactorForm'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Captcha Factor %s" % self.slug
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
verbose_name = _('Captcha Factor')
|
||||||
|
verbose_name_plural = _('Captcha Factors')
|
|
@ -1,25 +0,0 @@
|
||||||
"""Authentication Factor Manager"""
|
|
||||||
from logging import getLogger
|
|
||||||
|
|
||||||
LOGGER = getLogger(__name__)
|
|
||||||
|
|
||||||
class AuthenticationFactorManager:
|
|
||||||
"""Manager to hold all Factors."""
|
|
||||||
|
|
||||||
__factors = []
|
|
||||||
|
|
||||||
def factor(self):
|
|
||||||
"""Class decorator to register classes inline."""
|
|
||||||
def inner_wrapper(cls):
|
|
||||||
self.__factors.append(cls)
|
|
||||||
LOGGER.debug("Registered factor '%s'", cls.__name__)
|
|
||||||
return cls
|
|
||||||
return inner_wrapper
|
|
||||||
|
|
||||||
@property
|
|
||||||
def all(self):
|
|
||||||
"""Get list of all registered factors"""
|
|
||||||
return self.__factors
|
|
||||||
|
|
||||||
|
|
||||||
MANAGER = AuthenticationFactorManager()
|
|
|
@ -2,12 +2,10 @@
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from passbook.core.auth.factor import AuthenticationFactor
|
from passbook.core.auth.factor import AuthenticationFactor
|
||||||
from passbook.core.auth.factor_manager import MANAGER
|
|
||||||
|
|
||||||
LOGGER = getLogger(__name__)
|
LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@MANAGER.factor()
|
|
||||||
class DummyFactor(AuthenticationFactor):
|
class DummyFactor(AuthenticationFactor):
|
||||||
"""Dummy factor for testing with multiple factors"""
|
"""Dummy factor for testing with multiple factors"""
|
||||||
|
|
||||||
|
|
|
@ -8,19 +8,17 @@ from django.utils.translation import gettext as _
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
|
|
||||||
from passbook.core.auth.factor import AuthenticationFactor
|
from passbook.core.auth.factor import AuthenticationFactor
|
||||||
from passbook.core.auth.factor_manager import MANAGER
|
|
||||||
from passbook.core.auth.view import AuthenticationView
|
from passbook.core.auth.view import AuthenticationView
|
||||||
from passbook.core.forms.authentication import AuthenticationBackendFactorForm
|
from passbook.core.forms.authentication import PasswordFactorForm
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
LOGGER = getLogger(__name__)
|
LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@MANAGER.factor()
|
class PasswordFactor(FormView, AuthenticationFactor):
|
||||||
class AuthenticationBackendFactor(FormView, AuthenticationFactor):
|
|
||||||
"""Authentication factor which authenticates against django's AuthBackend"""
|
"""Authentication factor which authenticates against django's AuthBackend"""
|
||||||
|
|
||||||
form_class = AuthenticationBackendFactorForm
|
form_class = PasswordFactorForm
|
||||||
template_name = 'login/factors/backend.html'
|
template_name = 'login/factors/backend.html'
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
|
@ -29,10 +29,6 @@ class LoginForm(forms.Form):
|
||||||
validate_email(self.cleaned_data.get('uid_field'))
|
validate_email(self.cleaned_data.get('uid_field'))
|
||||||
return self.cleaned_data.get('uid_field')
|
return self.cleaned_data.get('uid_field')
|
||||||
|
|
||||||
class AuthenticationBackendFactorForm(forms.Form):
|
|
||||||
"""Password authentication form"""
|
|
||||||
|
|
||||||
password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': _('Password')}))
|
|
||||||
|
|
||||||
class SignUpForm(forms.Form):
|
class SignUpForm(forms.Form):
|
||||||
"""SignUp Form"""
|
"""SignUp Form"""
|
||||||
|
@ -86,3 +82,9 @@ class SignUpForm(forms.Form):
|
||||||
# TODO: Password policy? Via Plugin? via Policy?
|
# TODO: Password policy? Via Plugin? via Policy?
|
||||||
# return check_password(self)
|
# return check_password(self)
|
||||||
return self.cleaned_data.get('password_repeat')
|
return self.cleaned_data.get('password_repeat')
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordFactorForm(forms.Form):
|
||||||
|
"""Password authentication form"""
|
||||||
|
|
||||||
|
password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': _('Password')}))
|
||||||
|
|
|
@ -1,25 +1,30 @@
|
||||||
"""passbook administration forms"""
|
"""passbook administration forms"""
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from passbook.core.auth.factor_manager import MANAGER
|
from passbook.core.models import DummyFactor, PasswordFactor
|
||||||
from passbook.core.models import Factor
|
|
||||||
from passbook.lib.utils.reflection import class_to_path
|
|
||||||
|
|
||||||
|
GENERAL_FIELDS = ['name', 'slug', 'order', 'policies', 'enabled']
|
||||||
|
|
||||||
def get_factors():
|
class PasswordFactorForm(forms.ModelForm):
|
||||||
"""Return list of factors for Select Widget"""
|
"""Form to create/edit Password Factors"""
|
||||||
for factor in MANAGER.all:
|
|
||||||
yield (class_to_path(factor), factor.__name__)
|
|
||||||
|
|
||||||
class FactorForm(forms.ModelForm):
|
|
||||||
"""Form to create/edit Factors"""
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Factor
|
model = PasswordFactor
|
||||||
fields = ['name', 'slug', 'order', 'policies', 'type', 'enabled', 'arguments']
|
fields = GENERAL_FIELDS + ['backends']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(),
|
||||||
|
'order': forms.NumberInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
class DummyFactorForm(forms.ModelForm):
|
||||||
|
"""Form to create/edit Dummy Factor"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = DummyFactor
|
||||||
|
fields = GENERAL_FIELDS
|
||||||
widgets = {
|
widgets = {
|
||||||
'type': forms.Select(choices=get_factors()),
|
|
||||||
'name': forms.TextInput(),
|
'name': forms.TextInput(),
|
||||||
'order': forms.NumberInput(),
|
'order': forms.NumberInput(),
|
||||||
}
|
}
|
||||||
|
|
44
passbook/core/migrations/0009_auto_20190224_0950.py
Normal file
44
passbook/core/migrations/0009_auto_20190224_0950.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# Generated by Django 2.1.7 on 2019-02-24 09:50
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0008_auto_20190221_1516'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DummyFactor',
|
||||||
|
fields=[
|
||||||
|
('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('passbook_core.factor',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PasswordFactor',
|
||||||
|
fields=[
|
||||||
|
('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')),
|
||||||
|
('backends', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('passbook_core.factor',),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='factor',
|
||||||
|
name='arguments',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='factor',
|
||||||
|
name='type',
|
||||||
|
),
|
||||||
|
]
|
21
passbook/core/migrations/0010_auto_20190224_1016.py
Normal file
21
passbook/core/migrations/0010_auto_20190224_1016.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 2.1.7 on 2019-02-24 10:16
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0009_auto_20190224_0950'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='dummyfactor',
|
||||||
|
options={'verbose_name': 'Dummy Factor', 'verbose_name_plural': 'Dummy Factors'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='passwordfactor',
|
||||||
|
options={'verbose_name': 'Password Factor', 'verbose_name_plural': 'Password Factors'},
|
||||||
|
),
|
||||||
|
]
|
|
@ -6,7 +6,7 @@ from time import sleep
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
@ -68,13 +68,45 @@ class Factor(PolicyModel):
|
||||||
name = models.TextField()
|
name = models.TextField()
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
order = models.IntegerField()
|
order = models.IntegerField()
|
||||||
type = models.TextField(unique=True)
|
|
||||||
enabled = models.BooleanField(default=True)
|
enabled = models.BooleanField(default=True)
|
||||||
arguments = JSONField(default=dict, blank=True)
|
|
||||||
|
objects = InheritanceManager()
|
||||||
|
type = ''
|
||||||
|
form = ''
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Factor %s" % self.slug
|
return "Factor %s" % self.slug
|
||||||
|
|
||||||
|
class PasswordFactor(Factor):
|
||||||
|
"""Password-based Django-backend Authentication Factor"""
|
||||||
|
|
||||||
|
backends = ArrayField(models.TextField())
|
||||||
|
|
||||||
|
type = 'passbook.core.auth.factors.password.PasswordFactor'
|
||||||
|
form = 'passbook.core.forms.factors.PasswordFactorForm'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Password Factor %s" % self.slug
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
verbose_name = _('Password Factor')
|
||||||
|
verbose_name_plural = _('Password Factors')
|
||||||
|
|
||||||
|
class DummyFactor(Factor):
|
||||||
|
"""Dummy factor, mostly used to debug"""
|
||||||
|
|
||||||
|
type = 'passbook.core.auth.factors.dummy.DummyFactor'
|
||||||
|
form = 'passbook.core.forms.factors.DummyFactorForm'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Dummy Factor %s" % self.slug
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
verbose_name = _('Dummy Factor')
|
||||||
|
verbose_name_plural = _('Dummy Factors')
|
||||||
|
|
||||||
class Application(PolicyModel):
|
class Application(PolicyModel):
|
||||||
"""Every Application which uses passbook for authentication/identification/authorization
|
"""Every Application which uses passbook for authentication/identification/authorization
|
||||||
needs an Application record. Other authentication types can subclass this Model to
|
needs an Application record. Other authentication types can subclass this Model to
|
||||||
|
|
Reference in a new issue