Compare commits

..

54 Commits

Author SHA1 Message Date
80049413f0 bump version: 0.1.26-beta -> 0.1.27-beta 2019-03-22 14:51:13 +01:00
2739442d4a prepare 0.1.27 2019-03-22 14:51:09 +01:00
c679f0a67c bump version: 0.1.25-beta -> 0.1.26-beta 2019-03-22 12:48:00 +01:00
d9a952dd03 prepare 0.1.26 2019-03-22 12:47:57 +01:00
9a1a0f0aa8 Merge branch '35-better-error-templates' into 'master'
Resolve "Better Error templates"

Closes #35

See merge request BeryJu.org/passbook!20
2019-03-22 11:19:42 +00:00
4d6bb60134 add custom template views 2019-03-22 12:16:30 +01:00
80e6d59382 Merge branch '34-parsed-url-cached' into 'master'
Resolve "InvalidUpstream: Upstream URL scheme must be either 'http' or 'https' (https://ory1-esxi-prod-1.ory1.beryju.org)."

Closes #34

See merge request BeryJu.org/passbook!19
2019-03-22 09:58:44 +00:00
81ac951872 validate upstream in form 2019-03-22 10:55:26 +01:00
f33e553cfd always parse url instead of once 2019-03-22 10:55:04 +01:00
9b0240dc26 bump version: 0.1.24-beta -> 0.1.25-beta 2019-03-21 16:50:00 +01:00
c327310392 prepare 0.1.24-beta release 2019-03-21 16:49:57 +01:00
457375287c Merge branch '30-application-security-gateway' into 'master'
Resolve "Application Security Gateway (Reverse Proxy)"

Closes #30

See merge request BeryJu.org/passbook!17
2019-03-21 15:41:34 +00:00
7e87bfef5b validate server_name in form 2019-03-21 16:36:38 +01:00
a7af5268de Invalidate cache when ApplicationGateway instance is saved 2019-03-21 16:27:37 +01:00
6d916029bb implement actual Rewriting logic 2019-03-21 16:22:07 +01:00
81fdcbadad add compiled regex to RewriteRule 2019-03-21 16:21:51 +01:00
ec1e25fe71 cleanup property_mapping list 2019-03-21 16:21:11 +01:00
b5306e4a94 Redirect to login on reverse proxy 2019-03-21 15:15:01 +01:00
801b8a1e59 prevent ZeroDivisionError 2019-03-21 15:05:04 +01:00
3a52059793 cleanup post-migration mess 2019-03-21 15:02:33 +01:00
10b7d99b37 Merge branch 'master' into 30-application-security-gateway
# Conflicts:
#	passbook/core/policies.py
#	passbook/core/settings.py
2019-03-21 14:58:10 +01:00
6be8d0cbb2 Better handle policy timeouts 2019-03-21 14:53:57 +01:00
5b8e3689ec Check for policies in app_gw 2019-03-21 14:53:47 +01:00
25a5d8f5da Don't use LoginRequired for PermissionDenied View 2019-03-21 14:53:38 +01:00
883d439544 add timeout field to policy to prevent stuck policies 2019-03-21 14:48:51 +01:00
1c3b5889e5 Merge branch '33-cache-policy-results' into 'master'
Resolve "Cache Policy Results"

Closes #33

See merge request BeryJu.org/passbook!18
2019-03-21 10:40:36 +00:00
87012b65e1 add redis as service in CI for unittests 2019-03-21 11:35:40 +01:00
29913773a7 invalidate cache when policy is saved 2019-03-21 11:29:11 +01:00
0bc6a4fed4 explicitly use redis db 2019-03-21 11:28:57 +01:00
4645d8353f utilise cache in PolicyEngine 2019-03-21 11:08:32 +01:00
260c5555fa add redis dependency back in for caching 2019-03-21 11:08:08 +01:00
6f7b917c38 bump version: 0.1.23-beta -> 0.1.24-beta 2019-03-20 23:00:33 +01:00
1456ee6d3e prepare 0.1.24 2019-03-20 23:00:22 +01:00
ae3d3d0295 fix TypeError: can only concatenate list (not "str") to list 2019-03-20 22:50:09 +01:00
c23ceacd0b initial implementation of reverse proxy, using django-revproxy from within a middleware
add new config entry "primary_domain" which is used to set the cookie domain
2019-03-20 22:42:47 +01:00
5155204283 Merge branch '32-automatically-set-owner-field-when-creating-oauth-provider' into 'master'
Resolve "Automatically set owner field when creating OAuth Provider"

Closes #32

See merge request BeryJu.org/passbook!16
2019-03-20 21:20:54 +00:00
5509ec9b0f Merge branch '29-oauth-provider-add-extra-info-button-to-show-urls' into 'master'
Resolve "OAuth Provider: Add extra info button to show URLs"

Closes #29

See merge request BeryJu.org/passbook!15
2019-03-20 21:17:36 +00:00
d6f9b2e47d remove user field from form. Closes #32 2019-03-20 20:09:27 +01:00
67aa4aef11 add modal for OAuth Providers showing the URLs 2019-03-20 20:03:28 +01:00
9e46c8bfec bump version: 0.1.22-beta -> 0.1.23-beta 2019-03-18 20:54:31 +01:00
1eaa9b9733 prepare 0.1.23 2019-03-18 20:54:23 +01:00
ee05834b69 Merge branch '28-openid-connect-discovery' into 'master'
set issuer to root address instead of well-known path

Closes #28

See merge request BeryJu.org/passbook!13
2019-03-18 19:52:13 +00:00
fccc8f4959 set issuer to root address instead of well-known path 2019-03-18 20:42:32 +01:00
c721620f96 Merge branch '28-openid-connect-discovery' into 'master'
Resolve "OpenID Connect Discovery support"

Closes #28

See merge request BeryJu.org/passbook!12
2019-03-18 19:42:08 +00:00
c9f73d718e start implementing openid connect discovery 2019-03-18 20:35:11 +01:00
bfa58be721 bump version: 0.1.21-beta -> 0.1.22-beta 2019-03-14 21:22:15 +01:00
4bb602149e perapre 0.1.22 2019-03-14 21:20:55 +01:00
81ab9092fc Fix OAuth Client's disconnect view having invalid URL names 2019-03-14 21:19:14 +01:00
29d5962c4c add Azure AD Source 2019-03-14 21:18:55 +01:00
5c75339946 point to correct icons 2019-03-14 21:18:13 +01:00
4774d9a46c fix delete form not working 2019-03-14 21:17:41 +01:00
dbe16ba4fd fix layout when on mobile viewport and scrolling 2019-03-14 21:17:28 +01:00
6972cf00a0 move icons to single folder, cleanup 2019-03-14 21:17:07 +01:00
0445be9712 fix missing debug template 2019-03-14 21:16:27 +01:00
109 changed files with 1204 additions and 109 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.1.21-beta
current_version = 0.1.27-beta
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -53,3 +53,7 @@ values =
[bumpversion:file:passbook/otp/__init__.py]
[bumpversion:file:passbook/app_gw/__init__.py]
[bumpversion:file:passbook/suspicious_policy/__init__.py]

