Compare commits

..

28 Commits

Author SHA1 Message Date
4cf6c36f34 new release: 0.2.7-beta 2019-07-22 15:54:34 +02:00
75a6f6c875 deploy: remove old files from bumpversion 2019-07-22 15:54:10 +02:00
62abe3f256 suspicious_policy: fix Request IP lookup 2019-07-22 15:46:41 +02:00
9296c41650 ci: add missing packaging dependency 2019-07-22 15:35:11 +02:00
7fb48fde6d deploy: add static deployment, add resource reservations/limits 2019-07-22 15:22:10 +02:00
174472bb45 all: get rid of individual requirements file, remove version from every module 2019-07-22 15:20:17 +02:00
17575ed921 deploy: rewrite docker files to be more stackable 2019-07-22 15:18:33 +02:00
b1b1a27444 client-packages: now deprecated 2019-07-22 15:17:58 +02:00
f97a5eeefb api: *actually* fix drf dependency issue 2019-07-15 15:05:54 +00:00
10fd96981e new release: 0.2.6-beta 2019-07-15 13:42:18 +00:00
67e3eb549c api: fix wrong django-rest-framework dependency 2019-07-15 13:42:03 +00:00
30a6d1f0b1 new release: 0.2.5-beta 2019-07-15 13:31:04 +00:00
3d1fa9f048 app_gw: Rewrite redirect responses (replace upstream location with server_name) 2019-07-09 15:28:52 +02:00
1d2be6e68b root: fix sentry sending wrong release 2019-07-05 16:00:01 +02:00
c21e343986 oidc_provider: fix error when creating a new provider 2019-07-05 15:59:52 +02:00
ff37ed095c new release: 0.2.4-beta 2019-07-05 15:30:13 +02:00
8623a2c3fc oidc_provider: fix error trying to create RSA Key before migrations are run 2019-07-05 15:27:04 +02:00
23d277eaf1 remove oidc from OAuth2, add dedicated OIDC provider 2019-07-05 15:21:48 +02:00
75ced59451 helm: fix syntax error 2019-07-05 15:21:12 +02:00
bccf424c5e new release: 0.2.3-beta 2019-07-04 16:25:33 +02:00
2f9ae40d20 client-sentry: fix 400 failing upload 2019-07-04 16:25:17 +02:00
11e1eec3fb ci: fix new dependencies not being installed on the fly 2019-07-04 16:21:35 +02:00
765c5633df helm: add appgw to ingress 2019-07-04 16:07:16 +02:00
6344b1aafb helm: add deployment for appgw 2019-07-04 15:25:36 +02:00
ed25801e6e core: revert to cherrypy for main webserver and use daphne only for app_gw 2019-07-04 15:23:05 +02:00
4d0148193f root: migrate to new sentry instance 2019-07-03 17:35:54 +02:00
804ae15c2e new release: 0.2.2-beta 2019-06-25 18:50:41 +02:00
b35a9fad86 Fix linting errors with current build-base image 2019-06-25 18:50:37 +02:00
99 changed files with 899 additions and 1056 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.2.1-beta
current_version = 0.2.7-beta
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -15,10 +15,6 @@ values =
beta
stable
[bumpversion:file:client-packages/allauth/setup.py]
[bumpversion:file:client-packages/sentry-auth-passbook/setup.py]
[bumpversion:file:helm/passbook/values.yaml]
[bumpversion:file:helm/passbook/Chart.yaml]
@ -27,33 +23,5 @@ values =
[bumpversion:file:passbook/__init__.py]
[bumpversion:file:passbook/api/__init__.py]
[bumpversion:file:passbook/core/__init__.py]
[bumpversion:file:passbook/admin/__init__.py]
[bumpversion:file:passbook/captcha_factor/__init__.py]
[bumpversion:file:passbook/oauth_client/__init__.py]
[bumpversion:file:passbook/ldap/__init__.py]
[bumpversion:file:passbook/lib/__init__.py]
[bumpversion:file:passbook/hibp_policy/__init__.py]
[bumpversion:file:passbook/password_expiry_policy/__init__.py]
[bumpversion:file:passbook/saml_idp/__init__.py]
[bumpversion:file:passbook/audit/__init__.py]
[bumpversion:file:passbook/oauth_provider/__init__.py]
[bumpversion:file:passbook/otp/__init__.py]
[bumpversion:file:passbook/app_gw/__init__.py]
[bumpversion:file:passbook/suspicious_policy/__init__.py]
[bumpversion:file:passbook/core/nginx.conf]

View File

