Compare commits
	
		
			61 Commits
		
	
	
		
			version/0.
			...
			version/0.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 146edb45d4 | |||
| 045a802365 | |||
| c90d8ddcff | |||
| 3ff2ec929f | |||
| a3ef26b7ad | |||
| 19cd1624c1 | |||
| 366ef352c6 | |||
| a9031a6abc | |||
| a1a5223b58 | |||
| c723b0233f | |||
| b369eb28f1 | |||
| 9b8f390e31 | |||
| 11630c9a74 | |||
| c9ac10f6f6 | |||
| 04d613cb28 | |||
| 40866f9ecd | |||
| d8585eb872 | |||
| 35b6bb6b3f | |||
| eaa573c715 | |||
| 660972e303 | |||
| a21012bf0c | |||
| 8dbafa4bda | |||
| 80049413f0 | |||
| 2739442d4a | |||
| c679f0a67c | |||
| d9a952dd03 | |||
| 9a1a0f0aa8 | |||
| 4d6bb60134 | |||
| 80e6d59382 | |||
| 81ac951872 | |||
| f33e553cfd | |||
| 9b0240dc26 | |||
| c327310392 | |||
| 457375287c | |||
| 7e87bfef5b | |||
| a7af5268de | |||
| 6d916029bb | |||
| 81fdcbadad | |||
| ec1e25fe71 | |||
| b5306e4a94 | |||
| 801b8a1e59 | |||
| 3a52059793 | |||
| 10b7d99b37 | |||
| 6be8d0cbb2 | |||
| 5b8e3689ec | |||
| 25a5d8f5da | |||
| 883d439544 | |||
| 1c3b5889e5 | |||
| 87012b65e1 | |||
| 29913773a7 | |||
| 0bc6a4fed4 | |||
| 4645d8353f | |||
| 260c5555fa | |||
| 6f7b917c38 | |||
| 1456ee6d3e | |||
| ae3d3d0295 | |||
| c23ceacd0b | |||
| 5155204283 | |||
| 5509ec9b0f | |||
| d6f9b2e47d | |||
| 67aa4aef11 | 
| @ -1,5 +1,5 @@ | ||||
| [bumpversion] | ||||
| current_version = 0.1.23-beta | ||||
| current_version = 0.1.30-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] | ||||
|  | ||||
|  | ||||
| @ -12,6 +12,7 @@ stages: | ||||
| image: python:3.6 | ||||
| services: | ||||
|     - postgres:latest | ||||
|     - redis:latest | ||||
|  | ||||
| variables: | ||||
|     POSTGRES_DB: passbook | ||||
| @ -39,6 +40,7 @@ pylint: | ||||
|     stage: test | ||||
| coverage: | ||||
|     script: | ||||
|         - python manage.py collectstatic --no-input | ||||
|         - coverage run manage.py test | ||||
|         - coverage report | ||||
|     stage: test | ||||
| @ -54,7 +56,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.23-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.30-beta | ||||
|     stage: build | ||||
|     only: | ||||
|         - tags | ||||
|  | ||||
| @ -6,7 +6,7 @@ COPY ./requirements.txt /app/ | ||||
|  | ||||
| WORKDIR /app/ | ||||
|  | ||||
| RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev -y && \ | ||||
| RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev libpq-dev -y && \ | ||||
|     mkdir /app/static/ && \ | ||||
|     pip install -r requirements.txt && \ | ||||
|     pip install psycopg2 && \ | ||||
| @ -23,7 +23,7 @@ COPY --from=build /app/static /app/static/ | ||||
|  | ||||
| WORKDIR /app/ | ||||
|  | ||||
| RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev -y && \ | ||||
| RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev libpq-dev -y && \ | ||||
|     pip install -r requirements.txt && \ | ||||
|     pip install psycopg2 && \ | ||||
|     adduser --system --home /app/ passbook && \ | ||||
|  | ||||
| @ -3,7 +3,7 @@ from setuptools import setup | ||||
|  | ||||
| setup( | ||||
|     name='django-allauth-passbook', | ||||
|     version='0.1.23-beta', | ||||
|     version='0.1.30-beta', | ||||
|     description='passbook support for django-allauth', | ||||
|     # long_description='\n'.join(read_simple('docs/index.md')[2:]), | ||||
|     long_description_content_type='text/markdown', | ||||
|  | ||||
| @ -18,7 +18,7 @@ tests_require = [ | ||||
|  | ||||
| setup( | ||||
|     name='sentry-auth-passbook', | ||||
|     version='0.1.23-beta', | ||||
|     version='0.1.30-beta', | ||||
|     author='BeryJu.org', | ||||
|     author_email='support@beryju.org', | ||||
|     url='https://passbook.beryju.org', | ||||
|  | ||||
							
								
								
									
										83
									
								
								debian/changelog
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										83
									
								
								debian/changelog
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +1,86 @@ | ||||
| passbook (0.1.30) stable; urgency=medium | ||||
|  | ||||
|   * bump version: 0.1.28-beta -> 0.1.29-beta | ||||
|   * don't use context manager in web command | ||||
|  | ||||
|  -- Jens Langhammer <jens.langhammer@beryju.org>  Thu, 11 Apr 2019 12:21:58 +0000 | ||||
|  | ||||
| passbook (0.1.29) stable; urgency=medium | ||||
|  | ||||
|   * bump version: 0.1.27-beta -> 0.1.28-beta | ||||
|   * Add libpq-dev dependency so psycopg2 build works | ||||
|   * switch to whitenoise for static files | ||||
|   * replace cherrypy with daphne | ||||
|   * Run collectstatic before coverage, use autoreload on celery worker | ||||
|  | ||||
|  -- Jens Langhammer <jens.langhammer@beryju.org>  Thu, 11 Apr 2019 12:00:27 +0000 | ||||
|  | ||||
| passbook (0.1.28) stable; urgency=medium | ||||
|  | ||||
|   * bump version: 0.1.26-beta -> 0.1.27-beta | ||||
|   * fix allauth client's formatting | ||||
|   * switch from raven to sentry_sdk | ||||
|   * add ability to have non-expiring nonces, clean up expired nonces | ||||
|   * fully remove raven and switch WSGI and logging to sentry_sdk | ||||
|   * fix failing CI | ||||
|   * trigger autoreload from config files | ||||
|   * Choose upstream more cleverly | ||||
|   * Move code from django-revproxy to app_gw to fix cookie bug | ||||
|   * Implement websocket proxy | ||||
|   * switch kubernetes deployment to daphne server | ||||
|   * set default log level to warn, fix clean_nonces not working | ||||
|  | ||||
|  -- Jens Langhammer <jens.langhammer@beryju.org>  Thu, 11 Apr 2019 08:46:44 +0000 | ||||
|  | ||||
| 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 | ||||
|  | ||||
							
								
								
									
										4
									
								
								debian/control
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								debian/control
									
									
									
									
										vendored
									
									
								
							| @ -3,12 +3,12 @@ Section: admin | ||||
| Priority: optional | ||||
| Maintainer: BeryJu.org <support@beryju.org> | ||||
| Uploaders: Jens Langhammer <jens@beryju.org>, BeryJu.org <support@beryju.org> | ||||
| Build-Depends: debhelper (>= 10), dh-systemd (>= 1.5), dh-exec, wget, dh-exec, python3 (>= 3.5) | python3.6 | python3.7 | ||||
| Build-Depends: debhelper (>= 10), dh-systemd (>= 1.5), dh-exec, wget, dh-exec, python3 (>= 3.5) | python3.6 | python3.7, libpq-dev | ||||
| 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. | ||||
|  | ||||
							
								
								
									
										4
									
								
								debian/etc/passbook/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								debian/etc/passbook/config.yml
									
									
									
									
										vendored
									
									
								
							| @ -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 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| apiVersion: v1 | ||||
| appVersion: "0.1.23-beta" | ||||
| appVersion: "0.1.30-beta" | ||||
| description: A Helm chart for passbook. | ||||
| name: passbook | ||||
| version: "0.1.23-beta" | ||||
| version: "0.1.30-beta" | ||||
| icon: https://passbook.beryju.org/images/logo.png | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								helm/passbook/charts/redis-5.1.0.tgz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								helm/passbook/charts/redis-5.1.0.tgz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @ -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 | ||||
|  | ||||
| @ -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/ | ||||
|  | ||||
| @ -15,8 +15,8 @@ data: | ||||
|         port: '' | ||||
|     log: | ||||
|       level: | ||||
|         console: DEBUG | ||||
|         file: DEBUG | ||||
|         console: WARNING | ||||
|         file: WARNING | ||||
|       file: /dev/null | ||||
|       syslog: | ||||
|         host: 127.0.0.1 | ||||
| @ -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 }} | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
| replicaCount: 1 | ||||
|  | ||||
| image: | ||||
|   tag: 0.1.23-beta | ||||
|   tag: 0.1.30-beta | ||||
|  | ||||
| nameOverride: "" | ||||
|  | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook admin""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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 %} | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook api""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								passbook/app_gw/.DS_Store
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								passbook/app_gw/.DS_Store
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										2
									
								
								passbook/app_gw/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								passbook/app_gw/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| """passbook Application Security Gateway Header""" | ||||