View File

@ -12,6 +12,7 @@ stages:
image: python:3.6
services:
- postgres:latest
- redis:latest
variables:
POSTGRES_DB: passbook
@ -54,7 +55,7 @@ package-docker:
before_script:
- echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.1.21-beta
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.1.27-beta
stage: build
only:
- tags

View File

@ -3,7 +3,7 @@ from setuptools import setup
setup(
name='django-allauth-passbook',
version='0.1.21-beta',
version='0.1.27-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.21-beta',
version='0.1.27-beta',
author='BeryJu.org',
author_email='support@beryju.org',
url='https://passbook.beryju.org',

72
debian/changelog vendored
View File

@ -1,10 +1,78 @@
passbook (0.1.27) stable; urgency=medium
* bump version: 0.1.25-beta -> 0.1.26-beta
* fix broken app_gw
-- Jens Langhammer <jens.langhammer@beryju.org> Fri, 22 Mar 2019 13:50:31 +0000
passbook (0.1.26) stable; urgency=medium
* bump version: 0.1.24-beta -> 0.1.25-beta
* always parse url instead of once
* validate upstream in form
* add custom template views
-- Jens Langhammer <jens.langhammer@beryju.org> Fri, 22 Mar 2019 11:47:08 +0000
passbook (0.1.25) stable; urgency=medium
* initial implementation of reverse proxy, using django-revproxy from within a middleware
* fix TypeError: can only concatenate list (not "str") to list
* bump version: 0.1.23-beta -> 0.1.24-beta
* add redis dependency back in for caching
* utilise cache in PolicyEngine
* explicitly use redis db
* invalidate cache when policy is saved
* add redis as service in CI for unittests
* add timeout field to policy to prevent stuck policies
* Don't use LoginRequired for PermissionDenied View
* Check for policies in app_gw
* Better handle policy timeouts
* cleanup post-migration mess
* prevent ZeroDivisionError
* Redirect to login on reverse proxy
* cleanup property_mapping list
* add compiled regex to RewriteRule
* implement actual Rewriting logic
* Invalidate cache when ApplicationGateway instance is saved
* validate server_name in form
-- Jens Langhammer <jens.langhammer@beryju.org> Thu, 21 Mar 2019 15:47:58 +0000
passbook (0.1.24) stable; urgency=medium
* bump version: 0.1.22-beta -> 0.1.23-beta
* add modal for OAuth Providers showing the URLs
* remove user field from form. Closes #32
-- Jens Langhammer <jens.langhammer@beryju.org> Wed, 20 Mar 2019 21:59:21 +0000
passbook (0.1.23) stable; urgency=medium
* add support for OpenID-Connect Discovery
-- Jens Langhammer <jens.langhammer@beryju.org> Thu, 18 Mar 2019 20:19:27 +0000
passbook (0.1.22) stable; urgency=medium
* bump version: 0.1.20-beta -> 0.1.21-beta
* fix missing debug template
* move icons to single folder, cleanup
* fix layout when on mobile viewport and scrolling
* fix delete form not working
* point to correct icons
* add Azure AD Source
* Fix OAuth Client's disconnect view having invalid URL names
-- Jens Langhammer <jens.langhammer@beryju.org> Thu, 14 Mar 2019 20:19:27 +0000
passbook (0.1.21) stable; urgency=medium
* bump version: 0.1.19-beta -> 0.1.20-beta
* add request debug view
* detect HTTPS from reverse proxy
-- Jens Langhammer <jens.langhammer@beryju.org> Thu, 14 Mar 2019 17:01:49 +0000
-- Jens Langhammer <jens.langhammer@beryju.org> Thu, 14 Mar 2019 17:01:49 +0000
passbook (0.1.20) stable; urgency=medium
@ -12,7 +80,7 @@ passbook (0.1.20) stable; urgency=medium
* fix GitHub Pretend again
* add user settings for Sources
-- Jens Langhammer <jens.langhammer@beryju.org> Wed, 13 Mar 2019 15:49:44 +0000
-- Jens Langhammer <jens.langhammer@beryju.org> Wed, 13 Mar 2019 15:49:44 +0000
passbook (0.1.18) stable; urgency=medium

2
debian/control vendored
View File

@ -8,7 +8,7 @@ Standards-Version: 3.9.6
Package: passbook
Architecture: all
Recommends: mysql-server, rabbitmq-server
Recommends: mysql-server, rabbitmq-server, redis-server
Pre-Depends: adduser, libldap2-dev, libsasl2-dev
Depends: python3 (>= 3.5) | python3.6 | python3.7, python3-pip, dbconfig-pgsql | dbconfig-no-thanks, ${misc:Depends}
Description: Authentication Provider/Proxy supporting protocols like SAML, OAuth, LDAP and more.

View File

@ -11,9 +11,13 @@ debug: false
secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https
rabbitmq: guest:guest@localhost/passbook
redis: localhost/0
# Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: true
primary_domain: passbook.local
passbook:
sign_up:
# Enables signup, created users are stored in internal Database and created in LDAP if ldap.create_users is true

View File

@ -1,6 +1,6 @@
apiVersion: v1
appVersion: "0.1.21-beta"
appVersion: "0.1.27-beta"
description: A Helm chart for passbook.
name: passbook
version: "0.1.21-beta"
version: "0.1.27-beta"
icon: https://passbook.beryju.org/images/logo.png

Binary file not shown.

View File

@ -5,5 +5,8 @@ dependencies:
- name: postgresql
repository: https://kubernetes-charts.storage.googleapis.com/
version: 3.10.1
digest: sha256:c36e054785f7d706d7d3f525eb1b167dbc89b42f84da7fc167a18bbb6542c999
generated: 2019-03-11T20:36:35.125079+01:00
- name: redis
repository: https://kubernetes-charts.storage.googleapis.com/
version: 5.1.0
digest: sha256:8bf68bc928a2e3c0f05139635be05fa0840554c7bde4cecd624fac78fb5fa5a3
generated: 2019-03-21T11:06:51.553379+01:00

View File

@ -5,3 +5,6 @@ dependencies:
- name: postgresql
version: 3.10.1
repository: https://kubernetes-charts.storage.googleapis.com/
- name: redis
version: 5.1.0
repository: https://kubernetes-charts.storage.googleapis.com/

View File

@ -37,6 +37,7 @@ data:
secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https
rabbitmq: "user:{{ .Values.rabbitmq.rabbitmq.password }}@{{ .Release.Name }}-rabbitmq"
redis: ":{{ .Values.redis.password }}@{{ .Release.Name }}-redis-master/0"
# Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: {{ .Values.config.error_reporting }}
@ -46,6 +47,7 @@ data:
secret_key: {{ randAlphaNum 50 }}
{{- end }}
primary_domain: {{ .Values.primary_domain }}
domains:
{{- range .Values.ingress.hosts }}
- {{ . | quote }}
@ -123,6 +125,7 @@ data:
- passbook.oauth_client.source_types.reddit
- passbook.oauth_client.source_types.supervisr
- passbook.oauth_client.source_types.twitter
- passbook.oauth_client.source_types.azure_ad
saml_idp:
signing: true
autosubmit: false

View File

@ -5,7 +5,7 @@
replicaCount: 1
image:
tag: 0.1.21-beta
tag: 0.1.27-beta
nameOverride: ""

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = '0.1.21-beta'
__version__ = '0.1.27-beta'

View File

@ -1,2 +1,2 @@
"""passbook admin"""
__version__ = '0.1.21-beta'
__version__ = '0.1.27-beta'

View File

@ -0,0 +1,31 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load utils %}
{% block title %}
{% title %}
{% endblock %}
{% block content %}
<div class="container">
<h1><span class="pficon-applications"></span> {% trans "Request" %}</h1>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Key' %}</th>
<th>{% trans 'Value' %}</th>
</tr>
</thead>
<tbody>
{% for key, value in request_dict.items %}
<tr>
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -36,7 +36,7 @@
<tbody>
{% for property_mapping in object_list %}
<tr>
<td>{{ property_mapping.name }} ({{ property_mapping.slug }})</td>
<td>{{ property_mapping.name }}</td>
<td>{{ property_mapping|verbose_name }}</td>
<td>
<a class="btn btn-default btn-sm"