@ -1,109 +1,132 @@
# Global Variables
stages:
- build-buildimage
- build-base-image
- build-dev-image
- test
- build
- docs
- deploy
image: docker.beryju.org/passbook/build-base:latest
services:
- postgres:latest
- redis:latest
- package
image: docker.beryju.org/passbook/dev:latest
variables:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
create-build-image:
before_script:
# Ensure all dependencies are installed, even those not included in passbook/dev
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
create-base-image:
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.build-base --destination docker.beryju.org/passbook/build-base:latest --destination docker.beryju.org/passbook/build-base:0.2.1-beta
stage: build-buildimage
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.base --destination docker.beryju.org/passbook/base:latest --destination docker.beryju.org/passbook/base:0.2.7-beta
stage: build-base-image
only:
refs:
- tags
- /^version/.*$/
build-dev-image:
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.dev --destination docker.beryju.org/passbook/dev:latest --destination docker.beryju.org/passbook/dev:0.2.7-beta
stage: build-dev-image
only:
refs:
- tags
- /^version/.*$/
isort:
script:
- isort -c -sg env
stage: test
services:
- postgres:latest
- redis:latest
migrations:
script:
- python manage.py migrate
stage: test
services:
- postgres:latest
- redis:latest
prospector:
script:
- prospector
stage: test
services:
- postgres:latest
- redis:latest
pylint:
script:
- pylint passbook
stage: test
services:
- postgres:latest
- redis:latest
coverage:
script:
- python manage.py collectstatic --no-input
- coverage run manage.py test
- coverage report
- coverage html
stage: test
bandit:
script:
- bandit -r passbook
stage: test
services:
- postgres:latest
- redis:latest
package-docker:
package-passbook-server:
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.2.1-beta
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.2.7-beta
stage: build
only:
- tags
- /^version/.*$/
package-helm:
build-passbook-static:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.static --destination docker.beryju.org/passbook/static:latest --destination docker.beryju.org/passbook/static:0.2.7-beta
only:
- tags
- /^version/.*$/
# running collectstatic fully initialises django, hence we need that databases
services:
- postgres:latest
- redis:latest
package-helm:
image: debian:stretch-slim
stage: package
before_script:
- apt update && apt install -y curl
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
script:
- helm init --client-only
- helm dependency build helm/passbook
- helm package helm/passbook
artifacts:
paths:
- passbook-*.tgz
expire_in: 2 days
expire_in: 1 week
only:
- tags
- /^version/.*$/
package-client-package-allauth:
script:
- cd client-packages/allauth
- python setup.py sdist
- twine upload --username $TWINE_USERNAME --password $TWINE_PASSWORD dist/*
stage: build
only:
refs:
- tags
- /^version/.*$/
changes:
- client-packages/allauth/**
package-client-package-sentry:
script:
- cd client-packages/sentry-auth-passbook
- python setup.py sdist
- twine upload --username $TWINE_USERNAME --password $TWINE_PASSWORD dist/*
stage: build
only:
refs:
- tags
- /^version/.*$/
changes:
- client-packages/sentry-auth-passbook/**

View File

@ -1,34 +1,8 @@
FROM python:3.6-slim-stretch as build
FROM docker.beryju.org/passbook/base:latest
COPY ./passbook/ /app/passbook
COPY ./manage.py /app/
COPY ./requirements.txt /app/
WORKDIR /app/
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 && \
./manage.py collectstatic --no-input && \
apt-get remove --purge -y build-essential && \
apt-get autoremove --purge -y
FROM python:3.6-slim-stretch
COPY ./passbook/ /app/passbook
COPY ./manage.py /app/
COPY ./requirements.txt /app/
COPY --from=build /app/static /app/static/
WORKDIR /app/
RUN 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 && \
chown -R passbook /app/ && \
apt-get remove --purge -y build-essential && \
apt-get autoremove --purge -y
USER passbook
WORKDIR /app/

11
Dockerfile.base Normal file
View File

@ -0,0 +1,11 @@
FROM python:3.7-alpine
COPY ./requirements.txt /app/
WORKDIR /app/
RUN apk update && \
apk add --no-cache openssl-dev build-base libxml2-dev libxslt-dev libffi-dev gcc musl-dev libgcc zlib-dev postgresql-dev && \
pip install -r /app/requirements.txt --no-cache-dir && \
adduser -S passbook && \
chown -R passbook /app

View File

@ -1,12 +0,0 @@
FROM python:3.6
COPY ./passbook/ /app/passbook
COPY ./client-packages/ /app/client-packages
COPY ./requirements.txt /app/
COPY ./requirements-dev.txt /app/
WORKDIR /app/
RUN apt-get update && apt-get install libssl-dev libffi-dev libpq-dev -y && \
pip install -U -r requirements-dev.txt && \
rm -rf /app/*

5
Dockerfile.dev Normal file
View File

@ -0,0 +1,5 @@
FROM docker.beryju.org/passbook/base:latest
COPY ./requirements-dev.txt /app/
RUN pip install -r /app/requirements-dev.txt --no-cache-dir

14
Dockerfile.static Normal file
View File

@ -0,0 +1,14 @@
FROM docker.beryju.org/passbook/dev:latest as static-build
COPY ./passbook/ /app/passbook
COPY ./manage.py /app/
COPY ./requirements.txt /app/
WORKDIR /app/
RUN ./manage.py collectstatic --no-input
FROM nginx:latest
COPY --from=static-build /app/static /static/_/static/
COPY ./passbook/core/nginx.conf /etc/nginx/nginx.conf

View File

@ -1,35 +0,0 @@
"""passbook provider"""
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class PassbookAccount(ProviderAccount):
"""passbook account"""
def to_str(self):
dflt = super().to_str()
return self.account.extra_data.get('username', dflt)
class PassbookProvider(OAuth2Provider):
"""passbook provider"""
id = 'passbook'
name = 'passbook'
account_class = PassbookAccount
def extract_uid(self, data):
return str(data['sub'])
def extract_common_fields(self, data):
return {
'email': data.get('email'),
'username': data.get('preferred_username'),
'name': data.get('name'),
}
def get_default_scope(self):
return ['openid:userinfo']
provider_classes = [PassbookProvider] # noqa

View File

@ -1,6 +0,0 @@
"""passbook provider"""
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from allauth_passbook.provider import PassbookProvider
urlpatterns = default_urlpatterns(PassbookProvider)

View File

@ -1,37 +0,0 @@
"""passbook adapter"""
import requests
from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView)
from allauth_passbook.provider import PassbookProvider
class PassbookOAuth2Adapter(OAuth2Adapter):
"""passbook OAuth2 Adapter"""
provider_id = PassbookProvider.id
# pylint: disable=no-member
settings = app_settings.PROVIDERS.get(provider_id, {}) # noqa
provider_base_url = settings.get("PASSBOOK_URL", 'https://id.beryju.org')
access_token_url = '{0}/application/oauth/token/'.format(provider_base_url)
authorize_url = '{0}/application/oauth/authorize/'.format(provider_base_url)
profile_url = '{0}/api/v1/openid/'.format(
provider_base_url)
def complete_login(self, request, app, access_token, **kwargs):
headers = {
'Authorization': 'Bearer {0}'.format(access_token.token),
'Content-Type': 'application/json',
}
extra_data = requests.get(self.profile_url, headers=headers)
return self.get_provider().sociallogin_from_response(
request,
extra_data.json()
)
oauth2_login = OAuth2LoginView.adapter_view(PassbookOAuth2Adapter) # noqa
oauth2_callback = OAuth2CallbackView.adapter_view(PassbookOAuth2Adapter) # noqa

View File

@ -1 +0,0 @@
django-allauth

View File

@ -1,33 +0,0 @@
"""passbook allauth setup.py"""
from setuptools import setup
setup(
name='django-allauth-passbook',
version='0.2.1-beta',
description='passbook support for django-allauth',
# long_description='\n'.join(read_simple('docs/index.md')[2:]),
long_description_content_type='text/markdown',
author='BeryJu.org',
author_email='hello@beryju.org',
packages=['allauth_passbook'],
include_package_data=True,
install_requires=['django-allauth'],
keywords='django allauth passbook',
license='MIT',
classifiers=[
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries :: Python Modules',
'Environment :: Web Environment',
'Topic :: Internet',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Framework :: Django',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1',
],
)

View File

@ -1,5 +0,0 @@
*.pyc
*.egg-info/
*.eggs
/dist
/build

View File

@ -1,32 +0,0 @@
sudo: false
language: python
services:
- memcached
- postgresql
- redis-server
python:
- '2.7'
cache:
directories:
- node_modules
- "$HOME/.cache/pip"
deploy:
provider: pypi
user: getsentry
password:
secure: kVmxKHkBWRLYyZme05p+WZSJmb8GjHV9uyuaSCVMRlqWCW+GXRB7P1xXR2jb9URTlNdcs56Ab/UrwzCbMFGC8LmwCeFVgIR/ltytVZG2FgXZPWaeA4dH25qK2oGWgzJ/xeiMpmuJqN9hRl25MX6jG7FZKvrrOkG7+8tpPd1yO+uYWZQbnebZMjcPBqEpn7CC0hR39GSoyVAbydpMe5hwENGQM26CepcicdrelfawItoUrXrkJzBHkIQQTO/xRSbCtRJOtzI5lwtv3GP0hcbOy5tI5dhG/93pLwZRc5+dZaCaP7oaVeOcBjN0zfINRQobt8d6h2Qgvd/YyFkGi0/xKn1zMmKIVLOG6VsYwEAUq8wNOsP4A/jdm4Y0J/1oEZStCkpaGpx85TYi4kq1hWQdyqaVJSPhh4Tk4roIaS2zOYQl+nIpbHqmJ4FJrg1il+TCdjBXobATQ1mKRBUrjD+RDzH/r4ogbd8+UwvvvevpqS2K+/wgT6UD0MzDInv9S29CUQvuFhPoqyJb5XRddHMRE9EEK/2Z8tFN91sDATnqfXHgwnvu00q/nKP5JnijBPzGmx7ydgUViIukklDrlPvo9BbRJz0Vr2vbAvMTrLMLCXqi5CwTm+v+iaOf/YaCziaG2vx0eVASYjpOLCedSgRZBubPM8z4E/HMXhChN7sVDWk=
on:
tags: true
distributions: sdist bdist_wheel
env:
global:
- PIP_DOWNLOAD_CACHE=".pip_download_cache"
before_install:
- pip install codecov
install:
- make develop
script:
- PYFLAKES_NODOCTEST=1 flake8
- coverage run --source=. -m py.test tests
after_success:
- codecov

View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2016 Functional Software, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,3 +0,0 @@
include setup.py package.json webpack.config.js README.rst MANIFEST.in LICENSE AUTHORS
recursive-include sentry_auth_supervisr/templates *
global-exclude *~

View File

@ -1,26 +0,0 @@
.PHONY: clean develop install-tests lint publish test
develop:
pip install "pip>=7"
pip install -e .
make install-tests
install-tests:
pip install .[tests]
lint:
@echo "--> Linting python"
flake8
@echo ""
test:
@echo "--> Running Python tests"
py.test tests || exit 1
@echo ""
publish:
python setup.py sdist bdist_wheel upload
clean:
rm -rf *.egg-info src/*.egg-info
rm -rf dist build

View File

@ -1,55 +0,0 @@
GitHub Auth for Sentry
======================
An SSO provider for Sentry which enables GitHub organization-restricted authentication.
Install
-------
::
$ pip install https://github.com/getsentry/sentry-auth-github/archive/master.zip
Setup
-----
Create a new application under your organization in GitHub. Enter the **Authorization
callback URL** as the prefix to your Sentry installation:
::
https://example.sentry.com
Once done, grab your API keys and drop them in your ``sentry.conf.py``:
.. code-block:: python
GITHUB_APP_ID = ""
GITHUB_API_SECRET = ""
Verified email addresses can optionally be required:
.. code-block:: python
GITHUB_REQUIRE_VERIFIED_EMAIL = True
Optionally you may also specify the domain (for GHE users):
.. code-block:: python
GITHUB_BASE_DOMAIN = "git.example.com"
GITHUB_API_DOMAIN = "api.git.example.com"
If Subdomain isolation is disabled in GHE:
.. code-block:: python
GITHUB_BASE_DOMAIN = "git.example.com"
GITHUB_API_DOMAIN = "git.example.com/api/v3"

View File

@ -1,14 +0,0 @@
from __future__ import absolute_import
# Run tests against sqlite for simplicity
import os
import os.path
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
os.environ.setdefault('DB', 'sqlite')
pytest_plugins = [
'sentry.utils.pytest'
]

View File

@ -1,7 +0,0 @@
from __future__ import absolute_import
from sentry.auth import register
from .provider import PassbookOAuth2Provider
register('passbook', PassbookOAuth2Provider)

View File

@ -1,45 +0,0 @@
from __future__ import absolute_import, print_function
from requests.exceptions import RequestException
from sentry import http
from sentry.utils import json
from .constants import BASE_DOMAIN
class PassbookApiError(Exception):
def __init__(self, message='', status=0):
super(PassbookApiError, self).__init__(message)
self.status = status
class PassbookClient(object):
def __init__(self, client_id, client_secret):
self.client_id = client_id
self.client_secret = client_secret
self.http = http.build_session()
def _request(self, path, access_token):
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
}
headers = {
'Authorization': 'Bearer {0}'.format(access_token),
}
try:
req = self.http.get('https://{0}/{1}'.format(BASE_DOMAIN, path.lstrip('/')),
params=params,
headers=headers,
)
except RequestException as e:
raise PassbookApiError(unicode(e), status=getattr(e, 'status_code', 0))
if req.status_code < 200 or req.status_code >= 300:
raise PassbookApiError(req.content, status=req.status_code)
return json.loads(req.content)
def get_user(self, access_token):
return self._request('/api/v1/openid/', access_token)

View File

@ -1,14 +0,0 @@
from __future__ import absolute_import, print_function
from django.conf import settings
CLIENT_ID = getattr(settings, 'PASSBOOK_APP_ID', None)
CLIENT_SECRET = getattr(settings, 'PASSBOOK_API_SECRET', None)
SCOPE = 'openid:userinfo'
BASE_DOMAIN = getattr(settings, 'PASSBOOK_BASE_DOMAIN', 'id.beryju.org')
ACCESS_TOKEN_URL = 'https://{0}/application/oauth/token/'.format(BASE_DOMAIN)
AUTHORIZE_URL = 'https://{0}/application/oauth/authorize/'.format(BASE_DOMAIN)

View File

@ -1,62 +0,0 @@
from __future__ import absolute_import, print_function
from sentry.auth.exceptions import IdentityNotValid
from sentry.auth.providers.oauth2 import (OAuth2Callback, OAuth2Login,
OAuth2Provider)
from .client import PassbookApiError, PassbookClient
from .constants import (ACCESS_TOKEN_URL, AUTHORIZE_URL, CLIENT_ID,
CLIENT_SECRET, SCOPE)
from .views import FetchUser, PassbookConfigureView
class PassbookOAuth2Provider(OAuth2Provider):
access_token_url = ACCESS_TOKEN_URL
authorize_url = AUTHORIZE_URL
name = 'Passbook'
client_id = CLIENT_ID
client_secret = CLIENT_SECRET
def __init__(self, **config):
super(PassbookOAuth2Provider, self).__init__(**config)
def get_configure_view(self):
return PassbookConfigureView.as_view()
def get_auth_pipeline(self):
return [
OAuth2Login(
authorize_url=self.authorize_url,
client_id=self.client_id,
scope=SCOPE,
),
OAuth2Callback(
access_token_url=self.access_token_url,
client_id=self.client_id,
client_secret=self.client_secret,
),
FetchUser(
client_id=self.client_id,
client_secret=self.client_secret,
),
]
def get_refresh_token_url(self):
return ACCESS_TOKEN_URL
def build_identity(self, state):
data = state['data']
user_data = state['user']
return {
'id': user_data['email'],
'email': user_data['email'],
'name': user_data['name'],
'data': self.get_oauth_data(data),
}
def build_config(self, state):
return {}
def refresh_identity(self, auth_identity):
client = PassbookClient(self.client_id, self.client_secret)
access_token = auth_identity.data['access_token']

View File

@ -1,75 +0,0 @@
from __future__ import absolute_import, print_function
from django import forms
from sentry.auth.view import AuthView, ConfigureView
from sentry.models import AuthIdentity
from .client import PassbookClient
def _get_name_from_email(email):
"""
Given an email return a capitalized name. Ex. john.smith@example.com would return John Smith.
"""
name = email.rsplit('@', 1)[0]
name = ' '.join([n_part.capitalize() for n_part in name.split('.')])
return name
class FetchUser(AuthView):
def __init__(self, client_id, client_secret, *args, **kwargs):
self.client = PassbookClient(client_id, client_secret)
super(FetchUser, self).__init__(*args, **kwargs)
def handle(self, request, helper):
access_token = helper.fetch_state('data')['access_token']
user = self.client.get_user(access_token)
# A user hasn't set their name in their Passbook profile so it isn't
# populated in the response
if not user.get('name'):
user['name'] = _get_name_from_email(user['email'])
helper.bind_state('user', user)
return helper.next_step()
class ConfirmEmailForm(forms.Form):
email = forms.EmailField(label='Email')
class ConfirmEmail(AuthView):
def handle(self, request, helper):
user = helper.fetch_state('user')
# TODO(dcramer): this isnt ideal, but our current flow doesnt really
# support this behavior;
try:
auth_identity = AuthIdentity.objects.select_related('user').get(
auth_provider=helper.auth_provider,
ident=user['id'],
)
except AuthIdentity.DoesNotExist:
pass
else:
user['email'] = auth_identity.user.email
if user.get('email'):
return helper.next_step()
form = ConfirmEmailForm(request.POST or None)
if form.is_valid():
user['email'] = form.cleaned_data['email']
helper.bind_state('user', user)
return helper.next_step()
return self.respond('sentry_auth_passbook/enter-email.html', {
'form': form,
})
class PassbookConfigureView(ConfigureView):
def dispatch(self, request, organization, auth_provider):
return self.render('sentry_auth_passbook/configure.html')

View File

@ -1,12 +0,0 @@
[wheel]
universal = 1
[pytest]
python_files = test*.py
addopts = --tb=native -p no:doctest
norecursedirs = bin dist docs htmlcov script hooks node_modules .* {args}
[flake8]
ignore = F999,E501,E128,E124,E402,W503,E731,C901
max-line-length = 100
exclude = .tox,.git,*/migrations/*,node_modules/*,docs/*

View File

@ -1,45 +0,0 @@
#!/usr/bin/env python
"""
sentry-auth-passbook
==================
:copyright: (c) 2016 Functional Software, Inc
"""
from setuptools import find_packages, setup
install_requires = [
'sentry>=7.0.0',
]
tests_require = [
'mock',
'flake8>=2.0,<2.1',
]
setup(
name='sentry-auth-passbook',
version='0.2.1-beta',
author='BeryJu.org',
author_email='support@beryju.org',
url='https://passbook.beryju.org',
description='passbook authentication provider for Sentry',
long_description=__doc__,
license='MIT',
packages=find_packages(exclude=['tests']),
zip_safe=False,
install_requires=install_requires,
tests_require=tests_require,
extras_require={'tests': tests_require},
include_package_data=True,
entry_points={
'sentry.apps': [
'auth_passbook = sentry_auth_passbook',
],
},
classifiers=[
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'Operating System :: OS Independent',
'Topic :: Software Development'
],
)

View File

@ -1,6 +0,0 @@
from sentry.testutils import TestCase
class GitHubOAuth2ProviderTest(TestCase):
def test_simple(self):
pass

View File

@ -1,17 +0,0 @@
from __future__ import absolute_import, print_function
import pytest
from sentry_auth_sentry.views import _get_name_from_email
expected_data = [
('john.smith@example.com', 'John Smith'),
('john@example.com', 'John'),
('XYZ-234=3523@example.com', 'Xyz-234=3523'),
('XYZ.1111@example.com', 'Xyz 1111'),
('JOHN@example.com', 'John'),
]
@pytest.mark.parametrize("email,expected_name", expected_data)
def test_get_name_from_email(email, expected_name):
assert _get_name_from_email(email) == expected_name

View File

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

View File

@ -1,7 +1,7 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: {{ include "passbook.fullname" . }}-web
name: {{ include "passbook.fullname" . }}-appgw
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }}
@ -18,7 +18,7 @@ spec:
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: web
passbook.io/component: appgw
spec:
volumes:
- name: config-volume
@ -28,8 +28,10 @@ spec:
- name: {{ .Chart.Name }}
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command: ["/bin/sh","-c"]
args: ["./manage.py migrate && ./manage.py web"]
command:
- ./manage.py
args:
- app_gw_web
ports:
- name: http
containerPort: 8000
@ -52,16 +54,9 @@ spec:
- name: Host
value: kubernetes-healthcheck-host
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
requests:
cpu: 150m
memory: 300M
limits:
cpu: 500m
memory: 500M

View File

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

View File

@ -1,6 +1,5 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "passbook.fullname" . -}}
{{- $ingressPath := .Values.ingress.path -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
@ -30,9 +29,22 @@ spec:
- host: {{ . | quote }}
http:
paths:
- path: {{ $ingressPath }}
- path: /
backend:
serviceName: {{ $fullName }}
serviceName: {{ $fullName }}-web
servicePort: http
- path: /static/
backend:
serviceName: {{ $fullname }}-static
servicePort: http
{{- end }}
{{- range .Values.ingress.app_gw_hosts }}
- host: {{ . | quote }}
http:
paths:
- path: /
backend:
serviceName: {{ $fullName }}-appgw
servicePort: http
{{- end }}
{{- end }}

View File

@ -0,0 +1,56 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: {{ include "passbook.fullname" . }}-static
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
selector:
matchLabels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
k8s.passbook.io/component: static
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: '9113'
field.cattle.io/workloadMetrics: '[{"path":"/metrics","port":9113,"schema":"HTTP"}]'
spec:
containers:
- name: {{ .Chart.Name }}-static-prometheus
image: nginx/nginx-prometheus-exporter:0.4.1
imagePullPolicy: IfNotPresent
- name: {{ .Chart.Name }}-static
image: "docker.beryju.org/passbook/static:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
initialDelaySeconds: 10
timeoutSeconds: 5
httpGet:
path: /_/healthz
port: http
readinessProbe:
initialDelaySeconds: 10
timeoutSeconds: 5
httpGet:
path: /_/healthz
port: http
resources:
requests:
cpu: 10m
memory: 10M
limits:
cpu: 20m
memory: 20M

View File

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

View File

@ -0,0 +1,72 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: {{ include "passbook.fullname" . }}-web
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: web
spec:
volumes:
- name: config-volume
configMap:
name: {{ include "passbook.fullname" . }}-config
initContainers:
- name: passbook-database-migrations
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
command:
- ./manage.py
args:
- migrate
volumeMounts:
- mountPath: /etc/passbook
name: config-volume
containers:
- name: {{ .Chart.Name }}
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command:
- ./manage.py
args:
- web
ports:
- name: http
containerPort: 8000
protocol: TCP
volumeMounts:
- mountPath: /etc/passbook
name: config-volume
livenessProbe:
httpGet:
path: /
port: http
httpHeaders:
- name: Host
value: kubernetes-healthcheck-host
readinessProbe:
httpGet:
path: /
port: http
httpHeaders:
- name: Host
value: kubernetes-healthcheck-host
resources:
requests:
cpu: 50m
memory: 150M
limits:
cpu: 200m
memory: 300M

View File

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

View File

@ -28,7 +28,10 @@ spec:
- name: {{ .Chart.Name }}
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command: ["./manage.py", "worker"]
command:
- ./manage.py
args:
- worker
ports:
- name: http
containerPort: 8000
@ -37,16 +40,9 @@ spec:
- mountPath: /etc/passbook
name: config-volume
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
requests:
cpu: 150m
memory: 400M
limits:
cpu: 300m
memory: 600M

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
django-rest-framework
drf_yasg

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
django-revproxy
urllib3[secure]
channels
service_identity
websocket-client
daphne<2.3.0
asgiref~=2.3

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
django-recaptcha

View File

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

View File

@ -2,11 +2,12 @@
from logging import getLogger
from daphne.cli import CommandLineInterface
import cherrypy
from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils import autoreload
from passbook.lib.config import CONFIG
from passbook.root.wsgi import application
LOGGER = getLogger(__name__)
@ -15,16 +16,21 @@ class Command(BaseCommand):
"""Run CherryPy webserver"""
def handle(self, *args, **options):
"""passbook daphne server"""
autoreload.run_with_reloader(self.daphne_server)
def daphne_server(self):
"""Run daphne server within autoreload"""
autoreload.raise_last_exception()
CommandLineInterface().run([
'-p', str(CONFIG.y('web.port', 8000)),
'-b', CONFIG.y('web.listen', '0.0.0.0'), # nosec
'--access-log', '/dev/null',
'--application-close-timeout', '500',
'passbook.root.asgi:application'
])
"""passbook cherrypy server"""
cherrypy.config.update(CONFIG.get('web'))
cherrypy.tree.graft(application, '/')
# Mount NullObject to serve static files
cherrypy.tree.mount(None, settings.STATIC_URL, 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()
for file in CONFIG.loaded_file:
cherrypy.engine.autoreload.files.add(file)
LOGGER.info("Added '%s' to autoreload triggers", file)
cherrypy.engine.block()

66
passbook/core/nginx.conf Normal file
View File

@ -0,0 +1,66 @@
user nginx;
worker_processes 1;
error_log stderr warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format json_combined escape=json
'{'
'"time_local":"$time_local",'
'"remote_addr":"$remote_addr",'
'"remote_user":"$remote_user",'
'"request":"$request",'
'"status": "$status",'
'"body_bytes_sent":"$body_bytes_sent",'
'"request_time":"$request_time",'
'"http_referrer":"$http_referer",'
'"http_user_agent":"$http_user_agent"'
'}';
access_log /dev/stdout json_combined;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
server {
server_name _;
gzip on;
gzip_types application/javascript image/* text/css;
gunzip on;
add_header X-passbook-Version 0.2.7-beta;
add_header Vary X-passbook-Version;
root /static/;
location /_/healthz {
return 204;
}
location ~* \.(jpg|jpeg|png|gif|ico)$ {
expires 30d;
}
location ~* \.(css|js)$ {
expires 7d;
}
}
server {
listen 8080;
location = /stub_status {
stub_status;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
ldap3

View File

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

View File

@ -137,4 +137,6 @@ 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)

View File

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

View File

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

View File

@ -1,2 +0,0 @@
requests_oauthlib>=0.4.2
oauthlib>=2.0.6

View File

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

View File

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

View File

@ -25,8 +25,6 @@ class OAuth2Provider(Provider, AbstractApplication):
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:

View File

@ -1,2 +0,0 @@
django-oauth-toolkit
django-cors-middleware

View File

@ -31,19 +31,10 @@
</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>
</div>

View File

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

View File

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

View File

@ -1,35 +0,0 @@
"""passbook oauth provider OpenID Views"""
from django.http import HttpRequest, JsonResponse
from django.shortcuts import reverse
from django.views.generic import View
class OpenIDConfigurationView(View):
"""Return OpenID Configuration"""
def get_issuer_url(self, request):
"""Get correct issuer URL"""
full_url = request.build_absolute_uri(reverse('passbook_oauth_provider:openid-discovery'))
return full_url.replace(".well-known/openid-configuration", "")
def get(self, request: HttpRequest):
"""Get Response conform to https://openid.net/specs/openid-connect-discovery-1_0.html"""
return JsonResponse({
'issuer': self.get_issuer_url(request),
'authorization_endpoint': request.build_absolute_uri(
reverse('passbook_oauth_provider:oauth2-authorize')),
'token_endpoint': request.build_absolute_uri(reverse('passbook_oauth_provider:token')),
"jwks_uri": request.build_absolute_uri(reverse('passbook_oauth_provider:openid-jwks')),
"scopes_supported": [
"openid",
],
})
class JSONWebKeyView(View):
"""JSON Web Key View"""
def get(self, request: HttpRequest):
"""JSON Webkeys are not implemented yet, hence return an empty object"""
return JsonResponse({})

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.3 on 2019-07-09 14:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_oidc_provider', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='openidprovider',
options={'verbose_name': 'OpenID Provider', 'verbose_name_plural': 'OpenID Providers'},
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
django_otp
qrcode

View File

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

View File

@ -1,15 +0,0 @@
celery
colorlog
django-ipware
django-model-utils
django-redis
django>=2.0
djangorestframework
idna<2.8,>=2.5
markdown
psycopg2
PyYAML
sentry-sdk
pip
whitenoise
urllib3<1.25,>=1.21.1

View File

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

View File

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

View File

@ -1,5 +0,0 @@
beautifulsoup4>=4.6.0
lxml>=3.8.0
signxml
defusedxml
PyCryptodome

View File

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

View File

@ -11,11 +11,21 @@ from passbook.suspicious_policy.models import IPScore, UserScore
LOGGER = getLogger(__name__)
def get_remote_ip(request):
"""Small wrapper of get_client_ip to catch errors"""
try:
remote_ip, _ = get_client_ip(request)
if remote_ip:
return remote_ip
if 'ip' in request:
return request['ip']
except (AttributeError, ValueError):
pass
return '255.255.255.255'
def update_score(request, username, amount):
"""Update score for IP and User"""
remote_ip, _ = get_client_ip(request)
if not remote_ip:
remote_ip = '255.255.255.255'
remote_ip = get_remote_ip(request)
ip_score, _ = IPScore.objects.update_or_create(ip=remote_ip)
ip_score.score += amount
ip_score.save()

View File

@ -1,11 +1,10 @@
-r requirements.txt
-r client-packages/allauth/requirements.txt
coverage
isort
astroid==2.0.4
pylint==2.1.1
pylint-django==2.0.2
prospector
prospector==1.1.5
django-debug-toolbar
pycodestyle<2.4.0,>=2.0.0
bumpversion
@ -14,3 +13,4 @@ autopep8
bandit
bumpversion
twine
grpcio-tools

View File

@ -1,10 +1,52 @@
-r passbook/root/requirements.txt
-r passbook/oauth_client/requirements.txt
-r passbook/ldap/requirements.txt
-r passbook/saml_idp/requirements.txt
-r passbook/otp/requirements.txt
-r passbook/oauth_provider/requirements.txt
-r passbook/captcha_factor/requirements.txt
-r passbook/admin/requirements.txt
-r passbook/api/requirements.txt
-r passbook/app_gw/requirements.txt
# Root requirements
celery
colorlog
cherrypy
django-ipware
django-model-utils
django-redis
django>=2.0
idna<2.8,>=2.5
markdown
psycopg2
PyYAML
sentry-sdk
pip
urllib3<1.25,>=1.21.1
grpcio
grpcio-reflection
protobuf
packaging
# OAuth Client
requests_oauthlib>=0.4.2
oauthlib>=2.0.6
# LDAP Client
ldap3
# SAML IDP
beautifulsoup4>=4.6.0
lxml>=3.8.0
signxml
defusedxml
PyCryptodome
# OTP
django_otp
qrcode
# OAuth Provider
django-oauth-toolkit
django-cors-middleware
# ReCaptcha
django-recaptcha
# API
drf_yasg
djangorestframework==3.9.4
django-filters
# AppGW
django-revproxy
urllib3[secure]
channels
service_identity
websocket-client
daphne<2.3.0
asgiref~=2.3
# OIDC Provider
django-oidc-provider