Compare commits

..

35 Commits

Author SHA1 Message Date
10fd96981e new release: 0.2.6-beta 2019-07-15 13:42:18 +00:00
67e3eb549c api: fix wrong django-rest-framework dependency 2019-07-15 13:42:03 +00:00
30a6d1f0b1 new release: 0.2.5-beta 2019-07-15 13:31:04 +00:00
3d1fa9f048 app_gw: Rewrite redirect responses (replace upstream location with server_name) 2019-07-09 15:28:52 +02:00
1d2be6e68b root: fix sentry sending wrong release 2019-07-05 16:00:01 +02:00
c21e343986 oidc_provider: fix error when creating a new provider 2019-07-05 15:59:52 +02:00
ff37ed095c new release: 0.2.4-beta 2019-07-05 15:30:13 +02:00
8623a2c3fc oidc_provider: fix error trying to create RSA Key before migrations are run 2019-07-05 15:27:04 +02:00
23d277eaf1 remove oidc from OAuth2, add dedicated OIDC provider 2019-07-05 15:21:48 +02:00
75ced59451 helm: fix syntax error 2019-07-05 15:21:12 +02:00
bccf424c5e new release: 0.2.3-beta 2019-07-04 16:25:33 +02:00
2f9ae40d20 client-sentry: fix 400 failing upload 2019-07-04 16:25:17 +02:00
11e1eec3fb ci: fix new dependencies not being installed on the fly 2019-07-04 16:21:35 +02:00
765c5633df helm: add appgw to ingress 2019-07-04 16:07:16 +02:00
6344b1aafb helm: add deployment for appgw 2019-07-04 15:25:36 +02:00
ed25801e6e core: revert to cherrypy for main webserver and use daphne only for app_gw 2019-07-04 15:23:05 +02:00
4d0148193f root: migrate to new sentry instance 2019-07-03 17:35:54 +02:00
804ae15c2e new release: 0.2.2-beta 2019-06-25 18:50:41 +02:00
b35a9fad86 Fix linting errors with current build-base image 2019-06-25 18:50:37 +02:00
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
83 changed files with 727 additions and 179 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.1.37-beta current_version = 0.2.6-beta
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>.*)

View File

@ -15,6 +15,10 @@ variables:
POSTGRES_USER: passbook POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77" POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
before_script:
# Ensure all dependencies are installed, even those not included in passbook/build-base
- pip install -r requirements-dev.txt
create-build-image: create-build-image:
image: image:
name: gcr.io/kaniko-project/executor:debug name: gcr.io/kaniko-project/executor:debug
@ -22,7 +26,7 @@ create-build-image:
before_script: before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script: 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.37-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.6-beta
stage: build-buildimage stage: build-buildimage
only: only:
refs: refs:
@ -63,7 +67,7 @@ package-docker:
before_script: before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script: 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.37-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.6-beta
stage: build stage: build
only: only:
- tags - tags

View File

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

View File

@ -18,12 +18,13 @@ tests_require = [
setup( setup(
name='sentry-auth-passbook', name='sentry-auth-passbook',
version='0.1.37-beta', version='0.2.6-beta',
author='BeryJu.org', author='BeryJu.org',
author_email='support@beryju.org', author_email='support@beryju.org',
url='https://passbook.beryju.org', url='https://passbook.beryju.org',
description='passbook authentication provider for Sentry', description='passbook authentication provider for Sentry',
long_description=__doc__, long_description=__doc__,
long_description_content_type='text/markdown',
license='MIT', license='MIT',
packages=find_packages(exclude=['tests']), packages=find_packages(exclude=['tests']),
zip_safe=False, zip_safe=False,

View File

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

View File

@ -0,0 +1,67 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: {{ include "passbook.fullname" . }}-appgw
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: appgw
spec:
volumes:
- name: config-volume
configMap:
name: {{ include "passbook.fullname" . }}-config
containers:
- name: {{ .Chart.Name }}
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command: ["/bin/sh","-c"]
args: ["./manage.py migrate && ./manage.py app_gw_web"]
ports:
- name: http
containerPort: 8000
protocol: TCP
volumeMounts:
- mountPath: /etc/passbook
name: config-volume
livenessProbe:
httpGet:
path: /
port: http
httpHeaders:
- name: Host
value: kubernetes-healthcheck-host
readinessProbe:
httpGet:
path: /
port: http
httpHeaders:
- name: Host
value: kubernetes-healthcheck-host
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}

View File

@ -0,0 +1,20 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "passbook.fullname" . }}-appgw
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: appgw

View File