View File

@ -57,6 +57,10 @@
<a class="btn btn-default btn-sm"
href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %}
{% get_htmls provider as htmls %}
{% for html in htmls %}
{{ html|safe }}
{% endfor %}
</td>
</tr>
{% endfor %}

View File

@ -5,6 +5,8 @@ from logging import getLogger
from django import template
from django.db.models import Model
from passbook.lib.utils.template import render_to_string
register = template.Library()
LOGGER = getLogger(__name__)
@ -29,3 +31,24 @@ def get_links(model_instance):
pass
return links
@register.simple_tag(takes_context=True)
def get_htmls(context, model_instance):
"""Find all html_ methods on an object instance, run them and return as dict"""
prefix = 'html_'
htmls = []
if not isinstance(model_instance, Model):
LOGGER.warning("Model %s is not instance of Model", model_instance)
return htmls
try:
for name, method in inspect.getmembers(model_instance, predicate=inspect.ismethod):
if name.startswith(prefix):
template, _context = method(context.get('request'))
htmls.append(render_to_string(template, _context))
except NotImplementedError:
pass
return htmls

View File

@ -1,2 +1,2 @@
"""passbook api"""
__version__ = '0.1.21-beta'
__version__ = '0.1.27-beta'

BIN
passbook/app_gw/.DS_Store vendored Normal file

Binary file not shown.

View File

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

5
passbook/app_gw/admin.py Normal file
View File

@ -0,0 +1,5 @@
"""passbook Application Security Gateway model admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_app_gw')

16
passbook/app_gw/apps.py Normal file
View File

@ -0,0 +1,16 @@
"""passbook Application Security Gateway app"""
from importlib import import_module
from django.apps import AppConfig
class PassbookApplicationApplicationGatewayConfig(AppConfig):
"""passbook app_gw app"""
name = 'passbook.app_gw'
label = 'passbook_app_gw'
verbose_name = 'passbook Application Security Gateway'
mountpoint = 'app_gw/'
def ready(self):
import_module('passbook.app_gw.signals')

66
passbook/app_gw/forms.py Normal file
View File

@ -0,0 +1,66 @@
"""passbook Application Security Gateway Forms"""
from urllib.parse import urlparse
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms import ValidationError
from django.utils.translation import gettext as _
from passbook.app_gw.models import ApplicationGatewayProvider, RewriteRule
from passbook.lib.fields import DynamicArrayField
class ApplicationGatewayProviderForm(forms.ModelForm):
"""Security Gateway Provider form"""
def clean_server_name(self):
"""Check if server_name is in DB already, since
Postgres ArrayField doesn't suppport keys."""
current = self.cleaned_data.get('server_name')
if ApplicationGatewayProvider.objects \
.filter(server_name__overlap=current) \
.exclude(pk=self.instance.pk).exists():
raise ValidationError(_("Server Name already in use."))
return current
def clean_upstream(self):
"""Check that upstream begins with http(s)"""
for upstream in self.cleaned_data.get('upstream'):
_parsed_url = urlparse(upstream)
if _parsed_url.scheme not in ('http', 'https'):
raise ValidationError(_("URL Scheme must be either http or https"))
return self.cleaned_data.get('upstream')
class Meta:
model = ApplicationGatewayProvider
fields = ['server_name', 'upstream', 'enabled', 'authentication_header',
'default_content_type', 'upstream_ssl_verification', 'property_mappings']
widgets = {
'authentication_header': forms.TextInput(),
'default_content_type': forms.TextInput(),
'property_mappings': FilteredSelectMultiple(_('Property Mappings'), False)
}
field_classes = {
'server_name': DynamicArrayField,
'upstream': DynamicArrayField
}
labels = {
'upstream_ssl_verification': _('Verify upstream SSL Certificates?'),
'property_mappings': _('Rewrite Rules')
}
class RewriteRuleForm(forms.ModelForm):
"""Rewrite Rule Form"""
class Meta:
model = RewriteRule
fields = ['name', 'match', 'halt', 'replacement', 'redirect', 'conditions']
widgets = {
'name': forms.TextInput(),
'match': forms.TextInput(attrs={'data-is-monospace': True}),
'replacement': forms.TextInput(attrs={'data-is-monospace': True}),
'conditions': FilteredSelectMultiple(_('Conditions'), False)
}

View File

