Move Factor instances to database
This commit is contained in:
parent
57e5996513
commit
59a15c988f
|
@ -17,6 +17,9 @@
|
||||||
<li class="{% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
|
<li class="{% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
|
||||||
<a href="{% url 'passbook_admin:providers' %}">{% trans 'Providers' %}</a>
|
<a href="{% url 'passbook_admin:providers' %}">{% trans 'Providers' %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="{% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
|
||||||
|
<a href="{% url 'passbook_admin:factors' %}">{% trans 'Factors' %}</a>
|
||||||
|
</li>
|
||||||
<li class="{% is_active 'passbook_admin:rules' 'passbook_admin:rule-create' 'passbook_admin:rule-update' 'passbook_admin:rule-delete' 'passbook_admin:rule-test' %}">
|
<li class="{% is_active 'passbook_admin:rules' 'passbook_admin:rule-create' 'passbook_admin:rule-update' 'passbook_admin:rule-delete' 'passbook_admin:rule-test' %}">
|
||||||
<a href="{% url 'passbook_admin:rules' %}">{% trans 'Rules' %}</a>
|
<a href="{% url 'passbook_admin:rules' %}">{% trans 'Rules' %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
{% extends "administration/base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utils %}
|
||||||
|
{% load admin_reflection %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% title %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h1>{% trans "Factors" %}</h1>
|
||||||
|
<a href="{% url 'passbook_admin:factor-create' %}" class="btn btn-primary">
|
||||||
|
{% trans 'Create...' %}
|
||||||
|
</a>
|
||||||
|
<hr>
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
<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>
|
||||||
|
{% endblock %}
|
|
@ -2,8 +2,9 @@
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework_swagger.views import get_swagger_view
|
from rest_framework_swagger.views import get_swagger_view
|
||||||
|
|
||||||
from passbook.admin.views import (applications, audit, groups, invitations,
|
from passbook.admin.views import (applications, audit, factors, groups,
|
||||||
overview, providers, rules, sources, users)
|
invitations, overview, providers, rules,
|
||||||
|
sources, users)
|
||||||
|
|
||||||
schema_view = get_swagger_view(title='passbook Admin Internal API')
|
schema_view = get_swagger_view(title='passbook Admin Internal API')
|
||||||
|
|
||||||
|
@ -38,6 +39,14 @@ urlpatterns = [
|
||||||
providers.ProviderUpdateView.as_view(), name='provider-update'),
|
providers.ProviderUpdateView.as_view(), name='provider-update'),
|
||||||
path('providers/<int:pk>/delete/',
|
path('providers/<int:pk>/delete/',
|
||||||
providers.ProviderDeleteView.as_view(), name='provider-delete'),
|
providers.ProviderDeleteView.as_view(), name='provider-delete'),
|
||||||
|
# Factors
|
||||||
|
path('factors/', factors.FactorListView.as_view(), name='factors'),
|
||||||
|
path('factors/create/',
|
||||||
|
factors.FactorCreateView.as_view(), name='factor-create'),
|
||||||
|
path('factors/<uuid:pk>/update/',
|
||||||
|
factors.FactorUpdateView.as_view(), name='factor-update'),
|
||||||
|
path('factors/<uuid:pk>/delete/',
|
||||||
|
factors.FactorDeleteView.as_view(), name='factor-delete'),
|
||||||
# Invitations
|
# Invitations
|
||||||
path('invitations/', invitations.InvitationListView.as_view(), name='invitations'),
|
path('invitations/', invitations.InvitationListView.as_view(), name='invitations'),
|
||||||
path('invitations/create/',
|
path('invitations/create/',
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
"""passbook Factor administration"""
|
||||||
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||||
|
|
||||||
|
from passbook.admin.mixins import AdminRequiredMixin
|
||||||
|
from passbook.core.forms.factor import FactorForm
|
||||||
|
from passbook.core.models import Factor
|
||||||
|
|
||||||
|
|
||||||
|
class FactorListView(AdminRequiredMixin, ListView):
|
||||||
|
"""Show list of all factors"""
|
||||||
|
|
||||||
|
model = Factor
|
||||||
|
template_name = 'administration/factor/list.html'
|
||||||
|
ordering = 'order'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
kwargs['types'] = {
|
||||||
|
x.__name__: x._meta.verbose_name for x in Factor.__subclasses__()}
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
|
||||||
|
"""Create new Factor"""
|
||||||
|
|
||||||
|
template_name = 'generic/create.html'
|
||||||
|
success_url = reverse_lazy('passbook_admin:factors')
|
||||||
|
success_message = _('Successfully created Factor')
|
||||||
|
form_class = FactorForm
|
||||||
|
|
||||||
|
|
||||||
|
class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
|
||||||
|
"""Update factor"""
|
||||||
|
|
||||||
|
model = Factor
|
||||||
|
template_name = 'generic/update.html'
|
||||||
|
success_url = reverse_lazy('passbook_admin:factors')
|
||||||
|
success_message = _('Successfully updated Factor')
|
||||||
|
form_class = FactorForm
|
||||||
|
|
||||||
|
|
||||||
|
class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||||
|
"""Delete factor"""
|
||||||
|
|
||||||
|
model = Factor
|
||||||
|
template_name = 'generic/delete.html'
|
||||||
|
success_url = reverse_lazy('passbook_admin:factors')
|
||||||
|
success_message = _('Successfully updated Factor')
|
|
@ -4,8 +4,10 @@ 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"""
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
"""passbook core app config"""
|
"""passbook core app config"""
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
|
LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
class PassbookCoreConfig(AppConfig):
|
class PassbookCoreConfig(AppConfig):
|
||||||
"""passbook core app config"""
|
"""passbook core app config"""
|
||||||
|
@ -13,3 +17,10 @@ class PassbookCoreConfig(AppConfig):
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import_module('passbook.core.rules')
|
import_module('passbook.core.rules')
|
||||||
|
factors_to_load = CONFIG.y('passbook.factors', [])
|
||||||
|
for factors_to_load in factors_to_load:
|
||||||
|
try:
|
||||||
|
import_module(factors_to_load)
|
||||||
|
LOGGER.info("Loaded %s", factors_to_load)
|
||||||
|
except ImportError as exc:
|
||||||
|
LOGGER.debug(exc)
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""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()
|
|
@ -8,13 +8,15 @@ 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.mfa import MultiFactorAuthenticator
|
from passbook.core.auth.factor_manager import MANAGER
|
||||||
|
from passbook.core.auth.view import AuthenticationView
|
||||||
from passbook.core.forms.authentication import AuthenticationBackendFactorForm
|
from passbook.core.forms.authentication import AuthenticationBackendFactorForm
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
LOGGER = getLogger(__name__)
|
LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@MANAGER.factor()
|
||||||
class AuthenticationBackendFactor(FormView, AuthenticationFactor):
|
class AuthenticationBackendFactor(FormView, AuthenticationFactor):
|
||||||
"""Authentication factor which authenticates against django's AuthBackend"""
|
"""Authentication factor which authenticates against django's AuthBackend"""
|
||||||
|
|
||||||
|
@ -34,7 +36,7 @@ class AuthenticationBackendFactor(FormView, AuthenticationFactor):
|
||||||
if user:
|
if user:
|
||||||
# User instance returned from authenticate() has .backend property set
|
# User instance returned from authenticate() has .backend property set
|
||||||
self.authenticator.pending_user = user
|
self.authenticator.pending_user = user
|
||||||
self.request.session[MultiFactorAuthenticator.SESSION_USER_BACKEND] = user.backend
|
self.request.session[AuthenticationView.SESSION_USER_BACKEND] = user.backend
|
||||||
return self.authenticator.user_ok()
|
return self.authenticator.user_ok()
|
||||||
# No user was found -> invalid credentials
|
# No user was found -> invalid credentials
|
||||||
LOGGER.debug("Invalid credentials")
|
LOGGER.debug("Invalid credentials")
|
|
@ -2,10 +2,12 @@
|
||||||
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"""
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
"""passbook multi-factor authentication engine"""
|
"""passbook multi-factor authentication engine"""
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.shortcuts import get_object_or_404, redirect, reverse
|
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from passbook.core.models import User
|
from passbook.core.models import Factor, User
|
||||||
from passbook.core.views.utils import PermissionDeniedView
|
from passbook.core.views.utils import PermissionDeniedView
|
||||||
from passbook.lib.utils.reflection import class_to_path, path_to_class
|
from passbook.lib.utils.reflection import class_to_path, path_to_class
|
||||||
|
|
||||||
LOGGER = getLogger(__name__)
|
LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MultiFactorAuthenticator(UserPassesTestMixin, View):
|
class AuthenticationView(UserPassesTestMixin, View):
|
||||||
"""Wizard-like Multi-factor authenticator"""
|
"""Wizard-like Multi-factor authenticator"""
|
||||||
|
|
||||||
SESSION_FACTOR = 'passbook_factor'
|
SESSION_FACTOR = 'passbook_factor'
|
||||||
|
@ -25,8 +24,6 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
|
||||||
pending_user = None
|
pending_user = None
|
||||||
pending_factors = []
|
pending_factors = []
|
||||||
|
|
||||||
factors = settings.AUTHENTICATION_FACTORS.copy()
|
|
||||||
|
|
||||||
_current_factor = None
|
_current_factor = None
|
||||||
|
|
||||||
# Allow only not authenticated users to login
|
# Allow only not authenticated users to login
|
||||||
|
@ -34,29 +31,38 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
|
||||||
return self.request.user.is_authenticated is False
|
return self.request.user.is_authenticated is False
|
||||||
|
|
||||||
def handle_no_permission(self):
|
def handle_no_permission(self):
|
||||||
|
# Function from UserPassesTestMixin
|
||||||
if 'next' in self.request.GET:
|
if 'next' in self.request.GET:
|
||||||
return redirect(self.request.GET.get('next'))
|
return redirect(self.request.GET.get('next'))
|
||||||
return redirect(reverse('passbook_core:overview'))
|
return redirect(reverse('passbook_core:overview'))
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
# Extract pending user from session (only remember uid)
|
# Extract pending user from session (only remember uid)
|
||||||
if MultiFactorAuthenticator.SESSION_PENDING_USER in request.session:
|
if AuthenticationView.SESSION_PENDING_USER in request.session:
|
||||||
self.pending_user = get_object_or_404(
|
self.pending_user = get_object_or_404(
|
||||||
User, id=self.request.session[MultiFactorAuthenticator.SESSION_PENDING_USER])
|
User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER])
|
||||||
else:
|
else:
|
||||||
# No Pending user, redirect to login screen
|
# No Pending user, redirect to login screen
|
||||||
return redirect(reverse('passbook_core:auth-login'))
|
return redirect(reverse('passbook_core:auth-login'))
|
||||||
# Write pending factors to session
|
# Write pending factors to session
|
||||||
if MultiFactorAuthenticator.SESSION_PENDING_FACTORS in request.session:
|
if AuthenticationView.SESSION_PENDING_FACTORS in request.session:
|
||||||
self.pending_factors = request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS]
|
self.pending_factors = request.session[AuthenticationView.SESSION_PENDING_FACTORS]
|
||||||
else:
|
else:
|
||||||
self.pending_factors = self.factors.copy()
|
# Get an initial list of factors which are currently enabled
|
||||||
|
# and apply to the current user. We check rules here and block the request
|
||||||
|
_all_factors = Factor.objects.filter(enabled=True)
|
||||||
|
self.pending_factors = []
|
||||||
|
for factor in _all_factors:
|
||||||
|
if factor.passes(self.pending_user):
|
||||||
|
self.pending_factors.append(_all_factors)
|
||||||
|
# self.pending_factors = Factor
|
||||||
# Read and instantiate factor from session
|
# Read and instantiate factor from session
|
||||||
factor_class = None
|
factor_class = None
|
||||||
if MultiFactorAuthenticator.SESSION_FACTOR not in request.session:
|
if AuthenticationView.SESSION_FACTOR not in request.session:
|
||||||
factor_class = self.pending_factors[0]
|
factor_class = self.pending_factors[0]
|
||||||
else:
|
else:
|
||||||
factor_class = request.session[MultiFactorAuthenticator.SESSION_FACTOR]
|
factor_class = request.session[AuthenticationView.SESSION_FACTOR]
|
||||||
|
# Instantiate Next Factor and pass request
|
||||||
factor = path_to_class(factor_class)
|
factor = path_to_class(factor_class)
|
||||||
self._current_factor = factor(self)
|
self._current_factor = factor(self)
|
||||||
self._current_factor.request = request
|
self._current_factor.request = request
|
||||||
|
@ -81,11 +87,11 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
|
||||||
next_factor = None
|
next_factor = None
|
||||||
if self.pending_factors:
|
if self.pending_factors:
|
||||||
next_factor = self.pending_factors.pop()
|
next_factor = self.pending_factors.pop()
|
||||||
self.request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS] = \
|
self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] = \
|
||||||
self.pending_factors
|
self.pending_factors
|
||||||
self.request.session[MultiFactorAuthenticator.SESSION_FACTOR] = next_factor
|
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
|
||||||
LOGGER.debug("Rendering Factor is %s", next_factor)
|
LOGGER.debug("Rendering Factor is %s", next_factor)
|
||||||
return redirect(reverse('passbook_core:mfa'))
|
return redirect(reverse('passbook_core:auth-process', kwargs={'factor': next_factor}))
|
||||||
# User passed all factors
|
# User passed all factors
|
||||||
LOGGER.debug("User passed all factors, logging in")
|
LOGGER.debug("User passed all factors, logging in")
|
||||||
return self._user_passed()
|
return self._user_passed()
|
||||||
|
@ -94,12 +100,12 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
|
||||||
"""Show error message, user cannot login.
|
"""Show error message, user cannot login.
|
||||||
This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
|
This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
|
||||||
LOGGER.debug("User invalid")
|
LOGGER.debug("User invalid")
|
||||||
return redirect(reverse('passbook_core:mfa-denied'))
|
return redirect(reverse('passbook_core:auth-denied'))
|
||||||
|
|
||||||
def _user_passed(self):
|
def _user_passed(self):
|
||||||
"""User Successfully passed all factors"""
|
"""User Successfully passed all factors"""
|
||||||
# user = authenticate(request=self.request, )
|
# user = authenticate(request=self.request, )
|
||||||
backend = self.request.session[MultiFactorAuthenticator.SESSION_USER_BACKEND]
|
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
|
||||||
login(self.request, self.pending_user, backend=backend)
|
login(self.request, self.pending_user, backend=backend)
|
||||||
LOGGER.debug("Logged in user %s", self.pending_user)
|
LOGGER.debug("Logged in user %s", self.pending_user)
|
||||||
# Cleanup
|
# Cleanup
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""passbook administration forms"""
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from passbook.core.auth.factor_manager import MANAGER
|
||||||
|
from passbook.core.models import Factor
|
||||||
|
from passbook.lib.utils.reflection import class_to_path
|
||||||
|
|
||||||
|
|
||||||
|
def get_factors():
|
||||||
|
"""Return list of factors for Select Widget"""
|
||||||
|
for factor in MANAGER.all:
|
||||||
|
yield (class_to_path(factor), factor.__name__)
|
||||||
|
|
||||||
|
class FactorForm(forms.ModelForm):
|
||||||
|
"""Form to create/edit Factors"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = Factor
|
||||||
|
fields = ['name', 'slug', 'order', 'rules', 'type', 'enabled']
|
||||||
|
widgets = {
|
||||||
|
'type': forms.Select(choices=get_factors()),
|
||||||
|
'name': forms.TextInput(),
|
||||||
|
'order': forms.NumberInput(),
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 2.1.7 on 2019-02-14 15:41
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0002_auto_20190208_1514'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Factor',
|
||||||
|
fields=[
|
||||||
|
('rulemodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.RuleModel')),
|
||||||
|
('name', models.TextField()),
|
||||||
|
('slug', models.SlugField(unique=True)),
|
||||||
|
('order', models.IntegerField()),
|
||||||
|
('type', models.TextField()),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('passbook_core.rulemodel',),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 2.1.7 on 2019-02-15 15:34
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0003_factor'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='factor',
|
||||||
|
name='enabled',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='application',
|
||||||
|
name='provider',
|
||||||
|
field=models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='passbook_core.Provider'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -64,6 +64,18 @@ class RuleModel(UUIDModel, CreatedUpdatedModel):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@reversion.register()
|
@reversion.register()
|
||||||
|
class Factor(RuleModel):
|
||||||
|
"""Authentication factor, multiple instances of the same Factor can be used"""
|
||||||
|
|
||||||
|
name = models.TextField()
|
||||||
|
slug = models.SlugField(unique=True)
|
||||||
|
order = models.IntegerField()
|
||||||
|
type = models.TextField(unique=True)
|
||||||
|
enabled = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Factor %s" % self.slug
|
||||||
|
|
||||||
class Application(RuleModel):
|
class Application(RuleModel):
|
||||||
"""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
|
||||||
|
@ -73,7 +85,7 @@ class Application(RuleModel):
|
||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
launch_url = models.URLField(null=True, blank=True)
|
launch_url = models.URLField(null=True, blank=True)
|
||||||
icon_url = models.TextField(null=True, blank=True)
|
icon_url = models.TextField(null=True, blank=True)
|
||||||
provider = models.OneToOneField('Provider', null=True,
|
provider = models.OneToOneField('Provider', null=True, blank=True,
|
||||||
default=None, on_delete=models.SET_DEFAULT)
|
default=None, on_delete=models.SET_DEFAULT)
|
||||||
skip_authorization = models.BooleanField(default=False)
|
skip_authorization = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
|
@ -50,11 +50,6 @@ AUTHENTICATION_BACKENDS = [
|
||||||
'django.contrib.auth.backends.ModelBackend',
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
'passbook.oauth_client.backends.AuthorizedServiceBackend'
|
'passbook.oauth_client.backends.AuthorizedServiceBackend'
|
||||||
]
|
]
|
||||||
AUTHENTICATION_FACTORS = [
|
|
||||||
'passbook.core.auth.backend_factor.AuthenticationBackendFactor',
|
|
||||||
'passbook.core.auth.dummy.DummyFactor',
|
|
||||||
'passbook.captcha_factor.factor.CaptchaFactor',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
from passbook.core.auth import mfa
|
from passbook.core.auth import view
|
||||||
from passbook.core.views import authentication, overview, user
|
from passbook.core.views import authentication, overview, user
|
||||||
from passbook.lib.utils.reflection import get_apps
|
from passbook.lib.utils.reflection import get_apps
|
||||||
|
|
||||||
|
@ -19,8 +19,9 @@ core_urls = [
|
||||||
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
|
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
|
||||||
path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'),
|
path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'),
|
||||||
path('auth/sign_up/', authentication.SignUpView.as_view(), name='auth-sign-up'),
|
path('auth/sign_up/', authentication.SignUpView.as_view(), name='auth-sign-up'),
|
||||||
path('auth/mfa/', mfa.MultiFactorAuthenticator.as_view(), name='mfa'),
|
path('auth/process/', view.AuthenticationView.as_view(), name='auth-process'),
|
||||||
path('auth/mfa/denied/', mfa.MFAPermissionDeniedView.as_view(), name='mfa-denied'),
|
path('auth/process/<slug:factor>/', view.AuthenticationView.as_view(), name='auth-process'),
|
||||||
|
path('auth/process/denied/', view.MFAPermissionDeniedView.as_view(), name='auth-denied'),
|
||||||
# User views
|
# User views
|
||||||
path('user/', user.UserSettingsView.as_view(), name='user-settings'),
|
path('user/', user.UserSettingsView.as_view(), name='user-settings'),
|
||||||
path('user/delete/', user.UserDeleteView.as_view(), name='user-delete'),
|
path('user/delete/', user.UserDeleteView.as_view(), name='user-delete'),
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import ugettext as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
|
|
||||||
from passbook.core.auth.mfa import MultiFactorAuthenticator
|
from passbook.core.auth.view import AuthenticationView
|
||||||
from passbook.core.forms.authentication import LoginForm, SignUpForm
|
from passbook.core.forms.authentication import LoginForm, SignUpForm
|
||||||
from passbook.core.models import Invitation, Source, User
|
from passbook.core.models import Invitation, Source, User
|
||||||
from passbook.core.signals import invitation_used, user_signed_up
|
from passbook.core.signals import invitation_used, user_signed_up
|
||||||
|
@ -62,9 +62,9 @@ class LoginView(UserPassesTestMixin, FormView):
|
||||||
if not pre_user:
|
if not pre_user:
|
||||||
# No user found
|
# No user found
|
||||||
return self.invalid_login(self.request)
|
return self.invalid_login(self.request)
|
||||||
if MultiFactorAuthenticator.SESSION_FACTOR in self.request.session:
|
if AuthenticationView.SESSION_FACTOR in self.request.session:
|
||||||
del self.request.session[MultiFactorAuthenticator.SESSION_FACTOR]
|
del self.request.session[AuthenticationView.SESSION_FACTOR]
|
||||||
self.request.session[MultiFactorAuthenticator.SESSION_PENDING_USER] = pre_user.pk
|
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
|
||||||
return redirect(reverse('passbook_core:mfa'))
|
return redirect(reverse('passbook_core:mfa'))
|
||||||
|
|
||||||
def invalid_login(self, request: HttpRequest, disabled_user: User = None) -> HttpResponse:
|
def invalid_login(self, request: HttpRequest, disabled_user: User = None) -> HttpResponse:
|
||||||
|
|
|
@ -59,6 +59,10 @@ passbook:
|
||||||
uid_fields:
|
uid_fields:
|
||||||
- username
|
- username
|
||||||
- email
|
- email
|
||||||
|
factors:
|
||||||
|
- passbook.core.auth.factors.backend
|
||||||
|
- passbook.core.auth.factors.dummy
|
||||||
|
- passbook.captcha_factor.factor
|
||||||
session:
|
session:
|
||||||
remember_age: 2592000 # 60 * 60 * 24 * 30, one month
|
remember_age: 2592000 # 60 * 60 * 24 * 30, one month
|
||||||
# Provider-specific settings
|
# Provider-specific settings
|
||||||
|
|
Reference in New Issue