@ -32,7 +32,16 @@ spec:
paths: paths:
- path: {{ $ingressPath }} - path: {{ $ingressPath }}
backend: backend:
serviceName: {{ $fullName }} serviceName: {{ $fullName }}-web
servicePort: http
{{- end }}
{{- range .Values.ingress.app_gw_hosts }}
- host: {{ . | quote }}
http:
paths:
- path: {{ $ingressPath }}
backend:
serviceName: {{ $fullName }}-appgw
servicePort: http servicePort: http
{{- end }} {{- end }}
{{- end }} {{- end }}

View File

@ -1,7 +1,7 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: {{ include "passbook.fullname" . }} name: {{ include "passbook.fullname" . }}-web
labels: labels:
app.kubernetes.io/name: {{ include "passbook.name" . }} app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }} helm.sh/chart: {{ include "passbook.chart" . }}

View File

@ -5,7 +5,7 @@
replicaCount: 1 replicaCount: 1
image: image:
tag: 0.1.37-beta tag: 0.2.6-beta
nameOverride: "" nameOverride: ""
@ -37,6 +37,8 @@ ingress:
path: / path: /
hosts: hosts:
- passbook.k8s.local - passbook.k8s.local
app_gw_hosts:
- '*.passbook.k8s.local'
defaultHost: passbook.k8s.local defaultHost: passbook.k8s.local
tls: [] tls: []
# - secretName: chart-example-tls # - secretName: chart-example-tls

View File

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

View File

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

View File

@ -1,2 +1,2 @@
"""passbook admin""" """passbook admin"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

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

View File

@ -5,9 +5,9 @@ from django.views.generic import TemplateView
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core import __version__ from passbook.core import __version__
from passbook.core.celery import CELERY_APP
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
class AdministrationOverviewView(AdminRequiredMixin, TemplateView): class AdministrationOverviewView(AdminRequiredMixin, TemplateView):

View File

@ -1,2 +1,2 @@
"""passbook api""" """passbook api"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

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

View File

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

View File

@ -8,6 +8,6 @@ import os
import django import django
from channels.routing import get_default_application 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() django.setup()
application = get_default_application() application = get_default_application()

View File

View File

@ -0,0 +1,30 @@
"""passbook app_gw webserver management command"""
from logging import getLogger
from daphne.cli import CommandLineInterface
from django.core.management.base import BaseCommand
from django.utils import autoreload
from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__)
class Command(BaseCommand):
"""Run Daphne Webserver for app_gw"""
def handle(self, *args, **options):
"""passbook daphne server"""
autoreload.run_with_reloader(self.daphne_server)
def daphne_server(self):
"""Run daphne server within autoreload"""
autoreload.raise_last_exception()
CommandLineInterface().run([
'-p', str(CONFIG.y('app_gw.port', 8000)),
'-b', CONFIG.y('app_gw.listen', '0.0.0.0'), # nosec
'--access-log', '/dev/null',
'--application-close-timeout', '500',
'passbook.app_gw.asgi:application'
])

View File

@ -221,5 +221,13 @@ class RequestHandler:
self._set_content_type(proxy_response) self._set_content_type(proxy_response)
response = get_django_response(proxy_response, strict_cookies=False) response = get_django_response(proxy_response, strict_cookies=False)
# If response has a 'Location' header, we rewrite that location as well
if 'Location' in response:
LOGGER.debug("Rewriting Location header")
for server_name in self.app_gw.server_name:
response['Location'] = response['Location'].replace(
self._parsed_url.hostname, server_name)
LOGGER.debug(response['Location'])
# LOGGER.debug("RESPONSE RETURNED: %s", response) # LOGGER.debug("RESPONSE RETURNED: %s", response)
return response return response

View File

