Compare commits

...

12 Commits

29 changed files with 221 additions and 39 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.0.2-alpha current_version = 0.0.6-alpha
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -20,6 +20,8 @@ values =
[bumpversion:file:passbook/__init__.py] [bumpversion:file:passbook/__init__.py]
[bumpversion:file:passbook/api/__init__.py]
[bumpversion:file:passbook/core/__init__.py] [bumpversion:file:passbook/core/__init__.py]
[bumpversion:file:passbook/admin/__init__.py] [bumpversion:file:passbook/admin/__init__.py]
@ -30,6 +32,8 @@ values =
[bumpversion:file:passbook/ldap/__init__.py] [bumpversion:file:passbook/ldap/__init__.py]
[bumpversion:file:passbook/lib/__init__.py]
[bumpversion:file:passbook/saml_idp/__init__.py] [bumpversion:file:passbook/saml_idp/__init__.py]
[bumpversion:file:passbook/audit/__init__.py] [bumpversion:file:passbook/audit/__init__.py]

2
.gitignore vendored
View File

@ -189,5 +189,5 @@ pyvenv.cfg
pip-selfcheck.json pip-selfcheck.json
# End of https://www.gitignore.io/api/python,django # End of https://www.gitignore.io/api/python,django
/static/* static/
local.env.yml local.env.yml

View File

@ -46,7 +46,7 @@ package-docker:
before_script: before_script:
- echo "{\"auths\":{\"https://docker.$NEXUS_URL/\":{\"username\":\"$NEXUS_USER\",\"password\":\"$NEXUS_PASS\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"https://docker.$NEXUS_URL/\":{\"username\":\"$NEXUS_USER\",\"password\":\"$NEXUS_PASS\"}}}" > /kaniko/.docker/config.json
script: script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.0.2-alpha - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.0.6-alpha
stage: build stage: build
only: only:
- tags - tags
@ -55,6 +55,7 @@ package-helm:
stage: build stage: build
script: script:
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash - curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
- helm init --client-only
- helm package helm/passbook - helm package helm/passbook
- ./manage.py nexus_upload --method put --url $NEXUS_URL --user $NEXUS_USER --password $NEXUS_PASS --repo helm *.tgz - ./manage.py nexus_upload --method put --url $NEXUS_URL --user $NEXUS_USER --password $NEXUS_PASS --repo helm *.tgz
only: only:

View File

@ -1,14 +1,25 @@
FROM python:3.6-slim-stretch FROM python:3.6-slim-stretch as build
# LABEL version="1.8.8"
COPY ./passbook/ /app/passbook COPY ./passbook/ /app/passbook
COPY ./static/ /app/static
COPY ./manage.py /app/ COPY ./manage.py /app/
COPY ./requirements.txt /app/ COPY ./requirements.txt /app/
WORKDIR /app/ WORKDIR /app/
#RUN apk add --no-cache libffi-dev build-base py2-pip python2-dev libxml-dev && \ RUN mkdir /app/static/ && \
pip install -r requirements.txt && \
pip install psycopg2 && \
./manage.py collectstatic --no-input
FROM python:3.6-slim-stretch
COPY ./passbook/ /app/passbook
COPY ./manage.py /app/
COPY ./requirements.txt /app/
COPY --from=build /app/static/* /app/static/
WORKDIR /app/
RUN pip install -r requirements.txt && \ RUN pip install -r requirements.txt && \
pip install psycopg2 && \ pip install psycopg2 && \
adduser --system --home /app/ passbook && \ adduser --system --home /app/ passbook && \

View File

@ -1,5 +1,5 @@
apiVersion: v1 apiVersion: v1
appVersion: "0.0.2-alpha" appVersion: "0.0.6-alpha"
description: A Helm chart for passbook. description: A Helm chart for passbook.
name: passbook name: passbook
version: 1.0.0 version: 1.0.0

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = '0.0.2-alpha' __version__ = '0.0.6-alpha'

View File

@ -1,2 +1,2 @@
"""passbook admin""" """passbook admin"""
__version__ = '0.0.2-alpha' __version__ = '0.0.6-alpha'

View File

@ -8,3 +8,4 @@ class PassbookAdminConfig(AppConfig):
name = 'passbook.admin' name = 'passbook.admin'
label = 'passbook_admin' label = 'passbook_admin'
mountpoint = 'administration/' mountpoint = 'administration/'
verbose_name = 'passbook Admin'

View File

@ -1,2 +1,2 @@
"""passbook api""" """passbook api"""
__version__ = '0.0.1-alpha' __version__ = '0.0.6-alpha'

View File

@ -9,3 +9,4 @@ class PassbookAPIConfig(AppConfig):
name = 'passbook.api' name = 'passbook.api'
label = 'passbook_api' label = 'passbook_api'
mountpoint = 'api/' mountpoint = 'api/'
verbose_name = 'passbook API'

View File

@ -0,0 +1,3 @@
django-rest-framework
drf_yasg
django-filters

View File

@ -1,2 +1,2 @@
"""passbook audit Header""" """passbook audit Header"""
__version__ = '0.0.2-alpha' __version__ = '0.0.6-alpha'

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header""" """passbook captcha_factor Header"""
__version__ = '0.0.2-alpha' __version__ = '0.0.6-alpha'

View File

@ -1,2 +1,2 @@
"""passbook core""" """passbook core"""
__version__ = '0.0.2-alpha' __version__ = '0.0.6-alpha'

View File

@ -0,0 +1,71 @@
"""passbook nexus_upload management command"""
from getpass import getpass
import requests
from django.core.management.base import BaseCommand
class Command(BaseCommand):
"""Upload debian package to nexus repository"""
url = None
user = None
password = None
def add_arguments(self, parser):
parser.add_argument(
'--repo',
action='store',
help='Repository to upload to',
required=True)
parser.add_argument(
'--url',
action='store',
help='Nexus root URL',
required=True)
parser.add_argument(
'--user',
action='store',
help='Username to use for Nexus upload',
required=True)
parser.add_argument(
'--method',
action='store',
nargs='?',
const='post',
choices=['post', 'put'],
help=('Method used for uploading files to nexus. '
'Apt repositories use post, Helm uses put.'),
required=True)
parser.add_argument(
'--password',
action='store',
help=("Password to use for Nexus upload. "
"If parameter not given, we'll interactively ask"))
# Positional arguments
parser.add_argument('file', nargs='+', type=str)
def handle(self, *args, **options):
"""Upload debian package to nexus repository"""
if options.get('password') is None:
options['password'] = getpass()
responses = {}
url = 'https://%(url)s/repository/%(repo)s//' % options
method = options.get('method')
exit_code = 0
for file in options.get('file'):
if method == 'post':
responses[file] = requests.post(url, data=open(file, mode='rb'),
auth=(options.get('user'), options.get('password')))
else:
responses[file] = requests.put(url+file, data=open(file, mode='rb'),
auth=(options.get('user'), options.get('password')))
self.stdout.write('Upload results:\n')
sep = '-' * 60
self.stdout.write('%s\n' % sep)
for path, response in responses.items():
self.stdout.write('%-55s: %d\n' % (path, response.status_code))
if response.status_code >= 400:
exit_code = 1
self.stdout.write('%s\n' % sep)
exit(exit_code)

View File

@ -1,12 +1,13 @@
# Generated by Django 2.1.5 on 2019-02-08 10:42 # Generated by Django 2.1.5 on 2019-02-08 10:42
from django.conf import settings import uuid
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
import uuid from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -0,0 +1,35 @@
# Generated by Django 2.1.5 on 2019-02-08 15:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='PasswordPolicyRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Rule')),
('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 Rule',
'verbose_name_plural': 'Password Policy Rules',
},
bases=('passbook_core.rule',),
),
migrations.AlterField(
model_name='fieldmatcherrule',
name='user_field',
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')]),
),
]

View File

@ -82,7 +82,7 @@ class Application(RuleModel):
def user_is_authorized(self, user: User) -> bool: def user_is_authorized(self, user: User) -> bool:
"""Check if user is authorized to use this application""" """Check if user is authorized to use this application"""
from passbook.core.rules import RuleEngine from passbook.core.rules import RuleEngine
return RuleEngine(self).for_user(user).result return RuleEngine(self.rules.all()).for_user(user).result
def __str__(self): def __str__(self):
return self.name return self.name
@ -160,6 +160,7 @@ class FieldMatcherRule(Rule):
MATCH_CONTAINS = 'contains' MATCH_CONTAINS = 'contains'
MATCH_REGEXP = 'regexp' MATCH_REGEXP = 'regexp'
MATCH_EXACT = 'exact' MATCH_EXACT = 'exact'
MATCHES = ( MATCHES = (
(MATCH_STARTSWITH, _('Starts with')), (MATCH_STARTSWITH, _('Starts with')),
(MATCH_ENDSWITH, _('Ends with')), (MATCH_ENDSWITH, _('Ends with')),
@ -169,13 +170,13 @@ class FieldMatcherRule(Rule):
) )
USER_FIELDS = ( USER_FIELDS = (
('username', 'username',), ('username', _('Username'),),
('first_name', 'first_name',), ('first_name', _('First Name'),),
('last_name', 'last_name',), ('last_name', _('Last Name'),),
('email', 'email',), ('email', _('E-Mail'),),
('is_staff', 'is_staff',), ('is_staff', _('Is staff'),),
('is_active', 'is_active',), ('is_active', _('Is active'),),
('data_joined', 'data_joined',), ('data_joined', _('Date joined'),),
) )
user_field = models.TextField(choices=USER_FIELDS) user_field = models.TextField(choices=USER_FIELDS)
@ -218,6 +219,41 @@ class FieldMatcherRule(Rule):
verbose_name = _('Field matcher Rule') verbose_name = _('Field matcher Rule')
verbose_name_plural = _('Field matcher Rules') verbose_name_plural = _('Field matcher Rules')
@reversion.register()
class PasswordPolicyRule(Rule):
"""Rule 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"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
form = 'passbook.core.forms.rules.PasswordPolicyRuleForm'
def passes(self, user: User) -> bool:
# Only check if password is being set
if not hasattr(user, '__password__'):
return True
password = getattr(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))
LOGGER.debug("User got %r", result)
return result
class Meta:
verbose_name = _('Password Policy Rule')
verbose_name_plural = _('Password Policy Rules')
@reversion.register() @reversion.register()
class WebhookRule(Rule): class WebhookRule(Rule):
"""Rule that asks webhook""" """Rule that asks webhook"""

View File

@ -9,27 +9,32 @@ from passbook.core.models import Rule, User
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@CELERY_APP.task() @CELERY_APP.task()
def _rule_engine_task(user_pk, rule_pk): def _rule_engine_task(user_pk, rule_pk, **kwargs):
"""Task wrapper to run rule checking""" """Task wrapper to run rule checking"""
rule_obj = Rule.objects.filter(pk=rule_pk).select_subclasses().first() rule_obj = Rule.objects.filter(pk=rule_pk).select_subclasses().first()
user_obj = User.objects.get(pk=user_pk) user_obj = User.objects.get(pk=user_pk)
for key, value in kwargs.items():
setattr(user_obj, key, value)
LOGGER.debug("Running rule `%s`#%s for user %s...", rule_obj.name, rule_obj.pk.hex, user_obj) LOGGER.debug("Running rule `%s`#%s for user %s...", rule_obj.name, rule_obj.pk.hex, user_obj)
return rule_obj.passes(user_obj) return rule_obj.passes(user_obj)
class RuleEngine: class RuleEngine:
"""Orchestrate rule checking, launch tasks and return result""" """Orchestrate rule checking, launch tasks and return result"""
_rule_model = None rules = None
_group = None _group = None
def __init__(self, rule_model): def __init__(self, rules):
self._rule_model = rule_model self.rules = rules
def for_user(self, user): def for_user(self, user):
"""Check rules for user""" """Check rules for user"""
signatures = [] signatures = []
for rule in self._rule_model.rules.all(): kwargs = {
signatures.append(_rule_engine_task.s(user.pk, rule.pk.hex)) '__password__': getattr(user, '__password__')
}
for rule in self.rules:
signatures.append(_rule_engine_task.s(user.pk, rule.pk.hex, **kwargs))
self._group = group(signatures)() self._group = group(signatures)()
return self return self

View File

@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/2.1/ref/settings/
import importlib import importlib
import os import os
import sys
from django.contrib import messages from django.contrib import messages
@ -33,7 +34,7 @@ SECRET_KEY = CONFIG.get('secret_key')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.get('debug') DEBUG = CONFIG.get('debug')
INTERNAL_IPS = ['127.0.0.1'] INTERNAL_IPS = ['127.0.0.1']
ALLOWED_HOSTS = CONFIG.get('domains') ALLOWED_HOSTS = CONFIG.get('domains', [])
LOGIN_URL = 'passbook_core:auth-login' LOGIN_URL = 'passbook_core:auth-login'
# CSRF_FAILURE_VIEW = 'passbook.core.views.errors.CSRFErrorView.as_view' # CSRF_FAILURE_VIEW = 'passbook.core.views.errors.CSRFErrorView.as_view'
@ -288,6 +289,16 @@ with CONFIG.cd('log'):
} }
} }
TEST = False
TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
TEST_OUTPUT_VERBOSE = 2
TEST_OUTPUT_FILE_NAME = 'unittest.xml'
if any('test' in arg for arg in sys.argv):
LOGGING = None
TEST = True
_DISALLOWED_ITEMS = ['INSTALLED_APPS', 'MIDDLEWARE', 'AUTHENTICATION_BACKENDS'] _DISALLOWED_ITEMS = ['INSTALLED_APPS', 'MIDDLEWARE', 'AUTHENTICATION_BACKENDS']
# Load subapps's INSTALLED_APPS # Load subapps's INSTALLED_APPS
for _app in INSTALLED_APPS: for _app in INSTALLED_APPS:

View File

@ -1,2 +1,2 @@
"""Passbook ldap app Header""" """Passbook ldap app Header"""
__version__ = '0.0.2-alpha' __version__ = '0.0.6-alpha'

View File

@ -1,2 +1,2 @@
"""passbook lib""" """passbook lib"""
__version__ = '0.0.1-alpha' __version__ = '0.0.6-alpha'

View File

@ -7,3 +7,4 @@ class PassbookLibConfig(AppConfig):
name = 'passbook.lib' name = 'passbook.lib'
label = 'passbook_lib' label = 'passbook_lib'
verbose_name = 'passbook lib'

View File

@ -23,7 +23,7 @@ web:
listen: 0.0.0.0 listen: 0.0.0.0
port: 8000 port: 8000
threads: 30 threads: 30
debug: true debug: false
secure_proxy_header: secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https HTTP_X_FORWARDED_PROTO: https
redis: localhost redis: localhost

View File

@ -1,2 +1,2 @@
"""passbook oauth_client Header""" """passbook oauth_client Header"""
__version__ = '0.0.2-alpha' __version__ = '0.0.6-alpha'

View File

@ -1,2 +1,2 @@
"""passbook oauth_provider Header""" """passbook oauth_provider Header"""
__version__ = '0.0.2-alpha' __version__ = '0.0.6-alpha'

View File

@ -1,2 +1,2 @@
"""passbook saml_idp Header""" """passbook saml_idp Header"""
__version__ = '0.0.2-alpha' __version__ = '0.0.6-alpha'

View File

@ -1,2 +1,2 @@
"""passbook totp Header""" """passbook totp Header"""
__version__ = '0.0.2-alpha' __version__ = '0.0.6-alpha'

View File

@ -7,3 +7,4 @@
-r passbook/audit/requirements.txt -r passbook/audit/requirements.txt
-r passbook/captcha_factor/requirements.txt -r passbook/captcha_factor/requirements.txt
-r passbook/admin/requirements.txt -r passbook/admin/requirements.txt
-r passbook/api/requirements.txt