*(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

@ -90,23 +90,9 @@ data:
# create_users: true
# # Reset LDAP password when user reset their password
# reset_password: true
oauth_client:
# List of python packages with sources types to load.
types:
- passbook.oauth_client.source_types.discord
- passbook.oauth_client.source_types.facebook
- passbook.oauth_client.source_types.github
- passbook.oauth_client.source_types.google
- passbook.oauth_client.source_types.reddit
- passbook.oauth_client.source_types.supervisr
- passbook.oauth_client.source_types.twitter
- passbook.oauth_client.source_types.azure_ad
saml_idp:
signing: true
autosubmit: false
issuer: passbook
assertion_valid_for: 86400
# List of python packages with provider types to load.
types:
- passbook.saml_idp.processors.generic
- passbook.saml_idp.processors.salesforce

View file

@ -29,9 +29,13 @@ spec:
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command:
- ./manage.py
- celery
args:
- worker
- --autoscale=10,3
- -E
- -B
- -A passbook.root.celery
envFrom:
- configMapRef:
name: {{ include "passbook.fullname" . }}-config

View file

@ -3,8 +3,8 @@ from django.core.cache import cache
from django.shortcuts import redirect, reverse
from django.views.generic import TemplateView
from passbook import __version__
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core import __version__
from passbook.core.models import (Application, Factor, Invitation, Policy,
Provider, Source, User)
from passbook.root.celery import CELERY_APP

View file

@ -12,7 +12,7 @@ from passbook.admin.forms.policies import PolicyTestForm
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Policy
from passbook.lib.utils.reflection import path_to_class
from passbook.policy.engine import PolicyEngine
from passbook.policies.engine import PolicyEngine
class PolicyListView(AdminRequiredMixin, ListView):

Binary file not shown.

View file

@ -1,11 +0,0 @@
"""passbook Application Security Gateway app"""
from django.apps import AppConfig
class PassbookApplicationApplicationGatewayConfig(AppConfig):
"""passbook app_gw app"""
name = 'passbook.app_gw'
label = 'passbook_app_gw'
verbose_name = 'passbook Application Security Gateway'
# mountpoint = 'app_gw/'

Binary file not shown.

View file

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-21 15:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_app_gw', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='rewriterule',
name='conditions',
field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 2.2 on 2019-04-11 13:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_app_gw', '0002_auto_20190321_1521'),
]
operations = [
migrations.AlterField(
model_name='applicationgatewayprovider',
name='authentication_header',
field=models.TextField(blank=True, default='X-Remote-User'),
),
]

View file

@ -1,7 +1,8 @@
# Generated by Django 2.1.7 on 2019-02-16 09:13
# Generated by Django 2.2.6 on 2019-10-07 14:07
import uuid
import django.contrib.postgres.fields.jsonb
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
@ -23,7 +24,7 @@ class Migration(migrations.Migration):
('action', models.TextField(choices=[('login', 'login'), ('login_failed', 'login_failed'), ('logout', 'logout'), ('authorize_application', 'authorize_application'), ('suspicious_request', 'suspicious_request'), ('sign_up', 'sign_up'), ('password_reset', 'password_reset'), ('invitation_created', 'invitation_created'), ('invitation_used', 'invitation_used')])),
('date', models.DateTimeField(auto_now_add=True)),
('app', models.TextField()),
('_context', models.TextField()),
('context', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
('request_ip', models.GenericIPAddressField()),
('created', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
@ -33,19 +34,4 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'Audit Entries',
},
),
migrations.CreateModel(
name='LoginAttempt',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('target_uid', models.CharField(max_length=254)),
('request_ip', models.GenericIPAddressField()),
('attempts', models.IntegerField(default=1)),
],
),
migrations.AlterUniqueTogether(
name='loginattempt',
unique_together={('target_uid', 'request_ip', 'created')},
),
]

View file