@ -9,7 +9,7 @@ from passbook.app_gw.proxy.utils import (cookie_from_string,
#: Default number of bytes that are going to be read in a file lecture #: Default number of bytes that are going to be read in a file lecture
DEFAULT_AMT = 2 ** 16 DEFAULT_AMT = 2 ** 16
logger = logging.getLogger('revproxy.response') logger = logging.getLogger(__name__)
def get_django_response(proxy_response, strict_cookies=False): def get_django_response(proxy_response, strict_cookies=False):

View File

@ -1,2 +1,2 @@
"""passbook audit Header""" """passbook audit Header"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

@ -22,7 +22,7 @@ class AuditEntry(UUIDModel):
ACTION_AUTHORIZE_APPLICATION = 'authorize_application' ACTION_AUTHORIZE_APPLICATION = 'authorize_application'
ACTION_SUSPICIOUS_REQUEST = 'suspicious_request' ACTION_SUSPICIOUS_REQUEST = 'suspicious_request'
ACTION_SIGN_UP = 'sign_up' ACTION_SIGN_UP = 'sign_up'
ACTION_PASSWORD_RESET = 'password_reset' # noqa ACTION_PASSWORD_RESET = 'password_reset' # noqa # nosec
ACTION_INVITE_CREATED = 'invitation_created' ACTION_INVITE_CREATED = 'invitation_created'
ACTION_INVITE_USED = 'invitation_used' ACTION_INVITE_USED = 'invitation_used'
ACTIONS = ( ACTIONS = (

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header""" """passbook captcha_factor Header"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

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

View File

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

View File

@ -29,6 +29,7 @@ class AuthenticationView(UserPassesTestMixin, View):
SESSION_PENDING_FACTORS = 'passbook_pending_factors' SESSION_PENDING_FACTORS = 'passbook_pending_factors'
SESSION_PENDING_USER = 'passbook_pending_user' SESSION_PENDING_USER = 'passbook_pending_user'
SESSION_USER_BACKEND = 'passbook_user_backend' SESSION_USER_BACKEND = 'passbook_user_backend'
SESSION_IS_SSO_LOGIN = 'passbook_sso_login'
pending_user = None pending_user = None
pending_factors = [] pending_factors = []
@ -79,6 +80,10 @@ class AuthenticationView(UserPassesTestMixin, View):
if AuthenticationView.SESSION_FACTOR not in request.session: if AuthenticationView.SESSION_FACTOR not in request.session:
# Case when no factors apply to user, return error denied # Case when no factors apply to user, return error denied
if not self.pending_factors: 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() return self.user_invalid()
factor_uuid, factor_class = self.pending_factors[0] factor_uuid, factor_class = self.pending_factors[0]
else: else:

View File

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

View File

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

View File

@ -2,11 +2,12 @@
from logging import getLogger from logging import getLogger
from daphne.cli import CommandLineInterface import cherrypy
from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import autoreload
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
from passbook.root.wsgi import application
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@ -15,15 +16,21 @@ class Command(BaseCommand):
"""Run CherryPy webserver""" """Run CherryPy webserver"""
def handle(self, *args, **options): def handle(self, *args, **options):
"""passbook daphne server""" """passbook cherrypy server"""
autoreload.run_with_reloader(self.daphne_server) cherrypy.config.update(CONFIG.get('web'))
cherrypy.tree.graft(application, '/')
def daphne_server(self): # Mount NullObject to serve static files
"""Run daphne server within autoreload""" cherrypy.tree.mount(None, settings.STATIC_URL, config={
autoreload.raise_last_exception() '/': {
CommandLineInterface().run([ 'tools.staticdir.on': True,
'-p', str(CONFIG.y('web.port', 8000)), 'tools.staticdir.dir': settings.STATIC_ROOT,
'-b', CONFIG.y('web.listen', '0.0.0.0'), # nosec 'tools.expires.on': True,
'--access-log', '/dev/null', 'tools.expires.secs': 86400,
'passbook.core.asgi:application' '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

@ -5,7 +5,7 @@ from logging import getLogger
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import autoreload from django.utils import autoreload
from passbook.core.celery import CELERY_APP from passbook.root.celery import CELERY_APP
LOGGER = getLogger(__name__) 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() name = models.TextField()
slug = models.SlugField() slug = models.SlugField()
form = '' # ModelForm-based class ued to create/edit instance
enabled = models.BooleanField(default=True) enabled = models.BooleanField(default=True)
form = '' # ModelForm-based class ued to create/edit instance
objects = InheritanceManager() objects = InheritanceManager()
@property @property
@ -409,6 +410,21 @@ class GroupMembershipPolicy(Policy):
verbose_name = _('Group Membership Policy') verbose_name = _('Group Membership Policy')
verbose_name_plural = _('Group Membership Policies') 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): class Invitation(UUIDModel):
"""Single-use invitation link""" """Single-use invitation link"""

View File

@ -7,8 +7,8 @@ from celery.exceptions import TimeoutError as CeleryTimeoutError
from django.core.cache import cache from django.core.cache import cache
from ipware import get_client_ip from ipware import get_client_ip
from passbook.core.celery import CELERY_APP
from passbook.core.models import Policy, User from passbook.core.models import Policy, User
from passbook.root.celery import CELERY_APP
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@ -74,6 +74,7 @@ class PolicyEngine:
cached_policies = [] cached_policies = []
kwargs = { kwargs = {
'__password__': getattr(self.__user, '__password__', None), '__password__': getattr(self.__user, '__password__', None),
'session': dict(getattr(self.__request, 'session', {}).items()),
} }
if self.__request: if self.__request:
kwargs['remote_ip'], _ = get_client_ip(self.__request) kwargs['remote_ip'], _ = get_client_ip(self.__request)

View File

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

View File

@ -0,0 +1,54 @@
{% load static %}
{% load i18n %}
{% load utils %}
<!DOCTYPE html>
<html lang="en" class="layout-pf layout-pf-fixed transitions">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
{% block title %}
{% title %}
{% endblock %}
</title>
<link rel="icon" type="image/png" href="{% static 'img/logo.png' %}">
<link rel="shortcut icon" type="image/png" href="{% static 'img/logo.png' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly-additions.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/passbook.css' %}">
<style>
.login-pf {
background-attachment: fixed;
scroll-behavior: smooth;
background-size: cover;
}
</style>
{% block head %}
{% endblock %}
</head>
<body class="login-pf">
{% if 'impersonate_id' in request.session %}
<div class="experimental-pf-bar">
<span id="experimentalBar" class="experimental-pf-text">
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
<a href="?__unimpersonate=True" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
</span>
</div>
{% endif %}
{% block body %}
{% endblock %}
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/patternfly.min.js' %}"></script>
<script src="{% static 'js/passbook.js' %}"></script>
{% block scripts %}
{% endblock %}
<div class="modals">
{% include 'partials/about_modal.html' %}
</div>
</body>
</html>

View File

@ -1,4 +1,4 @@
{% extends 'base/skeleton.html' %} {% extends 'base/skeleton_login.html' %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}

View File

@ -1,25 +1,14 @@
"""passbook URL Configuration""" """passbook URL Configuration"""
from logging import getLogger from logging import getLogger
from django.conf import settings from django.urls import path
from django.contrib import admin
from django.urls import include, path
from django.views.generic import RedirectView
from passbook.core.auth import view from passbook.core.auth import view
from passbook.core.views import authentication, error, overview, user from passbook.core.views import authentication, overview, user
from passbook.lib.utils.reflection import get_apps
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
admin.autodiscover()
admin.site.login = RedirectView.as_view(pattern_name='passbook_core:auth-login')
handler400 = error.BadRequestView.as_view() urlpatterns = [
handler403 = error.ForbiddenView.as_view()
handler404 = error.NotFoundView.as_view()
handler500 = error.ServerErrorView.as_view()
core_urls = [
# Authentication views # Authentication views
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'), path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'), path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'),
@ -39,27 +28,3 @@ core_urls = [
# Overview # Overview
path('', overview.OverviewView.as_view(), name='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: if not pre_user:
# No user found # No user found
return self.invalid_login(self.request) return self.invalid_login(self.request)
self.request.session.flush() # self.request.session.flush()
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
return _redirect_with_qs('passbook_core:auth-process', self.request.GET) return _redirect_with_qs('passbook_core:auth-process', self.request.GET)

View File

@ -1,2 +1,2 @@
"""passbook hibp_policy""" """passbook hibp_policy"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

@ -1,2 +1,2 @@
"""Passbook ldap app Header""" """Passbook ldap app Header"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

@ -1,2 +1,2 @@
"""passbook lib""" """passbook lib"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

@ -137,4 +137,6 @@ def signal_handler(sender, **kwargs):
"""Add all loaded config files to autoreload watcher""" """Add all loaded config files to autoreload watcher"""
for path in CONFIG.loaded_file: for path in CONFIG.loaded_file:
sender.watch_file(path) sender.watch_file(path)
autoreload_started.connect(signal_handler) autoreload_started.connect(signal_handler)

View File

@ -23,9 +23,13 @@ email:
use_ssl: false use_ssl: false
from: passbook <passbook@domain.tld> from: passbook <passbook@domain.tld>
web: web:
listen: 0.0.0.0 server.socket_host: 0.0.0.0
port: 8000 server.socket_port: 8000
threads: 30 server.thread_pool: 20
log.screen: false
log.access_file: ''
log.error_file: ''
debug: false debug: false
secure_proxy_header: secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https HTTP_X_FORWARDED_PROTO: https
@ -96,3 +100,6 @@ saml_idp:
types: types:
- passbook.saml_idp.processors.generic - passbook.saml_idp.processors.generic
- passbook.saml_idp.processors.salesforce - passbook.saml_idp.processors.salesforce
app_gw:
listen: 0.0.0.0
port: 8000

View File

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

View File

@ -1,2 +1,2 @@
"""passbook oauth_client Header""" """passbook oauth_client Header"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

@ -29,14 +29,13 @@ class OAuthSource(Source):
def get_login_button(self): def get_login_button(self):
url = reverse_lazy('passbook_oauth_client:oauth-client-login', url = reverse_lazy('passbook_oauth_client:oauth-client-login',
kwargs={'source_slug': self.slug}) kwargs={'source_slug': self.slug})
# if self.provider_type == 'github':
# return url, 'github-logo', _('GitHub')
return url, self.provider_type, self.name return url, self.provider_type, self.name
@property @property
def additional_info(self): def additional_info(self):
return "Callback URL: '%s'" % reverse_lazy('passbook_oauth_client:oauth-client-callback', return "Callback URL: <pre>%s</pre>" % \
kwargs={'source_slug': self.slug}) reverse_lazy('passbook_oauth_client:oauth-client-callback',
kwargs={'source_slug': self.slug})
def has_user_settings(self): def has_user_settings(self):
"""Entrypoint to integrate with User settings. Can either return False if no """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.conf import settings
from django.contrib import messages 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.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render 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.utils.translation import ugettext as _
from django.views.generic import RedirectView, View 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.lib.utils.reflection import app
from passbook.oauth_client.clients import get_client from passbook.oauth_client.clients import get_client
from passbook.oauth_client.models import OAuthSource, UserOAuthSourceConnection from passbook.oauth_client.models import OAuthSource, UserOAuthSourceConnection
@ -128,11 +129,6 @@ class OAuthCallback(OAuthClientMixin, View):
"Return url to redirect on login failure." "Return url to redirect on login failure."
return settings.LOGIN_URL 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): def get_or_create_user(self, source, access, info):
"Create a shell auth.User." "Create a shell auth.User."
raise NotImplementedError() raise NotImplementedError()
@ -149,14 +145,22 @@ class OAuthCallback(OAuthClientMixin, View):
except KeyError: except KeyError:
return None 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 # pylint: disable=unused-argument
def handle_existing_user(self, source, user, access, info): def handle_existing_user(self, source, user, access, info):
"Login user and redirect." "Login user and redirect."
login(self.request, user)
messages.success(self.request, _("Successfully authenticated with %(source)s!" % { messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
'source': self.source.name '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): def handle_login_failure(self, source, reason):
"Message user and redirect on error." "Message user and redirect on error."
@ -176,12 +180,9 @@ class OAuthCallback(OAuthClientMixin, View):
access.user = user access.user = user
access.save() access.save()
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user) 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'): if app('passbook_audit'):
pass pass
# TODO: Create audit entry
# from passbook.audit.models import something # from passbook.audit.models import something
# something.event(user=user,) # something.event(user=user,)
# Event.create( # Event.create(
@ -197,10 +198,13 @@ class OAuthCallback(OAuthClientMixin, View):
return redirect(reverse('passbook_oauth_client:oauth-client-user', kwargs={ return redirect(reverse('passbook_oauth_client:oauth-client-user', kwargs={
'source_slug': self.source.slug '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!" % { messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
'source': self.source.name '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): class DisconnectView(LoginRequiredMixin, View):

View File

@ -1,2 +1,2 @@
"""passbook oauth_provider Header""" """passbook oauth_provider Header"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

@ -1,4 +1,4 @@
"""passbook OAuth2 IDP Forms""" """passbook OAuth2 Provider Forms"""
from django import forms from django import forms

View File

@ -25,8 +25,6 @@ class OAuth2Provider(Provider, AbstractApplication):
reverse('passbook_oauth_provider:token')), reverse('passbook_oauth_provider:token')),
'userinfo_url': request.build_absolute_uri( 'userinfo_url': request.build_absolute_uri(
reverse('passbook_api:openid')), reverse('passbook_api:openid')),
'openid_url': request.build_absolute_uri(
reverse('passbook_oauth_provider:openid-discovery'))
} }
class Meta: class Meta:

