Compare commits

...

27 Commits

Author SHA1 Message Date
a4f83bd28a new release: 0.2.1-beta 2019-06-25 18:25:27 +02:00
796f83c3d0 Fix requirements file importing wrong path 2019-06-25 18:24:07 +02:00
2099bbb713 new release: 0.2.0-beta 2019-06-25 18:16:14 +02:00
67beba8f78 Fix wrong URLConf being loaded 2019-06-25 18:11:08 +02:00
a798412e17 separate passbook.core into passbook.root and passbook.core
Move Main Django Project into passbook.root while passbook.core holds core functionality.

passbook.root contains main settings, ASGI & WSGI, celery and URLs.
2019-06-25 18:00:54 +02:00
3b2c2d781f Fix typo 2019-06-25 17:59:51 +02:00
98c844f3d6 improve OIDC compatibility 2019-06-25 17:57:25 +02:00
2645bd0132 new release: 0.1.38-beta 2019-04-29 23:26:18 +02:00
2c4fc56b49 Merge branch '27-rewrite-oauth-client-as-factor' into 'master'
Resolve "Rewrite OAuth Client as Factor"

Closes #27

See merge request BeryJu.org/passbook!14
2019-04-29 21:25:04 +00:00
0ec1468058 remove unused import 2019-04-29 23:22:54 +02:00
5d1a3043b2 create SSOLoginPolicy, which allows factors to be applied when user comes from SSO login
implement SESSIION_IS_SSO_LOGIN for OAuth Client and core MFA
2019-04-29 23:19:37 +02:00
b46958d1f9 send session to task 2019-04-29 23:18:51 +02:00
5daa8d5fe3 fix missing/wrong widget inputs 2019-04-29 23:16:04 +02:00
31846f1d05 Show redirect URL in <pre> element 2019-04-29 22:32:22 +02:00
1fac964b8b increase application close timeout 2019-04-29 22:19:26 +02:00
dfa6ed8ac2 add help to show how SAML Property Mapping substitutes variables 2019-04-29 22:19:13 +02:00
66fe10299e new release: 0.1.37-beta 2019-04-29 21:43:18 +02:00
e0a3ec033f fix IDP-Initiated SAML Login 2019-04-29 21:39:41 +02:00
7033ec0ab9 remove debug print 2019-04-29 21:39:30 +02:00
4004579905 remove nexus_upload command 2019-04-29 21:39:16 +02:00
9fe9e48a5c new release: 0.1.36-beta 2019-04-29 20:57:57 +02:00
595a6c7fe6 remove guardian completely 2019-04-29 20:57:54 +02:00
11b5860d4a new release: 0.1.35-beta 2019-04-29 20:47:32 +02:00
9bdbff4cda fix gitlab-ci using incorrect variables for docker access 2019-04-29 20:42:24 +02:00
e0d597eeac show cache on admin overview, add modal to clear cache, re-add logging to policy 2019-04-29 20:37:44 +02:00
f576985cc9 remove django guardian and check application access via PolicyEngine 2019-04-29 20:07:18 +02:00
22a6aef60b fix wrong import 2019-04-29 20:06:52 +02:00
59 changed files with 297 additions and 192 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.1.34-beta
current_version = 0.2.1-beta
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)

View File

@ -6,7 +6,6 @@ omit =
manage.py
*/migrations/*
*/apps.py
passbook/management/commands/nexus_upload.py
passbook/management/commands/web.py
passbook/management/commands/worker.py
docs/

View File

@ -20,9 +20,9 @@ create-build-image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.build-base --destination docker.beryju.org/passbook/build-base:latest --destination docker.beryju.org/passbook/build-base:0.1.34-beta
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.build-base --destination docker.beryju.org/passbook/build-base:latest --destination docker.beryju.org/passbook/build-base:0.2.1-beta
stage: build-buildimage
only:
refs:
@ -61,9 +61,9 @@ package-docker:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.1.34-beta
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.2.1-beta
stage: build
only:
- tags