| __version__ = '0.1.30-beta' | ||||
							
								
								
									
										5
									
								
								passbook/app_gw/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								passbook/app_gw/admin.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										16
									
								
								passbook/app_gw/apps.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										66
									
								
								passbook/app_gw/forms.py
									
									
									
									
									
										Normal 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) | ||||
|         } | ||||
							
								
								
									
										238
									
								
								passbook/app_gw/middleware.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								passbook/app_gw/middleware.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,238 @@ | ||||
| """passbook app_gw middleware""" | ||||
| import mimetypes | ||||
| from logging import getLogger | ||||
| from random import SystemRandom | ||||
| 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 passbook.app_gw.models import ApplicationGatewayProvider | ||||
| from passbook.app_gw.proxy.response import get_django_response | ||||
| from passbook.app_gw.proxy.utils import encode_items, normalize_request_headers | ||||
| 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 | ||||
|  | ||||
| SESSION_UPSTREAM_KEY = 'passbook_app_gw_upstream' | ||||
| 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: | ||||
|             # This should never happen | ||||
|             raise ValueError | ||||
|         app_gw = matches.first() | ||||
|         try: | ||||
|             # Check if ApplicationGateway is associated 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): | ||||
|         """Choose random upstream and save in session""" | ||||
|         if SESSION_UPSTREAM_KEY not in self.request.session: | ||||
|             self.request.session[SESSION_UPSTREAM_KEY] = {} | ||||
|         if self.app_gw.pk not in self.request.session[SESSION_UPSTREAM_KEY]: | ||||
|             upstream_index = SystemRandom().randrange(len(self.app_gw.upstream)) | ||||
|             self.request.session[SESSION_UPSTREAM_KEY][self.app_gw.pk] = upstream_index | ||||
|         return self.app_gw.upstream[self.request.session[SESSION_UPSTREAM_KEY][self.app_gw.pk]] | ||||
|  | ||||
|     def get_upstream(self): | ||||
|         """Get upstream as parsed url""" | ||||
|         upstream = self._get_upstream() | ||||
|  | ||||
|         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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								passbook/app_gw/migrations/.DS_Store
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										50
									
								
								passbook/app_gw/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								passbook/app_gw/migrations/0001_initial.py
									
									
									
									
									
										Normal 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',), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								passbook/app_gw/migrations/0002_auto_20190321_1521.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								passbook/app_gw/migrations/0002_auto_20190321_1521.py
									
									
									
									
									
										Normal 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'), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								passbook/app_gw/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/app_gw/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										74
									
								
								passbook/app_gw/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								passbook/app_gw/models.py
									
									
									
									
									
										Normal 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') | ||||
							
								
								
									
										0
									
								
								passbook/app_gw/proxy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/app_gw/proxy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										8
									
								
								passbook/app_gw/proxy/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								passbook/app_gw/proxy/exceptions.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| """Exception classes""" | ||||
|  | ||||
| class ReverseProxyException(Exception): | ||||
|     """Base for revproxy exception""" | ||||
|  | ||||
|  | ||||
| class InvalidUpstream(ReverseProxyException): | ||||
|     """Invalid upstream set""" | ||||
							
								
								
									
										63
									
								
								passbook/app_gw/proxy/response.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								passbook/app_gw/proxy/response.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | ||||