View File

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

View File

@ -31,19 +31,10 @@
</div> </div>
</div> </div>
</form> </form>
<hr>
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 control-label">{% trans 'OpenID Configuration URL' %}</label>
<div class="col-sm-9">
<input type="text"class="form-control" readonly value="{{ openid_url }}">
</div>
</div>
</form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">{% trans 'Close' %}</button> <button type="button" class="btn btn-primary" data-dismiss="modal">{% trans 'Close' %}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@
from django.urls import path from django.urls import path
from oauth2_provider import views from oauth2_provider import views
from passbook.oauth_provider.views import oauth2, openid from passbook.oauth_provider.views import oauth2
urlpatterns = [ urlpatterns = [
# Custom OAuth 2 Authorize View # Custom OAuth 2 Authorize View
@ -17,9 +17,4 @@ urlpatterns = [
path("token/", views.TokenView.as_view(), name="token"), path("token/", views.TokenView.as_view(), name="token"),
path("revoke_token/", views.RevokeTokenView.as_view(), name="revoke-token"), path("revoke_token/", views.RevokeTokenView.as_view(), name="revoke-token"),
path("introspect/", views.IntrospectTokenView.as_view(), name="introspect"), path("introspect/", views.IntrospectTokenView.as_view(), name="introspect"),
# OpenID-Connect Discovery
path('.well-known/openid-configuration', openid.OpenIDConfigurationView.as_view(),
name='openid-discovery'),
path('.well-known/jwks.json', openid.JSONWebKeyView.as_view(),
name='openid-jwks'),
] ]

View File

@ -57,10 +57,10 @@ class PassbookAuthorizationView(AccessMixin, AuthorizationView):
provider.save() provider.save()
self._application = application self._application = application
# Check permissions # Check permissions
passing, policy_meaages = self.user_has_access(self._application, request.user) passing, policy_messages = self.user_has_access(self._application, request.user)
if not passing: if not passing:
for policy_meaage in policy_meaages: for policy_message in policy_messages:
messages.error(request, policy_meaage) messages.error(request, policy_message)
return redirect('passbook_oauth_provider:oauth2-permission-denied') return redirect('passbook_oauth_provider:oauth2-permission-denied')
# Some clients don't pass response_type, so we default to code # Some clients don't pass response_type, so we default to code
if 'response_type' not in request.GET: if 'response_type' not in request.GET:

View File

@ -1,30 +0,0 @@
"""passbook oauth provider OpenID Views"""
from django.http import HttpRequest, JsonResponse
from django.shortcuts import reverse
from django.views.generic import View
class OpenIDConfigurationView(View):
"""Return 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')),
'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",
],
})
class JSONWebKeyView(View):
"""JSON Web Key View"""
def get(self, request: HttpRequest):
"""JSON Webkeys are not implemented yet, hence return an empty object"""
return JsonResponse({})

View File

View File

@ -0,0 +1,31 @@
"""passbook auth oidc provider app config"""
from logging import getLogger
from django.apps import AppConfig
from django.db.utils import InternalError, OperationalError, ProgrammingError
from django.urls import include, path
LOGGER = getLogger(__name__)
class PassbookOIDCProviderConfig(AppConfig):
"""passbook auth oidc provider app config"""
name = 'passbook.oidc_provider'
label = 'passbook_oidc_provider'
verbose_name = 'passbook OIDC Provider'
def ready(self):
try:
from Cryptodome.PublicKey import RSA
from oidc_provider.models import RSAKey
if not RSAKey.objects.exists():
key = RSA.generate(2048)
rsakey = RSAKey(key=key.exportKey('PEM').decode('utf8'))
rsakey.save()
LOGGER.info("Created key")
except (OperationalError, ProgrammingError, InternalError):
pass
from passbook.root import urls
urls.urlpatterns.append(
path('application/oidc/', include('oidc_provider.urls', namespace='oidc_provider')),
)

View File

@ -0,0 +1,38 @@
"""passbook OIDC IDP Forms"""
from django import forms
from oauth2_provider.generators import (generate_client_id,
generate_client_secret)
from oidc_provider.models import Client
from passbook.oidc_provider.models import OpenIDProvider
class OIDCProviderForm(forms.ModelForm):
"""OpenID Client form"""
def __init__(self, *args, **kwargs):
# Correctly load data from 1:1 rel
if 'instance' in kwargs and kwargs['instance']:
kwargs['instance'] = kwargs['instance'].oidc_client
super().__init__(*args, **kwargs)
self.fields['client_id'].initial = generate_client_id()
self.fields['client_secret'].initial = generate_client_secret()
def save(self, *args, **kwargs):
response = super().save(*args, **kwargs)
# Check if openidprovider class instance exists
if not OpenIDProvider.objects.filter(oidc_client=self.instance).exists():
OpenIDProvider.objects.create(oidc_client=self.instance)
return response
class Meta:
model = Client
fields = [
'name', 'client_type', 'client_id', 'client_secret', 'response_types',
'jwt_alg', 'reuse_consent', 'require_consent', '_redirect_uris', '_scope'
]
# exclude = ['owner', 'website_url', 'terms_url', 'contact_email', 'logo', ]
labels = {
'client_secret': "Client Secret"
}

View File

@ -0,0 +1,30 @@
"""OIDC Permission checking"""
from logging import getLogger
from django.contrib import messages
from django.shortcuts import redirect
from passbook.core.models import Application
from passbook.core.policies import PolicyEngine
LOGGER = getLogger(__name__)
def check_permissions(request, user, client):
"""Check permissions, used for
https://django-oidc-provider.readthedocs.io/en/latest/
sections/settings.html#oidc-after-userlogin-hook"""
try:
application = client.openidprovider.application
except Application.DoesNotExist:
return redirect('passbook_oauth_provider:oauth2-permission-denied')
LOGGER.debug("Checking permissions of %s on application %s...", user, application)
policy_engine = PolicyEngine(application.policies.all())
policy_engine.for_user(user).with_request(request).build()
# Check permissions
passing, policy_messages = policy_engine.result
if not passing:
for policy_message in policy_messages:
messages.error(request, policy_message)
return redirect('passbook_oauth_provider:oauth2-permission-denied')
return None

View File

@ -0,0 +1,25 @@
# Generated by Django 2.2.3 on 2019-07-05 12:16
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('oidc_provider', '0026_client_multiple_response_types'),
('passbook_core', '0024_ssologinpolicy'),
]
operations = [
migrations.CreateModel(
name='OpenIDProvider',
fields=[
('provider_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Provider')),
('oidc_client', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client')),
],
bases=('passbook_core.provider',),
),
]