View File

@ -3,7 +3,7 @@ from setuptools import setup
setup(
name='django-allauth-passbook',
version='0.1.34-beta',
version='0.2.1-beta',
description='passbook support for django-allauth',
# long_description='\n'.join(read_simple('docs/index.md')[2:]),
long_description_content_type='text/markdown',

View File

@ -18,7 +18,7 @@ tests_require = [
setup(
name='sentry-auth-passbook',
version='0.1.34-beta',
version='0.2.1-beta',
author='BeryJu.org',
author_email='support@beryju.org',
url='https://passbook.beryju.org',

View File

@ -1,6 +1,6 @@
apiVersion: v1
appVersion: "0.1.34-beta"
appVersion: "0.2.1-beta"
description: A Helm chart for passbook.
name: passbook
version: "0.1.34-beta"
version: "0.2.1-beta"
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png

View File

@ -5,7 +5,7 @@
replicaCount: 1
image:
tag: 0.1.34-beta
tag: 0.2.1-beta
nameOverride: ""

View File

@ -4,7 +4,7 @@ import os
import sys
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'passbook.core.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'passbook.root.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -1,2 +1,2 @@
"""passbook admin"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -152,10 +152,8 @@
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#">
<span class="pficon-bundle"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Version' %}
</a>
<span class="pficon-bundle"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Version' %}
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
@ -192,5 +190,59 @@
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<span class="pficon-server"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Cached Policies' %}
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification">
<a href="#" data-toggle="modal" data-target="#clearCacheMOdal">
{% if cached_policies < 1 %}
<span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right"
title="{% trans 'No policies cached. Users may experience slow response times.' %}"></span> {{ cached_policies }}
{% else %}
<span class="pficon pficon-ok"></span>{{ cached_policies }}
{% endif %}
</a>
</span>
</p>
</div>
</div>
</div>
</div>
<div class="modal fade" id="clearCacheMOdal" tabindex="-1" role="dialog" aria-labelledby="clearCacheMOdalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">
<span class="pficon pficon-close"></span>
</button>
<h4 class="modal-title" id="clearCacheMOdalLabel">{% trans 'Clear Cache' %}</h4>
</div>
<div class="modal-body">
<form method="post" id="clearForm">
{% csrf_token %}
<input type="hidden" name="clear">
<p>
{% blocktrans %}
Are you sure you want to clear the cache? This includes all user sessions and all cached Policy results.
{% endblocktrans %}
</p>
<h3>
{% blocktrans %}
This will also log you out.
{% endblocktrans %}
</h3>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button form="clearForm" type="submit" type="button" class="btn btn-danger">{% trans 'Clear' %}</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -36,7 +36,7 @@
<tr>
<td>{{ source.name }}</td>
<td>{{ source|fieldtype }}</td>
<td>{{ source.additional_info }}</td>
<td>{{ source.additional_info|safe }}</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>

View File

@ -1,11 +1,13 @@
"""passbook administration overview"""
from django.core.cache import cache
from django.shortcuts import redirect, reverse
from django.views.generic import TemplateView
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core import __version__
from passbook.core.celery import CELERY_APP
from passbook.core.models import (Application, Factor, Invitation, Policy,
Provider, Source, User)
from passbook.root.celery import CELERY_APP
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
@ -13,6 +15,13 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
template_name = 'administration/overview.html'
def post(self, *args, **kwargs):
"""Handle post (clear cache from modal)"""
if 'clear' in self.request.POST:
cache.clear()
return redirect(reverse('passbook_core:auth-login'))
return self.get(*args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['application_count'] = len(Application.objects.all())
kwargs['policy_count'] = len(Policy.objects.all())
@ -25,4 +34,5 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
kwargs['worker_count'] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs['providers_without_application'] = Provider.objects.filter(application=None)
kwargs['policies_without_attachment'] = len(Policy.objects.filter(policymodel__isnull=True))
kwargs['cached_policies'] = len(cache.keys('policy_*'))
return super().get_context_data(**kwargs)

View File

@ -1,2 +1,2 @@
"""passbook api"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -1,2 +1,2 @@
"""passbook Application Security Gateway Header"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -190,8 +190,6 @@ def cookie_from_string(cookie_string, strict_cookies=False):
cookie_dict['key'], cookie_dict['value'] = \
cookie_parts[0].split('=', 1)
cookie_dict['value'] = cookie_dict['value'].replace('"', '')
# print('aaaaaaaaaaaaaaaaaaaaaaaaaaaa')
# print(cookie_parts[0].split('=', 1))
except ValueError:
logger.warning('Invalid cookie: `%s`', cookie_string)
return None

View File

@ -1,2 +1,2 @@
"""passbook audit Header"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -1,2 +1,2 @@
"""passbook core"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -14,6 +14,7 @@ class PassbookCoreConfig(AppConfig):
name = 'passbook.core'
label = 'passbook_core'
verbose_name = 'passbook Core'
mountpoint = ''
def ready(self):
import_module('passbook.core.policies')

View File

@ -29,6 +29,7 @@ class AuthenticationView(UserPassesTestMixin, View):
SESSION_PENDING_FACTORS = 'passbook_pending_factors'
SESSION_PENDING_USER = 'passbook_pending_user'
SESSION_USER_BACKEND = 'passbook_user_backend'
SESSION_IS_SSO_LOGIN = 'passbook_sso_login'
pending_user = None
pending_factors = []
@ -79,6 +80,10 @@ class AuthenticationView(UserPassesTestMixin, View):
if AuthenticationView.SESSION_FACTOR not in request.session:
# Case when no factors apply to user, return error denied
if not self.pending_factors:
# Case when user logged in from SSO provider and no more factors apply
if AuthenticationView.SESSION_IS_SSO_LOGIN in request.session:
LOGGER.debug("User authenticated with SSO, logging in...")
return self._user_passed()
return self.user_invalid()
factor_uuid, factor_class = self.pending_factors[0]
else:

View File

@ -27,7 +27,8 @@ class PasswordFactorForm(forms.ModelForm):
'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False),
'backends': FilteredSelectMultiple(_('backends'), False,
choices=get_authentication_backends())
choices=get_authentication_backends()),
'password_policies': FilteredSelectMultiple(_('password policies'), False),
}
class DummyFactorForm(forms.ModelForm):

View File

@ -5,7 +5,7 @@ from django.utils.translation import gettext as _
from passbook.core.models import (DebugPolicy, FieldMatcherPolicy,
GroupMembershipPolicy, PasswordPolicy,
WebhookPolicy)
SSOLoginPolicy, WebhookPolicy)
GENERAL_FIELDS = ['name', 'action', 'negate', 'order', 'timeout']
@ -63,6 +63,19 @@ class GroupMembershipPolicyForm(forms.ModelForm):
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):

View File

@ -1,63 +0,0 @@
"""passbook nexus_upload management command"""
from base64 import b64decode
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(
'--auth',
action='store',
help='base64-encoded string of username:password',
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)
# Positional arguments
parser.add_argument('file', nargs='+', type=str)
def handle(self, *args, **options):
"""Upload debian package to nexus repository"""
auth = tuple(b64decode(options.get('auth')).decode('utf-8').split(':', 1))
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=auth)
else:
responses[file] = requests.put(url+file, data=open(file, mode='rb'), auth=auth)
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

@ -25,5 +25,6 @@ class Command(BaseCommand):
'-p', str(CONFIG.y('web.port', 8000)),
'-b', CONFIG.y('web.listen', '0.0.0.0'), # nosec
'--access-log', '/dev/null',
'passbook.core.asgi:application'
'--application-close-timeout', '500',
'passbook.root.asgi:application'
])

View File

@ -5,7 +5,7 @@ from logging import getLogger
from django.core.management.base import BaseCommand
from django.utils import autoreload
from passbook.core.celery import CELERY_APP
from passbook.root.celery import CELERY_APP
LOGGER = getLogger(__name__)

View File

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

View File

@ -165,9 +165,10 @@ class Source(PolicyModel):
name = models.TextField()
slug = models.SlugField()
form = '' # ModelForm-based class ued to create/edit instance
enabled = models.BooleanField(default=True)
form = '' # ModelForm-based class ued to create/edit instance
objects = InheritanceManager()
@property
@ -409,6 +410,21 @@ class GroupMembershipPolicy(Policy):
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, user):
"""Check if user instance passes this policy"""
from passbook.core.auth.view import AuthenticationView
return user.session.get(AuthenticationView.SESSION_IS_SSO_LOGIN, False), ""
class Meta:
verbose_name = _('SSO Login Policy')
verbose_name_plural = _('SSO Login Policies')
class Invitation(UUIDModel):
"""Single-use invitation link"""

View File

@ -1,18 +1,19 @@
"""passbook core policy engine"""
# from logging import getLogger
from logging import getLogger
from amqp.exceptions import UnexpectedFrame
from celery import group
from celery.exceptions import TimeoutError as CeleryTimeoutError
from django.core.cache import cache
from ipware import get_client_ip
from passbook.core.celery import CELERY_APP
from passbook.core.models import Policy, User
from passbook.root.celery import CELERY_APP
# LOGGER = getLogger(__name__)
LOGGER = getLogger(__name__)
def _cache_key(policy, user):
return "%s#%s" % (policy.uuid, user.pk)
return "policy_%s#%s" % (policy.uuid, user.pk)
@CELERY_APP.task()
def _policy_engine_task(user_pk, policy_pk, **kwargs):
@ -23,8 +24,8 @@ def _policy_engine_task(user_pk, policy_pk, **kwargs):
user_obj = User.objects.get(pk=user_pk)
for key, value in kwargs.items():
setattr(user_obj, key, value)
# LOGGER.debug("Running policy `%s`#%s for user %s...", policy_obj.name,
# policy_obj.pk.hex, user_obj)
LOGGER.debug("Running policy `%s`#%s for user %s...", policy_obj.name,
policy_obj.pk.hex, user_obj)
policy_result = policy_obj.passes(user_obj)
# Handle policy result correctly if result, message or just result
message = None
@ -33,10 +34,10 @@ def _policy_engine_task(user_pk, policy_pk, **kwargs):
# Invert result if policy.negate is set
if policy_obj.negate:
policy_result = not policy_result
# LOGGER.debug("Policy %r#%s got %s", policy_obj.name, policy_obj.pk.hex, policy_result)
LOGGER.debug("Policy %r#%s got %s", policy_obj.name, policy_obj.pk.hex, policy_result)
cache_key = _cache_key(policy_obj, user_obj)
cache.set(cache_key, (policy_obj.action, policy_result, message))
# LOGGER.debug("Cached entry as %s", cache_key)
LOGGER.debug("Cached entry as %s", cache_key)
return policy_obj.action, policy_result, message
class PolicyEngine:
@ -73,6 +74,7 @@ class PolicyEngine:
cached_policies = []
kwargs = {
'__password__': getattr(self.__user, '__password__', None),
'session': dict(getattr(self.__request, 'session', {}).items()),
}
if self.__request:
kwargs['remote_ip'], _ = get_client_ip(self.__request)
@ -81,16 +83,16 @@ class PolicyEngine:
for policy in self.policies:
cached_policy = cache.get(_cache_key(policy, self.__user), None)
if cached_policy:
# LOGGER.debug("Taking result from cache for %s", policy.pk.hex)
LOGGER.debug("Taking result from cache for %s", policy.pk.hex)
cached_policies.append(cached_policy)
else:
# LOGGER.debug("Evaluating policy %s", policy.pk.hex)
LOGGER.debug("Evaluating policy %s", policy.pk.hex)
signatures.append(_policy_engine_task.signature(
args=(self.__user.pk, policy.pk.hex),
kwargs=kwargs,
time_limit=policy.timeout))
self.__get_timeout += policy.timeout
# LOGGER.debug("Set total policy timeout to %r", self.__get_timeout)
LOGGER.debug("Set total policy timeout to %r", self.__get_timeout)
# If all policies are cached, we have an empty list here.
if signatures:
self.__group = group(signatures)()
@ -119,7 +121,7 @@ class PolicyEngine:
for policy_action, policy_result, policy_message in result:
passing = (policy_action == Policy.ACTION_ALLOW and policy_result) or \
(policy_action == Policy.ACTION_DENY and not policy_result)
# LOGGER.debug('Action=%s, Result=%r => %r', policy_action, policy_result, passing)
LOGGER.debug('Action=%s, Result=%r => %r', policy_action, policy_result, passing)
if policy_message:
messages.append(policy_message)
if not passing:

View File

@ -6,9 +6,9 @@ from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from passbook.core.celery import CELERY_APP
from passbook.core.models import Nonce
from passbook.lib.config import CONFIG
from passbook.root.celery import CELERY_APP
LOGGER = getLogger(__name__)

View File

@ -1,25 +1,14 @@
"""passbook URL Configuration"""
from logging import getLogger
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
from django.views.generic import RedirectView
from django.urls import path
from passbook.core.auth import view
from passbook.core.views import authentication, error, overview, user
from passbook.lib.utils.reflection import get_apps
from passbook.core.views import authentication, overview, user
LOGGER = getLogger(__name__)
admin.autodiscover()
admin.site.login = RedirectView.as_view(pattern_name='passbook_core:auth-login')
handler400 = error.BadRequestView.as_view()
handler403 = error.ForbiddenView.as_view()
handler404 = error.NotFoundView.as_view()
handler500 = error.ServerErrorView.as_view()
core_urls = [
urlpatterns = [
# Authentication views
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'),
@ -39,27 +28,3 @@ core_urls = [
# Overview
path('', overview.OverviewView.as_view(), name='overview'),
]
urlpatterns = [
# Core (include our own URLs so namespaces are used everywhere)
path('', include((core_urls, 'passbook_core'), namespace='passbook_core')),
]
for _passbook_app in get_apps():
if hasattr(_passbook_app, 'mountpoint'):
_path = path(_passbook_app.mountpoint, include((_passbook_app.name+'.urls',
_passbook_app.label),
namespace=_passbook_app.label))
urlpatterns.append(_path)
LOGGER.debug("Loaded %s's URLs", _passbook_app.name)
urlpatterns += [
# Administration
path('administration/django/', admin.site.urls),
]
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns

View File

@ -71,7 +71,7 @@ class LoginView(UserPassesTestMixin, FormView):
if not pre_user:
# No user found
return self.invalid_login(self.request)
self.request.session.flush()
# self.request.session.flush()
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
return _redirect_with_qs('passbook_core:auth-process', self.request.GET)

View File

@ -2,7 +2,9 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView
from guardian.shortcuts import get_objects_for_user
from passbook.core.models import Application
from passbook.core.policies import PolicyEngine
class OverviewView(LoginRequiredMixin, TemplateView):
@ -12,6 +14,11 @@ class OverviewView(LoginRequiredMixin, TemplateView):
template_name = 'overview/index.html'
def get_context_data(self, **kwargs):
kwargs['applications'] = get_objects_for_user(self.request.user,
'passbook_core.view_application')
kwargs['applications'] = []
for application in Application.objects.all():
engine = PolicyEngine(application.policies.all())
engine.for_user(self.request.user).with_request(self.request)
engine.build()
if engine.passing:
kwargs['applications'].append(application)
return super().get_context_data(**kwargs)

View File

@ -1,2 +1,2 @@
"""passbook hibp_policy"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -1,2 +1,2 @@
"""Passbook ldap app Header"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -1,2 +1,2 @@
"""passbook lib"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -1,17 +1,29 @@
"""passbook sentry integration"""
from logging import getLogger
LOGGER = getLogger(__name__)
def before_send(event, hint):
"""Check if error is database error, and ignore if so"""
from django.core.exceptions import OperationalError
from django_redis.exceptions import ConnectionInterrupted
ignored_classes = [
from django.db import OperationalError, InternalError
from rest_framework.exceptions import APIException
from billiard.exceptions import WorkerLostError
from django.core.exceptions import DisallowedHost
ignored_classes = (
OperationalError,
ConnectionInterrupted,
]
APIException,
InternalError,
ConnectionResetError,
WorkerLostError,
DisallowedHost,
ConnectionResetError,
)
if 'exc_info' in hint:
_exc_type, exc_value, _ = hint['exc_info']
if isinstance(exc_value, ignored_classes):
LOGGER.info("Supressing error %r", exc_value)
return None
return event

View File

@ -1,2 +1,2 @@
"""passbook oauth_client Header"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -29,14 +29,13 @@ class OAuthSource(Source):
def get_login_button(self):
url = reverse_lazy('passbook_oauth_client:oauth-client-login',
kwargs={'source_slug': self.slug})
# if self.provider_type == 'github':
# return url, 'github-logo', _('GitHub')
return url, self.provider_type, self.name
@property
def additional_info(self):
return "Callback URL: '%s'" % reverse_lazy('passbook_oauth_client:oauth-client-callback',
kwargs={'source_slug': self.slug})
return "Callback URL: <pre>%s</pre>" % \
reverse_lazy('passbook_oauth_client:oauth-client-callback',
kwargs={'source_slug': self.slug})
def has_user_settings(self):
"""Entrypoint to integrate with User settings. Can either return False if no

View File

@ -4,7 +4,7 @@ from logging import getLogger
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.contrib.auth import authenticate
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
@ -12,6 +12,7 @@ from django.urls import reverse
from django.utils.translation import ugettext as _
from django.views.generic import RedirectView, View
from passbook.core.auth.view import AuthenticationView, _redirect_with_qs
from passbook.lib.utils.reflection import app
from passbook.oauth_client.clients import get_client
from passbook.oauth_client.models import OAuthSource, UserOAuthSourceConnection
@ -128,11 +129,6 @@ class OAuthCallback(OAuthClientMixin, View):
"Return url to redirect on login failure."
return settings.LOGIN_URL
# pylint: disable=unused-argument
def get_login_redirect(self, source, user, access, new=False):
"Return url to redirect authenticated users."
return 'passbook_core:overview'
def get_or_create_user(self, source, access, info):
"Create a shell auth.User."
raise NotImplementedError()
@ -149,14 +145,22 @@ class OAuthCallback(OAuthClientMixin, View):
except KeyError:
return None
def handle_login(self, user, source, access):
"""Prepare AuthenticationView, redirect users to remaining Factors"""
user = authenticate(source=access.source,
identifier=access.identifier, request=self.request)
self.request.session[AuthenticationView.SESSION_PENDING_USER] = user.pk
self.request.session[AuthenticationView.SESSION_USER_BACKEND] = user.backend
self.request.session[AuthenticationView.SESSION_IS_SSO_LOGIN] = True
return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
# pylint: disable=unused-argument
def handle_existing_user(self, source, user, access, info):
"Login user and redirect."
login(self.request, user)
messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
'source': self.source.name
}))
return redirect(self.get_login_redirect(source, user, access))
return self.handle_login(user, source, access)
def handle_login_failure(self, source, reason):
"Message user and redirect on error."
@ -176,12 +180,9 @@ class OAuthCallback(OAuthClientMixin, View):
access.user = user
access.save()
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
if not was_authenticated:
user = authenticate(source=access.source,
identifier=access.identifier, request=self.request)
login(self.request, user)
if app('passbook_audit'):
pass
# TODO: Create audit entry
# from passbook.audit.models import something
# something.event(user=user,)
# Event.create(
@ -197,10 +198,13 @@ class OAuthCallback(OAuthClientMixin, View):
return redirect(reverse('passbook_oauth_client:oauth-client-user', kwargs={
'source_slug': self.source.slug
}))
# User was not authenticated, new user has been created
user = authenticate(source=access.source,
identifier=access.identifier, request=self.request)
messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
'source': self.source.name
}))
return redirect(self.get_login_redirect(source, user, access, True))
return self.handle_login(user, source, access)
class DisconnectView(LoginRequiredMixin, View):

