*(minor): small refactor

This commit is contained in:
Langhammer, Jens
2019-10-07 16:33:48 +02:00
parent d21ec6c9a5
commit f2acc154cd
300 changed files with 1420 additions and 1788 deletions

View File

@ -1,2 +0,0 @@
"""passbook core"""
__version__ = '0.2.6-beta'

View File

@ -2,12 +2,12 @@
from importlib import import_module
from django.apps import AppConfig
from django.conf import settings
from structlog import get_logger
from passbook.lib.config import CONFIG
LOGGER = get_logger()
class PassbookCoreConfig(AppConfig):
"""passbook core app config"""
@ -17,9 +17,7 @@ class PassbookCoreConfig(AppConfig):
mountpoint = ''
def ready(self):
import_module('passbook.policy.engine')
factors_to_load = CONFIG.y('passbook.factors', [])
for factors_to_load in factors_to_load:
for factors_to_load in settings.PASSBOOK_CORE_FACTORS:
try:
import_module(factors_to_load)
LOGGER.info("Loaded factor", factor_class=factors_to_load)

View File

@ -1,27 +0,0 @@
"""passbook multi-factor authentication engine"""
from django.utils.translation import gettext as _
from django.views.generic import TemplateView
from passbook.lib.config import CONFIG
class AuthenticationFactor(TemplateView):
"""Abstract Authentication factor, inherits TemplateView but can be combined with FormView"""
form = None
required = True
authenticator = None
pending_user = None
request = None
template_name = 'login/form_with_user.html'
def __init__(self, authenticator):
self.authenticator = authenticator
def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True
kwargs['title'] = _('Log in to your account')
kwargs['primary_action'] = _('Log in')
kwargs['user'] = self.pending_user
return super().get_context_data(**kwargs)

View File

@ -1,14 +0,0 @@
"""passbook multi-factor authentication engine"""
from structlog import get_logger
from passbook.core.auth.factor import AuthenticationFactor
LOGGER = get_logger()
class DummyFactor(AuthenticationFactor):
"""Dummy factor for testing with multiple factors"""
def post(self, request):
"""Just redirect to next factor"""
return self.authenticator.user_ok()

View File

@ -1,108 +0,0 @@
"""passbook multi-factor authentication engine"""
from inspect import Signature
from django.contrib import messages
from django.contrib.auth import _clean_credentials
from django.contrib.auth.signals import user_login_failed
from django.core.exceptions import PermissionDenied
from django.forms.utils import ErrorList
from django.shortcuts import redirect, reverse
from django.utils.translation import gettext as _
from django.views.generic import FormView
from structlog import get_logger
from passbook.core.auth.factor import AuthenticationFactor
from passbook.core.auth.view import AuthenticationView
from passbook.core.forms.authentication import PasswordFactorForm
from passbook.core.models import Nonce
from passbook.core.tasks import send_email
from passbook.lib.config import CONFIG
from passbook.lib.utils.reflection import path_to_class
LOGGER = get_logger()
def authenticate(request, backends, **credentials):
"""If the given credentials are valid, return a User object.
Customized version of django's authenticate, which accepts a list of backends"""
for backend_path in backends:
backend = path_to_class(backend_path)()
try:
signature = Signature.from_callable(backend.authenticate)
signature.bind(request, **credentials)
except TypeError:
LOGGER.debug("Backend doesn't accept our arguments", backend=backend)
# This backend doesn't accept these credentials as arguments. Try the next one.
continue
LOGGER.debug('Attempting authentication...', backend=backend)
try:
user = backend.authenticate(request, **credentials)
except PermissionDenied:
LOGGER.debug('Backend threw PermissionDenied', backend=backend)
# This backend says to stop in our tracks - this user should not be allowed in at all.
break
if user is None:
continue
# Annotate the user object with the path of the backend.
user.backend = backend_path
return user
# The credentials supplied are invalid to all backends, fire signal
user_login_failed.send(sender=__name__, credentials=_clean_credentials(
credentials), request=request)
class PasswordFactor(FormView, AuthenticationFactor):
"""Authentication factor which authenticates against django's AuthBackend"""
form_class = PasswordFactorForm
template_name = 'login/factors/backend.html'
def get_context_data(self, **kwargs):
kwargs['show_password_forget_notice'] = CONFIG.y('passbook.password_reset.enabled')
return super().get_context_data(**kwargs)
def get(self, request, *args, **kwargs):
if 'password-forgotten' in request.GET:
nonce = Nonce.objects.create(user=self.pending_user)
LOGGER.debug("DEBUG %s", str(nonce.uuid))
# Send mail to user
send_email.delay(self.pending_user.email, _('Forgotten password'),
'email/account_password_reset.html', {
'url': self.request.build_absolute_uri(
reverse('passbook_core:auth-password-reset',
kwargs={
'nonce': nonce.uuid
})
)
})
self.authenticator.cleanup()
messages.success(request, _('Check your E-Mails for a password reset link.'))
return redirect('passbook_core:auth-login')
return super().get(request, *args, **kwargs)
def form_valid(self, form):
"""Authenticate against django's authentication backend"""
uid_fields = CONFIG.y('passbook.uid_fields')
kwargs = {
'password': form.cleaned_data.get('password'),
}
for uid_field in uid_fields:
kwargs[uid_field] = getattr(self.authenticator.pending_user, uid_field)
try:
user = authenticate(self.request, self.authenticator.current_factor.backends, **kwargs)
if user:
# User instance returned from authenticate() has .backend property set
self.authenticator.pending_user = user
self.request.session[AuthenticationView.SESSION_USER_BACKEND] = user.backend
return self.authenticator.user_ok()
# No user was found -> invalid credentials
LOGGER.debug("Invalid credentials")
# Manually inject error into form
# pylint: disable=protected-access
errors = form._errors.setdefault("password", ErrorList())
errors.append(_("Invalid password"))
return self.form_invalid(form)
except PermissionDenied:
# User was found, but permission was denied (i.e. user is not active)
LOGGER.debug("Denied access", **kwargs)
return self.authenticator.user_invalid()

View File