| """response functions from django-revproxy""" | ||||
| import logging | ||||
|  | ||||
| from django.http import HttpResponse, StreamingHttpResponse | ||||
|  | ||||
| from passbook.app_gw.proxy.utils import (cookie_from_string, | ||||
|                                          set_response_headers, should_stream) | ||||
|  | ||||
| #: Default number of bytes that are going to be read in a file lecture | ||||
| DEFAULT_AMT = 2 ** 16 | ||||
|  | ||||
| logger = logging.getLogger('revproxy.response') | ||||
|  | ||||
|  | ||||
| def get_django_response(proxy_response, strict_cookies=False): | ||||
|     """This method is used to create an appropriate response based on the | ||||
|     Content-Length of the proxy_response. If the content is bigger than | ||||
|     MIN_STREAMING_LENGTH, which is found on utils.py, | ||||
|     than django.http.StreamingHttpResponse will be created, | ||||
|     else a django.http.HTTPResponse will be created instead | ||||
|  | ||||
|     :param proxy_response: An Instance of urllib3.response.HTTPResponse that | ||||
|                            will create an appropriate response | ||||
|     :param strict_cookies: Whether to only accept RFC-compliant cookies | ||||
|     :returns: Returns an appropriate response based on the proxy_response | ||||
|               content-length | ||||
|     """ | ||||
|     status = proxy_response.status | ||||
|     headers = proxy_response.headers | ||||
|  | ||||
|     logger.debug('Proxy response headers: %s', headers) | ||||
|  | ||||
|     content_type = headers.get('Content-Type') | ||||
|  | ||||
|     logger.debug('Content-Type: %s', content_type) | ||||
|  | ||||
|     if should_stream(proxy_response): | ||||
|         logger.info('Content-Length is bigger than %s', DEFAULT_AMT) | ||||
|         response = StreamingHttpResponse(proxy_response.stream(DEFAULT_AMT), | ||||
|                                          status=status, | ||||
|                                          content_type=content_type) | ||||
|     else: | ||||
|         content = proxy_response.data or b'' | ||||
|         response = HttpResponse(content, status=status, | ||||
|                                 content_type=content_type) | ||||
|  | ||||
|     logger.info('Normalizing response headers') | ||||
|     set_response_headers(response, headers) | ||||
|  | ||||
|     logger.debug('Response headers: %s', getattr(response, '_headers')) | ||||
|  | ||||
|     cookies = proxy_response.headers.getlist('set-cookie') | ||||
|     logger.info('Checking for invalid cookies') | ||||
|     for cookie_string in cookies: | ||||
|         cookie_dict = cookie_from_string(cookie_string, | ||||
|                                          strict_cookies=strict_cookies) | ||||
|         # if cookie is invalid cookie_dict will be None | ||||
|         if cookie_dict: | ||||
|             response.set_cookie(**cookie_dict) | ||||
|  | ||||
|     logger.debug('Response cookies: %s', response.cookies) | ||||
|  | ||||
|     return response | ||||
							
								
								
									
										227
									
								
								passbook/app_gw/proxy/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								passbook/app_gw/proxy/utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,227 @@ | ||||