View File

@ -0,0 +1,45 @@
"""oidc models"""
from django.db import models
from django.shortcuts import reverse
from django.utils.translation import gettext as _
from oidc_provider.models import Client
from passbook.core.models import Provider
class OpenIDProvider(Provider):
"""Proxy model for OIDC Client"""
# Since oidc_provider doesn't currently support swappable models
# (https://github.com/juanifioren/django-oidc-provider/pull/305)
# we have a 1:1 relationship, and update oidc_client when the form is saved.
oidc_client = models.OneToOneField(Client, on_delete=models.CASCADE)
form = 'passbook.oidc_provider.forms.OIDCProviderForm'
@property
def name(self):
"""Name property for UI"""
return self.oidc_client.name
def __str__(self):
return "OpenID Connect Provider %s" % self.oidc_client.__str__()
def html_setup_urls(self, request):
"""return template and context modal with URLs for authorize, token, openid-config, etc"""
return "oidc_provider/setup_url_modal.html", {
'provider': self,
'authorize': request.build_absolute_uri(
reverse('oidc_provider:authorize')),
'token': request.build_absolute_uri(
reverse('oidc_provider:token')),
'userinfo': request.build_absolute_uri(
reverse('oidc_provider:userinfo')),
'provider_info': request.build_absolute_uri(
reverse('oidc_provider:provider-info')),
}
class Meta:
verbose_name = _('OpenID Provider')
verbose_name_plural = _('OpenID Providers')