@ -1,165 +0,0 @@
"""passbook multi-factor authentication engine"""
from typing import List, Tuple
from django.contrib.auth import login
from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.http import urlencode
from django.views.generic import View
from structlog import get_logger
from passbook.core.models import Factor, User
from passbook.core.views.utils import PermissionDeniedView
from passbook.lib.utils.reflection import class_to_path, path_to_class
from passbook.lib.utils.urls import is_url_absolute
from passbook.policy.engine import PolicyEngine
LOGGER = get_logger()
def _redirect_with_qs(view, get_query_set=None):
"""Wrapper to redirect whilst keeping GET Parameters"""
target = reverse(view)
if get_query_set:
target += '?' + urlencode({key: value for key, value in get_query_set.items()})
return redirect(target)
class AuthenticationView(UserPassesTestMixin, View):
"""Wizard-like Multi-factor authenticator"""
SESSION_FACTOR = 'passbook_factor'
SESSION_PENDING_FACTORS = 'passbook_pending_factors'
SESSION_PENDING_USER = 'passbook_pending_user'
SESSION_USER_BACKEND = 'passbook_user_backend'
SESSION_IS_SSO_LOGIN = 'passbook_sso_login'
pending_user: User
pending_factors: List[Tuple[str, str]] = []
_current_factor_class: Factor
current_factor: Factor
# Allow only not authenticated users to login
def test_func(self):
return AuthenticationView.SESSION_PENDING_USER in self.request.session
def handle_no_permission(self):
# Function from UserPassesTestMixin
if 'next' in self.request.GET:
return redirect(self.request.GET.get('next'))
if self.request.user.is_authenticated:
return _redirect_with_qs('passbook_core:overview', self.request.GET)
return _redirect_with_qs('passbook_core:auth-login', self.request.GET)
def get_pending_factors(self):
"""Loading pending factors from Database or load from session variable"""
# Write pending factors to session
if AuthenticationView.SESSION_PENDING_FACTORS in self.request.session:
return self.request.session[AuthenticationView.SESSION_PENDING_FACTORS]
# Get an initial list of factors which are currently enabled
# and apply to the current user. We check policies here and block the request
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
pending_factors = []
for factor in _all_factors:
LOGGER.debug("Checking if factor applies to user",
factor=factor, user=self.pending_user)
policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(self.pending_user).with_request(self.request).build()
if policy_engine.passing:
pending_factors.append((factor.uuid.hex, factor.type))
LOGGER.debug("Factor applies", factor=factor, user=self.pending_user)
return pending_factors
def dispatch(self, request, *args, **kwargs):
# Check if user passes test (i.e. SESSION_PENDING_USER is set)
user_test_result = self.get_test_func()()
if not user_test_result:
return self.handle_no_permission()
# Extract pending user from session (only remember uid)
self.pending_user = get_object_or_404(
User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER])
self.pending_factors = self.get_pending_factors()
# Read and instantiate factor from session
factor_uuid, factor_class = None, None
if AuthenticationView.SESSION_FACTOR not in request.session:
# Case when no factors apply to user, return error denied
if not self.pending_factors:
# Case when user logged in from SSO provider and no more factors apply
if AuthenticationView.SESSION_IS_SSO_LOGIN in request.session:
LOGGER.debug("User authenticated with SSO, logging in...")
return self._user_passed()
return self.user_invalid()
factor_uuid, factor_class = self.pending_factors[0]
else:
factor_uuid, factor_class = request.session[AuthenticationView.SESSION_FACTOR]
# Lookup current factor object
self.current_factor = Factor.objects.filter(uuid=factor_uuid).select_subclasses().first()
# Instantiate Next Factor and pass request
factor = path_to_class(factor_class)
self._current_factor_class = factor(self)
self._current_factor_class.pending_user = self.pending_user
self._current_factor_class.request = request
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""pass get request to current factor"""
LOGGER.debug("Passing GET", view_class=class_to_path(self._current_factor_class.__class__))
return self._current_factor_class.get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""pass post request to current factor"""
LOGGER.debug("Passing POST", view_class=class_to_path(self._current_factor_class.__class__))
return self._current_factor_class.post(request, *args, **kwargs)
def user_ok(self):
"""Redirect to next Factor"""
LOGGER.debug("Factor passed",
factor_class=class_to_path(self._current_factor_class.__class__))
# Remove passed factor from pending factors
current_factor_tuple = (self.current_factor.uuid.hex,
class_to_path(self._current_factor_class.__class__))
if current_factor_tuple in self.pending_factors:
self.pending_factors.remove(current_factor_tuple)
next_factor = None
if self.pending_factors:
next_factor = self.pending_factors.pop()
# Save updated pening_factor list to session
self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] = \
self.pending_factors
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
LOGGER.debug("Rendering Factor", next_factor=next_factor)
return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
# User passed all factors
LOGGER.debug("User passed all factors, logging in")
return self._user_passed()
def user_invalid(self):
"""Show error message, user cannot login.
This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
LOGGER.debug("User invalid")
self.cleanup()
return _redirect_with_qs('passbook_core:auth-denied', self.request.GET)
def _user_passed(self):
"""User Successfully passed all factors"""
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
login(self.request, self.pending_user, backend=backend)
LOGGER.debug("Logged in", user=self.pending_user)
# Cleanup
self.cleanup()
next_param = self.request.GET.get('next', None)
if next_param and not is_url_absolute(next_param):
return redirect(next_param)
return _redirect_with_qs('passbook_core:overview')
def cleanup(self):
"""Remove temporary data from session"""
session_keys = [self.SESSION_FACTOR, self.SESSION_PENDING_FACTORS,
self.SESSION_PENDING_USER, self.SESSION_USER_BACKEND, ]
for key in session_keys:
if key in self.request.session:
del self.request.session[key]
LOGGER.debug("Cleaned up sessions")
class FactorPermissionDeniedView(PermissionDeniedView):
"""User could not be authenticated"""

View File

@ -1,10 +0,0 @@
"""passbook core exceptions"""
class PasswordPolicyInvalid(Exception):
"""Exception raised when a Password Policy fails"""
messages = []
def __init__(self, *messages):
super().__init__()
self.messages = messages

View File

@ -16,7 +16,7 @@ class ApplicationForm(forms.ModelForm):
model = Application
fields = ['name', 'slug', 'launch_url', 'icon_url',
'policies', 'provider', 'skip_authorization']
'provider', 'policies', 'skip_authorization']
widgets = {
'name': forms.TextInput(),
'launch_url': forms.TextInput(),

View File

@ -1,45 +0,0 @@
"""passbook administration forms"""
from django import forms
from django.conf import settings
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from passbook.core.models import DummyFactor, PasswordFactor
from passbook.lib.utils.reflection import path_to_class
GENERAL_FIELDS = ['name', 'slug', 'order', 'policies', 'enabled']
def get_authentication_backends():
"""Return all available authentication backends as tuple set"""
for backend in settings.AUTHENTICATION_BACKENDS:
klass = path_to_class(backend)
yield backend, getattr(klass(), 'name', '%s (%s)' % (klass.__name__, klass.__module__))
class PasswordFactorForm(forms.ModelForm):
"""Form to create/edit Password Factors"""
class Meta:
model = PasswordFactor
fields = GENERAL_FIELDS + ['backends', 'password_policies']
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False),
'backends': FilteredSelectMultiple(_('backends'), False,
choices=get_authentication_backends()),
'password_policies': FilteredSelectMultiple(_('password policies'), False),
}
class DummyFactorForm(forms.ModelForm):
"""Form to create/edit Dummy Factor"""
class Meta:
model = DummyFactor
fields = GENERAL_FIELDS
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False)
}

View File

@ -3,40 +3,8 @@
from django import forms
from django.utils.translation import gettext as _
from passbook.core.models import (DebugPolicy, FieldMatcherPolicy,
GroupMembershipPolicy, PasswordPolicy,
SSOLoginPolicy, WebhookPolicy)
GENERAL_FIELDS = ['name', 'action', 'negate', 'order', 'timeout']
class FieldMatcherPolicyForm(forms.ModelForm):
"""FieldMatcherPolicy Form"""
class Meta:
model = FieldMatcherPolicy
fields = GENERAL_FIELDS + ['user_field', 'match_action', 'value', ]
widgets = {
'name': forms.TextInput(),
'value': forms.TextInput(),
}
class WebhookPolicyForm(forms.ModelForm):
"""WebhookPolicyForm Form"""
class Meta:
model = WebhookPolicy
fields = GENERAL_FIELDS + ['url', 'method', 'json_body', 'json_headers',
'result_jsonpath', 'result_json_value', ]
widgets = {
'name': forms.TextInput(),
'json_body': forms.TextInput(),
'json_headers': forms.TextInput(),
'result_jsonpath': forms.TextInput(),
'result_json_value': forms.TextInput(),
}
from passbook.core.models import DebugPolicy
from passbook.policies.forms import GENERAL_FIELDS
class DebugPolicyForm(forms.ModelForm):
@ -52,49 +20,3 @@ class DebugPolicyForm(forms.ModelForm):
labels = {
'result': _('Allow user')
}
class GroupMembershipPolicyForm(forms.ModelForm):
"""GroupMembershipPolicy Form"""
class Meta:
model = GroupMembershipPolicy
fields = GENERAL_FIELDS + ['group', ]
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
}
class SSOLoginPolicyForm(forms.ModelForm):
"""Edit SSOLoginPolicy instances"""
class Meta:
model = SSOLoginPolicy
fields = GENERAL_FIELDS
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
}
class PasswordPolicyForm(forms.ModelForm):
"""PasswordPolicy Form"""
class Meta:
model = PasswordPolicy
fields = GENERAL_FIELDS + ['amount_uppercase', 'amount_lowercase',
'amount_symbols', 'length_min', 'symbol_charset',
'error_message']
widgets = {
'name': forms.TextInput(),
'symbol_charset': forms.TextInput(),
'error_message': forms.TextInput(),
}
labels = {
'amount_uppercase': _('Minimum amount of Uppercase Characters'),
'amount_lowercase': _('Minimum amount of Lowercase Characters'),
'amount_symbols': _('Minimum amount of Symbols Characters'),
'length_min': _('Minimum Length'),
}

View File

@ -1,45 +0,0 @@
"""passbook import_users management command"""
from csv import DictReader
from django.core.management.base import BaseCommand
from django.core.validators import EmailValidator, ValidationError
from structlog import get_logger
from passbook.core.models import User
LOGGER = get_logger()
class Command(BaseCommand):
"""Import users from CSV file"""
def add_arguments(self, parser):
# Positional arguments
parser.add_argument('file', nargs='+', type=str)
def handle(self, *args, **options):
"""Create Users from CSV file"""
for file in options.get('file'):
with open(file, 'r') as _file:
reader = DictReader(_file)
for user in reader:
LOGGER.debug('User %s', user.get('username'))
try:
# only import users with valid email addresses
if user.get('email'):
validator = EmailValidator()
validator(user.get('email'))
# use combination of username and email to check for existing user
if User.objects.filter(
username=user.get('username'),
email=user.get('email')).exists():
LOGGER.debug('User %s exists already, skipping', user.get('username'))
# Create user
User.objects.create(
username=user.get('username'),
email=user.get('email'),
name=user.get('name'),
password=user.get('password'))
LOGGER.debug('Created User %s', user.get('username'))
except ValidationError as exc:
LOGGER.warning('User %s caused %r, skipping', user.get('username'), exc)
continue

View File

@ -1,35 +0,0 @@
"""passbook Webserver management command"""
import cherrypy
from django.conf import settings
from django.core.management.base import BaseCommand
from structlog import get_logger
from passbook.lib.config import CONFIG
from passbook.root.wsgi import application
LOGGER = get_logger()
class Command(BaseCommand):
"""Run CherryPy webserver"""
def handle(self, *args, **options):
"""passbook cherrypy server"""
cherrypy.config.update(CONFIG.y('web'))
cherrypy.tree.graft(application, '/')
# Mount NullObject to serve static files
cherrypy.tree.mount(None, settings.STATIC_URL, config={
'/': {
'tools.staticdir.on': True,
'tools.staticdir.dir': settings.STATIC_ROOT,
'tools.expires.on': True,
'tools.expires.secs': 86400,
'tools.gzip.on': True,
}
})
cherrypy.engine.start()
for file in CONFIG.loaded_file:
cherrypy.engine.autoreload.files.add(file)
LOGGER.info("Added '%s' to autoreload triggers", file)
cherrypy.engine.block()

View File

@ -1,22 +0,0 @@
"""passbook Worker management command"""
from django.core.management.base import BaseCommand
from django.utils import autoreload
from structlog import get_logger
from passbook.root.celery import CELERY_APP
LOGGER = get_logger()
class Command(BaseCommand):
"""Run Celery Worker"""
def handle(self, *args, **options):
"""celery worker"""
autoreload.run_with_reloader(self.celery_worker)
def celery_worker(self):
"""Run celery worker within autoreload"""
autoreload.raise_last_exception()
CELERY_APP.worker_main(['worker', '--autoscale=10,3', '-E', '-B'])

View File

@ -1,21 +1,24 @@
# Generated by Django 2.1.7 on 2019-02-16 09:10
# Generated by Django 2.2.6 on 2019-10-07 14:06
import uuid
import django.contrib.auth.models
import django.contrib.auth.validators
import django.contrib.postgres.fields.jsonb
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
import passbook.core.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0009_alter_user_last_name_max_length'),
('auth', '0011_update_proxy_permissions'),
]
operations = [
@ -34,6 +37,8 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
('name', models.TextField()),
('password_change_date', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'user',
@ -44,39 +49,17 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Group',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=80, verbose_name='name')),
('extra_data', models.TextField(blank=True)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='passbook_core.Group')),
],
),
migrations.CreateModel(
name='Invitation',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.DateTimeField(blank=True, default=None, null=True)),
('fixed_username', models.TextField(blank=True, default=None)),
('fixed_email', models.TextField(blank=True, default=None)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Invitation',
'verbose_name_plural': 'Invitations',
},
),
migrations.CreateModel(
name='Policy',
fields=[
('created', models.DateField(auto_now_add=True)),
('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField(blank=True, null=True)),
('action', models.CharField(choices=[('allow', 'allow'), ('deny', 'deny')], max_length=20)),
('negate', models.BooleanField(default=False)),
('order', models.IntegerField(default=0)),
('timeout', models.IntegerField(default=30)),
],
options={
'abstract': False,
@ -85,28 +68,136 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='PolicyModel',
fields=[
('created', models.DateField(auto_now_add=True)),
('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('policies', models.ManyToManyField(blank=True, to='passbook_core.Policy')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PropertyMapping',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField()),
],
options={
'verbose_name': 'Property Mapping',
'verbose_name_plural': 'Property Mappings',
},
),
migrations.CreateModel(
name='DebugPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('result', models.BooleanField(default=False)),
('wait_min', models.IntegerField(default=5)),
('wait_max', models.IntegerField(default=30)),
],
options={
'verbose_name': 'Debug Policy',
'verbose_name_plural': 'Debug Policies',
},
bases=('passbook_core.policy',),
),
migrations.CreateModel(
name='Factor',
fields=[
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField(unique=True)),
('order', models.IntegerField()),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='Source',
fields=[
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField()),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('property_mappings', models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping')),
],
),
migrations.CreateModel(
name='Nonce',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.DateTimeField(default=passbook.core.models.default_nonce_duration)),
('expiring', models.BooleanField(default=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Nonce',
'verbose_name_plural': 'Nonces',
},
),
migrations.CreateModel(
name='Invitation',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.DateTimeField(blank=True, default=None, null=True)),
('fixed_username', models.TextField(blank=True, default=None)),
('fixed_email', models.TextField(blank=True, default=None)),
('needs_confirmation', models.BooleanField(default=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Invitation',
'verbose_name_plural': 'Invitations',
},
),
migrations.CreateModel(
name='Group',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=80, verbose_name='name')),
('tags', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='passbook_core.Group')),
],
options={
'unique_together': {('name', 'parent')},
},
),
migrations.AddField(
model_name='user',
name='groups',
field=models.ManyToManyField(to='passbook_core.Group'),
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
),
migrations.CreateModel(
name='UserSourceConnection',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Source')),
],
options={
'unique_together': {('user', 'source')},
},
),
migrations.CreateModel(
name='Application',
@ -124,131 +215,9 @@ class Migration(migrations.Migration):
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='DebugPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('result', models.BooleanField(default=False)),
('wait_min', models.IntegerField(default=5)),
('wait_max', models.IntegerField(default=30)),
],
options={
'verbose_name': 'Debug Policy',
'verbose_name_plural': 'Debug Policys',
},
bases=('passbook_core.policy',),
),
migrations.CreateModel(
name='Factor',
fields=[
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField(unique=True)),
('order', models.IntegerField()),
('type', models.TextField(unique=True)),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='FieldMatcherPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('user_field', models.TextField(choices=[('username', 'Username'), ('first_name', 'First Name'), ('last_name', 'Last Name'), ('email', 'E-Mail'), ('is_staff', 'Is staff'), ('is_active', 'Is active'), ('data_joined', 'Date joined')])),
('match_action', models.CharField(choices=[('startswith', 'Starts with'), ('endswith', 'Ends with'), ('endswith', 'Contains'), ('regexp', 'Regexp'), ('exact', 'Exact')], max_length=50)),
('value', models.TextField()),
],
options={
'verbose_name': 'Field matcher Policy',
'verbose_name_plural': 'Field matcher Policys',
},
bases=('passbook_core.policy',),
),
migrations.CreateModel(
name='PasswordPolicyPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('amount_uppercase', models.IntegerField(default=0)),
('amount_lowercase', models.IntegerField(default=0)),
('amount_symbols', models.IntegerField(default=0)),
('length_min', models.IntegerField(default=0)),
('symbol_charset', models.TextField(default='!\\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ')),
],
options={
'verbose_name': 'Password Policy Policy',
'verbose_name_plural': 'Password Policy Policys',
},
bases=('passbook_core.policy',),
),
migrations.CreateModel(
name='Source',
fields=[
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField()),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='WebhookPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('url', models.URLField()),
('method', models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PATCH', 'PATCH'), ('DELETE', 'DELETE'), ('PUT', 'PUT')], max_length=10)),
('json_body', models.TextField()),
('json_headers', models.TextField()),
('result_jsonpath', models.TextField()),
('result_json_value', models.TextField()),
],
options={
'verbose_name': 'Webhook Policy',
'verbose_name_plural': 'Webhook Policys',
},
bases=('passbook_core.policy',),
),
migrations.AddField(
model_name='policymodel',
name='policies',
field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
),
migrations.AddField(
model_name='user',
name='groups',
field=models.ManyToManyField(to='passbook_core.Group'),
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
),
migrations.AddField(
model_name='usersourceconnection',
name='source',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Source'),
),
migrations.AlterUniqueTogether(
name='group',
unique_together={('name', 'parent')},
),
migrations.AddField(
model_name='user',
name='applications',
field=models.ManyToManyField(to='passbook_core.Application'),
),
migrations.AddField(
model_name='user',
name='sources',
field=models.ManyToManyField(through='passbook_core.UserSourceConnection', to='passbook_core.Source'),
),
migrations.AlterUniqueTogether(
name='usersourceconnection',
unique_together={('user', 'source')},
),
]

View File

@ -1,29 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-16 10:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='debugpolicy',
options={'verbose_name': 'Debug Policy', 'verbose_name_plural': 'Debug Policies'},
),
migrations.AlterModelOptions(
name='fieldmatcherpolicy',
options={'verbose_name': 'Field matcher Policy', 'verbose_name_plural': 'Field matcher Policies'},
),
migrations.AlterModelOptions(
name='passwordpolicypolicy',
options={'verbose_name': 'Password Policy Policy', 'verbose_name_plural': 'Password Policy Policies'},
),
migrations.AlterModelOptions(
name='webhookpolicy',
options={'verbose_name': 'Webhook Policy', 'verbose_name_plural': 'Webhook Policies'},
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-16 10:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0002_auto_20190216_1002'),
]
operations = [
migrations.RenameModel(
old_name='PasswordPolicyPolicy',
new_name='PasswordPolicy',
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-16 10:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0003_auto_20190216_1004'),
]
operations = [
migrations.AlterModelOptions(
name='passwordpolicy',
options={'verbose_name': 'Password Policy', 'verbose_name_plural': 'Password Policies'},
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-21 12:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0004_auto_20190216_1013'),
]
operations = [
migrations.AlterField(
model_name='policy',
name='created',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='policymodel',
name='created',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='usersourceconnection',
name='created',
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-21 12:32
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0005_auto_20190221_1201'),
]
operations = [
migrations.AddField(
model_name='factor',
name='arguments',
field=django.contrib.postgres.fields.jsonb.JSONField(default=dict),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-21 12:33
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0006_factor_arguments'),
]
operations = [
migrations.AlterField(
model_name='factor',
name='arguments',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-21 15:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0007_auto_20190221_1233'),
]
operations = [
migrations.AlterField(
model_name='fieldmatcherpolicy',
name='match_action',
field=models.CharField(choices=[('startswith', 'Starts with'), ('endswith', 'Ends with'), ('contains', 'Contains'), ('regexp', 'Regexp'), ('exact', 'Exact')], max_length=50),
),
]

View File

@ -1,44 +0,0 @@
# 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',
),
]

View File

@ -1,21 +0,0 @@
# 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'},
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-25 14:38
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0010_auto_20190224_1016'),
]
operations = [
migrations.AddField(
model_name='passwordfactor',
name='password_policies',
field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
),
migrations.AddField(
model_name='user',
name='password_change_date',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@ -1,31 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-25 19:12
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import passbook.core.models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0011_auto_20190225_1438'),
]
operations = [
migrations.CreateModel(
name='Nonce',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.DateTimeField(default=passbook.core.models.default_nonce_duration)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Nonce',
'verbose_name_plural': 'Nonces',
},
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-25 19:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0012_nonce'),
]
operations = [
migrations.AddField(
model_name='invitation',
name='needs_confirmation',
field=models.BooleanField(default=True),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-26 08:50
from django.db import migrations
def create_initial_factor(apps, schema_editor):
"""Create initial PasswordFactor if none exists"""
PasswordFactor = apps.get_model("passbook_core", "PasswordFactor")
if not PasswordFactor.objects.exists():
PasswordFactor.objects.create(
name='password',
slug='password',
order=0,
backends=['django.contrib.auth.backends.ModelBackend']
)
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0013_invitation_needs_confirmation'),
]
operations = [
migrations.RunPython(create_initial_factor)
]

View File

@ -1,19 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-26 14:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0014_auto_20190226_0850'),
]
operations = [
migrations.AddField(
model_name='passwordpolicy',
name='error_message',
field=models.TextField(default=''),
preserve_default=False,
),
]

View File

@ -1,38 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-27 13:55
from django.db import migrations, models
def migrate_names(apps, schema_editor):
"""migrate first_name and last_name to name"""
User = apps.get_model("passbook_core", "User")
for user in User.objects.all():
user.name = '%s %s' % (user.first_name, user.last_name)
user.save()
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0015_passwordpolicy_error_message'),
]
operations = [
migrations.AddField(
model_name='user',
name='name',
field=models.TextField(default=''),
preserve_default=False,
),
migrations.RunPython(migrate_names),
migrations.AlterField(
model_name='user',
name='name',
field=models.TextField(),
preserve_default=False,
),
migrations.AlterField(
model_name='fieldmatcherpolicy',
name='user_field',
field=models.TextField(choices=[('username', 'Username'), ('name', 'Name'), ('email', 'E-Mail'), ('is_staff', 'Is staff'), ('is_active', 'Is active'), ('data_joined', 'Date joined')]),
),
]

View File

@ -1,26 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-08 10:40
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0016_auto_20190227_1355'),
]
operations = [
migrations.CreateModel(
name='PropertyMapping',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField()),
],
options={
'verbose_name': 'Property Mapping',
'verbose_name_plural': 'Property Mappings',
},
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-08 10:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0017_propertymapping'),
]
operations = [
migrations.AddField(
model_name='provider',
name='property_mappings',
field=models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping'),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-10 16:15
import django.contrib.postgres.fields.hstore
from django.contrib.postgres.operations import HStoreExtension
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0018_provider_property_mappings'),
]
operations = [
migrations.RemoveField(
model_name='group',
name='extra_data',
),
HStoreExtension(),
migrations.AddField(
model_name='group',
name='tags',
field=django.contrib.postgres.fields.hstore.HStoreField(default=dict),
),
]

View File

@ -1,26 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-10 18:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0019_auto_20190310_1615'),
]
operations = [
migrations.CreateModel(
name='GroupMembershipPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Group')),
],
options={
'verbose_name': 'Group Membership Policy',
'verbose_name_plural': 'Group Membership Policies',
},
bases=('passbook_core.policy',),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-21 12:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0020_groupmembershippolicy'),
]
operations = [
migrations.AddField(
model_name='policy',
name='timeout',
field=models.IntegerField(default=30),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-04-04 19:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0021_policy_timeout'),
]
operations = [
migrations.AddField(
model_name='nonce',
name='expiring',
field=models.BooleanField(default=True),
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 2.2 on 2019-04-13 15:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0022_nonce_expiring'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='applications',
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 2.2 on 2019-04-29 21:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0023_remove_user_applications'),
]
operations = [
migrations.CreateModel(
name='SSOLoginPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
],
options={
'verbose_name': 'SSO Login Policy',
'verbose_name_plural': 'SSO Login Policies',
},
bases=('passbook_core.policy',),
),
]

View File

@ -1,12 +1,11 @@
"""passbook core models"""
import re
from datetime import timedelta
from random import SystemRandom
from time import sleep
from uuid import uuid4
from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import ArrayField, HStoreField
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.urls import reverse_lazy
from django.utils.timezone import now
@ -16,8 +15,8 @@ from structlog import get_logger
from passbook.core.signals import password_changed
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
from passbook.policy.exceptions import PolicyException
from passbook.policy.struct import PolicyRequest, PolicyResult
from passbook.policies.exceptions import PolicyException
from passbook.policies.struct import PolicyRequest, PolicyResult
LOGGER = get_logger()
@ -32,10 +31,10 @@ class Group(UUIDModel):
name = models.CharField(_('name'), max_length=80)
parent = models.ForeignKey('Group', blank=True, null=True,
on_delete=models.SET_NULL, related_name='children')
tags = HStoreField(default=dict)
tags = JSONField(default=dict, blank=True)
def __str__(self):
return "Group %s" % self.name
return f"Group {self.name}"
class Meta:
@ -94,48 +93,8 @@ class Factor(PolicyModel):
return False
def __str__(self):
return "Factor %s" % self.slug
return f"Factor {self.slug}"
class PasswordFactor(Factor):
"""Password-based Django-backend Authentication Factor"""
backends = ArrayField(models.TextField())
password_policies = models.ManyToManyField('Policy', blank=True)
type = 'passbook.core.auth.factors.password.PasswordFactor'
form = 'passbook.core.forms.factors.PasswordFactorForm'
def has_user_settings(self):
return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password'
def password_passes(self, user: User) -> bool:
"""Return true if user's password passes, otherwise False or raise Exception"""
for policy in self.policies.all():
if not policy.passes(user):
return False
return True
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):
"""Every Application which uses passbook for authentication/identification/authorization
@ -161,6 +120,7 @@ class Application(PolicyModel):
def __str__(self):
return self.name
class Source(PolicyModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
@ -196,6 +156,7 @@ class Source(PolicyModel):
def __str__(self):
return self.name
class UserSourceConnection(CreatedUpdatedModel):
"""Connection between User and Source."""
@ -206,6 +167,7 @@ class UserSourceConnection(CreatedUpdatedModel):
unique_together = (('user', 'source'),)
class Policy(UUIDModel, CreatedUpdatedModel):
"""Policies which specify if a user is authorized to use an Application. Can be overridden by
other types to add other fields, more logic, etc."""
@ -228,148 +190,12 @@ class Policy(UUIDModel, CreatedUpdatedModel):
def __str__(self):
if self.name:
return self.name
return "%s action %s" % (self.name, self.action)
return f"{self.name} action {self.action}"
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if user instance passes this policy"""
raise PolicyException()
class FieldMatcherPolicy(Policy):
"""Policy which checks if a field of the User model matches/doesn't match a
certain pattern"""
MATCH_STARTSWITH = 'startswith'
MATCH_ENDSWITH = 'endswith'
MATCH_CONTAINS = 'contains'
MATCH_REGEXP = 'regexp'
MATCH_EXACT = 'exact'
MATCHES = (
(MATCH_STARTSWITH, _('Starts with')),
(MATCH_ENDSWITH, _('Ends with')),
(MATCH_CONTAINS, _('Contains')),
(MATCH_REGEXP, _('Regexp')),
(MATCH_EXACT, _('Exact')),
)
USER_FIELDS = (
('username', _('Username'),),
('name', _('Name'),),
('email', _('E-Mail'),),
('is_staff', _('Is staff'),),
('is_active', _('Is active'),),
('data_joined', _('Date joined'),),
)
user_field = models.TextField(choices=USER_FIELDS)
match_action = models.CharField(max_length=50, choices=MATCHES)
value = models.TextField()
form = 'passbook.core.forms.policies.FieldMatcherPolicyForm'
def __str__(self):
description = "%s, user.%s %s '%s'" % (self.name, self.user_field,
self.match_action, self.value)
if self.name:
description = "%s: %s" % (self.name, description)
return description
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if user instance passes this role"""
if not hasattr(request.user, self.user_field):
raise ValueError("Field does not exist")
user_field_value = getattr(request.user, self.user_field, None)
LOGGER.debug("Checking field", value=user_field_value,
action=self.match_action, should_be=self.value)
passes = False
if self.match_action == FieldMatcherPolicy.MATCH_STARTSWITH:
passes = user_field_value.startswith(self.value)
if self.match_action == FieldMatcherPolicy.MATCH_ENDSWITH:
passes = user_field_value.endswith(self.value)
if self.match_action == FieldMatcherPolicy.MATCH_CONTAINS:
passes = self.value in user_field_value
if self.match_action == FieldMatcherPolicy.MATCH_REGEXP:
pattern = re.compile(self.value)
passes = bool(pattern.match(user_field_value))
if self.match_action == FieldMatcherPolicy.MATCH_EXACT:
passes = user_field_value == self.value
return PolicyResult(passes)
class Meta:
verbose_name = _('Field matcher Policy')
verbose_name_plural = _('Field matcher Policies')
class PasswordPolicy(Policy):
"""Policy to make sure passwords have certain properties"""
amount_uppercase = models.IntegerField(default=0)
amount_lowercase = models.IntegerField(default=0)
amount_symbols = models.IntegerField(default=0)
length_min = models.IntegerField(default=0)
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
error_message = models.TextField()
form = 'passbook.core.forms.policies.PasswordPolicyForm'
def passes(self, request: PolicyRequest) -> PolicyResult:
# Only check if password is being set
if not hasattr(request.user, '__password__'):
return PolicyResult(True)
password = getattr(request.user, '__password__')
filter_regex = r''
if self.amount_lowercase > 0:
filter_regex += r'[a-z]{%d,}' % self.amount_lowercase
if self.amount_uppercase > 0:
filter_regex += r'[A-Z]{%d,}' % self.amount_uppercase
if self.amount_symbols > 0:
filter_regex += r'[%s]{%d,}' % (self.symbol_charset, self.amount_symbols)
result = bool(re.compile(filter_regex).match(password))
if not result:
return PolicyResult(result, self.error_message)
return PolicyResult(result)
class Meta:
verbose_name = _('Password Policy')
verbose_name_plural = _('Password Policies')
class WebhookPolicy(Policy):
"""Policy that asks webhook"""
METHOD_GET = 'GET'
METHOD_POST = 'POST'
METHOD_PATCH = 'PATCH'
METHOD_DELETE = 'DELETE'
METHOD_PUT = 'PUT'
METHODS = (
(METHOD_GET, METHOD_GET),
(METHOD_POST, METHOD_POST),
(METHOD_PATCH, METHOD_PATCH),
(METHOD_DELETE, METHOD_DELETE),
(METHOD_PUT, METHOD_PUT),
)
url = models.URLField()
method = models.CharField(max_length=10, choices=METHODS)
json_body = models.TextField()
json_headers = models.TextField()
result_jsonpath = models.TextField()
result_json_value = models.TextField()
form = 'passbook.core.forms.policies.WebhookPolicyForm'
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Call webhook asynchronously and report back"""
raise NotImplementedError()
class Meta:
verbose_name = _('Webhook Policy')
verbose_name_plural = _('Webhook Policies')
class DebugPolicy(Policy):
"""Policy used for debugging the PolicyEngine. Returns a fixed result,
@ -393,36 +219,6 @@ class DebugPolicy(Policy):
verbose_name = _('Debug Policy')
verbose_name_plural = _('Debug Policies')
class GroupMembershipPolicy(Policy):
"""Policy to check if the user is member in a certain group"""
group = models.ForeignKey('Group', on_delete=models.CASCADE)
form = 'passbook.core.forms.policies.GroupMembershipPolicyForm'
def passes(self, request: PolicyRequest) -> PolicyResult:
return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists())
class Meta:
verbose_name = _('Group Membership Policy')
verbose_name_plural = _('Group Membership Policies')
class SSOLoginPolicy(Policy):
"""Policy that applies to users that have authenticated themselves through SSO"""
form = 'passbook.core.forms.policies.SSOLoginPolicyForm'
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if user instance passes this policy"""
from passbook.core.auth.view import AuthenticationView
is_sso_login = request.user.session.get(AuthenticationView.SESSION_IS_SSO_LOGIN, False)
return PolicyResult(is_sso_login)
class Meta:
verbose_name = _('SSO Login Policy')
verbose_name_plural = _('SSO Login Policies')
class Invitation(UUIDModel):
"""Single-use invitation link"""
@ -436,10 +232,10 @@ class Invitation(UUIDModel):
@property
def link(self):
"""Get link to use invitation"""
return reverse_lazy('passbook_core:auth-sign-up') + '?invitation=%s' % self.uuid
return reverse_lazy('passbook_core:auth-sign-up') + f'?invitation={self.uuid.hex}'
def __str__(self):
return "Invitation %s created by %s" % (self.uuid, self.created_by)
return f"Invitation {self.uuid.hex} created by {self.created_by}"
class Meta:
@ -454,7 +250,7 @@ class Nonce(UUIDModel):
expiring = models.BooleanField(default=True)
def __str__(self):
return "Nonce %s (expires=%s)" % (self.uuid.hex, self.expires)
return f"Nonce f{self.uuid.hex} (expires={self.expires})"
class Meta:
@ -470,7 +266,7 @@ class PropertyMapping(UUIDModel):
objects = InheritanceManager()
def __str__(self):
return "Property Mapping %s" % self.name
return f"Property Mapping {self.name}"
class Meta:

