*(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 # create_users: true
# # Reset LDAP password when user reset their password # # Reset LDAP password when user reset their password
# reset_password: true # 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: saml_idp:
signing: true signing: true
autosubmit: false autosubmit: false
issuer: passbook issuer: passbook
assertion_valid_for: 86400 assertion_valid_for: 86400
# List of python packages with provider types to load. # 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 }}" image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: command:
- ./manage.py - celery
args: args:
- worker - worker
- --autoscale=10,3
- -E
- -B
- -A passbook.root.celery
envFrom: envFrom:
- configMapRef: - configMapRef:
name: {{ include "passbook.fullname" . }}-config 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.shortcuts import redirect, reverse
from django.views.generic import TemplateView from django.views.generic import TemplateView
from passbook import __version__
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core import __version__
from passbook.core.models import (Application, Factor, Invitation, Policy, from passbook.core.models import (Application, Factor, Invitation, Policy,
Provider, Source, User) Provider, Source, User)
from passbook.root.celery import CELERY_APP 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.admin.mixins import AdminRequiredMixin
from passbook.core.models import Policy from passbook.core.models import Policy
from passbook.lib.utils.reflection import path_to_class 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): 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 uuid
import django.contrib.postgres.fields.jsonb
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models 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')])), ('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)), ('date', models.DateTimeField(auto_now_add=True)),
('app', models.TextField()), ('app', models.TextField()),
('_context', models.TextField()), ('context', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
('request_ip', models.GenericIPAddressField()), ('request_ip', models.GenericIPAddressField()),
('created', models.DateTimeField(auto_now_add=True)), ('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)), ('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', '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 importlib import import_module
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings
from structlog import get_logger from structlog import get_logger
from passbook.lib.config import CONFIG
LOGGER = get_logger() LOGGER = get_logger()
class PassbookCoreConfig(AppConfig): class PassbookCoreConfig(AppConfig):
"""passbook core app config""" """passbook core app config"""
@ -17,9 +17,7 @@ class PassbookCoreConfig(AppConfig):
mountpoint = '' mountpoint = ''
def ready(self): def ready(self):
import_module('passbook.policy.engine') for factors_to_load in settings.PASSBOOK_CORE_FACTORS:
factors_to_load = CONFIG.y('passbook.factors', [])
for factors_to_load in factors_to_load:
try: try:
import_module(factors_to_load) import_module(factors_to_load)
LOGGER.info("Loaded factor", factor_class=factors_to_load) LOGGER.info("Loaded factor", factor_class=factors_to_load)

View File

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

View File

@ -3,40 +3,8 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import (DebugPolicy, FieldMatcherPolicy, from passbook.core.models import DebugPolicy
GroupMembershipPolicy, PasswordPolicy, from passbook.policies.forms import GENERAL_FIELDS
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(),
}
class DebugPolicyForm(forms.ModelForm): class DebugPolicyForm(forms.ModelForm):
@ -52,49 +20,3 @@ class DebugPolicyForm(forms.ModelForm):
labels = { labels = {
'result': _('Allow user') '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 uuid
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
import django.contrib.postgres.fields.jsonb
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import passbook.core.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('auth', '0009_alter_user_last_name_max_length'), ('auth', '0011_update_proxy_permissions'),
] ]
operations = [ 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')), ('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')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False)), ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
('name', models.TextField()),
('password_change_date', models.DateTimeField(auto_now_add=True)),
], ],
options={ options={
'verbose_name': 'user', 'verbose_name': 'user',
@ -44,39 +49,17 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()), ('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( migrations.CreateModel(
name='Policy', name='Policy',
fields=[ fields=[
('created', models.DateField(auto_now_add=True)), ('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)), ('last_updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField(blank=True, null=True)), ('name', models.TextField(blank=True, null=True)),
('action', models.CharField(choices=[('allow', 'allow'), ('deny', 'deny')], max_length=20)), ('action', models.CharField(choices=[('allow', 'allow'), ('deny', 'deny')], max_length=20)),
('negate', models.BooleanField(default=False)), ('negate', models.BooleanField(default=False)),
('order', models.IntegerField(default=0)), ('order', models.IntegerField(default=0)),
('timeout', models.IntegerField(default=30)),
], ],
options={ options={
'abstract': False, 'abstract': False,
@ -85,28 +68,136 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='PolicyModel', name='PolicyModel',
fields=[ fields=[
('created', models.DateField(auto_now_add=True)), ('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)), ('last_updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('policies', models.ManyToManyField(blank=True, to='passbook_core.Policy')),
], ],
options={ options={
'abstract': False, '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( migrations.CreateModel(
name='Provider', name='Provider',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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( migrations.CreateModel(
name='UserSourceConnection', name='UserSourceConnection',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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)), ('last_updated', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('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( migrations.CreateModel(
name='Application', name='Application',
@ -124,131 +215,9 @@ class Migration(migrations.Migration):
}, },
bases=('passbook_core.policymodel',), 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( migrations.AddField(
model_name='user', model_name='user',
name='sources', name='sources',
field=models.ManyToManyField(through='passbook_core.UserSourceConnection', to='passbook_core.Source'), 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""" """passbook core models"""
import re
from datetime import timedelta from datetime import timedelta
from random import SystemRandom from random import SystemRandom
from time import sleep from time import sleep
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import ArrayField, HStoreField from django.contrib.postgres.fields import JSONField
from django.db import models from django.db import models
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.timezone import now from django.utils.timezone import now
@ -16,8 +15,8 @@ from structlog import get_logger
from passbook.core.signals import password_changed from passbook.core.signals import password_changed
from passbook.lib.models import CreatedUpdatedModel, UUIDModel from passbook.lib.models import CreatedUpdatedModel, UUIDModel
from passbook.policy.exceptions import PolicyException from passbook.policies.exceptions import PolicyException
from passbook.policy.struct import PolicyRequest, PolicyResult from passbook.policies.struct import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()
@ -32,10 +31,10 @@ class Group(UUIDModel):
name = models.CharField(_('name'), max_length=80) name = models.CharField(_('name'), max_length=80)
parent = models.ForeignKey('Group', blank=True, null=True, parent = models.ForeignKey('Group', blank=True, null=True,
on_delete=models.SET_NULL, related_name='children') on_delete=models.SET_NULL, related_name='children')
tags = HStoreField(default=dict) tags = JSONField(default=dict, blank=True)
def __str__(self): def __str__(self):
return "Group %s" % self.name return f"Group {self.name}"
class Meta: class Meta:
@ -94,48 +93,8 @@ class Factor(PolicyModel):
return False return False
def __str__(self): 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): class Application(PolicyModel):
"""Every Application which uses passbook for authentication/identification/authorization """Every Application which uses passbook for authentication/identification/authorization
@ -161,6 +120,7 @@ class Application(PolicyModel):
def __str__(self): def __str__(self):
return self.name return self.name
class Source(PolicyModel): class Source(PolicyModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
@ -196,6 +156,7 @@ class Source(PolicyModel):
def __str__(self): def __str__(self):
return self.name return self.name
class UserSourceConnection(CreatedUpdatedModel): class UserSourceConnection(CreatedUpdatedModel):
"""Connection between User and Source.""" """Connection between User and Source."""
@ -206,6 +167,7 @@ class UserSourceConnection(CreatedUpdatedModel):
unique_together = (('user', 'source'),) unique_together = (('user', 'source'),)
class Policy(UUIDModel, CreatedUpdatedModel): class Policy(UUIDModel, CreatedUpdatedModel):
"""Policies which specify if a user is authorized to use an Application. Can be overridden by """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.""" other types to add other fields, more logic, etc."""
@ -228,148 +190,12 @@ class Policy(UUIDModel, CreatedUpdatedModel):
def __str__(self): def __str__(self):
if self.name: if self.name:
return 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: def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if user instance passes this policy""" """Check if user instance passes this policy"""
raise PolicyException() 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): class DebugPolicy(Policy):
"""Policy used for debugging the PolicyEngine. Returns a fixed result, """Policy used for debugging the PolicyEngine. Returns a fixed result,
@ -393,36 +219,6 @@ class DebugPolicy(Policy):
verbose_name = _('Debug Policy') verbose_name = _('Debug Policy')
verbose_name_plural = _('Debug Policies') 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): class Invitation(UUIDModel):
"""Single-use invitation link""" """Single-use invitation link"""
@ -436,10 +232,10 @@ class Invitation(UUIDModel):
@property @property
def link(self): def link(self):
"""Get link to use invitation""" """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): 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: class Meta:
@ -454,7 +250,7 @@ class Nonce(UUIDModel):
expiring = models.BooleanField(default=True) expiring = models.BooleanField(default=True)
def __str__(self): 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: class Meta:
@ -470,7 +266,7 @@ class PropertyMapping(UUIDModel):
objects = InheritanceManager() objects = InheritanceManager()
def __str__(self): def __str__(self):
return "Property Mapping %s" % self.name return f"Property Mapping {self.name}"
class Meta: 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 django.dispatch import receiver
from structlog import get_logger from structlog import get_logger
from passbook.core.exceptions import PasswordPolicyInvalid
LOGGER = get_logger() LOGGER = get_logger()
user_signed_up = Signal(providing_args=['request', 'user']) 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']) invitation_used = Signal(providing_args=['request', 'invitation', 'user'])
password_changed = Signal(providing_args=['user', 'password']) 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) @receiver(post_save)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def invalidate_policy_cache(sender, instance, **kwargs): def invalidate_policy_cache(sender, instance, **_):
"""Invalidate Policy cache when policy is updated""" """Invalidate Policy cache when policy is updated"""
from passbook.core.models import Policy from passbook.core.models import Policy
if isinstance(instance, Policy): if isinstance(instance, Policy):

View File

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

View File

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

View File

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

View File

@ -12,12 +12,12 @@ from django.views import View
from django.views.generic import FormView from django.views.generic import FormView
from structlog import get_logger 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.forms.authentication import LoginForm, SignUpForm
from passbook.core.models import Invitation, Nonce, Source, User from passbook.core.models import Invitation, Nonce, Source, User
from passbook.core.signals import invitation_used, user_signed_up from passbook.core.signals import invitation_used, user_signed_up
from passbook.core.tasks import send_email 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 from passbook.lib.config import CONFIG
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -4,7 +4,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView from django.views.generic import TemplateView
from passbook.core.models import Application from passbook.core.models import Application
from passbook.policy.engine import PolicyEngine from passbook.policies.engine import PolicyEngine
class OverviewView(LoginRequiredMixin, TemplateView): 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.utils.translation import gettext as _
from django.views.generic import DeleteView, FormView, UpdateView from django.views.generic import DeleteView, FormView, UpdateView
from passbook.core.exceptions import PasswordPolicyInvalid
from passbook.core.forms.users import PasswordChangeForm, UserDetailForm from passbook.core.forms.users import PasswordChangeForm, UserDetailForm
from passbook.factors.password.exceptions import PasswordPolicyInvalid
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG

View File

@ -1,21 +1,25 @@
"""passbook multi-factor authentication engine""" """passbook multi-factor authentication engine"""
from django.forms import ModelForm
from django.http import HttpRequest
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
from passbook.core.models import User
from passbook.factors.view import AuthenticationView
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
class AuthenticationFactor(TemplateView): class AuthenticationFactor(TemplateView):
"""Abstract Authentication factor, inherits TemplateView but can be combined with FormView""" """Abstract Authentication factor, inherits TemplateView but can be combined with FormView"""
form = None form: ModelForm = None
required = True required: bool = True
authenticator = None authenticator: AuthenticationView = None
pending_user = None pending_user: User = None
request = None request: HttpRequest = None
template_name = 'login/form_with_user.html' template_name = 'login/form_with_user.html'
def __init__(self, authenticator): def __init__(self, authenticator: AuthenticationView):
self.authenticator = authenticator self.authenticator = authenticator
def get_context_data(self, **kwargs): 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 django.views.generic import FormView
from passbook.captcha_factor.forms import CaptchaForm from passbook.factors.base import AuthenticationFactor
from passbook.core.auth.factor import AuthenticationFactor from passbook.factors.captcha.forms import CaptchaForm
class CaptchaFactor(FormView, AuthenticationFactor): class CaptchaFactor(FormView, AuthenticationFactor):
@ -16,6 +16,7 @@ class CaptchaFactor(FormView, AuthenticationFactor):
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = CaptchaForm(**self.get_form_kwargs()) form = CaptchaForm(**self.get_form_kwargs())
# TODO: uuuhm
form.fields['captcha'].public_key = '6Lfi1w8TAAAAAELH-YiWp0OFItmMzvjGmw2xkvUN' form.fields['captcha'].public_key = '6Lfi1w8TAAAAAELH-YiWp0OFItmMzvjGmw2xkvUN'
form.fields['captcha'].private_key = '6Lfi1w8TAAAAAMQI3f86tGMvd1QkcqqVQyBWI23D' form.fields['captcha'].private_key = '6Lfi1w8TAAAAAMQI3f86tGMvd1QkcqqVQyBWI23D'
form.fields['captcha'].widget.attrs["data-sitekey"] = form.fields['captcha'].public_key form.fields['captcha'].widget.attrs["data-sitekey"] = form.fields['captcha'].public_key

View File

@ -2,8 +2,8 @@
from captcha.fields import ReCaptchaField from captcha.fields import ReCaptchaField
from django import forms from django import forms
from passbook.captcha_factor.models import CaptchaFactor from passbook.factors.captcha.models import CaptchaFactor
from passbook.core.forms.factors import GENERAL_FIELDS from passbook.factors.forms import GENERAL_FIELDS
class CaptchaForm(forms.Form): 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 import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -9,7 +9,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('passbook_core', '0010_auto_20190224_1016'), ('passbook_core', '0001_initial'),
] ]
operations = [ operations = [

View File

@ -11,11 +11,11 @@ class CaptchaFactor(Factor):
public_key = models.TextField() public_key = models.TextField()
private_key = models.TextField() private_key = models.TextField()
type = 'passbook.captcha_factor.factor.CaptchaFactor' type = 'passbook.factors.captcha.factor.CaptchaFactor'
form = 'passbook.captcha_factor.forms.CaptchaFactorForm' form = 'passbook.factors.captcha.forms.CaptchaFactorForm'
def __str__(self): def __str__(self):
return "Captcha Factor %s" % self.slug return f"Captcha Factor {self.slug}"
class Meta: 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_PUBLIC_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'
RECAPTCHA_PRIVATE_KEY = '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe' RECAPTCHA_PRIVATE_KEY = '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe'
NOCAPTCHA = True NOCAPTCHA = True
INSTALLED_APPS = [ INSTALLED_APPS = [
'captcha' '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""" """passbook multi-factor authentication engine"""
from structlog import get_logger from django.http import HttpRequest
from passbook.core.auth.factor import AuthenticationFactor from passbook.factors.base import AuthenticationFactor
LOGGER = get_logger()
class DummyFactor(AuthenticationFactor): class DummyFactor(AuthenticationFactor):
"""Dummy factor for testing with multiple factors""" """Dummy factor for testing with multiple factors"""
def post(self, request): def post(self, request: HttpRequest):
"""Just redirect to next factor""" """Just redirect to next factor"""
return self.authenticator.user_ok() 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 django_otp import match_token, user_has_device
from structlog import get_logger from structlog import get_logger
from passbook.core.auth.factor import AuthenticationFactor from passbook.factors.base import AuthenticationFactor
from passbook.otp.forms import OTPVerifyForm from passbook.factors.otp.forms import OTPVerifyForm
from passbook.otp.views import OTP_SETTING_UP_KEY, EnableView from passbook.factors.otp.views import OTP_SETTING_UP_KEY, EnableView
LOGGER = get_logger() LOGGER = get_logger()

View File

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

View File

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

View File

@ -2,7 +2,7 @@
from django.urls import path from django.urls import path
from passbook.otp import views from passbook.factors.otp import views
urlpatterns = [ urlpatterns = [
path('', views.UserSettingsView.as_view(), name='otp-user-settings'), 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 qrcode.image.svg import SvgPathImage
from structlog import get_logger 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.boilerplate import NeverCacheMixin
from passbook.lib.config import CONFIG 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_SESSION_KEY = 'passbook_otp_key'
OTP_SETTING_UP_KEY = 'passbook_otp_setup' 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): class PasswordPolicyInvalid(Exception):
"""Exception raised when a Password Policy fails""" """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 django.views.generic import FormView
from structlog import get_logger 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.forms.authentication import PasswordFactorForm
from passbook.core.models import Nonce from passbook.core.models import Nonce
from passbook.core.tasks import send_email 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.config import CONFIG
from passbook.lib.utils.reflection import path_to_class from passbook.lib.utils.reflection import path_to_class
@ -24,6 +24,7 @@ LOGGER = get_logger()
def authenticate(request, backends, **credentials): def authenticate(request, backends, **credentials):
"""If the given credentials are valid, return a User object. """If the given credentials are valid, return a User object.
Customized version of django's authenticate, which accepts a list of backends""" Customized version of django's authenticate, which accepts a list of backends"""
for backend_path in backends: for backend_path in backends:
backend = path_to_class(backend_path)() 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.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _ 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 from passbook.lib.utils.reflection import path_to_class
GENERAL_FIELDS = ['name', 'slug', 'order', 'policies', 'enabled']
def get_authentication_backends(): def get_authentication_backends():
"""Return all available authentication backends as tuple set""" """Return all available authentication backends as tuple set"""
@ -15,6 +15,7 @@ def get_authentication_backends():
klass = path_to_class(backend) klass = path_to_class(backend)
yield backend, getattr(klass(), 'name', '%s (%s)' % (klass.__name__, klass.__module__)) yield backend, getattr(klass(), 'name', '%s (%s)' % (klass.__name__, klass.__module__))
class PasswordFactorForm(forms.ModelForm): class PasswordFactorForm(forms.ModelForm):
"""Form to create/edit Password Factors""" """Form to create/edit Password Factors"""
@ -30,16 +31,3 @@ class PasswordFactorForm(forms.ModelForm):
choices=get_authentication_backends()), choices=get_authentication_backends()),
'password_policies': FilteredSelectMultiple(_('password policies'), False), '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 from django.db import migrations
def create_initial_factor(apps, schema_editor): def create_initial_factor(apps, schema_editor):
"""Create initial PasswordFactor if none exists""" """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(): if not PasswordFactor.objects.exists():
PasswordFactor.objects.create( PasswordFactor.objects.create(
name='password', name='password',
@ -17,7 +17,7 @@ def create_initial_factor(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('passbook_core', '0013_invitation_needs_confirmation'), ('passbook_factors_password', '0001_initial'),
] ]
operations = [ 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.test import RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
from passbook.core.auth.view import AuthenticationView from passbook.core.models import User
from passbook.core.models import DummyFactor, PasswordFactor, User from passbook.factors.dummy.models import DummyFactor
from passbook.factors.password.models import PasswordFactor
from passbook.factors.view import AuthenticationView
class TestFactorAuthentication(TestCase): 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.core.views.utils import PermissionDeniedView
from passbook.lib.utils.reflection import class_to_path, path_to_class from passbook.lib.utils.reflection import class_to_path, path_to_class
from passbook.lib.utils.urls import is_url_absolute from passbook.lib.utils.urls import is_url_absolute
from passbook.policy.engine import PolicyEngine from passbook.policies.engine import PolicyEngine
LOGGER = get_logger() 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()}) target += '?' + urlencode({key: value for key, value in get_query_set.items()})
return redirect(target) return redirect(target)
# Argument used to redirect user after login
NEXT_ARG_NAME = 'next'
class AuthenticationView(UserPassesTestMixin, View): class AuthenticationView(UserPassesTestMixin, View):
"""Wizard-like Multi-factor authenticator""" """Wizard-like Multi-factor authenticator"""
@ -45,8 +49,8 @@ class AuthenticationView(UserPassesTestMixin, View):
def handle_no_permission(self): def handle_no_permission(self):
# Function from UserPassesTestMixin # Function from UserPassesTestMixin
if 'next' in self.request.GET: if NEXT_ARG_NAME in self.request.GET:
return redirect(self.request.GET.get('next')) return redirect(self.request.GET.get(NEXT_ARG_NAME))
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
return _redirect_with_qs('passbook_core:overview', self.request.GET) return _redirect_with_qs('passbook_core:overview', self.request.GET)
return _redirect_with_qs('passbook_core:auth-login', 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) LOGGER.debug("Logged in", user=self.pending_user)
# Cleanup # Cleanup
self.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): if next_param and not is_url_absolute(next_param):
return redirect(next_param) return redirect(next_param)
return _redirect_with_qs('passbook_core:overview') 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