View File

@ -0,0 +1 @@
django-oidc-provider

View File

@ -0,0 +1,7 @@
"""passbook OIDC Provider"""
INSTALLED_APPS = [
'oidc_provider',
]
OIDC_AFTER_USERLOGIN_HOOK = "passbook.oidc_provider.lib.check_permissions"

View File

@ -0,0 +1,70 @@
{% extends "login/base.html" %}
{% load utils %}
{% load i18n %}
{% block title %}
{% title 'Authorize Application' %}
{% endblock %}
{% block card %}
<header class="login-pf-header">
<h1>{% trans 'Authorize Application' %}</h1>
</header>
<form method="POST">
{% csrf_token %}
{% if not error %}
{% csrf_token %}
{% for field in form %}
{% if field.is_hidden %}
{{ field }}
{% endif %}
{% endfor %}
<div class="form-group">
<p class="subtitle">
{% blocktrans with remote=client.name %}
You're about to sign into {{ remote }}
{% endblocktrans %}
</p>
<p>{% trans "Application requires following permissions" %}</p>
<ul>
{% for scope in scopes %}
<li>{{ scope.name }}</li>
{% endfor %}
</ul>
{{ hidden_inputs }}
{{ form.errors }}
{{ form.non_field_errors }}
<p>
{% blocktrans with user=user %}
You are logged in as {{ user }}. Not you?
{% endblocktrans %}
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
</p>
<div class="form-group">
<input type="submit" class="btn btn-success btn-disabled btn-lg click-spinner" name="allow" value="{% trans 'Continue' %}">
<a href="{% back %}" class="btn btn-default btn-lg">{% trans "Cancel" %}</a>
</div>
<div class="form-group spinner-hidden hidden">
<div class="spinner"></div>
</div>
</div>
{% else %}
<div class="login-group">
<p class="subtitle">
{% blocktrans with err=error.error %}Error: {{ err }}{% endblocktrans %}
</p>
<p>{{ error.description }}</p>
</div>
{% endif %}
</form>
{% endblock %}
{% block scripts %}
<script>
$('.click-spinner').on('click', function (e) {
$('.spinner-hidden').removeClass('hidden');
$(e.target).addClass('disabled');
})
</script>
{% endblock %}