@ -0,0 +1,228 @@
"""passbook app_gw middleware"""
import mimetypes
from logging import getLogger
from urllib.parse import urlparse
import certifi
import urllib3
from django.core.cache import cache
from django.utils.http import urlencode
from django.views.generic import RedirectView
from revproxy.exceptions import InvalidUpstream
from revproxy.response import get_django_response
from revproxy.utils import encode_items, normalize_request_headers
from passbook.app_gw.models import ApplicationGatewayProvider
from passbook.app_gw.rewrite import Rewriter
from passbook.core.models import Application
from passbook.core.policies import PolicyEngine
from passbook.lib.config import CONFIG
IGNORED_HOSTNAMES_KEY = 'passbook_app_gw_ignored'
LOGGER = getLogger(__name__)
QUOTE_SAFE = r'<.;>\(}*+|~=-$/_:^@)[{]&\'!,"`'
ERRORS_MESSAGES = {
'upstream-no-scheme': ("Upstream URL scheme must be either "
"'http' or 'https' (%s).")
}
# pylint: disable=too-many-instance-attributes
class ApplicationGatewayMiddleware:
"""Check if request should be proxied or handeled normally"""
ignored_hosts = []
request = None
app_gw = None
http = None
http_no_verify = None
host_header = ''
_parsed_url = None
_request_headers = None
def __init__(self, get_response):
self.get_response = get_response
self.ignored_hosts = cache.get(IGNORED_HOSTNAMES_KEY, [])
self.http_no_verify = urllib3.PoolManager()
self.http = urllib3.PoolManager(
cert_reqs='CERT_REQUIRED',
ca_certs=certifi.where())
def precheck(self, request):
"""Check if a request should be proxied or forwarded to passbook"""
# Check if hostname is in cached list of ignored hostnames
# This saves us having to query the database on each request
self.host_header = request.META.get('HTTP_HOST')
if self.host_header in self.ignored_hosts:
LOGGER.debug("%s is ignored", self.host_header)
return True, None
# Look through all ApplicationGatewayProviders and check hostnames
matches = ApplicationGatewayProvider.objects.filter(
server_name__contains=[self.host_header],
enabled=True)
if not matches.exists():
# Mo matching Providers found, add host header to ignored list
self.ignored_hosts.append(self.host_header)
cache.set(IGNORED_HOSTNAMES_KEY, self.ignored_hosts)
LOGGER.debug("Ignoring %s", self.host_header)
return True, None
# At this point we're certain there's a matching ApplicationGateway
if len(matches) > 1:
# TODO This should never happen
raise ValueError
app_gw = matches.first()
try:
# Check if ApplicationGateway is associcaited with application
getattr(app_gw, 'application')
return False, app_gw
except Application.DoesNotExist:
LOGGER.debug("ApplicationGateway not associated with Application")
return True, None
return True, None
def __call__(self, request):
forward, self.app_gw = self.precheck(request)
if forward:
return self.get_response(request)
self.request = request
return self.dispatch(request)
def get_upstream(self):
"""Get upstream as parsed url"""
# TODO: How to choose upstream?
upstream = self.app_gw.upstream[0]
self._parsed_url = urlparse(upstream)
if self._parsed_url.scheme not in ('http', 'https'):
raise InvalidUpstream(ERRORS_MESSAGES['upstream-no-scheme'] %
upstream)
return upstream
def _format_path_to_redirect(self, request):
LOGGER.debug("Path before: %s", request.get_full_path())
rewriter = Rewriter(self.app_gw, request)
after = rewriter.build()
LOGGER.debug("Path after: %s", after)
return after
def get_proxy_request_headers(self, request):
"""Get normalized headers for the upstream
Gets all headers from the original request and normalizes them.
Normalization occurs by removing the prefix ``HTTP_`` and
replacing and ``_`` by ``-``. Example: ``HTTP_ACCEPT_ENCODING``
becames ``Accept-Encoding``.
.. versionadded:: 0.9.1
:param request: The original HTTPRequest instance
:returns: Normalized headers for the upstream
"""
return normalize_request_headers(request)
def get_request_headers(self):
"""Return request headers that will be sent to upstream.
The header REMOTE_USER is set to the current user
if AuthenticationMiddleware is enabled and
the view's add_remote_user property is True.
.. versionadded:: 0.9.8
"""
request_headers = self.get_proxy_request_headers(self.request)
request_headers[self.app_gw.authentication_header] = self.request.user.get_username()
LOGGER.info("%s set", self.app_gw.authentication_header)
return request_headers
def check_permission(self):
"""Check if user is authenticated and has permission to access app"""
if not hasattr(self.request, 'user'):
return False
if not self.request.user.is_authenticated:
return False
policy_engine = PolicyEngine(self.app_gw.application.policies.all())
policy_engine.for_user(self.request.user).with_request(self.request).build()
passing, _messages = policy_engine.result
return passing
def get_encoded_query_params(self):
"""Return encoded query params to be used in proxied request"""
get_data = encode_items(self.request.GET.lists())
return urlencode(get_data)
def _created_proxy_response(self, request, path):
request_payload = request.body
LOGGER.debug("Request headers: %s", self._request_headers)
request_url = self.get_upstream() + path
LOGGER.debug("Request URL: %s", request_url)
if request.GET:
request_url += '?' + self.get_encoded_query_params()
LOGGER.debug("Request URL: %s", request_url)
http = self.http
if not self.app_gw.upstream_ssl_verification:
http = self.http_no_verify
try:
proxy_response = http.urlopen(request.method,
request_url,
redirect=False,
retries=None,
headers=self._request_headers,
body=request_payload,
decode_content=False,
preload_content=False)
LOGGER.debug("Proxy response header: %s",
proxy_response.getheaders())
except urllib3.exceptions.HTTPError as error:
LOGGER.exception(error)
raise
return proxy_response
def _replace_host_on_redirect_location(self, request, proxy_response):
location = proxy_response.headers.get('Location')
if location:
if request.is_secure():
scheme = 'https://'
else:
scheme = 'http://'
request_host = scheme + self.host_header
upstream_host_http = 'http://' + self._parsed_url.netloc
upstream_host_https = 'https://' + self._parsed_url.netloc
location = location.replace(upstream_host_http, request_host)
location = location.replace(upstream_host_https, request_host)
proxy_response.headers['Location'] = location
LOGGER.debug("Proxy response LOCATION: %s",
proxy_response.headers['Location'])
def _set_content_type(self, request, proxy_response):
content_type = proxy_response.headers.get('Content-Type')
if not content_type:
content_type = (mimetypes.guess_type(request.path)[0] or
self.app_gw.default_content_type)
proxy_response.headers['Content-Type'] = content_type
LOGGER.debug("Proxy response CONTENT-TYPE: %s",
proxy_response.headers['Content-Type'])
def dispatch(self, request):
"""Build proxied request and pass to upstream"""
if not self.check_permission():
to_url = 'https://%s/?next=%s' % (CONFIG.get('domains')[0], request.get_full_path())
return RedirectView.as_view(url=to_url)(request)
self._request_headers = self.get_request_headers()
path = self._format_path_to_redirect(request)
proxy_response = self._created_proxy_response(request, path)
self._replace_host_on_redirect_location(request, proxy_response)
self._set_content_type(request, proxy_response)
response = get_django_response(proxy_response, strict_cookies=False)
LOGGER.debug("RESPONSE RETURNED: %s", response)
return response