| """Utils from django-revproxy, slightly adjusted""" | ||||
| import logging | ||||
| import re | ||||
| from wsgiref.util import is_hop_by_hop | ||||
|  | ||||
| try: | ||||
|     from http.cookies import SimpleCookie | ||||
|     COOKIE_PREFIX = '' | ||||
| except ImportError: | ||||
|     from Cookie import SimpleCookie | ||||
|     COOKIE_PREFIX = 'Set-Cookie: ' | ||||
|  | ||||
|  | ||||
| #: List containing string constant that are used to represent headers that can | ||||
| #: be ignored in the required_header function | ||||
| IGNORE_HEADERS = ( | ||||
|     'HTTP_ACCEPT_ENCODING',  # We want content to be uncompressed so | ||||
|                              # we remove the Accept-Encoding from | ||||
|                              # original request | ||||
|     'HTTP_HOST', | ||||
|     'HTTP_REMOTE_USER', | ||||
| ) | ||||
|  | ||||
|  | ||||
| # Default from HTTP RFC 2616 | ||||
| #   See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1 | ||||
| #: Variable that represent the default charset used | ||||
| DEFAULT_CHARSET = 'latin-1' | ||||
|  | ||||
| #: List containing string constants that represents possible html content type | ||||
| HTML_CONTENT_TYPES = ( | ||||
|     'text/html', | ||||
|     'application/xhtml+xml' | ||||
| ) | ||||
|  | ||||
| #: Variable used to represent a minimal content size required for response | ||||
| #: to be turned into stream | ||||
| MIN_STREAMING_LENGTH = 4 * 1024  # 4KB | ||||
|  | ||||
| #: Regex used to find charset in a html content type | ||||
| _get_charset_re = re.compile(r';\s*charset=(?P<charset>[^\s;]+)', re.I) | ||||
|  | ||||
|  | ||||
| def is_html_content_type(content_type): | ||||
|     """Function used to verify if the parameter is a proper html content type | ||||
|  | ||||
|     :param content_type: String variable that represent a content-type | ||||
|     :returns:  A boolean value stating if the content_type is a valid html | ||||
|                content type | ||||
|     """ | ||||
|     for html_content_type in HTML_CONTENT_TYPES: | ||||
|         if content_type.startswith(html_content_type): | ||||
|             return True | ||||
|  | ||||
|     return False | ||||
|  | ||||
|  | ||||
| def should_stream(proxy_response): | ||||
|     """Function to verify if the proxy_response must be converted into | ||||
|     a stream.This will be done by checking the proxy_response content-length | ||||
|     and verify if its length is bigger than one stipulated | ||||
|     by MIN_STREAMING_LENGTH. | ||||
|  | ||||
|     :param proxy_response: An Instance of urllib3.response.HTTPResponse | ||||
|     :returns: A boolean stating if the proxy_response should | ||||
|               be treated as a stream | ||||
|     """ | ||||
|     content_type = proxy_response.headers.get('Content-Type') | ||||
|  | ||||
|     if is_html_content_type(content_type): | ||||
|         return False | ||||
|  | ||||
|     try: | ||||
|         content_length = int(proxy_response.headers.get('Content-Length', 0)) | ||||
|     except ValueError: | ||||
|         content_length = 0 | ||||
|  | ||||
|     if not content_length or content_length > MIN_STREAMING_LENGTH: | ||||
|         return True | ||||
|  | ||||
|     return False | ||||
|  | ||||
|  | ||||
| def get_charset(content_type): | ||||
|     """Function used to retrieve the charset from a content-type.If there is no | ||||
|     charset in the content type then the charset defined on DEFAULT_CHARSET | ||||
|     will be returned | ||||
|  | ||||
|     :param  content_type:   A string containing a Content-Type header | ||||
|     :returns:               A string containing the charset | ||||
|     """ | ||||
|     if not content_type: | ||||
|         return DEFAULT_CHARSET | ||||
|  | ||||
|     matched = _get_charset_re.search(content_type) | ||||
|     if matched: | ||||
|         # Extract the charset and strip its double quotes | ||||
|         return matched.group('charset').replace('"', '') | ||||
|     return DEFAULT_CHARSET | ||||
|  | ||||
|  | ||||
| def required_header(header): | ||||
|     """Function that verify if the header parameter is a essential header | ||||
|  | ||||
|     :param header:  A string represented a header | ||||
|     :returns:       A boolean value that represent if the header is required | ||||
|     """ | ||||
|     if header in IGNORE_HEADERS: | ||||
|         return False | ||||
|  | ||||
|     if header.startswith('HTTP_') or header == 'CONTENT_TYPE': | ||||
|         return True | ||||
|  | ||||
|     return False | ||||
|  | ||||
|  | ||||
| def set_response_headers(response, response_headers): | ||||
|     """Set response's header""" | ||||
|     for header, value in response_headers.items(): | ||||
|         if is_hop_by_hop(header) or header.lower() == 'set-cookie': | ||||
|             continue | ||||
|  | ||||
|         response[header.title()] = value | ||||
|  | ||||
|     logger.debug('Response headers: %s', getattr(response, '_headers')) | ||||
|  | ||||
|  | ||||
| def normalize_request_headers(request): | ||||
|     """Function used to transform header, replacing 'HTTP\\_' to '' | ||||
|     and replace '_' to '-' | ||||
|  | ||||
|     :param request:  A HttpRequest that will be transformed | ||||
|     :returns:        A dictionary with the normalized headers | ||||
|     """ | ||||
|     norm_headers = {} | ||||
|     for header, value in request.META.items(): | ||||
|         if required_header(header): | ||||
|             norm_header = header.replace('HTTP_', '').title().replace('_', '-') | ||||
|             norm_headers[norm_header] = value | ||||
|  | ||||
|     return norm_headers | ||||
|  | ||||
|  | ||||
| def encode_items(items): | ||||
|     """Function that encode all elements in the list of items passed as | ||||
|     a parameter | ||||
|  | ||||
|     :param items:  A list of tuple | ||||
|     :returns:      A list of tuple with all items encoded in 'utf-8' | ||||
|     """ | ||||
|     encoded = [] | ||||
|     for key, values in items: | ||||
|         for value in values: | ||||
|             encoded.append((key.encode('utf-8'), value.encode('utf-8'))) | ||||
|     return encoded | ||||
|  | ||||
|  | ||||
| logger = logging.getLogger('revproxy.cookies') | ||||
|  | ||||
|  | ||||
| def cookie_from_string(cookie_string, strict_cookies=False): | ||||
|     """Parser for HTTP header set-cookie | ||||
|     The return from this function will be used as parameters for | ||||
|     django's response.set_cookie method. Because set_cookie doesn't | ||||
|     have parameter comment, this cookie attribute will be ignored. | ||||
|  | ||||
|     :param  cookie_string: A string representing a valid cookie | ||||
|     :param  strict_cookies: Whether to only accept RFC-compliant cookies | ||||
|     :returns: A dictionary containing the cookie_string attributes | ||||
|     """ | ||||
|  | ||||
|     if strict_cookies: | ||||
|  | ||||
|         cookies = SimpleCookie(COOKIE_PREFIX + cookie_string) | ||||
|         if not cookies.keys(): | ||||
|             return None | ||||
|         cookie_name, = cookies.keys() | ||||
|         cookie_dict = {k: v for k, v in cookies[cookie_name].items() | ||||
|                        if v and k != 'comment'} | ||||
|         cookie_dict['key'] = cookie_name | ||||
|         cookie_dict['value'] = cookies[cookie_name].value | ||||
|         return cookie_dict | ||||
|     valid_attrs = ('path', 'domain', 'comment', 'expires', | ||||
|                    'max_age', 'httponly', 'secure') | ||||
|  | ||||
|     cookie_dict = {} | ||||
|  | ||||
|     cookie_parts = cookie_string.split(';') | ||||
|     try: | ||||
|         cookie_dict['key'], cookie_dict['value'] = \ | ||||
|             cookie_parts[0].split('=', 1) | ||||
|         cookie_dict['value'] = cookie_dict['value'].replace('"', '') | ||||
|         # print('aaaaaaaaaaaaaaaaaaaaaaaaaaaa') | ||||
|         # print(cookie_parts[0].split('=', 1)) | ||||
|     except ValueError: | ||||
|         logger.warning('Invalid cookie: `%s`', cookie_string) | ||||
|         return None | ||||
|  | ||||
|     if cookie_dict['value'].startswith('='): | ||||
|         logger.warning('Invalid cookie: `%s`', cookie_string) | ||||
|         return None | ||||
|  | ||||
|     for part in cookie_parts[1:]: | ||||
|         if '=' in part: | ||||
|             attr, value = part.split('=', 1) | ||||
|             value = value.strip() | ||||
|         else: | ||||
|             attr = part | ||||
|             value = '' | ||||
|  | ||||
|         attr = attr.strip().lower() | ||||
|         if not attr: | ||||
|             continue | ||||
|  | ||||
|         if attr in valid_attrs: | ||||
|             if attr in ('httponly', 'secure'): | ||||
|                 cookie_dict[attr] = True | ||||
|             elif attr in 'comment': | ||||
|                 # ignoring comment attr as explained in the | ||||
|                 # function docstring | ||||
|                 continue | ||||
|             else: | ||||
|                 cookie_dict[attr] = value | ||||
|         else: | ||||
|             logger.warning('Unknown cookie attribute %s', attr) | ||||
|  | ||||
|         return cookie_dict | ||||
							
								
								
									
										7
									
								
								passbook/app_gw/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								passbook/app_gw/requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| django-revproxy | ||||
| urllib3[secure] | ||||
| channels | ||||
| service_identity | ||||
| websocket-client | ||||
| daphne<2.3.0 | ||||
| asgiref~=2.3 | ||||
							
								
								
									
										38
									
								
								passbook/app_gw/rewrite.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								passbook/app_gw/rewrite.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										5
									
								
								passbook/app_gw/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								passbook/app_gw/settings.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| """Application Security Gateway settings""" | ||||
| INSTALLED_APPS = [ | ||||
|     'channels' | ||||
| ] | ||||
| ASGI_APPLICATION = "passbook.app_gw.websocket.routing.application" | ||||
							
								
								
									
										20
									
								
								passbook/app_gw/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								passbook/app_gw/signals.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										2
									
								
								passbook/app_gw/urls.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| """passbook app_gw urls""" | ||||