View File

@ -0,0 +1,49 @@
{% load i18n %}
<button class="btn btn-default btn-sm" data-toggle="modal" data-target="#{{ provider.pk }}">{% trans 'View Setup URLs' %}</button>
<div class="modal fade" id="{{ provider.pk }}" tabindex="-1" role="dialog" aria-labelledby="{{ provider.pk }}Label" 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" aria-label="Close">
<span class="pficon pficon-close"></span>
</button>
<h4 class="modal-title" id="{{ provider.pk }}Label">{% trans 'Setup URLs' %}</h4>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 control-label">{% trans 'Authorize URL' %}</label>
<div class="col-sm-9">
<input type="text"class="form-control" readonly value="{{ authorize }}">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans 'Token URL' %}</label>
<div class="col-sm-9">
<input type="text" class="form-control" readonly value="{{ token }}">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans 'Userinfo Endpoint' %}</label>
<div class="col-sm-9">
<input type="text" class="form-control" readonly value="{{ userinfo }}">
</div>
</div>
</form>
<hr>
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 control-label">{% trans 'OpenID Configuration URL' %}</label>
<div class="col-sm-9">
<input type="text"class="form-control" readonly value="{{ provider_info }}">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>

View File

@ -1,2 +1,2 @@
"""passbook otp Header""" """passbook otp Header"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

@ -1,2 +1,2 @@
"""passbook password_expiry""" """passbook password_expiry"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