@ -1,18 +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_audit', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='loginattempt',
name='created',
field=models.DateTimeField(auto_now_add=True),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-21 12:40
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0002_auto_20190221_1201'),
]
operations = [
migrations.RemoveField(
model_name='auditentry',
name='_context',
),
migrations.AddField(
model_name='auditentry',
name='context',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
),
]

View file

@ -1,16 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-08 14:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0003_auto_20190221_1240'),
]
operations = [
migrations.DeleteModel(
name='LoginAttempt',
),
]

View file

@ -1,10 +0,0 @@
"""passbook captcha app"""
from django.apps import AppConfig
class PassbookCaptchaFactorConfig(AppConfig):
"""passbook captcha app"""
name = 'passbook.captcha_factor'
label = 'passbook_captcha_factor'
verbose_name = 'passbook Captcha'

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

@ -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

@ -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,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,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,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

@ -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

View file

@ -1,21 +1,25 @@
"""passbook multi-factor authentication engine"""
from django.forms import ModelForm
from django.http import HttpRequest
from django.utils.translation import gettext as _
from django.views.generic import TemplateView
from passbook.core.models import User
from passbook.factors.view import AuthenticationView
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
form: ModelForm = None
required: bool = True
authenticator: AuthenticationView = None
pending_user: User = None
request: HttpRequest = None
template_name = 'login/form_with_user.html'
def __init__(self, authenticator):
def __init__(self, authenticator: AuthenticationView):
self.authenticator = authenticator
def get_context_data(self, **kwargs):

View file

@ -0,0 +1,10 @@
"""passbook captcha app"""
from django.apps import AppConfig
class PassbookFactorCaptchaConfig(AppConfig):
"""passbook captcha app"""
name = 'passbook.factors.captcha'
label = 'passbook_factors_captcha'
verbose_name = 'passbook Factors.Captcha'

View file

@ -2,8 +2,8 @@
from django.views.generic import FormView
from passbook.captcha_factor.forms import CaptchaForm
from passbook.core.auth.factor import AuthenticationFactor
from passbook.factors.base import AuthenticationFactor
from passbook.factors.captcha.forms import CaptchaForm
class CaptchaFactor(FormView, AuthenticationFactor):
@ -16,6 +16,7 @@ class CaptchaFactor(FormView, AuthenticationFactor):
def get_form(self, form_class=None):
form = CaptchaForm(**self.get_form_kwargs())
# TODO: uuuhm
form.fields['captcha'].public_key = '6Lfi1w8TAAAAAELH-YiWp0OFItmMzvjGmw2xkvUN'
form.fields['captcha'].private_key = '6Lfi1w8TAAAAAMQI3f86tGMvd1QkcqqVQyBWI23D'
form.fields['captcha'].widget.attrs["data-sitekey"] = form.fields['captcha'].public_key

View file

@ -2,8 +2,8 @@
from captcha.fields import ReCaptchaField
from django import forms
from passbook.captcha_factor.models import CaptchaFactor
from passbook.core.forms.factors import GENERAL_FIELDS
from passbook.factors.captcha.models import CaptchaFactor
from passbook.factors.forms import GENERAL_FIELDS
class CaptchaForm(forms.Form):

View file