BIN
passbook/app_gw/migrations/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,50 @@
# Generated by Django 2.1.7 on 2019-03-20 21:38
import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('passbook_core', '0020_groupmembershippolicy'),
]
operations = [
migrations.CreateModel(
name='ApplicationGatewayProvider',
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')),
('server_name', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)),
('upstream', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)),
('enabled', models.BooleanField(default=True)),
('authentication_header', models.TextField(default='X-Remote-User')),
('default_content_type', models.TextField(default='application/octet-stream')),
('upstream_ssl_verification', models.BooleanField(default=True)),
],
options={
'verbose_name': 'Application Gateway Provider',
'verbose_name_plural': 'Application Gateway Providers',
},
bases=('passbook_core.provider',),
),
migrations.CreateModel(
name='RewriteRule',
fields=[
('propertymapping_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PropertyMapping')),
('match', models.TextField()),
('halt', models.BooleanField(default=False)),
('replacement', models.TextField()),
('redirect', models.CharField(choices=[('internal', 'Internal'), (301, 'Moved Permanently'), (302, 'Found')], max_length=50)),
('conditions', models.ManyToManyField(to='passbook_core.Policy')),
],
options={
'verbose_name': 'Rewrite Rule',
'verbose_name_plural': 'Rewrite Rules',
},
bases=('passbook_core.propertymapping',),
),
]

View File

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

View File

74
passbook/app_gw/models.py Normal file
View File

@ -0,0 +1,74 @@
"""passbook app_gw models"""
import re
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext as _
from passbook.core.models import Policy, PropertyMapping, Provider
class ApplicationGatewayProvider(Provider):
"""Virtual server which proxies requests to any hostname in server_name to upstream"""
server_name = ArrayField(models.TextField())
upstream = ArrayField(models.TextField())
enabled = models.BooleanField(default=True)
authentication_header = models.TextField(default='X-Remote-User')
default_content_type = models.TextField(default='application/octet-stream')
upstream_ssl_verification = models.BooleanField(default=True)
form = 'passbook.app_gw.forms.ApplicationGatewayProviderForm'
@property
def name(self):
"""since this model has no name property, return a joined list of server_names as name"""
return ', '.join(self.server_name)
def __str__(self):
return "Application Gateway %s" % ', '.join(self.server_name)
class Meta:
verbose_name = _('Application Gateway Provider')
verbose_name_plural = _('Application Gateway Providers')
class RewriteRule(PropertyMapping):
"""Rewrite requests matching `match` with `replacement`, if all polcies in `conditions` apply"""
REDIRECT_INTERNAL = 'internal'
REDIRECT_PERMANENT = 301
REDIRECT_FOUND = 302
REDIRECTS = (
(REDIRECT_INTERNAL, _('Internal')),
(REDIRECT_PERMANENT, _('Moved Permanently')),
(REDIRECT_FOUND, _('Found')),
)
match = models.TextField()
halt = models.BooleanField(default=False)
conditions = models.ManyToManyField(Policy, blank=True)
replacement = models.TextField() # python formatted strings, use {match.1}
redirect = models.CharField(max_length=50, choices=REDIRECTS)
form = 'passbook.app_gw.forms.RewriteRuleForm'
_matcher = None
@property
def compiled_matcher(self):
"""Cache the compiled regex in memory"""
if not self._matcher:
self._matcher = re.compile(self.match)
return self._matcher
def __str__(self):
return "Rewrite Rule %s" % self.name
class Meta:
verbose_name = _('Rewrite Rule')
verbose_name_plural = _('Rewrite Rules')

View File

@ -0,0 +1,2 @@
django-revproxy
urllib3[secure]

View File

@ -0,0 +1,38 @@
"""passbook app_gw rewriter"""
from passbook.app_gw.models import RewriteRule
class Context:
"""Empty class which we dynamically add attributes to"""
class Rewriter:
"""Apply rewrites"""
__application = None
__request = None
def __init__(self, application, request):
self.__application = application
self.__request = request
def __build_context(self, matches):
"""Build object with .0, .1, etc as groups and give access to request"""
context = Context()
for index, group_match in enumerate(matches.groups()):
setattr(context, "g%d" % (index + 1), group_match)
setattr(context, 'request', self.__request)
return context
def build(self):
"""Run all rules over path and return final path"""
path = self.__request.get_full_path()
for rule in RewriteRule.objects.filter(provider__in=[self.__application]):
matches = rule.compiled_matcher.search(path)
if not matches:
continue
replace_context = self.__build_context(matches)
path = rule.replacement.format(context=replace_context)
if rule.halt:
return path
return path

View File

@ -0,0 +1,5 @@
"""Application Security Gateway settings"""
# INSTALLED_APPS = [
# 'revproxy'
# ]

View File

@ -0,0 +1,20 @@
"""passbook app_gw cache clean signals"""
from logging import getLogger
from django.core.cache import cache
from django.db.models.signals import post_save
from django.dispatch import receiver
from passbook.app_gw.middleware import IGNORED_HOSTNAMES_KEY
from passbook.app_gw.models import ApplicationGatewayProvider
LOGGER = getLogger(__name__)
@receiver(post_save)
# pylint: disable=unused-argument
def invalidate_app_gw_cache(sender, instance, **kwargs):
"""Invalidate Policy cache when app_gw is updated"""
if isinstance(instance, ApplicationGatewayProvider):
LOGGER.debug("Invalidating cache for ignored hostnames")
cache.delete(IGNORED_HOSTNAMES_KEY)

2
passbook/app_gw/urls.py Normal file
View File

@ -0,0 +1,2 @@
"""passbook app_gw urls"""
urlpatterns = []

View File