| urlpatterns = [] | ||||
							
								
								
									
										0
									
								
								passbook/app_gw/websocket/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/app_gw/websocket/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										83
									
								
								passbook/app_gw/websocket/consumer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								passbook/app_gw/websocket/consumer.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| """websocket proxy consumer""" | ||||
| import threading | ||||
| from logging import getLogger | ||||
| from ssl import CERT_NONE | ||||
|  | ||||
| import websocket | ||||
| from channels.generic.websocket import WebsocketConsumer | ||||
|  | ||||
| from passbook.app_gw.models import ApplicationGatewayProvider | ||||
|  | ||||
| LOGGER = getLogger(__name__) | ||||
|  | ||||
| class ProxyConsumer(WebsocketConsumer): | ||||
|     """Proxy websocket connection to upstream""" | ||||
|  | ||||
|     _headers_dict = {} | ||||
|     _app_gw = None | ||||
|     _client = None | ||||
|     _thread = None | ||||
|  | ||||
|     def _fix_headers(self, input_dict): | ||||
|         """Fix headers from bytestrings to normal strings""" | ||||
|         return { | ||||
|             key.decode('utf-8'): value.decode('utf-8') | ||||
|             for key, value in dict(input_dict).items() | ||||
|         } | ||||
|  | ||||
|     def connect(self): | ||||
|         """Extract host header, lookup in database and proxy connection""" | ||||
|         self._headers_dict = self._fix_headers(dict(self.scope.get('headers'))) | ||||
|         host = self._headers_dict.pop('host') | ||||
|         query_string = self.scope.get('query_string').decode('utf-8') | ||||
|         matches = ApplicationGatewayProvider.objects.filter( | ||||
|             server_name__contains=[host], | ||||
|             enabled=True) | ||||
|         if matches.exists(): | ||||
|             self._app_gw = matches.first() | ||||
|             # TODO: Get upstream that starts with wss or | ||||
|             upstream = self._app_gw.upstream[0].replace('http', 'ws') + self.scope.get('path') | ||||
|             if query_string: | ||||
|                 upstream += '?' + query_string | ||||
|             sslopt = {} | ||||
|             if not self._app_gw.upstream_ssl_verification: | ||||
|                 sslopt = {"cert_reqs": CERT_NONE} | ||||
|             self._client = websocket.WebSocketApp( | ||||
|                 url=upstream, | ||||
|                 subprotocols=self.scope.get('subprotocols'), | ||||
|                 header=self._headers_dict, | ||||
|                 on_message=self._client_on_message_handler(), | ||||
|                 on_error=self._client_on_error_handler(), | ||||
|                 on_close=self._client_on_close_handler(), | ||||
|                 on_open=self._client_on_open_handler()) | ||||
|             LOGGER.debug("Accepting connection for %s", host) | ||||
|             self._thread = threading.Thread(target=lambda: self._client.run_forever(sslopt=sslopt)) | ||||
|             self._thread.start() | ||||
|  | ||||
|     def _client_on_open_handler(self): | ||||
|         return lambda ws: self.accept(self._client.sock.handshake_response.subprotocol) | ||||
|  | ||||
|     def _client_on_message_handler(self): | ||||
|         # pylint: disable=unused-argument,invalid-name | ||||
|         def message_handler(ws, message): | ||||
|             if isinstance(message, str): | ||||
|                 self.send(text_data=message) | ||||
|             else: | ||||
|                 self.send(bytes_data=message) | ||||
|         return message_handler | ||||
|  | ||||
|     def _client_on_error_handler(self): | ||||
|         return lambda ws, error: print(error) | ||||
|  | ||||
|     def _client_on_close_handler(self): | ||||
|         return lambda ws: self.disconnect(0) | ||||
|  | ||||
|     def disconnect(self, code): | ||||
|         self._client.close() | ||||
|  | ||||
|     def receive(self, text_data=None, bytes_data=None): | ||||
|         if text_data: | ||||
|             opcode = websocket.ABNF.OPCODE_TEXT | ||||
|         if bytes_data: | ||||
|             opcode = websocket.ABNF.OPCODE_BINARY | ||||
|         self._client.send(text_data or bytes_data, opcode) | ||||
							
								
								
									
										17
									
								
								passbook/app_gw/websocket/routing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								passbook/app_gw/websocket/routing.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| """app_gw websocket proxy""" | ||||