@ -1,4 +1,4 @@
# Generated by Django 2.1.7 on 2019-02-24 21:35
# Generated by Django 2.2.6 on 2019-10-07 14:07
import django.db.models.deletion
from django.db import migrations, models
@ -9,7 +9,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('passbook_core', '0010_auto_20190224_1016'),
('passbook_core', '0001_initial'),
]
operations = [

View file

@ -11,11 +11,11 @@ class CaptchaFactor(Factor):
public_key = models.TextField()
private_key = models.TextField()
type = 'passbook.captcha_factor.factor.CaptchaFactor'
form = 'passbook.captcha_factor.forms.CaptchaFactorForm'
type = 'passbook.factors.captcha.factor.CaptchaFactor'
form = 'passbook.factors.captcha.forms.CaptchaFactorForm'
def __str__(self):
return "Captcha Factor %s" % self.slug
return f"Captcha Factor {self.slug}"
class Meta:

View file

@ -1,6 +1,8 @@
"""passbook captcha_facot settings"""
"""passbook captcha_factor settings"""
# https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do
RECAPTCHA_PUBLIC_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'
RECAPTCHA_PRIVATE_KEY = '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe'
NOCAPTCHA = True
INSTALLED_APPS = [
'captcha'

View file

@ -0,0 +1,5 @@
"""dummy factor admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_factors_dummy')

View file

@ -0,0 +1,11 @@
"""passbook dummy factor config"""
from django.apps import AppConfig
class PassbookFactorDummyConfig(AppConfig):
"""passbook dummy factor config"""
name = 'passbook.factors.dummy'
label = 'passbook_factors_dummy'
verbose_name = 'passbook Factors.Dummy'

View file

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

View file

@ -0,0 +1,21 @@
"""passbook administration forms"""
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from passbook.factors.dummy.models import DummyFactor
from passbook.factors.forms import GENERAL_FIELDS
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

@ -0,0 +1,27 @@
# Generated by Django 2.2.6 on 2019-10-07 14:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('passbook_core', '0001_initial'),
]
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={
'verbose_name': 'Dummy Factor',
'verbose_name_plural': 'Dummy Factors',
},
bases=('passbook_core.factor',),
),
]

View file

@ -0,0 +1,19 @@
"""dummy factor models"""
from django.utils.translation import gettext as _
from passbook.core.models import Factor
class DummyFactor(Factor):
"""Dummy factor, mostly used to debug"""
type = 'passbook.factors.dummy.factor.DummyFactor'
form = 'passbook.factors.dummy.forms.DummyFactorForm'
def __str__(self):
return f"Dummy Factor {self.slug}"
class Meta:
verbose_name = _('Dummy Factor')
verbose_name_plural = _('Dummy Factors')

View file

@ -0,0 +1,3 @@
"""factor forms"""
GENERAL_FIELDS = ['name', 'slug', 'order', 'policies', 'enabled']

View file

@ -0,0 +1,12 @@
"""passbook OTP AppConfig"""
from django.apps.config import AppConfig
class PassbookFactorOTPConfig(AppConfig):
"""passbook OTP AppConfig"""
name = 'passbook.factors.otp'
label = 'passbook_factors_otp'
verbose_name = 'passbook Factors.OTP'
mountpoint = 'user/otp/'

View file

@ -5,9 +5,9 @@ from django.views.generic import FormView
from django_otp import match_token, user_has_device
from structlog import get_logger
from passbook.core.auth.factor import AuthenticationFactor
from passbook.otp.forms import OTPVerifyForm
from passbook.otp.views import OTP_SETTING_UP_KEY, EnableView
from passbook.factors.base import AuthenticationFactor
from passbook.factors.otp.forms import OTPVerifyForm
from passbook.factors.otp.views import OTP_SETTING_UP_KEY, EnableView
LOGGER = get_logger()

View file

@ -5,9 +5,10 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
from django.core.validators import RegexValidator
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django_otp.models import Device
from passbook.core.forms.factors import GENERAL_FIELDS
from passbook.otp.models import OTPFactor
from passbook.factors.forms import GENERAL_FIELDS
from passbook.factors.otp.models import OTPFactor
OTP_CODE_VALIDATOR = RegexValidator(r'^[0-9a-z]{6,8}$',
_('Only alpha-numeric characters are allowed.'))
@ -17,7 +18,7 @@ class PictureWidget(forms.widgets.Widget):
"""Widget to render value as img-tag"""
def render(self, name, value, attrs=None, renderer=None):
return mark_safe("<img src=\"%s\" />" % value) # nosec
return mark_safe(f'<img src="{value}" />') # nosec
class OTPVerifyForm(forms.Form):
@ -33,13 +34,14 @@ class OTPVerifyForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# This is a little helper so the field is focused by default
# TODO: Tell browser to not suggest any values
self.fields['code'].widget.attrs.update({'autofocus': 'autofocus'})
class OTPSetupForm(forms.Form):
"""OTP Setup form"""
title = _('Set up OTP')
device = None
device: Device = None
qr_code = forms.CharField(widget=PictureWidget, disabled=True, required=False,
label=_('Scan this Code with your OTP App.'))
code = forms.CharField(label=_('Code'), validators=[OTP_CODE_VALIDATOR],

View file

@ -1,4 +1,4 @@
# Generated by Django 2.1.7 on 2019-02-25 09:42
# Generated by Django 2.2.6 on 2019-10-07 14:07
import django.db.models.deletion
from django.db import migrations, models
@ -9,7 +9,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('passbook_core', '0010_auto_20190224_1016'),
('passbook_core', '0001_initial'),
]
operations = [

View file

@ -12,14 +12,14 @@ class OTPFactor(Factor):
enforced = models.BooleanField(default=False, help_text=('Enforce enabled OTP for Users '
'this factor applies to.'))
type = 'passbook.otp.factors.OTPFactor'
form = 'passbook.otp.forms.OTPFactorForm'
type = 'passbook.factors.otp.factors.OTPFactor'
form = 'passbook.factors.otp.forms.OTPFactorForm'
def has_user_settings(self):
return _('OTP'), 'pficon-locked', 'passbook_otp:otp-user-settings'
def __str__(self):
return "OTP Factor %s" % self.slug
return f"OTP Factor {self.slug}"
class Meta:

View file

@ -2,7 +2,7 @@
from django.urls import path
from passbook.otp import views
from passbook.factors.otp import views
urlpatterns = [
path('', views.UserSettingsView.as_view(), name='otp-user-settings'),

View file

@ -16,10 +16,10 @@ from qrcode import make
from qrcode.image.svg import SvgPathImage
from structlog import get_logger
from passbook.factors.otp.forms import OTPSetupForm
from passbook.factors.otp.utils import otpauth_url
from passbook.lib.boilerplate import NeverCacheMixin
from passbook.lib.config import CONFIG
from passbook.otp.forms import OTPSetupForm
from passbook.otp.utils import otpauth_url
OTP_SESSION_KEY = 'passbook_otp_key'
OTP_SETTING_UP_KEY = 'passbook_otp_setup'

View file

@ -0,0 +1,5 @@
"""password factor admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_factors_password')

View file

@ -0,0 +1,15 @@
"""passbook core app config"""
from importlib import import_module
from django.apps import AppConfig
class PassbookFactorPasswordConfig(AppConfig):
"""passbook password factor config"""
name = 'passbook.factors.password'
label = 'passbook_factors_password'
verbose_name = 'passbook Factors.Password'
def ready(self):
import_module('passbook.factors.password.signals')

View file

@ -1,4 +1,4 @@
"""passbook core exceptions"""
"""passbook password policy exceptions"""
class PasswordPolicyInvalid(Exception):
"""Exception raised when a Password Policy fails"""

View file

@ -11,11 +11,11 @@ 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.factors.base import AuthenticationFactor
from passbook.factors.view import AuthenticationView
from passbook.lib.config import CONFIG
from passbook.lib.utils.reflection import path_to_class
@ -24,6 +24,7 @@ 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)()

View file

@ -4,10 +4,10 @@ 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.factors.forms import GENERAL_FIELDS
from passbook.factors.password.models import 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"""
@ -15,6 +15,7 @@ def get_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"""
@ -30,16 +31,3 @@ class PasswordFactorForm(forms.ModelForm):
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

@ -0,0 +1,30 @@
# Generated by Django 2.2.6 on 2019-10-07 14:07
import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('passbook_core', '0001_initial'),
]
operations = [
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)),
('password_policies', models.ManyToManyField(blank=True, to='passbook_core.Policy')),
],
options={
'verbose_name': 'Password Factor',
'verbose_name_plural': 'Password Factors',
},
bases=('passbook_core.factor',),
),
]