View File

@ -1,2 +1,2 @@
"""passbook oauth_provider Header"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -20,6 +20,7 @@ OAUTH2_PROVIDER_APPLICATION_MODEL = 'passbook_oauth_provider.OAuth2Provider'
OAUTH2_PROVIDER = {
# this is the list of available scopes
'SCOPES': {
'openid': 'Access OpenID Userinfo',
'openid:userinfo': 'Access OpenID Userinfo',
# 'write': 'Write scope',
# 'groups': 'Access to your groups',

View File

@ -8,16 +8,21 @@ from django.views.generic import View
class OpenIDConfigurationView(View):
"""Return OpenID Configuration"""
def get_issuer_url(self, request):
"""Get correct issuer URL"""
full_url = request.build_absolute_uri(reverse('passbook_oauth_provider:openid-discovery'))
return full_url.replace(".well-known/openid-configuration", "")
def get(self, request: HttpRequest):
"""Get Response conform to https://openid.net/specs/openid-connect-discovery-1_0.html"""
return JsonResponse({
'issuer': request.build_absolute_uri(reverse('passbook_core:overview')),
'issuer': self.get_issuer_url(request),
'authorization_endpoint': request.build_absolute_uri(
reverse('passbook_oauth_provider:oauth2-authorize')),
'token_endpoint': request.build_absolute_uri(reverse('passbook_oauth_provider:token')),
"jwks_uri": request.build_absolute_uri(reverse('passbook_oauth_provider:openid-jwks')),
"scopes_supported": [
"openid:userinfo",
"openid",
],
})