| from channels.auth import AuthMiddlewareStack | ||||
| from channels.routing import ProtocolTypeRouter, URLRouter | ||||
| from django.conf.urls import url | ||||
|  | ||||
| from passbook.app_gw.websocket.consumer import ProxyConsumer | ||||
|  | ||||
| websocket_urlpatterns = [ | ||||
|     url(r'^(.*)$', ProxyConsumer), | ||||
| ] | ||||
|  | ||||
| application = ProtocolTypeRouter({ | ||||
|     # (http->django views is added by default) | ||||
|     'websocket': AuthMiddlewareStack( | ||||
|         URLRouter(websocket_urlpatterns) | ||||
|     ), | ||||
| }) | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook audit Header""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook captcha_factor Header""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook core""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
							
								
								
									
										13
									
								
								passbook/core/asgi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								passbook/core/asgi.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| """ | ||||
| ASGI entrypoint. Configures Django and then runs the application | ||||
| defined in the ASGI_APPLICATION setting. | ||||
| """ | ||||
|  | ||||
| import os | ||||
|  | ||||
| import django | ||||
| from channels.routing import get_default_application | ||||
|  | ||||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.core.settings") | ||||
| django.setup() | ||||
| application = get_default_application() | ||||
| @ -3,10 +3,8 @@ | ||||
| import logging | ||||
| import os | ||||
|  | ||||
| import celery | ||||
| from celery import Celery, signals | ||||
| from django.conf import settings | ||||
| from raven import Client | ||||
| from raven.contrib.celery import register_logger_signal, register_signal | ||||
|  | ||||
| # set the default Django settings module for the 'celery' program. | ||||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.core.settings") | ||||
| @ -14,31 +12,18 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.core.settings") | ||||
| LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class Celery(celery.Celery): | ||||
|     """Custom Celery class with Raven configured""" | ||||
|  | ||||
|     # pylint: disable=method-hidden | ||||
|     def on_configure(self): | ||||
|         """Update raven client""" | ||||
|         try: | ||||
|             client = Client(settings.RAVEN_CONFIG.get('dsn')) | ||||
|             # register a custom filter to filter out duplicate logs | ||||
|             register_logger_signal(client) | ||||
|             # hook into the Celery error handler | ||||
|             register_signal(client) | ||||
|         except RecursionError:  # This error happens when pdoc is running | ||||
|             pass | ||||
| CELERY_APP = Celery('passbook') | ||||
|  | ||||
|  | ||||
| # pylint: disable=unused-argument | ||||
| @celery.signals.setup_logging.connect | ||||
| @signals.setup_logging.connect | ||||
| def config_loggers(*args, **kwags): | ||||
|     """Apply logging settings from settings.py to celery""" | ||||
|     logging.config.dictConfig(settings.LOGGING) | ||||
|  | ||||
|  | ||||
| # pylint: disable=unused-argument | ||||
| @celery.signals.after_task_publish.connect | ||||
| @signals.after_task_publish.connect | ||||
| def after_task_publish(sender=None, headers=None, body=None, **kwargs): | ||||
|     """Log task_id after it was published""" | ||||
|     info = headers if 'task' in headers else body | ||||
| @ -46,22 +31,20 @@ def after_task_publish(sender=None, headers=None, body=None, **kwargs): | ||||
|  | ||||
|  | ||||
| # pylint: disable=unused-argument | ||||
| @celery.signals.task_prerun.connect | ||||
| @signals.task_prerun.connect | ||||
| def task_prerun(task_id, task, *args, **kwargs): | ||||
|     """Log task_id on worker""" | ||||
|     LOGGER.debug('%-40s started (name=%s)', task_id, task.__name__) | ||||
|  | ||||
|  | ||||
| # pylint: disable=unused-argument | ||||
| @celery.signals.task_postrun.connect | ||||
| @signals.task_postrun.connect | ||||
| def task_postrun(task_id, task, *args, retval=None, state=None, **kwargs): | ||||
|     """Log task_id on worker""" | ||||
|     LOGGER.debug('%-40s finished (name=%s, state=%s)', | ||||
|                  task_id, task.__name__, state) | ||||
|  | ||||
|  | ||||
| CELERY_APP = Celery('passbook') | ||||
|  | ||||
| # Using a string here means the worker doesn't have to serialize | ||||
| # the configuration object to child processes. | ||||
| # - namespace='CELERY' means all celery-related configuration keys | ||||
|  | ||||
| @ -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""" | ||||
|  | ||||
| @ -2,11 +2,11 @@ | ||||
|  | ||||
| from logging import getLogger | ||||
|  | ||||
| import cherrypy | ||||
| from django.conf import settings | ||||
| from daphne.cli import CommandLineInterface | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import autoreload | ||||
|  | ||||
| from passbook.core.wsgi import application | ||||
| from passbook.lib.config import CONFIG | ||||
|  | ||||
| LOGGER = getLogger(__name__) | ||||
|  | ||||
| @ -15,20 +15,15 @@ class Command(BaseCommand): | ||||
|     """Run CherryPy webserver""" | ||||
|  | ||||
|     def handle(self, *args, **options): | ||||
|         """passbook cherrypy server""" | ||||
|         config = settings.CHERRYPY_SERVER | ||||
|         config.update(**options) | ||||
|         cherrypy.config.update(config) | ||||
|         cherrypy.tree.graft(application, '/') | ||||
|         # Mount NullObject to serve static files | ||||
|         cherrypy.tree.mount(None, '/static', config={ | ||||
|             '/': { | ||||
|                 'tools.staticdir.on': True, | ||||
|                 'tools.staticdir.dir': settings.STATIC_ROOT, | ||||
|                 'tools.expires.on': True, | ||||
|                 'tools.expires.secs': 86400, | ||||
|                 'tools.gzip.on': True, | ||||
|             } | ||||
|         }) | ||||
|         cherrypy.engine.start() | ||||
|         cherrypy.engine.block() | ||||
|         """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('web.port', 8000)), | ||||
|             '-b', CONFIG.y('web.listen', '0.0.0.0'),  # nosec | ||||
|             '--access-log', '/dev/null', | ||||
|             'passbook.core.asgi:application' | ||||
|         ]) | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
| from logging import getLogger | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import autoreload | ||||
|  | ||||
| from passbook.core.celery import CELERY_APP | ||||
|  | ||||
| @ -14,4 +15,9 @@ class Command(BaseCommand): | ||||
|  | ||||
|     def handle(self, *args, **options): | ||||
|         """celery worker""" | ||||
|         CELERY_APP.worker_main(['worker', '--autoscale=10,3', '-E']) | ||||
|         autoreload.run_with_reloader(self.celery_worker) | ||||
|  | ||||
|     def celery_worker(self): | ||||
|         """Run celery worker within autoreload""" | ||||
|         autoreload.raise_last_exception() | ||||
|         CELERY_APP.worker_main(['worker', '--autoscale=10,3', '-E', '-B']) | ||||
|  | ||||
							
								
								
									
										18
									
								
								passbook/core/migrations/0021_policy_timeout.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								passbook/core/migrations/0021_policy_timeout.py
									
									
									
									
									
										Normal 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), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								passbook/core/migrations/0022_nonce_expiring.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								passbook/core/migrations/0022_nonce_expiring.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| # Generated by Django 2.1.7 on 2019-04-04 19:42 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('passbook_core', '0021_policy_timeout'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='nonce', | ||||