View File

@ -0,0 +1,5 @@
"""core settings"""
PASSBOOK_CORE_FACTORS = [
]

View File

@ -5,8 +5,6 @@ from django.db.models.signals import post_save
from django.dispatch import receiver
from structlog import get_logger
from passbook.core.exceptions import PasswordPolicyInvalid
LOGGER = get_logger()
user_signed_up = Signal(providing_args=['request', 'user'])
@ -14,24 +12,9 @@ invitation_created = Signal(providing_args=['request', 'invitation'])
invitation_used = Signal(providing_args=['request', 'invitation', 'user'])
password_changed = Signal(providing_args=['user', 'password'])
@receiver(password_changed)
# pylint: disable=unused-argument
def password_policy_checker(sender, password, **kwargs):
"""Run password through all password policies which are applied to the user"""
from passbook.core.models import PasswordFactor
from passbook.policy.engine import PolicyEngine
setattr(sender, '__password__', password)
_all_factors = PasswordFactor.objects.filter(enabled=True).order_by('order')
for factor in _all_factors:
policy_engine = PolicyEngine(factor.password_policies.all().select_subclasses())
policy_engine.for_user(sender).build()
passing, messages = policy_engine.result
if not passing:
raise PasswordPolicyInvalid(*messages)
@receiver(post_save)
# pylint: disable=unused-argument
def invalidate_policy_cache(sender, instance, **kwargs):
def invalidate_policy_cache(sender, instance, **_):
"""Invalidate Policy cache when policy is updated"""
from passbook.core.models import Policy
if isinstance(instance, Policy):