@ -1,2 +1,2 @@
"""passbook audit Header"""
__version__ = '0.1.21-beta'
__version__ = '0.1.27-beta'

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header"""
__version__ = '0.1.21-beta'
__version__ = '0.1.27-beta'

View File

@ -1,2 +1,2 @@
"""passbook core"""
__version__ = '0.1.21-beta'
__version__ = '0.1.27-beta'

View File

@ -7,7 +7,7 @@ from passbook.core.models import (DebugPolicy, FieldMatcherPolicy,
GroupMembershipPolicy, PasswordPolicy,
WebhookPolicy)
GENERAL_FIELDS = ['name', 'action', 'negate', 'order', ]
GENERAL_FIELDS = ['name', 'action', 'negate', 'order', 'timeout']
class FieldMatcherPolicyForm(forms.ModelForm):
"""FieldMatcherPolicy Form"""

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.7 on 2019-03-21 12:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0020_groupmembershippolicy'),
]
operations = [
migrations.AddField(
model_name='policy',
name='timeout',
field=models.IntegerField(default=30),
),
]

View File

@ -220,6 +220,7 @@ class Policy(UUIDModel, CreatedUpdatedModel):
action = models.CharField(max_length=20, choices=ACTIONS)
negate = models.BooleanField(default=False)
order = models.IntegerField(default=0)
timeout = models.IntegerField(default=30)
objects = InheritanceManager()

View File

@ -1,7 +1,10 @@
"""passbook core policy engine"""
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
@ -9,6 +12,9 @@ from passbook.core.models import Policy, User
LOGGER = getLogger(__name__)
def _cache_key(policy, user):
return "%s#%s" % (policy.uuid, user.pk)
@CELERY_APP.task()
def _policy_engine_task(user_pk, policy_pk, **kwargs):
"""Task wrapper to run policy checking"""
@ -29,58 +35,89 @@ def _policy_engine_task(user_pk, policy_pk, **kwargs):
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)
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)
return policy_obj.action, policy_result, message
class PolicyEngine:
"""Orchestrate policy checking, launch tasks and return result"""
__group = None
__cached = None
policies = None
_group = None
_request = None
_user = None
__get_timeout = 0
__request = None
__user = None
def __init__(self, policies):
self.policies = policies
self._request = None
self._user = None
self.__request = None
self.__user = None
def for_user(self, user):
"""Check policies for user"""
self._user = user
self.__user = user
return self
def with_request(self, request):
"""Set request"""
self._request = request
self.__request = request
return self
def build(self):
"""Build task group"""
if not self._user:
if not self.__user:
raise ValueError("User not set.")
signatures = []
cached_policies = []
kwargs = {
'__password__': getattr(self._user, '__password__', None),
'__password__': getattr(self.__user, '__password__', None),
}
if self._request:
kwargs['remote_ip'], _ = get_client_ip(self._request)
if self.__request:
kwargs['remote_ip'], _ = get_client_ip(self.__request)
if not kwargs['remote_ip']:
kwargs['remote_ip'] = '255.255.255.255'
for policy in self.policies:
signatures.append(_policy_engine_task.s(self._user.pk, policy.pk.hex, **kwargs))
self._group = group(signatures)()
cached_policy = cache.get(_cache_key(policy, self.__user), None)
if cached_policy:
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)
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)
# If all policies are cached, we have an empty list here.
if signatures:
self.__group = group(signatures)()
self.__get_timeout += 3
self.__get_timeout = (self.__get_timeout / len(self.policies)) * 1.5
self.__cached = cached_policies
return self
@property
def result(self):
"""Get policy-checking result"""
messages = []
result = []
try:
# ValueError can be thrown from _policy_engine_task when user is None
group_result = self._group.get()
if self.__group:
# ValueError can be thrown from _policy_engine_task when user is None
result += self.__group.get(timeout=self.__get_timeout)
result += self.__cached
except ValueError as exc:
return False, str(exc)
for policy_action, policy_result, policy_message in group_result:
# ValueError can be thrown from _policy_engine_task when user is None
return False, [str(exc)]
except UnexpectedFrame as exc:
return False, [str(exc)]
except CeleryTimeoutError as exc:
return False, [str(exc)]
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)

View File

@ -1,12 +1,13 @@
django>=2.0
django-model-utils
celery
cherrypy
colorlog
django-ipware
django-model-utils
django-redis
django>=2.0
djangorestframework
idna<2.8,>=2.5
markdown
psycopg2
PyYAML
raven
markdown
colorlog
celery
psycopg2
idna<2.8,>=2.5
cherrypy

View File

@ -34,7 +34,7 @@ SECRET_KEY = CONFIG.get('secret_key')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.get('debug')
INTERNAL_IPS = ['127.0.0.1']
ALLOWED_HOSTS = CONFIG.get('domains', [])
ALLOWED_HOSTS = CONFIG.get('domains', []) + [CONFIG.get('primary_domain')]
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
LOGIN_URL = 'passbook_core:auth-login'
@ -45,6 +45,9 @@ AUTH_USER_MODEL = 'passbook_core.User'
CSRF_COOKIE_NAME = 'passbook_csrf'
SESSION_COOKIE_NAME = 'passbook_session'
SESSION_COOKIE_DOMAIN = CONFIG.get('primary_domain')
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
LANGUAGE_COOKIE_NAME = 'passbook_language'
AUTHENTICATION_BACKENDS = [
@ -79,6 +82,7 @@ INSTALLED_APPS = [
'passbook.pretend.apps.PassbookPretendConfig',
'passbook.password_expiry_policy.apps.PassbookPasswordExpiryPolicyConfig',
'passbook.suspicious_policy.apps.PassbookSuspiciousPolicyConfig',
'passbook.app_gw.apps.PassbookApplicationApplicationGatewayConfig',
]
# Message Tag fix for bootstrap CSS Classes
@ -99,12 +103,23 @@ REST_FRAMEWORK = {
]
}
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://%s" % CONFIG.get('redis'),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'passbook.app_gw.middleware.ApplicationGatewayMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',

View File

@ -1,10 +1,15 @@
"""passbook core signals"""
from logging import getLogger
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models.signals import post_save
from django.dispatch import receiver
from passbook.core.exceptions import PasswordPolicyInvalid
LOGGER = getLogger(__name__)
user_signed_up = Signal(providing_args=['request', 'user'])
invitation_created = Signal(providing_args=['request', 'invitation'])
invitation_used = Signal(providing_args=['request', 'invitation', 'user'])
@ -24,3 +29,14 @@ def password_policy_checker(sender, password, **kwargs):
passing, messages = policy_engine.result
if not passing:
raise PasswordPolicyInvalid(*messages)
@receiver(post_save)
# pylint: disable=unused-argument
def invalidate_policy_cache(sender, instance, **kwargs):
"""Invalidate Policy cache when policy is updated"""
from passbook.core.models import Policy
if isinstance(instance, Policy):
LOGGER.debug("Invalidating cache for %s", instance.pk)
keys = cache.keys("%s#*" % instance.pk)
cache.delete_many(keys)
LOGGER.debug("Deleted %d keys", len(keys))

View File

@ -195,3 +195,7 @@ form .form-row p.datetime {
.selector-remove {
background: url(../admin/img/selector-icons.svg) 0 -64px no-repeat;
}
input[data-is-monospace] {
font-family: monospace;
}

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="20px" height="20px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FBBB00;}
.st1{fill:#518EF8;}
.st2{fill:#28B446;}
.st3{fill:#F14336;}
</style>
<path class="st0" d="M4.4,12.1l-0.7,2.6l-2.5,0.1C0.4,13.3,0,11.7,0,10c0-1.7,0.4-3.2,1.1-4.6h0l2.3,0.4l1,2.3
C4.2,8.7,4.1,9.3,4.1,10C4.1,10.7,4.2,11.4,4.4,12.1z"/>
<path class="st1" d="M19.8,8.1C19.9,8.7,20,9.4,20,10c0,0.7-0.1,1.4-0.2,2.1c-0.5,2.3-1.8,4.3-3.5,5.7l0,0l-2.9-0.1L13,15.1
c1.2-0.7,2.1-1.8,2.6-3h-5.3v-4h5.4H19.8L19.8,8.1z"/>
<path class="st2" d="M16.3,17.8L16.3,17.8C14.5,19.2,12.4,20,10,20c-3.8,0-7.1-2.1-8.8-5.3l3.2-2.7c0.8,2.3,3,3.9,5.6,3.9
c1.1,0,2.1-0.3,3-0.8L16.3,17.8z"/>
<path class="st3" d="M16.4,2.3L13.1,5c-0.9-0.6-2-0.9-3.1-0.9c-2.6,0-4.8,1.7-5.6,4L1.1,5.4h0C2.8,2.2,6.1,0,10,0
C12.4,0,14.7,0.9,16.4,2.3z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21"><title>MS-SymbolLockup</title><rect x="1" y="1" width="9" height="9" fill="#f25022"/><rect x="1" y="11" width="9" height="9" fill="#00a4ef"/><rect x="11" y="1" width="9" height="9" fill="#7fba00"/><rect x="11" y="11" width="9" height="9" fill="#ffb900"/></svg>

After

Width:  |  Height:  |  Size: 343 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 750 B

After

Width:  |  Height:  |  Size: 750 B

View File

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 814 B

View File

Before

Width:  |  Height:  |  Size: 788 B

After

Width:  |  Height:  |  Size: 788 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 783 B

After

Width:  |  Height:  |  Size: 783 B

View File

Before

Width:  |  Height:  |  Size: 688 B

After

Width:  |  Height:  |  Size: 688 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 889 B

After

Width:  |  Height:  |  Size: 889 B

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -3,7 +3,7 @@
{% load utils %}
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="layout-pf layout-pf-fixed transitions">
<head>
<meta charset="UTF-8">

View File

@ -0,0 +1,26 @@
{% extends 'login/base.html' %}
{% load static %}
{% load i18n %}
{% load utils %}
{% block head %}
{{ block.super }}
<style>
.pf-icon {
font-size: 48px;
text-align: center;
}
</style>
{% endblock %}
{% block card %}
<header class="login-pf-header">
<h1>{% trans 'Bad Request' %}</h1>
</header>
<form>
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends 'login/base.html' %}
{% load static %}
{% load i18n %}
{% load utils %}
{% block head %}
{{ block.super }}
<style>
.pf-icon {
font-size: 48px;
text-align: center;
}
</style>
{% endblock %}
{% block card %}
<header class="login-pf-header">
<h1>{% trans 'Forbidden' %}</h1>
</header>
<form>
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends 'login/base.html' %}
{% load static %}
{% load i18n %}
{% load utils %}
{% block head %}
{{ block.super }}
<style>
.pf-icon {
font-size: 48px;
text-align: center;
}
</style>
{% endblock %}
{% block card %}
<header class="login-pf-header">
<h1>{% trans 'Not Found' %}</h1>
</header>
<form>
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends 'login/base.html' %}
{% load static %}
{% load i18n %}
{% load utils %}
{% block head %}
{{ block.super }}
<style>
.pf-icon {
font-size: 48px;
text-align: center;
}
</style>
{% endblock %}
{% block card %}
<header class="login-pf-header">
<h1>{% trans 'Server Error' %}</h1>
</header>
<form>
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% endblock %}

View File

@ -16,6 +16,7 @@
Are you sure you want to delete {{ object_type }} "{{ object }}"?
{% endblocktrans %}
</p>
<input type="hidden" name="confirmdelete" value="yes">
<a href="{% back %}" class="btn btn-default">{% trans 'Back' %}</a>
<input type="submit" class="btn btn-danger" value="{% trans 'Delete' %}" />
</form>

View File

@ -51,7 +51,7 @@
{% for url, icon, name in sources %}
<li class="login-pf-social-link">
<a href="{{ url }}">
<img src="{% static 'img/' %}{{ icon }}.svg" alt="{{ name }}"> {{ name }}
<img src="{% static 'img/logos/' %}{{ icon }}.svg" alt="{{ name }}"> {{ name }}
</a>
</li>
{% endfor %}

View File

@ -185,13 +185,14 @@
{% endif %}
</ul>
</div>
<div class="container-fluid container-cards-pf">
<div class="container-fluid container-cards-pf container-pf-nav-pf-vertical hide-nav-pf">
{% block content %}
{% endblock %}
</div>
{% endblock %}
{% block scripts %}
{{ block.super }}
<script>
$(document).ready(function () {
// initialize tooltips

View File

@ -30,7 +30,7 @@
{% for name, icon, link in us %}
<li class="{% if link == request.get_full_path %} active {% endif %}">
<a href="{{ link }}">
<img src="{% static icon %}" alt=""> {{ name }}
<i class="{{ icon }}"></i> {{ name }}
</a>
</li>
{% endfor %}

View File

@ -7,13 +7,18 @@ from django.urls import include, path
from django.views.generic import RedirectView
from passbook.core.auth import view
from passbook.core.views import authentication, overview, user
from passbook.core.views import authentication, error, overview, user
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.BadRequestView.as_view()
core_urls = [
# Authentication views
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),

View File

@ -0,0 +1,60 @@
"""passbook core error views"""
from django.http.response import (HttpResponseBadRequest,
HttpResponseForbidden, HttpResponseNotFound,
HttpResponseServerError)
from django.template.response import TemplateResponse
from django.views.generic import TemplateView
class BadRequestTemplateResponse(TemplateResponse, HttpResponseBadRequest):
"""Combine Template response with Http Code 400"""
class ForbiddenTemplateResponse(TemplateResponse, HttpResponseForbidden):
"""Combine Template response with Http Code 403"""
class NotFoundTemplateResponse(TemplateResponse, HttpResponseNotFound):
"""Combine Template response with Http Code 404"""
class ServerErrorTemplateResponse(TemplateResponse, HttpResponseServerError):
"""Combine Template response with Http Code 500"""
class BadRequestView(TemplateView):
"""Show Bad Request message"""
response_class = BadRequestTemplateResponse
template_name = 'error/400.html'
extra_context = {
'is_login': True
}
class ForbiddenView(TemplateView):
"""Show Forbidden message"""
response_class = ForbiddenTemplateResponse
template_name = 'error/403.html'
extra_context = {
'is_login': True
}
class NotFoundView(TemplateView):
"""Show Not Found message"""
response_class = NotFoundTemplateResponse
template_name = 'error/404.html'
extra_context = {
'is_login': True
}
class ServerErrorView(TemplateView):
"""Show Server Error message"""
response_class = ServerErrorTemplateResponse
template_name = 'error/500.html'
extra_context = {
'is_login': True
}

View File

@ -1,5 +1,4 @@
"""passbook core utils view"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
@ -21,7 +20,7 @@ class LoadingView(TemplateView):
kwargs['target_url'] = self.get_url()
return super().get_context_data(**kwargs)
class PermissionDeniedView(LoginRequiredMixin, TemplateView):
class PermissionDeniedView(TemplateView):
"""Generic Permission denied view"""
template_name = 'login/denied.html'