View File

@ -7,7 +7,7 @@ from celery import Celery, signals
from django.conf import settings from django.conf import settings
# set the default Django settings module for the 'celery' program. # 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__) LOGGER = logging.getLogger(__name__)

View File

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

View File

@ -82,6 +82,7 @@ INSTALLED_APPS = [
'passbook.ldap.apps.PassbookLdapConfig', 'passbook.ldap.apps.PassbookLdapConfig',
'passbook.oauth_client.apps.PassbookOAuthClientConfig', 'passbook.oauth_client.apps.PassbookOAuthClientConfig',
'passbook.oauth_provider.apps.PassbookOAuthProviderConfig', 'passbook.oauth_provider.apps.PassbookOAuthProviderConfig',
'passbook.oidc_provider.apps.PassbookOIDCProviderConfig',
'passbook.saml_idp.apps.PassbookSAMLIDPConfig', 'passbook.saml_idp.apps.PassbookSAMLIDPConfig',
'passbook.otp.apps.PassbookOTPConfig', 'passbook.otp.apps.PassbookOTPConfig',
'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig', 'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig',
@ -122,7 +123,6 @@ CACHES = {
MIDDLEWARE = [ MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'passbook.app_gw.middleware.ApplicationGatewayMiddleware', 'passbook.app_gw.middleware.ApplicationGatewayMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
@ -132,7 +132,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
ROOT_URLCONF = 'passbook.core.urls' ROOT_URLCONF = 'passbook.root.urls'
TEMPLATES = [ TEMPLATES = [
{ {
@ -221,7 +221,7 @@ CELERY_BEAT_SCHEDULE = {
if not DEBUG: if not DEBUG:
sentry_init( sentry_init(
dsn="https://55b5dd780bc14f4c96bba69b7a9abbcc@sentry.services.beryju.org/8", dsn="https://33cdbcb23f8b436dbe0ee06847410b67@sentry.beryju.org/3",
integrations=[ integrations=[
DjangoIntegration(), DjangoIntegration(),
CeleryIntegration(), CeleryIntegration(),
@ -232,14 +232,13 @@ if not DEBUG:
], ],
send_default_pii=True, send_default_pii=True,
before_send=before_send, before_send=before_send,
release='p2@%s' % __version__ release='passbook@%s' % __version__
) )
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/ # https://docs.djangoproject.com/en/2.1/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
with CONFIG.cd('log'): with CONFIG.cd('log'):
LOGGING = { LOGGING = {

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""" """passbook saml_idp Header"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

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

View File

@ -1,2 +1,2 @@
"""passbook suspicious_policy""" """passbook suspicious_policy"""
__version__ = '0.1.37-beta' __version__ = '0.2.6-beta'

View File

@ -5,7 +5,7 @@ isort
astroid==2.0.4 astroid==2.0.4
pylint==2.1.1 pylint==2.1.1
pylint-django==2.0.2 pylint-django==2.0.2
prospector prospector==1.1.5
django-debug-toolbar django-debug-toolbar
pycodestyle<2.4.0,>=2.0.0 pycodestyle<2.4.0,>=2.0.0
bumpversion bumpversion

View File

@ -1,4 +1,4 @@
-r passbook/core/requirements.txt -r passbook/root/requirements.txt
-r passbook/oauth_client/requirements.txt -r passbook/oauth_client/requirements.txt
-r passbook/ldap/requirements.txt -r passbook/ldap/requirements.txt
-r passbook/saml_idp/requirements.txt -r passbook/saml_idp/requirements.txt
@ -7,4 +7,5 @@
-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 -r passbook/api/requirements.txt
-r passbook/app_gw/requirements.txt -r passbook/app_gw/requirements.txt
-r passbook/oidc_provider/requirements.txt