|             name='expiring', | ||||
|             field=models.BooleanField(default=True), | ||||
|         ), | ||||
|     ] | ||||
| @ -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() | ||||
|  | ||||
| @ -436,6 +437,7 @@ class Nonce(UUIDModel): | ||||
|  | ||||
|     expires = models.DateTimeField(default=default_nonce_duration) | ||||
|     user = models.ForeignKey('User', on_delete=models.CASCADE) | ||||
|     expiring = models.BooleanField(default=True) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return "Nonce %s (expires=%s)" % (self.uuid.hex, self.expires) | ||||
|  | ||||
| @ -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: | ||||
|             if self.__group: | ||||
|                 # ValueError can be thrown from _policy_engine_task when user is None | ||||
|             group_result = self._group.get() | ||||
|                 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) | ||||
|  | ||||
| @ -1,12 +1,14 @@ | ||||
| django>=2.0 | ||||
| django-model-utils | ||||
| django-ipware | ||||
| djangorestframework | ||||
| PyYAML | ||||
| raven | ||||
| markdown | ||||
| colorlog | ||||
| celery | ||||
| psycopg2 | ||||
| colorlog | ||||
| django-ipware | ||||
| django-model-utils | ||||
| django-redis | ||||
| django>=2.0 | ||||
| djangorestframework | ||||
| idna<2.8,>=2.5 | ||||
| cherrypy | ||||
| markdown | ||||
| psycopg2 | ||||
| PyYAML | ||||
| sentry-sdk | ||||
| pip | ||||
| whitenoise | ||||
|  | ||||
| @ -11,10 +11,16 @@ https://docs.djangoproject.com/en/2.1/ref/settings/ | ||||
| """ | ||||
|  | ||||
| import importlib | ||||
| import logging | ||||
| import os | ||||
| import sys | ||||
|  | ||||
| from celery.schedules import crontab | ||||
| from django.contrib import messages | ||||
| from sentry_sdk import init as sentry_init | ||||
| from sentry_sdk.integrations.celery import CeleryIntegration | ||||
| from sentry_sdk.integrations.django import DjangoIntegration | ||||
| from sentry_sdk.integrations.logging import LoggingIntegration | ||||
|  | ||||
| from passbook import __version__ | ||||
| from passbook.lib.config import CONFIG | ||||
| @ -34,7 +40,8 @@ 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')] | ||||
| ALLOWED_HOSTS = ['*'] | ||||
| SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') | ||||
|  | ||||
| LOGIN_URL = 'passbook_core:auth-login' | ||||
| @ -45,6 +52,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 = [ | ||||
| @ -63,7 +73,6 @@ INSTALLED_APPS = [ | ||||
|     'django.contrib.postgres', | ||||
|     'rest_framework', | ||||
|     'drf_yasg', | ||||
|     'raven.contrib.django.raven_compat', | ||||
|     'passbook.core.apps.PassbookCoreConfig', | ||||
|     'passbook.admin.apps.PassbookAdminConfig', | ||||
|     'passbook.api.apps.PassbookAPIConfig', | ||||
| @ -79,6 +88,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,15 +109,26 @@ 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', | ||||
|     'whitenoise.middleware.WhiteNoiseMiddleware', | ||||
|     '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', | ||||
| ] | ||||
|  | ||||
| ROOT_URLCONF = 'passbook.core.urls' | ||||
| @ -189,32 +210,34 @@ CELERY_BROKER_URL = 'amqp://%s' % CONFIG.get('rabbitmq') | ||||
| CELERY_RESULT_BACKEND = 'rpc://' | ||||
| CELERY_ACKS_LATE = True | ||||
| CELERY_BROKER_HEARTBEAT = 0 | ||||
|  | ||||
| # Raven settings | ||||
| RAVEN_CONFIG = { | ||||
|     'dsn': ('https://55b5dd780bc14f4c96bba69b7a9abbcc:449af483bd0745' | ||||
|             '0d83be640d834e5458@sentry.services.beryju.org/8'), | ||||
|     'release': VERSION, | ||||
|     'environment': 'dev' if DEBUG else 'production', | ||||
| CELERY_BEAT_SCHEDULE = { | ||||
|     'cleanup-expired-nonces': { | ||||
|         'task': 'passbook.core.tasks.clean_nonces', | ||||
|         'schedule': crontab(hour=1, minute=1) | ||||
|     } | ||||
| } | ||||
|  | ||||
| # CherryPY settings | ||||
| with CONFIG.cd('web'): | ||||
|     CHERRYPY_SERVER = { | ||||
|         'server.socket_host': CONFIG.get('listen', '0.0.0.0'),  # nosec | ||||
|         'server.socket_port': CONFIG.get('port', 8000), | ||||
|         'server.thread_pool': CONFIG.get('threads', 30), | ||||
|         'log.screen': False, | ||||
|         'log.access_file': '', | ||||
|         'log.error_file': '', | ||||
|     } | ||||
| sentry_init( | ||||
|     dsn=("https://55b5dd780bc14f4c96bba69b7a9abbcc:449af483bd0745" | ||||
|          "0d83be640d834e5458@sentry.services.beryju.org/8"), | ||||
|     integrations=[ | ||||
|         DjangoIntegration(), | ||||
|         CeleryIntegration(), | ||||
|         LoggingIntegration( | ||||
|             level=logging.INFO, | ||||
|             event_level=logging.ERROR | ||||
|         ) | ||||
|     ], | ||||
|     send_default_pii=True | ||||
| ) | ||||
|  | ||||
| # Static files (CSS, JavaScript, Images) | ||||
| # https://docs.djangoproject.com/en/2.1/howto/static-files/ | ||||
|  | ||||
| STATIC_URL = '/static/' | ||||
| STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' | ||||
|  | ||||
| LOG_HANDLERS = ['console', 'syslog', 'file', 'sentry'] | ||||
| LOG_HANDLERS = ['console', 'syslog', 'file'] | ||||
|  | ||||
| with CONFIG.cd('log'): | ||||
|     LOGGING = { | ||||
| @ -245,10 +268,6 @@ with CONFIG.cd('log'): | ||||
|                 'class': 'logging.StreamHandler', | ||||
|                 'formatter': 'color', | ||||
|             }, | ||||
|             'sentry': { | ||||
|                 'level': 'ERROR', | ||||
|                 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', | ||||
|             }, | ||||
|             'syslog': { | ||||
|                 'level': CONFIG.get('level').get('file'), | ||||
|                 'class': 'logging.handlers.SysLogHandler', | ||||
| @ -294,6 +313,11 @@ with CONFIG.cd('log'): | ||||
|                 'level': 'DEBUG', | ||||
|                 'propagate': True, | ||||
|             }, | ||||
|             'daphne': { | ||||
|                 'handlers': LOG_HANDLERS, | ||||
|                 'level': 'DEBUG', | ||||
|                 'propagate': True, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -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)) | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -1,11 +1,16 @@ | ||||
| """passbook core tasks""" | ||||
| from datetime import datetime | ||||
| from logging import getLogger | ||||
|  | ||||
| from django.core.mail import EmailMultiAlternatives | ||||
| from django.template.loader import render_to_string | ||||
| from django.utils.html import strip_tags | ||||
|  | ||||
| from passbook.core.celery import CELERY_APP | ||||
| from passbook.core.models import Nonce | ||||
| from passbook.lib.config import CONFIG | ||||
|  | ||||
| LOGGER = getLogger(__name__) | ||||
|  | ||||
| @CELERY_APP.task() | ||||
| def send_email(to_address, subject, template, context): | ||||
| @ -15,3 +20,9 @@ def send_email(to_address, subject, template, context): | ||||
|     msg = EmailMultiAlternatives(subject, text_content, CONFIG.y('email.from'), [to_address]) | ||||
|     msg.attach_alternative(html_content, "text/html") | ||||
|     msg.send() | ||||
|  | ||||
| @CELERY_APP.task() | ||||
| def clean_nonces(): | ||||
|     """Remove expired nonces""" | ||||
|     amount, _ = Nonce.objects.filter(expires__lt=datetime.now(), expiring=True).delete() | ||||
|     LOGGER.debug("Deleted expired %d nonces", amount) | ||||
|  | ||||
							
								
								
									
										26
									
								
								passbook/core/templates/error/400.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								passbook/core/templates/error/400.html
									
									
									
									
									
										Normal 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 %} | ||||
							
								
								
									
										26
									
								
								passbook/core/templates/error/403.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								passbook/core/templates/error/403.html
									
									
									
									
									
										Normal 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 %} | ||||
							
								
								
									
										26
									
								
								passbook/core/templates/error/404.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								passbook/core/templates/error/404.html
									
									
									
									
									
										Normal 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 %} | ||||
							
								
								
									
										26
									
								
								passbook/core/templates/error/500.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								passbook/core/templates/error/500.html
									
									
									
									
									
										Normal 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 %} | ||||
| @ -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.ServerErrorView.as_view() | ||||
|  | ||||
| core_urls = [ | ||||
|     # Authentication views | ||||
|     path('auth/login/', authentication.LoginView.as_view(), name='auth-login'), | ||||
|  | ||||
							
								
								
									
										65
									
								
								passbook/core/views/error.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								passbook/core/views/error.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| """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 | ||||