View File

@ -1,2 +1,2 @@
"""passbook hibp_policy"""
__version__ = '0.1.21-beta'
__version__ = '0.1.27-beta'

View File

@ -1,2 +1,2 @@
"""Passbook ldap app Header"""
__version__ = '0.1.21-beta'
__version__ = '0.1.27-beta'

View File

@ -1,2 +1,2 @@
"""passbook lib"""
__version__ = '0.1.21-beta'
__version__ = '0.1.27-beta'

View File

@ -30,10 +30,13 @@ debug: false
secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https
rabbitmq: guest:guest@localhost/passbook
redis: localhost/0
# Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: true
secret_key: 9$@r!d^1^jrn#fk#1#@ks#9&i$^s#1)_13%$rwjrhd=e8jfi_s
primary_domain: 'localhost'
passbook:
sign_up:
# Enables signup, created users are stored in internal Database and created in LDAP if ldap.create_users is true
@ -85,6 +88,7 @@ oauth_client:
- passbook.oauth_client.source_types.reddit
- passbook.oauth_client.source_types.supervisr
- passbook.oauth_client.source_types.twitter
- passbook.oauth_client.source_types.azure_ad
saml_idp:
# List of python packages with provider types to load.
types:

View File

@ -1,2 +1,2 @@
"""passbook oauth_client Header"""
__version__ = '0.1.21-beta'
__version__ = '0.1.27-beta'