View File

@ -1,14 +1,15 @@
"""passbook user settings template tags"""
from django import template
from django.template.context import RequestContext
from passbook.core.models import Factor, Source
from passbook.policy.engine import PolicyEngine
from passbook.policies.engine import PolicyEngine
register = template.Library()
@register.simple_tag(takes_context=True)
def user_factors(context):
def user_factors(context: RequestContext):
"""Return list of all factors which apply to user"""
user = context.get('request').user
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
@ -22,7 +23,7 @@ def user_factors(context):
return matching_factors
@register.simple_tag(takes_context=True)
def user_sources(context):
def user_sources(context: RequestContext):
"""Return a list of all sources which are enabled for the user"""
user = context.get('request').user
_all_sources = Source.objects.filter(enabled=True).select_subclasses()

View File

@ -1,133 +0,0 @@
"""passbook Core Authentication Test"""
import string
from random import SystemRandom
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import RequestFactory, TestCase
from django.urls import reverse
from passbook.core.auth.view import AuthenticationView
from passbook.core.models import DummyFactor, PasswordFactor, User
class TestFactorAuthentication(TestCase):
"""passbook Core Authentication Test"""
def setUp(self):
super().setUp()
self.password = ''.join(SystemRandom().choice(
string.ascii_uppercase + string.digits) for _ in range(8))
self.factor, _ = PasswordFactor.objects.get_or_create(slug='password', defaults={
'name': 'password',
'slug': 'password',
'order': 0,
'backends': ['django.contrib.auth.backends.ModelBackend']
})
self.user = User.objects.create_user(username='test',
email='test@test.test',
password=self.password)
def test_unauthenticated_raw(self):
"""test direct call to AuthenticationView"""
response = self.client.get(reverse('passbook_core:auth-process'))
# Response should be 302 since no pending user is set
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('passbook_core:auth-login'))
def test_unauthenticated_prepared(self):
"""test direct call but with pending_uesr in session"""
request = RequestFactory().get(reverse('passbook_core:auth-process'))
request.user = AnonymousUser()
request.session = {}
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
response = AuthenticationView.as_view()(request)
self.assertEqual(response.status_code, 200)
def test_no_factors(self):
"""Test with all factors disabled"""
self.factor.enabled = False
self.factor.save()
request = RequestFactory().get(reverse('passbook_core:auth-process'))
request.user = AnonymousUser()
request.session = {}
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
response = AuthenticationView.as_view()(request)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('passbook_core:auth-denied'))
self.factor.enabled = True
self.factor.save()
def test_authenticated(self):
"""Test with already logged in user"""
self.client.force_login(self.user)
response = self.client.get(reverse('passbook_core:auth-process'))
# Response should be 302 since no pending user is set
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('passbook_core:overview'))
self.client.logout()
def test_unauthenticated_post(self):
"""Test post request as unauthenticated user"""
request = RequestFactory().post(reverse('passbook_core:auth-process'), data={
'password': self.password
})
request.user = AnonymousUser()
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save() # pylint: disable=no-member
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
response = AuthenticationView.as_view()(request)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('passbook_core:overview'))
self.client.logout()
def test_unauthenticated_post_invalid(self):
"""Test post request as unauthenticated user"""
request = RequestFactory().post(reverse('passbook_core:auth-process'), data={
'password': self.password + 'a'
})
request.user = AnonymousUser()
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save() # pylint: disable=no-member
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
response = AuthenticationView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.client.logout()
def test_multifactor(self):
"""Test view with multiple active factors"""
DummyFactor.objects.get_or_create(name='dummy',
slug='dummy',
order=1)
request = RequestFactory().post(reverse('passbook_core:auth-process'), data={
'password': self.password
})
request.user = AnonymousUser()
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save() # pylint: disable=no-member
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
response = AuthenticationView.as_view()(request)
session_copy = request.session.items()
self.assertEqual(response.status_code, 302)
# Verify view redirects to itself after auth
self.assertEqual(response.url, reverse('passbook_core:auth-process'))
# Run another request with same session which should result in a logged in user
request = RequestFactory().post(reverse('passbook_core:auth-process'))
request.user = AnonymousUser()
middleware = SessionMiddleware()
middleware.process_request(request)
for key, value in session_copy:
request.session[key] = value
request.session.save() # pylint: disable=no-member
response = AuthenticationView.as_view()(request)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('passbook_core:overview'))