View File

@ -1,2 +1,2 @@
"""passbook otp Header"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -1,2 +1,2 @@
"""passbook password_expiry"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

View File

@ -8,6 +8,6 @@ import os
import django
from channels.routing import get_default_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.core.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
django.setup()
application = get_default_application()

View File

@ -7,7 +7,7 @@ from celery import Celery, signals
from django.conf import settings
# set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.core.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
LOGGER = logging.getLogger(__name__)

View File

@ -1,6 +1,5 @@
celery
colorlog
django-guardian
django-ipware
django-model-utils
django-redis
@ -13,3 +12,4 @@ PyYAML
sentry-sdk
pip
whitenoise
urllib3<1.25,>=1.21.1

View File

@ -60,7 +60,6 @@ LANGUAGE_COOKIE_NAME = 'passbook_language'
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'guardian.backends.ObjectPermissionBackend',
]
# Application definition
@ -75,7 +74,6 @@ INSTALLED_APPS = [
'django.contrib.postgres',
'rest_framework',
'drf_yasg',
'guardian',
'passbook.core.apps.PassbookCoreConfig',
'passbook.admin.apps.PassbookAdminConfig',
'passbook.api.apps.PassbookAPIConfig',
@ -134,7 +132,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'passbook.core.urls'
ROOT_URLCONF = 'passbook.root.urls'
TEMPLATES = [
{

41
passbook/root/urls.py Normal file
View File

@ -0,0 +1,41 @@
"""passbook URL Configuration"""
from logging import getLogger
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
from django.views.generic import RedirectView
from passbook.core.views import error
from passbook.lib.utils.reflection import get_apps
LOGGER = getLogger(__name__)
admin.autodiscover()
admin.site.login = RedirectView.as_view(pattern_name='passbook_core:auth-login')
handler400 = error.BadRequestView.as_view()
handler403 = error.ForbiddenView.as_view()
handler404 = error.NotFoundView.as_view()
handler500 = error.ServerErrorView.as_view()
urlpatterns = [
]
for _passbook_app in get_apps():
if hasattr(_passbook_app, 'mountpoint'):
_path = path(_passbook_app.mountpoint, include((_passbook_app.name+'.urls',
_passbook_app.label),
namespace=_passbook_app.label))
urlpatterns.append(_path)
LOGGER.debug("Loaded %s's URLs", _passbook_app.name)
urlpatterns += [
# Administration
path('administration/django/', admin.site.urls),
]
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns

View File

@ -1,2 +1,2 @@
"""passbook saml_idp Header"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -33,6 +33,8 @@ class Processor:
"""Base SAML 2.0 AuthnRequest to Response Processor.
Sub-classes should provide Service Provider-specific functionality."""
is_idp_initiated = False
_audience = ''
_assertion_params = None
_assertion_xml = None
@ -291,7 +293,10 @@ class Processor:
def generate_response(self):
"""Processes request and returns template variables suitable for a response."""
# Build the assertion and response.
self.can_handle(self._django_request)
# Only call can_handle if SP initiated Request, otherwise we have no Request
if not self.is_idp_initiated:
self.can_handle(self._django_request)
self._validate_user()
self._build_assertion()
self._format_assertion()

View File

@ -54,3 +54,6 @@ class SAMLPropertyMappingForm(forms.ModelForm):
field_classes = {
'values': DynamicArrayField
}
help_texts = {
'values': 'String substitution uses a syntax like "{variable} test}".'
}

View File

@ -1,4 +1,5 @@
"""passbook saml_idp Models"""
from logging import getLogger
from django.contrib.postgres.fields import ArrayField
from django.db import models
@ -9,6 +10,8 @@ from passbook.core.models import PropertyMapping, Provider
from passbook.lib.utils.reflection import class_to_path, path_to_class
from passbook.saml_idp.base import Processor
LOGGER = getLogger(__name__)
class SAMLProvider(Provider):
"""Model to save information about a Remote SAML Endpoint"""
@ -36,7 +39,8 @@ class SAMLProvider(Provider):
if not self._processor:
try:
self._processor = path_to_class(self.processor_path)(self)
except ModuleNotFoundError:
except ModuleNotFoundError as exc:
LOGGER.warning(exc)
self._processor = None
return self._processor

View File

@ -231,4 +231,5 @@ class InitiateLoginView(AccessRequiredView):
def get(self, request, application):
"""Initiates an IdP-initiated link to a simple SP resource/target URL."""
self.provider.processor.init_deep_link(request, '')
self.provider.processor.is_idp_initiated = True
return _generate_response(request, self.provider)

View File

@ -1,2 +1,2 @@
"""passbook suspicious_policy"""
__version__ = '0.1.34-beta'
__version__ = '0.2.1-beta'

View File

@ -1,4 +1,4 @@
-r passbook/core/requirements.txt
-r passbook/root/requirements.txt
-r passbook/oauth_client/requirements.txt
-r passbook/ldap/requirements.txt
-r passbook/saml_idp/requirements.txt
@ -7,4 +7,4 @@
-r passbook/captcha_factor/requirements.txt
-r passbook/admin/requirements.txt
-r passbook/api/requirements.txt
-r passbook/app_gw/requirements.txt
-r passbook/app_gw/requirements.txt