View file

@ -1,11 +1,11 @@
# Generated by Django 2.1.7 on 2019-02-26 08:50
# Generated by Django 2.2.6 on 2019-10-07 14:11
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")
PasswordFactor = apps.get_model("passbook_factors_password", "PasswordFactor")
if not PasswordFactor.objects.exists():
PasswordFactor.objects.create(
name='password',
@ -17,7 +17,7 @@ def create_initial_factor(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0013_invitation_needs_confirmation'),
('passbook_factors_password', '0001_initial'),
]
operations = [

View file

@ -0,0 +1,34 @@
"""password factor models"""
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext_lazy as _
from passbook.core.models import Factor, Policy, User
class PasswordFactor(Factor):
"""Password-based Django-backend Authentication Factor"""
backends = ArrayField(models.TextField())
password_policies = models.ManyToManyField(Policy, blank=True)
type = 'passbook.factors.password.factor.PasswordFactor'
form = 'passbook.factors.password.forms.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')

View file

@ -0,0 +1,20 @@
"""passbook password factor signals"""
from django.dispatch import receiver
from passbook.core.signals import password_changed
from passbook.factors.password.exceptions import PasswordPolicyInvalid
@receiver(password_changed)
def password_policy_checker(sender, password, **_):
"""Run password through all password policies which are applied to the user"""
from passbook.factors.password.models import PasswordFactor
from passbook.policies.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)