View File

@ -2,8 +2,8 @@
from django.urls import path
from structlog import get_logger
from passbook.core.auth import view
from passbook.core.views import authentication, overview, user
from passbook.factors import view
LOGGER = get_logger()

View File

@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Application, Provider, User
from passbook.policy.engine import PolicyEngine
from passbook.policies.engine import PolicyEngine
LOGGER = get_logger()

View File

@ -12,12 +12,12 @@ from django.views import View
from django.views.generic import FormView
from structlog import get_logger
from passbook.core.auth.view import AuthenticationView, _redirect_with_qs
from passbook.core.exceptions import PasswordPolicyInvalid
from passbook.core.forms.authentication import LoginForm, SignUpForm
from passbook.core.models import Invitation, Nonce, Source, User
from passbook.core.signals import invitation_used, user_signed_up
from passbook.core.tasks import send_email
from passbook.factors.password.exceptions import PasswordPolicyInvalid
from passbook.factors.view import AuthenticationView, _redirect_with_qs
from passbook.lib.config import CONFIG
LOGGER = get_logger()

View File

@ -4,7 +4,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView
from passbook.core.models import Application
from passbook.policy.engine import PolicyEngine
from passbook.policies.engine import PolicyEngine
class OverviewView(LoginRequiredMixin, TemplateView):

View File

@ -9,8 +9,8 @@ from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import DeleteView, FormView, UpdateView
from passbook.core.exceptions import PasswordPolicyInvalid
from passbook.core.forms.users import PasswordChangeForm, UserDetailForm
from passbook.factors.password.exceptions import PasswordPolicyInvalid
from passbook.lib.config import CONFIG