|     } | ||||
|  | ||||
|     # pylint: disable=useless-super-delegation | ||||
|     def dispatch(self, *args, **kwargs): | ||||
|         """Little wrapper so django accepts this function""" | ||||
|         return super().dispatch(*args, **kwargs) | ||||
| @ -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' | ||||
|  | ||||
| @ -10,8 +10,8 @@ https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ | ||||
| import os | ||||
|  | ||||
| from django.core.wsgi import get_wsgi_application | ||||
| from raven.contrib.django.raven_compat.middleware.wsgi import Sentry | ||||
| from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware | ||||
|  | ||||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'passbook.settings') | ||||
|  | ||||
| application = Sentry(get_wsgi_application()) | ||||
| application = SentryWsgiMiddleware(get_wsgi_application()) | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook hibp_policy""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """Passbook ldap app Header""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook lib""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -8,6 +8,7 @@ from typing import Any | ||||
|  | ||||
| import yaml | ||||
| from django.conf import ImproperlyConfigured | ||||
| from django.utils.autoreload import autoreload_started | ||||
|  | ||||
| SEARCH_PATHS = [ | ||||
|     'passbook/lib/default.yml', | ||||
| @ -21,6 +22,8 @@ ENVIRONMENT = os.getenv('PASSBOOK_ENV', 'local') | ||||
| class ConfigLoader: | ||||
|     """Search through SEARCH_PATHS and load configuration""" | ||||
|  | ||||
|     loaded_file = [] | ||||
|  | ||||
|     __config = {} | ||||
|     __context_default = None | ||||
|     __sub_dicts = [] | ||||
| @ -69,6 +72,8 @@ class ConfigLoader: | ||||
|             with open(path) as file: | ||||
|                 try: | ||||
|                     self.update(self.__config, yaml.safe_load(file)) | ||||
|                     LOGGER.debug("Loaded %s", path) | ||||
|                     self.loaded_file.append(path) | ||||
|                 except yaml.YAMLError as exc: | ||||
|                     raise ImproperlyConfigured from exc | ||||
|         except PermissionError as exc: | ||||
| @ -126,3 +131,10 @@ class ConfigLoader: | ||||
|  | ||||
|  | ||||
| CONFIG = ConfigLoader() | ||||
|  | ||||
| # pylint: disable=unused-argument | ||||
| def signal_handler(sender, **kwargs): | ||||
|     """Add all loaded config files to autoreload watcher""" | ||||
|     for path in CONFIG.loaded_file: | ||||
|         sender.watch_file(path) | ||||
| autoreload_started.connect(signal_handler) | ||||
|  | ||||
| @ -30,10 +30,15 @@ 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 | ||||
|  | ||||
| domains: | ||||
|     - passbook.local | ||||
| 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 | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook oauth_client Header""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook oauth_provider Header""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -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', ] | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| """Oauth2 provider product extension""" | ||||
|  | ||||
| from django.shortcuts import reverse | ||||
| from django.utils.translation import gettext as _ | ||||
| from oauth2_provider.models import AbstractApplication | ||||
|  | ||||
| @ -14,6 +15,20 @@ class OAuth2Provider(Provider, AbstractApplication): | ||||
|     def __str__(self): | ||||
|         return "OAuth2 Provider %s" % self.name | ||||
|  | ||||
|     def html_setup_urls(self, request): | ||||
|         """return template and context modal with URLs for authorize, token, openid-config, etc""" | ||||
|         return "oauth2_provider/setup_url_modal.html", { | ||||
|             'provider': self, | ||||
|             'authorize_url': request.build_absolute_uri( | ||||
|                 reverse('passbook_oauth_provider:oauth2-authorize')), | ||||
|             'token_url': request.build_absolute_uri( | ||||
|                 reverse('passbook_oauth_provider:token')), | ||||
|             'userinfo_url': request.build_absolute_uri( | ||||
|                 reverse('passbook_api:openid')), | ||||
|             'openid_url': request.build_absolute_uri( | ||||
|                 reverse('passbook_oauth_provider:openid-discovery')) | ||||
|         } | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         verbose_name = _('OAuth2 Provider') | ||||
|  | ||||
| @ -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 'Authroize URL' %}</label> | ||||
|             <div class="col-sm-9"> | ||||
|               <input type="text"class="form-control" readonly value="{{ authorize_url }}"> | ||||
|             </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_url }}"> | ||||
|             </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_url }}"> | ||||
|             </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="{{ openid_url }}"> | ||||
|             </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> | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook otp Header""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook password_expiry""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook saml_idp Header""" | ||||
| __version__ = '0.1.23-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook suspicious_policy""" | ||||
| __version__ = '0.1.1-beta' | ||||
| __version__ = '0.1.30-beta' | ||||
|  | ||||
| @ -7,3 +7,4 @@ | ||||
| -r passbook/captcha_factor/requirements.txt | ||||
| -r passbook/admin/requirements.txt | ||||
| -r passbook/api/requirements.txt | ||||
| -r passbook/app_gw/requirements.txt | ||||
		Reference in New Issue
	
	Block a user
	