View file

@ -7,8 +7,10 @@ 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
from passbook.core.models import User
from passbook.factors.dummy.models import DummyFactor
from passbook.factors.password.models import PasswordFactor
from passbook.factors.view import AuthenticationView
class TestFactorAuthentication(TestCase):

View file

@ -12,7 +12,7 @@ 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
from passbook.policies.engine import PolicyEngine
LOGGER = get_logger()
@ -23,6 +23,10 @@ def _redirect_with_qs(view, get_query_set=None):
target += '?' + urlencode({key: value for key, value in get_query_set.items()})
return redirect(target)
# Argument used to redirect user after login
NEXT_ARG_NAME = 'next'
class AuthenticationView(UserPassesTestMixin, View):
"""Wizard-like Multi-factor authenticator"""
@ -45,8 +49,8 @@ class AuthenticationView(UserPassesTestMixin, View):
def handle_no_permission(self):
# Function from UserPassesTestMixin
if 'next' in self.request.GET:
return redirect(self.request.GET.get('next'))
if NEXT_ARG_NAME in self.request.GET:
return redirect(self.request.GET.get(NEXT_ARG_NAME))
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)
@ -147,7 +151,7 @@ class AuthenticationView(UserPassesTestMixin, View):
LOGGER.debug("Logged in", user=self.pending_user)
# Cleanup
self.cleanup()
next_param = self.request.GET.get('next', None)
next_param = self.request.GET.get(NEXT_ARG_NAME, None)
if next_param and not is_url_absolute(next_param):
return redirect(next_param)
return _redirect_with_qs('passbook_core:overview')

View file

@ -1,11 +0,0 @@
"""Passbook hibp app config"""
from django.apps import AppConfig
class PassbookHIBPConfig(AppConfig):
"""Passbook hibp app config"""
name = 'passbook.hibp_policy'
label = 'passbook_hibp_policy'
verbose_name = 'passbook HaveIBeenPwned Policy'

View file

@ -1,17 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-25 19:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_hibp_policy', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='haveibeenpwendpolicy',
options={'verbose_name': 'have i been pwned Policy', 'verbose_name_plural': 'have i been pwned Policies'},
),
]

Some files were not shown because too many files have changed in this diff Show more