View File

@ -108,3 +108,17 @@ class GoogleOAuthSourceForm(OAuthSourceForm):
'access_token_url': 'https://accounts.google.com/o/oauth2/token',
'profile_url': ' https://www.googleapis.com/oauth2/v1/userinfo',
}
class AzureADOAuthSourceForm(OAuthSourceForm):
"""OAuth Source form with pre-determined URL for AzureAD"""
class Meta(OAuthSourceForm.Meta):
overrides = {
'provider_type': 'azure_ad',
'request_token_url': '',
'authorization_url': 'https://login.microsoftonline.com/common/oauth2/authorize',
'access_token_url': 'https://login.microsoftonline.com/common/oauth2/token',
'profile_url': ' https://graph.windows.net/myorganization/me?api-version=1.6',
}

View File

@ -29,9 +29,9 @@ 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, 'generic', _('Generic')
# if self.provider_type == 'github':
# return url, 'github-logo', _('GitHub')
return url, self.provider_type, self.name
@property
def additional_info(self):
@ -42,9 +42,12 @@ class OAuthSource(Source):
"""Entrypoint to integrate with User settings. Can either return False if no
user settings are available, or a tuple or string, string, string where the first string
is the name the item has, the second string is the icon and the third is the view-name."""
icon = 'img/%s.svg' % self.get_login_button[1]
icon_type = self.provider_type
if icon_type == 'azure ad':
icon_type = 'windows'
icon_class = 'fa fa-%s' % icon_type
view_name = 'passbook_oauth_client:oauth-client-user'
return self.name, icon, reverse((view_name), kwargs={
return self.name, icon_class, reverse((view_name), kwargs={
'source_slug': self.slug
})
@ -113,6 +116,19 @@ class GoogleOAuthSource(OAuthSource):
verbose_name = _('Google OAuth Source')
verbose_name_plural = _('Google OAuth Sources')
class AzureADOAuthSource(OAuthSource):
"""Abstract subclass of OAuthSource to specify AzureAD Form"""
form = 'passbook.oauth_client.forms.AzureADOAuthSourceForm'
class Meta:
abstract = True
verbose_name = _('Azure AD OAuth Source')
verbose_name_plural = _('Azure AD OAuth Sources')
class UserOAuthSourceConnection(UserSourceConnection):
"""Authorized remote OAuth provider."""

View File

@ -0,0 +1,52 @@
"""AzureAD OAuth2 Views"""
import json
import uuid
from logging import getLogger
from requests.exceptions import RequestException
from passbook.oauth_client.clients import OAuth2Client
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback
LOGGER = getLogger(__name__)
class AzureADOAuth2Client(OAuth2Client):
"""AzureAD OAuth2 Client"""
def get_profile_info(self, raw_token):
"Fetch user profile information."
try:
token = json.loads(raw_token)['access_token']
headers = {
'Authorization': 'Bearer %s' % token
}
response = self.request('get', self.source.profile_url,
headers=headers)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning('Unable to fetch user profile: %s', exc)
return None
else:
return response.json() or response.text
@MANAGER.source(kind=RequestKind.callback, name='Azure AD')
class AzureADOAuthCallback(OAuthCallback):
"""AzureAD OAuth2 Callback"""
client_class = AzureADOAuth2Client
def get_user_id(self, source, info):
return uuid.UUID(info.get('objectId')).int
def get_or_create_user(self, source, access, info):
user_data = {
'username': info.get('displayName'),
'email': info.get('mail', None) or info.get('otherMails')[0],
'name': info.get('displayName'),
'password': None,
}
return user_get_or_create(**user_data)

View File

@ -194,7 +194,9 @@ class OAuthCallback(OAuthClientMixin, View):
messages.success(self.request, _("Successfully linked %(source)s!" % {
'source': self.source.name
}))
return redirect(reverse('user_settings'))
return redirect(reverse('passbook_oauth_client:oauth-client-user', kwargs={
'source_slug': self.source.slug
}))
messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
'source': self.source.name
}))
@ -207,26 +209,28 @@ class DisconnectView(LoginRequiredMixin, View):
source = None
aas = None
def dispatch(self, request, source):
self.source = get_object_or_404(OAuthSource, name=source)
def dispatch(self, request, source_slug):
self.source = get_object_or_404(OAuthSource, slug=source_slug)
self.aas = get_object_or_404(UserOAuthSourceConnection,
source=self.source, user=request.user)
return super().dispatch(request, source)
return super().dispatch(request, source_slug)
def post(self, request, source):
def post(self, request, source_slug):
"""Delete connection object"""
if 'confirmdelete' in request.POST:
# User confirmed deletion
self.aas.delete()
messages.success(request, _('Connection successfully deleted'))
return redirect(reverse('user_settings'))
return self.get(request, source)
return redirect(reverse('passbook_oauth_client:oauth-client-user', kwargs={
'source_slug': self.source.slug
}))
return self.get(request, source_slug)
def get(self, request, source):
"""Show delete form"""
return render(request, 'generic/delete.html', {
'object': 'OAuth Connection with %s' % self.source.name,
'delete_url': reverse('oauth-client-disconnect', kwargs={
'source': self.source.name,
'object': self.source,
'delete_url': reverse('passbook_oauth_client:oauth-client-disconnect', kwargs={
'source_slug': self.source.slug,
})
})

View File

@ -1,2 +1,2 @@
"""passbook oauth_provider Header"""
__version__ = '0.1.21-beta'
__version__ = '0.1.27-beta'

View File

@ -11,5 +11,5 @@ class OAuth2ProviderForm(forms.ModelForm):
class Meta:
model = OAuth2Provider
fields = ['name', 'user', 'redirect_uris', 'client_type',
fields = ['name', 'redirect_uris', 'client_type',
'authorization_grant_type', 'client_id', 'client_secret', ]

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