Compare commits

...

53 Commits

Author SHA1 Message Date
696cd1f247 new release: 0.7.8-beta 2020-01-02 14:03:36 +01:00
b7b3abc462 actions: automatically create release when version/* tag is created, run tests before creating release 2020-01-02 13:49:24 +01:00
575739d07c ci: add bandit for static security checks 2020-01-02 13:41:49 +01:00
2d7e70eebf audit: fix import order 2020-01-02 13:20:41 +01:00
387f3c981f audit: fix error when trying to save models with UUID as PK 2020-01-02 13:12:23 +01:00
865435fb25 actions: fix path to helm chart 2020-01-02 11:38:54 +01:00
b10c5306b9 actions: ensure release gets only executed on release creation 2020-01-02 11:37:46 +01:00
7c706369cd new release: 0.7.7-beta 2020-01-02 11:22:08 +01:00
20dd6355c1 actions: run unittests in final docker images after build 2020-01-02 11:20:32 +01:00
ba8d5d6e27 actions: push both versioned and :latest tags 2020-01-02 11:19:55 +01:00
c448f87027 new release: 0.7.6-beta 2020-01-02 10:34:34 +01:00
2b8c70a61f actions: separate actions files for ci and release 2020-01-02 10:33:04 +01:00
9d7ed9a0ed new release: 0.7.7-beta 2019-12-31 14:02:01 +01:00
ff69b4affe actions: fix build not running correctly 2019-12-31 14:01:58 +01:00
d77afd1ded new release: 0.7.6-beta 2019-12-31 13:47:39 +01:00
c3909f9196 actions: run build only on release 2019-12-31 13:44:27 +01:00
fa55ba5ef0 actions: since actions has no easy way to get tags, hardcode version in ci and bump with bumpversion 2019-12-31 13:40:24 +01:00
766518ee0e audit: sanitize kwargs when creating audit event 2019-12-31 13:33:07 +01:00
74b2b26a20 ci: disable pylint's bad-continuation to please black 2019-12-31 13:17:35 +01:00
4ebbc6f065 gh-actions: fix dependencies on isort 2019-12-31 12:52:15 +01:00
3bd1eadd51 all: implement black as code formatter 2019-12-31 12:51:16 +01:00
8eb3f0f708 ci: upgrade pylint to latest version
core: also upgrade kombu as https://github.com/celery/kombu/issues/1101 is fixed now
2019-12-31 12:45:29 +01:00
31ea2e7139 audit: fix internal server error from passing models 2019-12-31 11:40:03 +01:00
323b4b4a5d actions: fix helm using wrong path for chart 2019-12-30 10:42:46 +01:00
7b8e1bea92 docker: fix old dockerfiles being used, remove all gitlab references 2019-12-30 10:34:31 +01:00
f986dc89ad all: migrate to github 2019-12-30 10:25:35 +01:00
b21fd10093 new release: 0.7.5-beta 2019-12-16 22:05:22 +01:00
6f9c19b142 misc: update bumpversion config 2019-12-16 22:05:16 +01:00
f45643ca87 Merge branch '45-helm-3' into 'master'
Resolve "Upgrade to helm 3 for packaging"

Closes #45

See merge request BeryJu.org/passbook!35
2019-12-16 20:49:34 +00:00
85f8bea784 ci: replace helm with helm3 2019-12-14 14:34:34 +01:00
b428ec5237 providers/oidc: remove duplicate fields 2019-12-14 14:28:36 +01:00
92428529ad docs: add sentry 2019-12-14 14:28:14 +01:00
f6761b5b0b docs: fix harbor site not being included 2019-12-13 15:45:50 +01:00
307b04f4ca docs: add harbor integration, cleanup 2019-12-13 15:36:09 +01:00
6a520a5697 docs: add rancher integration 2019-12-13 13:53:30 +01:00
f22dbba931 providers/saml: add UID field 2019-12-13 13:45:10 +01:00
82cf482fba Merge branch 'docs' into 'master'
Docs

See merge request BeryJu.org/passbook!33
2019-12-12 22:06:20 +00:00
a6afb99edd docs: build docs on new version 2019-12-12 18:13:38 +01:00
ac5f8465b9 docs: add GitLab integration docs 2019-12-12 18:12:14 +01:00
218acb9e38 docs: add providers and sources 2019-12-12 18:00:23 +01:00
927c718fdd docs: add some more info to mkdocs 2019-12-12 09:55:10 +01:00
b7a6d6e739 docs: add docs for property mappings, switch to material theme 2019-12-10 11:25:34 +01:00
0946d6a25d docs: add initial structure, add docs for policies and factors 2019-12-09 21:00:45 +01:00
c1e98e2f0c Merge branch 'master' into docs 2019-12-09 16:49:05 +01:00
807cbbeaaf audit: rewrite to be independent of django http requests, allow custom actions 2019-12-05 16:14:08 +01:00
6c358c4e0a misc: run coverage before other tasks to find bugs easier 2019-12-05 16:03:31 +01:00
74cd0bc08f all(minor): remove old, unused code 2019-12-05 15:07:37 +01:00
b08ec0477e all(minor): replace django-ipware with custom implementation 2019-12-05 14:33:55 +01:00
328c999cb9 ci(minor): reenable prospector 2019-12-05 14:31:51 +01:00
c37e382c15 root(minor): fix incorrect user IP being shown 2019-12-02 18:05:06 +01:00
784dd0fdd6 root(minor): fix unnecessary redirect for prometheus 2019-12-02 18:04:55 +01:00
e6256cb9c8 root(minor): add script to run coverage 2019-12-02 16:43:50 +01:00
4520e3f8b8 deploy(minor): fix wrong health-check for static deployment 2019-11-20 15:55:39 +01:00
367 changed files with 6208 additions and 4597 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.7.4-beta current_version = 0.7.8-beta
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -15,11 +15,11 @@ values =
beta beta
stable stable
[bumpversion:file:helm/passbook/values.yaml] [bumpversion:file:helm/values.yaml]
[bumpversion:file:helm/passbook/Chart.yaml] [bumpversion:file:helm/Chart.yaml]
[bumpversion:file:.gitlab-ci.yml] [bumpversion:file:.github/workflows/release.yml]
[bumpversion:file:passbook/__init__.py] [bumpversion:file:passbook/__init__.py]

147
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,147 @@
name: passbook-ci
on:
- push
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
jobs:
# Linting
pylint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.7'
- uses: actions/cache@v1
with:
path: ~/.local/share/virtualenvs/
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
- name: Install dependencies
run: pip install -U pip pipenv && pipenv install --dev
- name: Lint with pylint
run: pipenv run pylint passbook
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.7'
- uses: actions/cache@v1
with:
path: ~/.local/share/virtualenvs/
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
- name: Install dependencies
run: pip install -U pip pipenv && pipenv install --dev
- name: Lint with black
run: pipenv run black --check passbook
prospector:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.7'
- uses: actions/cache@v1
with:
path: ~/.local/share/virtualenvs/
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
- name: Install dependencies
run: pip install -U pip pipenv && pipenv install --dev
- name: Lint with prospector
run: pipenv run prospector
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.7'
- uses: actions/cache@v1
with:
path: ~/.local/share/virtualenvs/
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
- name: Install dependencies
run: pip install -U pip pipenv && pipenv install --dev
- name: Lint with bandit
run: pipenv run bandit -r passbook
# Actual CI tests
migrations:
needs:
- pylint
- black
- prospector
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
ports:
- 5432:5432
redis:
image: redis:latest
ports:
- 6379:6379
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.7'
- uses: actions/cache@v1
with:
path: ~/.local/share/virtualenvs/
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
- name: Install dependencies
run: pip install -U pip pipenv && pipenv install --dev
- name: Run migrations
run: pipenv run ./manage.py migrate
coverage:
needs:
- pylint
- black
- prospector
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
ports:
- 5432:5432
redis:
image: redis:latest
ports:
- 6379:6379
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.7'
- uses: actions/cache@v1
with:
path: ~/.local/share/virtualenvs/
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
- name: Install dependencies
run: pip install -U pip pipenv && pipenv install --dev
- name: Run coverage
run: pipenv run ./scripts/coverage.sh

70
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,70 @@
name: passbook-release
on:
release:
types:
- created
jobs:
# Build
build-server:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/passbook:0.7.8-beta
-t beryju/passbook:latest
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook:0.7.8-beta
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook:latest
build-static:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
redis:
image: redis:latest
steps:
- uses: actions/checkout@v1
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
run: docker build
--no-cache
--network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:0.7.8-beta
-t beryju/passbook-static:latest
-f static.Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-static:0.7.8-beta
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-static:latest
test-release:
needs:
- build-server
- build-static
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Run test suite in final docker images
run: |
export PASSBOOK_DOMAIN=localhost
docker-compose pull
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"

53
.github/workflows/tag.yml vendored Normal file
View File

@ -0,0 +1,53 @@
on:
push:
tags:
- 'version/*'
name: Create Release from Tag
jobs:
build:
name: Create Release from Tag
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Pre-release test
run: |
export PASSBOOK_DOMAIN=localhost
docker-compose pull
docker build
--no-cache
-t beryju/passbook:latest
-f Dockerfile .
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
- name: Install Helm
run: |
apt update && apt install -y curl
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
- name: Helm package
run: |
helm dependency update helm/
helm package helm/
mv passbook-*.tgz passbook-chart.tgz
- name: Create Release
id: create_release
uses: actions/create-release@v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Create Release from Tag
id: upload-release-asset
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./passbook-chart.tgz
asset_name: passbook-chart.tgz
asset_content_type: application/gzip

2
.gitignore vendored
View File

@ -63,6 +63,7 @@ coverage.xml
*.cover *.cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
unittest.xml
# Translations # Translations
*.mo *.mo
@ -184,7 +185,6 @@ dmypy.json
[Ii]nclude [Ii]nclude
[Ll]ib64 [Ll]ib64
[Ll]ocal [Ll]ocal
[Ss]cripts
pyvenv.cfg pyvenv.cfg
pip-selfcheck.json pip-selfcheck.json

View File

@ -1,164 +0,0 @@
# Global Variables
stages:
- build-base-image
- build-dev-image
- test
- build
- package
- post-release
image: docker.beryju.org/passbook/dev:latest
variables:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
before_script:
- pip install pipenv
# Ensure all dependencies are installed, even those not included in passbook/dev
# According to pipenv docs, -d outputs all packages, however it actually does not
- pipenv lock -r > requirements-all.txt
- pipenv lock -rd >> requirements-all.txt
- pip install -r requirements-all.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/base.Dockerfile --destination docker.beryju.org/passbook/base:latest
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/dev.Dockerfile --destination docker.beryju.org/passbook/dev:latest
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:
- coverage run --concurrency=multiprocessing manage.py test
- coverage combine
- coverage report
stage: test
services:
- postgres:latest
- redis:latest
build-passbook-server:
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 --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.7.4-beta
only:
- tags
- /^version/.*$/
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/static.Dockerfile --destination docker.beryju.org/passbook/static:latest --destination docker.beryju.org/passbook/static:0.7.4-beta
only:
- tags
- /^version/.*$/
# running collectstatic fully initialises django, hence we need that databases
services:
- postgres:latest
- redis:latest
# build-passbook-gatekeeper:
# 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/gatekeeper --dockerfile $CI_PROJECT_DIR/gatekeeper/Dockerfile --destination docker.beryju.org/passbook/gatekeeper:latest --destination docker.beryju.org/passbook/gatekeeper:0.7.4-beta
# only:
# - tags
# - /^version/.*$/
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 update helm/passbook
- helm package helm/passbook
artifacts:
paths:
- passbook-*.tgz
expire_in: 1 week
only:
- tags
- /^version/.*$/
notify-sentry:
image: getsentry/sentry-cli
stage: post-release
variables:
SENTRY_URL: https://sentry.beryju.org
SENTRY_ORG: beryjuorg
SENTRY_PROJECT: passbook
before_script:
- apk add curl
script:
- sentry-cli releases new passbook@0.7.4-beta
- sentry-cli releases set-commits --auto passbook@0.7.4-beta
only:
- tags
- /^version/.*$/

View File

@ -1,6 +1,6 @@
[MASTER] [MASTER]
disable=redefined-outer-name,arguments-differ,no-self-use,cyclic-import,fixme,locally-disabled,unpacking-non-sequence,too-many-ancestors,too-many-branches,too-few-public-methods disable=redefined-outer-name,arguments-differ,no-self-use,cyclic-import,fixme,locally-disabled,unpacking-non-sequence,too-many-ancestors,too-many-branches,too-few-public-methods,import-outside-toplevel,bad-continuation
load-plugins=pylint_django,pylint.extensions.bad_builtin load-plugins=pylint_django,pylint.extensions.bad_builtin
extension-pkg-whitelist=lxml extension-pkg-whitelist=lxml
const-rgx=[a-zA-Z0-9_]{1,40}$ const-rgx=[a-zA-Z0-9_]{1,40}$

View File

@ -1,4 +1,26 @@
FROM docker.beryju.org/passbook/base:latest FROM python:3.7-slim-buster as locker
COPY ./Pipfile /app/
COPY ./Pipfile.lock /app/
WORKDIR /app/
RUN pip install pipenv && \
pipenv lock -r > requirements.txt && \
pipenv lock -rd > requirements-dev.txt
FROM python:3.7-slim-buster
COPY --from=locker /app/requirements.txt /app/
COPY --from=locker /app/requirements-dev.txt /app/
WORKDIR /app/
RUN apt-get update && \
apt-get install -y --no-install-recommends postgresql-client-11 && \
rm -rf /var/lib/apt/ && \
pip install -r requirements.txt --no-cache-dir && \
adduser --system --no-create-home --uid 1000 --group --home /app passbook
COPY ./passbook/ /app/passbook COPY ./passbook/ /app/passbook
COPY ./manage.py /app/ COPY ./manage.py /app/

10
Pipfile
View File

@ -12,7 +12,6 @@ django-cors-middleware = "*"
django-dbbackup = "*" django-dbbackup = "*"
django-filter = "*" django-filter = "*"
django-guardian = "*" django-guardian = "*"
django-ipware = "*"
django-model-utils = "*" django-model-utils = "*"
django-oauth-toolkit = "*" django-oauth-toolkit = "*"
django-oidc-provider = "*" django-oidc-provider = "*"
@ -24,7 +23,7 @@ django-rest-framework = "*"
django-storages = "*" django-storages = "*"
djangorestframework-guardian = "*" djangorestframework-guardian = "*"
drf-yasg = "*" drf-yasg = "*"
kombu = "==4.5.0" kombu = "*"
ldap3 = "*" ldap3 = "*"
lxml = "*" lxml = "*"
oauthlib = "*" oauthlib = "*"
@ -52,8 +51,11 @@ bumpversion = "*"
colorama = "*" colorama = "*"
coverage = "*" coverage = "*"
django-debug-toolbar = "*" django-debug-toolbar = "*"
isort = "*"
prospector = "*" prospector = "*"
pylint = "==2.3.1" pylint = "*"
pylint-django = "*" pylint-django = "*"
unittest-xml-reporting = "*" unittest-xml-reporting = "*"
black = "*"
[pipenv]
allow_prereleases = true

618
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "716683e8e7794821723dcb671c58b3af32c061c52410148e5b5b6c8fc503c935" "sha256": "138816efaba5be0b175cfd5b5e6a0b58e5ba551567f0efb441740344da3986d8"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -46,33 +46,33 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:228cea7e2b3be79e5393719641854d4000826d7a7baebede903a616b505b8e17", "sha256:982823e7c992d27e5954c81db93238ffc42c7a1210d863b4f5e048fdc088040e",
"sha256:ad6d50dd5726a12c6442c23aabec0c7e09ef610834d9fbda010bade6888d7677" "sha256:f05ee90a738c2f1ec8088121030229f26ef6a809fb9a1338de2118fd088dd99a"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.10.13" "version": "==1.10.45"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:33ee13a42ee1cc2391a3cd3ce12c84026db20cc76a5700d94fbe07a136d0c354", "sha256:88ee646f7a0fe6a418681c6f119a590fae23d8439c48c2aec6878f7f89430b1f",
"sha256:d1c6f01486566521b59fd5d4f6ba0adf526ed0d1807a0c0ba6604e982d014f3d" "sha256:f48ba1ef04b25323c1d27fa6399795baa0ca9d316911b87be4d33acda5cef07c"
], ],
"version": "==1.13.13" "version": "==1.13.45"
}, },
"celery": { "celery": {
"hashes": [ "hashes": [
"sha256:4c4532aa683f170f40bd76f928b70bc06ff171a959e06e71bf35f2f9d6031ef9", "sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f",
"sha256:528e56767ae7e43a16cfef24ee1062491f5754368d38fcfffa861cdb9ef219be" "sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.3.0" "version": "==4.4.0"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
], ],
"version": "==2019.9.11" "version": "==2019.11.28"
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
@ -169,19 +169,19 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:16040e1288c6c9f68c6da2fe75ebde83c0a158f6f5d54f4c5177b0c1478c5b86", "sha256:662a1ff78792e3fd77f16f71b1f31149489434de4b62a74895bd5d6534e635a5",
"sha256:89c2007ca4fa5b351a51a279eccff298520783b713bf28efb89dfb81c80ea49b" "sha256:687c37153486cf26c3fdcbdd177ef16de38dc3463f094b5f9c9955d91f277b14"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.2.7" "version": "==2.2.9"
}, },
"django-cors-middleware": { "django-cors-middleware": {
"hashes": [ "hashes": [
"sha256:85904a3401e7bc0c86502ff2b01d726917af3aaa7dafb77799b27ace637e8c92", "sha256:5bbdea85e22909d596e26f6e0dbc174d5521429fa3943ae02a2c6c48e76c88c7",
"sha256:bca8888ed33a94ba5472bde37ed71ec3d08231d6817fd4d799296b016073da95" "sha256:856dbe4d7aae65844ccc68acb49c6da7dbf7cbacaf5bcf37019f4c0c60b3be84"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.4.0" "version": "==1.5.0"
}, },
"django-dbbackup": { "django-dbbackup": {
"hashes": [ "hashes": [
@ -206,20 +206,13 @@
"index": "pypi", "index": "pypi",
"version": "==2.1.0" "version": "==2.1.0"
}, },
"django-ipware": {
"hashes": [
"sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b"
],
"index": "pypi",
"version": "==2.1.0"
},
"django-model-utils": { "django-model-utils": {
"hashes": [ "hashes": [
"sha256:3f130a262e45d73e0950d2be76af4bf4ee86804dd60e5f90afc5cd948fcfe760", "sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c",
"sha256:682f58c1de330cedcda58cc85d5232c5b47a9e2cb67bef4541fb43fdaeb18e96" "sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.2.0" "version": "==4.0.0"
}, },
"django-oauth-toolkit": { "django-oauth-toolkit": {
"hashes": [ "hashes": [
@ -237,19 +230,19 @@
}, },
"django-otp": { "django-otp": {
"hashes": [ "hashes": [
"sha256:0009211222388d8ba4a4840b6de21ff24461fd4aad6c6c194926e3091ac65f06", "sha256:1f16c2b93fe484706ff16ac6f5e64ecc73dd240318c333e0560384ba548d3837",
"sha256:a9d39b35f7aa8eee82d6d9769d8004ec538e7d7c2f5a1c5e5525cda90d0e9b69" "sha256:cd4975539be478417033561e9832a1a69a583189f680e92a649f412c661f90aa"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.7.3" "version": "==0.7.5"
}, },
"django-prometheus": { "django-prometheus": {
"hashes": [ "hashes": [
"sha256:60f331788f9846891e9ea8d7ccd2928b1042e2e99c8d673f97e2b85f5bc20112", "sha256:f0657d4b887309086b71b55f6aa4a95f967b35fe115128b501f95422c423b12c",
"sha256:bb2d4f8acd681fa5787df77e7482391017f0090c70473bccd2aa7cad327800ad" "sha256:f645016ae5270ac2025a70788cd2bd636244a0c5705b323cc086994bf828181e"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.1.0" "version": "==2.0.0.dev124"
}, },
"django-recaptcha": { "django-recaptcha": {
"hashes": [ "hashes": [
@ -261,11 +254,11 @@
}, },
"django-redis": { "django-redis": {
"hashes": [ "hashes": [
"sha256:af0b393864e91228dd30d8c85b5c44d670b5524cb161b7f9e41acc98b6e5ace7", "sha256:a5b1e3ffd3198735e6c529d9bdf38ca3fcb3155515249b98dc4d966b8ddf9d2b",
"sha256:f46115577063d00a890867c6964ba096057f07cb756e78e0503b89cd18e4e083" "sha256:e1aad4cc5bd743d8d0b13d5cae0cef5410eaace33e83bff5fc3a139ad8db50b4"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.10.0" "version": "==4.11.0"
}, },
"django-rest-framework": { "django-rest-framework": {
"hashes": [ "hashes": [
@ -276,18 +269,18 @@
}, },
"django-storages": { "django-storages": {
"hashes": [ "hashes": [
"sha256:87287b7ad2e789cd603373439994e1ac6f94d9dc2e5f8173d2a87aa3ed458bd9", "sha256:0a9b7e620e969fb0797523695329ed223bf540bbfdf6cd163b061fc11dab2d1c",
"sha256:f3b3def96493d3ccde37b864cea376472baf6e8a596504b209278801c510b807" "sha256:9322ab74ba6371e2e0fccc350c741686ade829e43085597b26b07ae8955a0a00"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.7.2" "version": "==1.8"
}, },
"djangorestframework": { "djangorestframework": {
"hashes": [ "hashes": [
"sha256:5488aed8f8df5ec1d70f04b2114abc52ae6729748a176c453313834a9ee179c8", "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4",
"sha256:dc81cbf9775c6898a580f6f1f387c4777d12bd87abf0f5406018d32ccae71090" "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f"
], ],
"version": "==3.10.3" "version": "==3.11.0"
}, },
"djangorestframework-guardian": { "djangorestframework-guardian": {
"hashes": [ "hashes": [
@ -315,16 +308,16 @@
}, },
"eight": { "eight": {
"hashes": [ "hashes": [
"sha256:b3ceecbfeb58fe68f726a69ac4225bbff554f5436c274646b75c63b039626c9e", "sha256:0a7f0e7725f2a478a97676cf9c49266d95f922f8ed621ec314eeccb333927dc2",
"sha256:eebadb79193c9a3ed95a74f59462267f05ca41c23a804f9f0dd80e597c9a9f8e" "sha256:d148aa1fac6cafb5ff806ff634914b05e3f9357aa8dbd82cd7908821d7f93f43"
], ],
"version": "==0.4.2" "version": "==1.0.0"
}, },
"future": { "future": {
"hashes": [ "hashes": [
"sha256:e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb" "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
], ],
"version": "==0.16.0" "version": "==0.18.2"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
@ -335,10 +328,11 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", "sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" "sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
], ],
"version": "==0.23" "markers": "python_version < '3.8'",
"version": "==1.3.0"
}, },
"inflection": { "inflection": {
"hashes": [ "hashes": [
@ -368,18 +362,18 @@
}, },
"jsonschema": { "jsonschema": {
"hashes": [ "hashes": [
"sha256:2fa0684276b6333ff3c0b1b27081f4b2305f0a36cf702a23db50edb141893c3f", "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163",
"sha256:94c0a13b4a0616458b42529091624e66700a17f847453e52279e35509a5b7631" "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"
], ],
"version": "==3.1.1" "version": "==3.2.0"
}, },
"kombu": { "kombu": {
"hashes": [ "hashes": [
"sha256:389ba09e03b15b55b1a7371a441c894fd8121d174f5583bbbca032b9ea8c9edd", "sha256:2a9e7adff14d046c9996752b2c48b6d9185d0b992106d5160e1a179907a5d4ac",
"sha256:7b92303af381ef02fad6899fd5f5a9a96031d781356cd8e505fa54ae5ddee181" "sha256:67b32ccb6fea030f8799f8fd50dd08e03a4b99464ebc4952d71d8747b1a52ad1"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.5.0" "version": "==4.6.7"
}, },
"ldap3": { "ldap3": {
"hashes": [ "hashes": [
@ -391,35 +385,35 @@
}, },
"lxml": { "lxml": {
"hashes": [ "hashes": [
"sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4", "sha256:00ac0d64949fef6b3693813fe636a2d56d97a5a49b5bbb86e4cc4cc50ebc9ea2",
"sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc", "sha256:0571e607558665ed42e450d7bf0e2941d542c18e117b1ebbf0ba72f287ad841c",
"sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1", "sha256:0e3f04a7615fdac0be5e18b2406529521d6dbdb0167d2a690ee328bef7807487",
"sha256:1409b14bf83a7d729f92e2a7fbfe7ec929d4883ca071b06e95c539ceedb6497c", "sha256:13cf89be53348d1c17b453867da68704802966c433b2bb4fa1f970daadd2ef70",
"sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046", "sha256:217262fcf6a4c2e1c7cb1efa08bd9ebc432502abc6c255c4abab611e8be0d14d",
"sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36", "sha256:223e544828f1955daaf4cefbb4853bc416b2ec3fd56d4f4204a8b17007c21250",
"sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5", "sha256:277cb61fede2f95b9c61912fefb3d43fbd5f18bf18a14fae4911b67984486f5d",
"sha256:2e8f77db25b0a96af679e64ff9bf9dddb27d379c9900c3272f3041c4d1327c9d", "sha256:3213f753e8ae86c396e0e066866e64c6b04618e85c723b32ecb0909885211f74",
"sha256:4dffd405390a45ecb95ab5ab1c1b847553c18b0ef8ed01e10c1c8b1a76452916", "sha256:4690984a4dee1033da0af6df0b7a6bde83f74e1c0c870623797cec77964de34d",
"sha256:6b899931a5648862c7b88c795eddff7588fb585e81cecce20f8d9da16eff96e0", "sha256:4fcc472ef87f45c429d3b923b925704aa581f875d65bac80f8ab0c3296a63f78",
"sha256:726c17f3e0d7a7200718c9a890ccfeab391c9133e363a577a44717c85c71db27", "sha256:61409bd745a265a742f2693e4600e4dbd45cc1daebe1d5fad6fcb22912d44145",
"sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc", "sha256:678f1963f755c5d9f5f6968dded7b245dd1ece8cf53c1aa9d80e6734a8c7f41d",
"sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7", "sha256:6c6d03549d4e2734133badb9ab1c05d9f0ef4bcd31d83e5d2b4747c85cfa21da",
"sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38", "sha256:6e74d5f4d6ecd6942375c52ffcd35f4318a61a02328f6f1bd79fcb4ffedf969e",
"sha256:9277562f175d2334744ad297568677056861070399cec56ff06abbe2564d1232", "sha256:7b4fc7b1ecc987ca7aaf3f4f0e71bbfbd81aaabf87002558f5bc95da3a865bcd",
"sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5", "sha256:7ed386a40e172ddf44c061ad74881d8622f791d9af0b6f5be20023029129bc85",
"sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832", "sha256:8f54f0924d12c47a382c600c880770b5ebfc96c9fd94cf6f6bdc21caf6163ea7",
"sha256:ae88588d687bd476be588010cbbe551e9c2872b816f2da8f01f6f1fda74e1ef0", "sha256:ad9b81351fdc236bda538efa6879315448411a81186c836d4b80d6ca8217cdb9",
"sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a", "sha256:bbd00e21ea17f7bcc58dccd13869d68441b32899e89cf6cfa90d624a9198ce85",
"sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f", "sha256:c3c289762cc09735e2a8f8a49571d0e8b4f57ea831ea11558247b5bdea0ac4db",
"sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9", "sha256:cf4650942de5e5685ad308e22bcafbccfe37c54aa7c0e30cd620c2ee5c93d336",
"sha256:c7fccd08b14aa437fe096c71c645c0f9be0655a9b1a4b7cffc77bcb23b3d61d2", "sha256:cfcbc33c9c59c93776aa41ab02e55c288a042211708b72fdb518221cc803abc8",
"sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692", "sha256:e301055deadfedbd80cf94f2f65ff23126b232b0d1fea28f332ce58137bcdb18",
"sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84", "sha256:ebbfe24df7f7b5c6c7620702496b6419f6a9aa2fd7f005eb731cc80d7b4692b9",
"sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79", "sha256:eff69ddbf3ad86375c344339371168640951c302450c5d3e9936e98d6459db06",
"sha256:fe489d486cd00b739be826e8c1be188ddb74c7a1ca784d93d06fda882a6a1681" "sha256:f6ed60a62c5f1c44e789d2cf14009423cb1646b44a43e40a9cf6a21f077678a1"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.4.1" "version": "==4.4.2"
}, },
"markupsafe": { "markupsafe": {
"hashes": [ "hashes": [
@ -456,10 +450,10 @@
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
], ],
"version": "==7.2.0" "version": "==8.0.2"
}, },
"oauthlib": { "oauthlib": {
"hashes": [ "hashes": [
@ -507,11 +501,13 @@
"sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e", "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e",
"sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103", "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103",
"sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6", "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6",
"sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1",
"sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9", "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9",
"sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e", "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e",
"sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f", "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f",
"sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd", "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd",
"sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8", "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8",
"sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f",
"sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4", "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4",
"sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964", "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964",
"sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08" "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08"
@ -521,10 +517,10 @@
}, },
"pyasn1": { "pyasn1": {
"hashes": [ "hashes": [
"sha256:62cdade8b5530f0b185e09855dd422bc05c0bbff6b72ff61381c09dac7befd8c", "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:a9495356ca1d66ed197a0f72b41eb1823cf7ea8b5bd07191673e8147aecf8604" "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
], ],
"version": "==0.4.7" "version": "==0.4.8"
}, },
"pyasn1-modules": { "pyasn1-modules": {
"hashes": [ "hashes": [
@ -541,78 +537,78 @@
}, },
"pycryptodome": { "pycryptodome": {
"hashes": [ "hashes": [
"sha256:0aa49f3fa110f8dc090bad1671a768cc17d3d3bd01566641ffc0d10d0fec8d49", "sha256:042ae873baadd0c33b4d699a5c5b976ade3233a979d972f98ca82314632d868c",
"sha256:0fafd3c4fb76c6992f34bf2d074f582f388e3b8062b8ba5d65b020634cc221e6", "sha256:0502876279772b1384b660ccc91563d04490d562799d8e2e06b411e2d81128a9",
"sha256:17eb9bd5d30a71b0c8a832e3e9cd2b7723f99907c38dc5dd23e59e8c368a70e2", "sha256:2de33ed0a95855735d5a0fc0c39603314df9e78ee8bbf0baa9692fb46b3b8bbb",
"sha256:2776255d5c748782f095ec422d42da2eadd8392ac9de7da23db4aed4231272bd", "sha256:319e568baf86620b419d53063b18c216abf924875966efdfe06891b987196a45",
"sha256:3500826dc3b9a8fdb762bebe551106081a6bdecd4181a3d1bd0206e48bba8974", "sha256:4372ec7518727172e1605c0843cdc5375d4771e447b8148c787b860260aae151",
"sha256:3aa0d30326dcdef24c632d5c03b8e4d379c6ae0645082b27dd69ea816bb97ecb", "sha256:48821950ffb9c836858d8fa09d7840b6df52eadd387a3c5acece55cb387743f9",
"sha256:3c7769bdadcc4809508e71997008912cc6d94fd7b5b1f3ef121683ebcac71d81", "sha256:4b9533d4166ca07abdd49ce9d516666b1df944997fe135d4b21ac376aa624aff",
"sha256:3e8c97a38dac6dafd180b4696a522b1581dd1a8e0ea60763458be547bac97361", "sha256:54456cf85130e01674d21fb1ab89ffccacb138a8ade88d72fa2b0ac898d2798b",
"sha256:5aca5125a46e458b308b5571ce8fe36d2229f161aa7db27b3ecacded70c6aa8b", "sha256:56fdd0e425f1b8fd3a00b6d96351f86226674974814c50534864d0124d48871f",
"sha256:62beb75f0688f406946312bfef8923d8ab23f5b8013acded931413625299d317", "sha256:57b1b707363490c495ad0eeb38bd1b0e1697c497af25fad78d3a1ebf0477fd5b",
"sha256:7725643de3c884a9945a086670787dce637037f32c5c2df7fd602bd5967f3486", "sha256:5c485ed6e9718ebcaa81138fa70ace9c563d202b56a8cee119b4085b023931f5",
"sha256:872191a02a0c2a3b98dc75c62b32912b220a8ae5ff6ac9e39868f903f55dd6a4", "sha256:63c103a22cbe9752f6ea9f1a0de129995bad91c4d03a66c67cffcf6ee0c9f1e1",
"sha256:8c501e80960d12328d49e1d409daf426f29364a37c602f257c99509999654650", "sha256:68fab8455efcbfe87c5d75015476f9b606227ffe244d57bfd66269451706e899",
"sha256:9512638bfef8ffc94c62751965a4733c3792104dc84771ba54ce0f80f49134df", "sha256:6c2720696b10ae356040e888bde1239b8957fe18885ccf5e7b4e8dec882f0856",
"sha256:962043051afa7a5ab071b0d8996dc00e564327a18566d3e574a39cb6e097b462", "sha256:72166c2ac520a5dbd2d90208b9c279161ec0861662a621892bd52fb6ca13ab91",
"sha256:9db72b18b30902a83fa57b0d7dae4ce24f85186695e3bea0d423f1ec7c5b3fbe", "sha256:7c52308ac5b834331b2f107a490b2c27de024a229b61df4cdc5c131d563dfe98",
"sha256:9ffd4f0bfb5949dfa0e5cedef836364f18da0deb2fba04671607fb3b59b29112", "sha256:87d8d85b4792ca5e730fb7a519fbc3ed976c59dcf79c5204589c59afd56b9926",
"sha256:a26819f693cf5fc0a2373a3e4b91c38e359cad9f00020a885b667c77f28738d5", "sha256:896e9b6fd0762aa07b203c993fbbee7a1f1a4674c6886afd7bfa86f3d1be98a8",
"sha256:a3efc575a53511c48361d933e12e07c2eb940db1afda0995285176c372ab7352", "sha256:8a799bea3c6617736e914a2e77c409f52893d382f619f088f8a80e2e21f573c1",
"sha256:ababd6685b9d94729a851a0615482156afdacbeaabeea60f67961db0e975b1af", "sha256:9d9945ac8375d5d8e60bd2a2e1df5882eaa315522eedf3ca868b1546dfa34eba",
"sha256:b0e9c8c270cd3f8c73b53139f0708f257189a00bbc898be6d3f03995e5f7edc2", "sha256:9ef966c727de942de3e41aa8462c4b7b4bca70f19af5a3f99e31376589c11aac",
"sha256:b74173b13c221ee96b608212b9adc2c459a73d3632f04490df42e4f07e7041e6", "sha256:a168e73879619b467072509a223282a02c8047d932a48b74fbd498f27224aa04",
"sha256:bed297f75ba19cefe2d10beb4959f4f8cb62c2560a3998ad87479485098ee939", "sha256:a30f501bbb32e01a49ef9e09ca1260e5ab49bf33a257080ec553e08997acc487",
"sha256:c639f09e8ce8ad5af9884233f952ade4b73a11b7d41d3b9bb7d4e64d9e1df164", "sha256:a8ca2450394d3699c9f15ef25e8de9a24b401933716a1e39d37fa01f5fe3c58b",
"sha256:c7bc308be67288af1cd44668d59e36356f0ce518337899079ddb0235bd55db79", "sha256:aec4d42deb836b8fb3ba32f2ba1ef0d33dd3dc9d430b1479ee7a914490d15b5e",
"sha256:cca152dcebc318833ba70499190ce17ee81b525404e2a7548c77f52b439306a7", "sha256:b4af098f2a50f8d048ab12cabb59456585c0acf43d90ee79782d2d6d0ed59dba",
"sha256:d5261d22bc3a54db26f11dabcda14bbaab72080977e083d795b4b1d1b510c774", "sha256:b55c60c321ac91945c60a40ac9896ac7a3d432bb3e8c14006dfd82ad5871c331",
"sha256:d81111e3da7fc9eee825ba7d8a68b3c1464f41110ef98a7280e0c7fb82c91e73", "sha256:c53348358408d94869059e16fba5ff3bef8c52c25b18421472aba272b9bb450f",
"sha256:d95fafa899abb9f82e55ff43f423e100784312b43932514f2c05d41cbb20323e", "sha256:cbfd97f9e060f0d30245cd29fa267a9a84de9da97559366fca0a3f7655acc63f",
"sha256:de411a64d4105d4424441833bd25943208e58c846abf981bba5bbeeba88a49c3", "sha256:d3fe3f33ad52bf0c19ee6344b695ba44ffbfa16f3c29ca61116b48d97bd970fb",
"sha256:e02c7b3d05b88ff1a236e49a252b2bf8444d3a1d04a056784af766c0909eba36", "sha256:e3a79a30d15d9c7c284a7734036ee8abdb5ca3a6f5774d293cdc9e1358c1dc10",
"sha256:fbafe9b01b717e0bfbc83cd740ff5bf5cdd3f208815be470ea203942b899bbdf" "sha256:eec0689509389f19875f66ae8dedd59f982240cdab31b9f78a8dc266011df93a"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.9.1" "version": "==3.9.4"
}, },
"pycryptodomex": { "pycryptodomex": {
"hashes": [ "hashes": [
"sha256:0713fc29cddb14f977887ccf3199d1a00d0b040e8c35785df20d107ad59efabc", "sha256:0943b65fb41b7403a9def6214061fdd9ab9afd0bbc581e553c72eebe60bded36",
"sha256:110651378be063d5e0e653d107a14b511bd45c355968a32270f5b1bf8c093056", "sha256:0a1dbb5c4d975a4ea568fb7686550aa225d94023191fb0cca8747dc5b5d77857",
"sha256:158428c0f337984cb3611484d9f61faea973aec624c8f88c5809ab88adab0884", "sha256:0f43f1608518347fdcb9c8f443fa5cabedd33f94188b13e4196a3a7ba90d169c",
"sha256:17625d9f9442d3567b2532795c9232ed80cc1d6c91064ad48c802f3bff2b937d", "sha256:11ce5fec5990e34e3981ed14897ba601c83957b577d77d395f1f8f878a179f98",
"sha256:179125d0b2bcbf5cf9ddf9fb74fe13e30d19fb1c2691cac43b8b37d74df9ddf6", "sha256:17a09e38fdc91e4857cf5a7ce82f3c0b229c3977490f2146513e366923fc256b",
"sha256:227e660ee3835284fc6195163c467f8d21a1de51d0aa85d32157a1fc4bf16b9a", "sha256:22d970cee5c096b9123415e183ae03702b2cd4d3ba3f0ced25c4e1aba3967167",
"sha256:2f651173c4bb8de6a96493e5cc03b2838eedf4bb1cbfbe2b354e40a2f2f245fc", "sha256:2a1793efcbae3a2264c5e0e492a2629eb10d895d6e5f17dbbd00eb8b489c6bda",
"sha256:33b0e5c9ca02c099ec537138e8ffee1e4d054e49d69258062d89ddbd9f660000", "sha256:30a8a148a0fe482cec1aaf942bbd0ade56ec197c14fe058b2a94318c57e1f991",
"sha256:42bead6e7dbca9328a6601ff41d25554606847d92b0fd198ca3f6c971c662c07", "sha256:32fbbaf964c5184d3f3e349085b0536dd28184b02e2b014fc900f58bbc126339",
"sha256:478cce6245e8ff8cda8f733ef1a1161ee6bf5aaa45312e1ace6c7b80fbc1e01f", "sha256:347d67faee36d449dc9632da411cc318df52959079062627f1243001b10dc227",
"sha256:4ce38cb16b6f41c4b579e3e9a9d66c36ba24192cc0518ce09313c25ae44d2d74", "sha256:45f4b4e5461a041518baabc52340c249b60833aa84cea6377dc8016a2b33c666",
"sha256:4e0bc594c61bd1db86c0060a5eb351c22a6c4c154315a52af1c8cd24c4e6a8a3", "sha256:4717daec0035034b002d31c42e55431c970e3e38a78211f43990e1b7eaf19e28",
"sha256:4e1e616d12f79f256109de14aebcee1bf7e0a78d00b3de6c9a0cf2eb2a80785c", "sha256:51a1ac9e7dda81da444fed8be558a60ec88dfc73b2aa4b0efa310e87acb75838",
"sha256:5e4b459ccd6bfe55cc6b030b8983040bc8956f5757b621ae32dd0a26b0f85a91", "sha256:53e9dcc8f14783f6300b70da325a50ac1b0a3dbaee323bd9dc3f71d409c197a1",
"sha256:61a586b0cb85bc8c60af4ddcae24928a3476c944cb37eb7b9066965bc1d4b4d8", "sha256:5519a2ed776e193688b7ddb61ab709303f6eb7d1237081e298283c72acc44271",
"sha256:643bea8898e875e54177c546f2ac704317937230379a9d295ece844c79e00cdb", "sha256:583450e8e80a0885c453211ed2bd69ceea634d8c904f23ff8687f677fe810e95",
"sha256:7403d7addaaa4649777ce487832ef8421222960a10d7a95b0f2c9efd217a93e6", "sha256:60f862bd2a07133585a4fc2ce2b1a8ec24746b07ac44307d22ef2b767cb03435",
"sha256:7f36378a699f201aea3e431a3c217c16e63abbe84ddb8d9bd0af9b28e3f826aa", "sha256:612091f1d3c84e723bec7cb855cf77576e646045744794c9a3f75ba80737762f",
"sha256:9146a6cf9eeb4683cfffabc7093fd1063076185d790680596f7a2dfb40f6b4b9", "sha256:629a87b87c8203b8789ccefc7f2f2faecd2daaeb56bdd0b4e44cd89565f2db07",
"sha256:9829d8aa2fb52646eda9041b785e9c6825fc1f1054f2254046fb7628800acb8e", "sha256:6e56ec4c8938fb388b6f250ddd5e21c15e8f25a76e0ad0e2abae9afee09e67b4",
"sha256:aa18ad3da8da74cbd119a6c5460079c7357ba8775b2edbc5a78722fc1e52f881", "sha256:8e8092651844a11ec7fa534395f3dfe99256ce4edca06f128efc9d770d6e1dc1",
"sha256:ad39a8d3be6c5aad42b1ef839c49a50185618b26d5f1b555b1edd4d9d700e3b9", "sha256:8f5f260629876603e08f3ce95c8ccd9b6b83bf9a921c41409046796267f7adc5",
"sha256:b10bb3c640d7666993d5b0aec0e5334131386eddbd200aabcc123fe07c2b8928", "sha256:9a6b74f38613f54c56bd759b411a352258f47489bbefd1d57c930a291498b35b",
"sha256:b17b2f5f65dffdeddf06bb82eb73a6aa55766322c3c45bc5032f9e3259adfdb0", "sha256:a5a13ebb52c4cd065fb673d8c94f39f30823428a4de19e1f3f828b63a8882d1e",
"sha256:ba5bce9e1fc21160c27015a705e80f49901f1c42aa8bf96ed1d650ce4b5311bd", "sha256:a77ca778a476829876a3a70ae880073379160e4a465d057e3c4e1c79acdf1b8a",
"sha256:c2b867277ef5a996b2198bec149abaeaeddbe57a77a4f6840882be382af72297", "sha256:a9f7be3d19f79429c2118fd61bc2ec4fa095e93b56fb3a5f3009822402c4380f",
"sha256:c43d5d7516b0dc8436aef6bf9ebb9fbeaebcbbc4cb1b6a23be4a5f843c2614e3", "sha256:dc15a467c4f9e4b43748ba2f97aea66f67812bfd581818284c47cadc81d4caec",
"sha256:ce65a7dc9162a6e676f336e45f6602297981afa82f8e7ccc690667316c6b449b", "sha256:e13cdeea23059f7577c230fd580d2c8178e67ebe10e360041abe86c33c316f1c",
"sha256:d9a38c3a85dd3dc6cae43eac94b73485fd7e5a1daf74bb510d7220a8b18482d2", "sha256:e45b85c8521bca6bdfaf57e4987743ade53e9f03529dd3adbc9524094c6d55c4",
"sha256:de39d7c456147755e5610177bd50cb7c89f74477d608b5ac055fed4e7c4c35c1", "sha256:e87f17867b260f57c88487f943eb4d46c90532652bb37046e764842c3b66cbb1",
"sha256:ea368b7b4f36c5524d7b47aa583db604085958b92ff6580075230c8d7c88cdbe", "sha256:ee40a5b156f6c1192bc3082e9d73d0479904433cdda83110546cd67f5a15a5be",
"sha256:ff75fa26b7f8e1eaeba9edfc50b1d21bca913e743ce993a189b07bf483bedda0" "sha256:ef63ffde3b267043579af8830fc97fc3b9b8a526a24e3ba23af9989d4e9e689a"
], ],
"version": "==3.9.1" "version": "==3.9.4"
}, },
"pyjwkest": { "pyjwkest": {
"hashes": [ "hashes": [
@ -622,31 +618,31 @@
}, },
"pyopenssl": { "pyopenssl": {
"hashes": [ "hashes": [
"sha256:26ff56a6b5ecaf3a2a59f132681e2a80afcc76b4f902f612f518f92c2a1bf854", "sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504",
"sha256:6488f1423b00f73b7ad5167885312bb0ce410d3312eb212393795b53c8caa580" "sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507"
], ],
"version": "==18.0.0" "version": "==19.1.0"
}, },
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
"sha256:4acadc9a2b96c19fe00932a38ca63e601180c39a189a696abce1eaab641447e1", "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
"sha256:61b5ed888beab19ddccab3478910e2076a6b5a0295dffc43021890e136edf764" "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
], ],
"version": "==2.4.4" "version": "==2.4.6"
}, },
"pyrsistent": { "pyrsistent": {
"hashes": [ "hashes": [
"sha256:eb6545dbeb1aa69ab1fb4809bfbf5a8705e44d92ef8fc7c2361682a47c46c778" "sha256:f3b280d030afb652f79d67c5586157c5c1355c9a58dfc7940566e28d28f3df1b"
], ],
"version": "==0.15.5" "version": "==0.15.6"
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
], ],
"markers": "python_version >= '2.7'", "markers": "python_version >= '2.7'",
"version": "==2.8.0" "version": "==2.8.1"
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
@ -689,22 +685,20 @@
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "sha256:21a8e19e2007a4047ffabbd8f0ee32c0dabae3b7f4b6c645110ae53e7714b470",
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "sha256:74ad685bfb065f4bdd36d24aa97092f04bcbb1179b5ffdd3d5f994023fb8c292",
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "sha256:79c3ba1da22e61c2a71aaa382c57518ab492278c8974c40187b900b50f3e0282",
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "sha256:94ad913ab3fd967d14ecffda8182d7d0e1f7dd919b352773c492ec51890d3224",
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "sha256:998db501e3a627c3e5678d6505f0e182d1529545df289db036cdc717f35d8058",
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "sha256:9b69d4645bff5820713e8912bc61c4277dc127a6f8c197b52b6436503c42600f",
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "sha256:9da13b536533518343a04f3c6564782ec8a13c705310b26b4832d77fa4d92a47",
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "sha256:a76159f13b47fb44fb2acac8fef798a1940dd31b4acec6f4560bd11b2d92d31b",
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "sha256:a9e9175c1e47a089a2b45d9e2afc6aae1f1f725538c32eec761894a42ba1227f",
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "sha256:ea51ce7b96646ecd3bb12c2702e570c2bd7dd4d9f146db7fa83c5008ede35f66",
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "sha256:ffbaaa05de60fc444eda3f6300d1af27d965b09b67f1fb4ebcc88dd0fb4ab1b4"
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.1.2" "version": "==5.3b1"
}, },
"qrcode": { "qrcode": {
"hashes": [ "hashes": [
@ -758,6 +752,7 @@
"sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5", "sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5",
"sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070", "sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070",
"sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c", "sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c",
"sha256:be018933c2f4ee7de55e7bd7d0d801b3dfb09d21dad0cce8a97995fd3e44be30",
"sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947", "sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947",
"sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc", "sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc",
"sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973", "sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973",
@ -776,11 +771,11 @@
}, },
"sentry-sdk": { "sentry-sdk": {
"hashes": [ "hashes": [
"sha256:09e1e8f00f22ea580348f83bbbd880adf40b29f1dec494a8e4b33e22f77184fb", "sha256:05285942901d38c7ce2498aba50d8e87b361fc603281a5902dda98f3f8c5e145",
"sha256:ff1fa7fb85703ae9414c8b427ee73f8363232767c9cd19158f08f6e4f0b58fc7" "sha256:c6b919623e488134a728f16326c6f0bcdab7e3f59e7f4c472a90eea4d6d8fe82"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.13.2" "version": "==0.13.5"
}, },
"service-identity": { "service-identity": {
"hashes": [ "hashes": [
@ -792,11 +787,11 @@
}, },
"signxml": { "signxml": {
"hashes": [ "hashes": [
"sha256:70e3edbb07b89bec94d39db2cdced724c540f9258366474177c746b9f903d9c4", "sha256:2e186c117284fe5a0c543f5bcdde68f5a2341eeae219af9eb7e512dacf4bfce7",
"sha256:9540efcddd94e45399fa26ee2d24af43d162d55cbe3a2b36fddb394741993dd5" "sha256:7d6af724542cae915bbb9000d333a52ce495d0b3cdcb4dc590c3c4a149b079ed"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.6.0" "version": "==2.7.2"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@ -830,23 +825,22 @@
}, },
"uritemplate": { "uritemplate": {
"hashes": [ "hashes": [
"sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd", "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
"sha256:1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd", "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
"sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d"
], ],
"version": "==3.0.0" "version": "==3.0.1"
}, },
"urllib3": { "urllib3": {
"extras": [ "extras": [
"secure" "secure"
], ],
"hashes": [ "hashes": [
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
], ],
"index": "pypi", "index": "pypi",
"markers": null, "markers": null,
"version": "==1.25.6" "version": "==1.25.7"
}, },
"vine": { "vine": {
"hashes": [ "hashes": [
@ -864,12 +858,33 @@
} }
}, },
"develop": { "develop": {
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"asgiref": {
"hashes": [
"sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
"sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5"
],
"version": "==3.2.3"
},
"astroid": { "astroid": {
"hashes": [ "hashes": [
"sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a",
"sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4" "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"
], ],
"version": "==2.2.5" "version": "==2.3.3"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
}, },
"autopep8": { "autopep8": {
"hashes": [ "hashes": [
@ -886,6 +901,14 @@
"index": "pypi", "index": "pypi",
"version": "==1.6.2" "version": "==1.6.2"
}, },
"black": {
"hashes": [
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
],
"index": "pypi",
"version": "==19.10b0"
},
"bumpversion": { "bumpversion": {
"hashes": [ "hashes": [
"sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e", "sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e",
@ -894,67 +917,73 @@
"index": "pypi", "index": "pypi",
"version": "==0.5.3" "version": "==0.5.3"
}, },
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"colorama": { "colorama": {
"hashes": [ "hashes": [
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.4.1" "version": "==0.4.3"
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "sha256:0101888bd1592a20ccadae081ba10e8b204d20235d18d05c6f7d5e904a38fc10",
"sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "sha256:04b961862334687549eb91cd5178a6fbe977ad365bddc7c60f2227f2f9880cf4",
"sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "sha256:1ca43dbd739c0fc30b0a3637a003a0d2c7edc1dd618359d58cc1e211742f8bd1",
"sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "sha256:1cbb88b34187bdb841f2599770b7e6ff8e259dc3bb64fc7893acf44998acf5f8",
"sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "sha256:232f0b52a5b978288f0bbc282a6c03fe48cd19a04202df44309919c142b3bb9c",
"sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "sha256:24bcfa86fd9ce86b73a8368383c39d919c497a06eebb888b6f0c12f13e920b1a",
"sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "sha256:25b8f60b5c7da71e64c18888f3067d5b6f1334b9681876b2fb41eea26de881ae",
"sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "sha256:2714160a63da18aed9340c70ed514973971ee7e665e6b336917ff4cca81a25b1",
"sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "sha256:2ca2cd5264e84b2cafc73f0045437f70c6378c0d7dbcddc9ee3fe192c1e29e5d",
"sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "sha256:2cc707fc9aad2592fc686d63ef72dc0031fc98b6fb921d2f5395d9ab84fbc3ef",
"sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "sha256:348630edea485f4228233c2f310a598abf8afa5f8c716c02a9698089687b6085",
"sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "sha256:40fbfd6b044c9db13aeec1daf5887d322c710d811f944011757526ef6e323fd9",
"sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "sha256:46c9c6a1d1190c0b75ec7c0f339088309952b82ae8d67a79ff1319eb4e749b96",
"sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "sha256:591506e088901bdc25620c37aec885e82cc896528f28c57e113751e3471fc314",
"sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "sha256:5ac71bba1e07eab403b082c4428f868c1c9e26a21041436b4905c4c3d4e49b08",
"sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "sha256:5f622f19abda4e934938e24f1d67599249abc201844933a6f01aaa8663094489",
"sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "sha256:65bead1ac8c8930cf92a1ccaedcce19a57298547d5d1db5c9d4d068a0675c38b",
"sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "sha256:7362a7f829feda10c7265b553455de596b83d1623b3d436b6d3c51c688c57bf6",
"sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "sha256:7f2675750c50151f806070ec11258edf4c328340916c53bac0adbc465abd6b1e",
"sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "sha256:960d7f42277391e8b1c0b0ae427a214e1b31a1278de6b73f8807b20c2e913bba",
"sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "sha256:a50b0888d8a021a3342d36a6086501e30de7d840ab68fca44913e97d14487dc1",
"sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "sha256:b7dbc5e8c39ea3ad3db22715f1b5401cd698a621218680c6daf42c2f9d36e205",
"sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "sha256:bb3d29df5d07d5399d58a394d0ef50adf303ab4fbf66dfd25b9ef258effcb692",
"sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "sha256:c0fff2733f7c2950f58a4fd09b5db257b00c6fec57bf3f68c5bae004d804b407",
"sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "sha256:c792d3707a86c01c02607ae74364854220fb3e82735f631cd0a345dea6b4cee5",
"sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "sha256:c90bda74e16bcd03861b09b1d37c0a4158feda5d5a036bb2d6e58de6ff65793e",
"sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "sha256:cfce79ce41cc1a1dc7fc85bb41eeeb32d34a4cf39a645c717c0550287e30ff06",
"sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "sha256:eeafb646f374988c22c8e6da5ab9fb81367ecfe81c70c292623373d2a021b1a1",
"sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "sha256:f425f50a6dd807cb9043d15a4fcfba3b5874a54d9587ccbb748899f70dc18c47",
"sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "sha256:fcd4459fe35a400b8f416bc57906862693c9f88b66dc925e7f2a933e77f6b18b",
"sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "sha256:ff3936dd5feaefb4f91c8c1f50a06c588b5dc69fba4f7d9c79a6617ad80bb7df"
"sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.5.4" "version": "==5.0.1"
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:16040e1288c6c9f68c6da2fe75ebde83c0a158f6f5d54f4c5177b0c1478c5b86", "sha256:662a1ff78792e3fd77f16f71b1f31149489434de4b62a74895bd5d6534e635a5",
"sha256:89c2007ca4fa5b351a51a279eccff298520783b713bf28efb89dfb81c80ea49b" "sha256:687c37153486cf26c3fdcbdd177ef16de38dc3463f094b5f9c9955d91f277b14"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.2.7" "version": "==2.2.9"
}, },
"django-debug-toolbar": { "django-debug-toolbar": {
"hashes": [ "hashes": [
"sha256:17c53cd6bf4e7d69902aedf9a1d26c5d3b7369b54c5718744704f27b5a72f35d", "sha256:24c157bc6c0e1648e0a6587511ecb1b007a00a354ce716950bff2de12693e7a8",
"sha256:9a23ada2e43cd989195db3c18710b5d7451134a0d48127ab64c1d2ad81700342" "sha256:77cfba1d6e91b9bc3d36dc7dc74a9bb80be351948db5f880f2562a0cbf20b6c5"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0" "version": "==2.1"
}, },
"dodgy": { "dodgy": {
"hashes": [ "hashes": [
@ -971,17 +1000,16 @@
}, },
"gitpython": { "gitpython": {
"hashes": [ "hashes": [
"sha256:3237caca1139d0a7aa072f6735f5fd2520de52195e0fa1d8b83a9b212a2498b2", "sha256:9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42",
"sha256:a7d6bef0775f66ba47f25911d285bcd692ce9053837ff48a120c2b8cf3a71389" "sha256:c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245"
], ],
"version": "==3.0.4" "version": "==3.0.5"
}, },
"isort": { "isort": {
"hashes": [ "hashes": [
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
], ],
"index": "pypi",
"version": "==4.3.21" "version": "==4.3.21"
}, },
"lazy-object-proxy": { "lazy-object-proxy": {
@ -1017,12 +1045,19 @@
], ],
"version": "==0.6.1" "version": "==0.6.1"
}, },
"pathspec": {
"hashes": [
"sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424",
"sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"
],
"version": "==0.7.0"
},
"pbr": { "pbr": {
"hashes": [ "hashes": [
"sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b",
"sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9" "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"
], ],
"version": "==5.4.3" "version": "==5.4.4"
}, },
"pep8-naming": { "pep8-naming": {
"hashes": [ "hashes": [
@ -1033,10 +1068,10 @@
}, },
"prospector": { "prospector": {
"hashes": [ "hashes": [
"sha256:aba551e53dc1a5a432afa67385eaa81d7b4cf4c162dc1a4d0ee00b3a0712ad90" "sha256:ea910794b53cfefcb5dfb6b4eb0323e42d1a88132e165b85b016cc7f0b6ae635"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.1.7" "version": "==1.2.0"
}, },
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [
@ -1047,25 +1082,25 @@
}, },
"pydocstyle": { "pydocstyle": {
"hashes": [ "hashes": [
"sha256:04c84e034ebb56eb6396c820442b8c4499ac5eb94a3bda88951ac3dc519b6058", "sha256:4167fe954b8f27ebbbef2fbcf73c6e8ad1e7bb31488fce44a69fdfc4b0cd0fae",
"sha256:66aff87ffe34b1e49bff2dd03a88ce6843be2f3346b0c9814410d34987fbab59" "sha256:a0de36e549125d0a16a72a8c8c6c9ba267750656e72e466e994c222f1b6e92cb"
], ],
"version": "==4.0.1" "version": "==5.0.1"
}, },
"pyflakes": { "pyflakes": {
"hashes": [ "hashes": [
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
], ],
"version": "==1.6.0" "version": "==2.1.1"
}, },
"pylint": { "pylint": {
"hashes": [ "hashes": [
"sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd",
"sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.3.1" "version": "==2.4.4"
}, },
"pylint-celery": { "pylint-celery": {
"hashes": [ "hashes": [
@ -1075,11 +1110,11 @@
}, },
"pylint-django": { "pylint-django": {
"hashes": [ "hashes": [
"sha256:75c69d1ec2275918c37f175976da20e2f1e1e62e067098a685cd263ffa833dfd", "sha256:9bdb0e022b19881218a25ffb8ad05e83b83bc5cdbc58e5ee8ffbe99965193f6c",
"sha256:c7cb6384ea7b33ea77052a5ae07358c10d377807390ef27b2e6ff997303fadb7" "sha256:9eea6a026eaa5ecfad5fed7a33faf77ef55a43cc78afbcaf2f6ddd071156b3f8"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.10" "version": "==2.0.12"
}, },
"pylint-flask": { "pylint-flask": {
"hashes": [ "hashes": [
@ -1103,22 +1138,46 @@
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "sha256:21a8e19e2007a4047ffabbd8f0ee32c0dabae3b7f4b6c645110ae53e7714b470",
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "sha256:74ad685bfb065f4bdd36d24aa97092f04bcbb1179b5ffdd3d5f994023fb8c292",
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "sha256:79c3ba1da22e61c2a71aaa382c57518ab492278c8974c40187b900b50f3e0282",
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "sha256:94ad913ab3fd967d14ecffda8182d7d0e1f7dd919b352773c492ec51890d3224",
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "sha256:998db501e3a627c3e5678d6505f0e182d1529545df289db036cdc717f35d8058",
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "sha256:9b69d4645bff5820713e8912bc61c4277dc127a6f8c197b52b6436503c42600f",
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "sha256:9da13b536533518343a04f3c6564782ec8a13c705310b26b4832d77fa4d92a47",
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "sha256:a76159f13b47fb44fb2acac8fef798a1940dd31b4acec6f4560bd11b2d92d31b",
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "sha256:a9e9175c1e47a089a2b45d9e2afc6aae1f1f725538c32eec761894a42ba1227f",
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "sha256:ea51ce7b96646ecd3bb12c2702e570c2bd7dd4d9f146db7fa83c5008ede35f66",
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "sha256:ffbaaa05de60fc444eda3f6300d1af27d965b09b67f1fb4ebcc88dd0fb4ab1b4"
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.1.2" "version": "==5.3b1"
},
"regex": {
"hashes": [
"sha256:032fdcc03406e1a6485ec09b826eac78732943840c4b29e503b789716f051d8d",
"sha256:0e6cf1e747f383f52a0964452658c04300a9a01e8a89c55ea22813931b580aa8",
"sha256:106e25a841921d8259dcef2a42786caae35bc750fb996f830065b3dfaa67b77e",
"sha256:1768cf42a78a11dae63152685e7a1d90af7a8d71d2d4f6d2387edea53a9e0588",
"sha256:27d1bd20d334f50b7ef078eba0f0756a640fd25f5f1708d3b5bed18a5d6bced9",
"sha256:29b20f66f2e044aafba86ecf10a84e611b4667643c42baa004247f5dfef4f90b",
"sha256:4850c78b53acf664a6578bba0e9ebeaf2807bb476c14ec7e0f936f2015133cae",
"sha256:57eacd38a5ec40ed7b19a968a9d01c0d977bda55664210be713e750dd7b33540",
"sha256:724eb24b92fc5fdc1501a1b4df44a68b9c1dda171c8ef8736799e903fb100f63",
"sha256:77ae8d926f38700432807ba293d768ba9e7652df0cbe76df2843b12f80f68885",
"sha256:78b3712ec529b2a71731fbb10b907b54d9c53a17ca589b42a578bc1e9a2c82ea",
"sha256:7bbbdbada3078dc360d4692a9b28479f569db7fc7f304b668787afc9feb38ec8",
"sha256:8d9ef7f6c403e35e73b7fc3cde9f6decdc43b1cb2ff8d058c53b9084bfcb553e",
"sha256:a83049eb717ae828ced9cf607845929efcb086a001fc8af93ff15c50012a5716",
"sha256:adc35d38952e688535980ae2109cad3a109520033642e759f987cf47fe278aa1",
"sha256:c29a77ad4463f71a506515d9ec3a899ed026b4b015bf43245c919ff36275444b",
"sha256:cfd31b3300fefa5eecb2fe596c6dee1b91b3a05ece9d5cfd2631afebf6c6fadd",
"sha256:d3ee0b035816e0520fac928de31b6572106f0d75597f6fa3206969a02baba06f",
"sha256:d508875793efdf6bab3d47850df8f40d4040ae9928d9d80864c1768d6aeaf8e3",
"sha256:ef0b828a7e22e58e06a1cceddba7b4665c6af8afeb22a0d8083001330572c147",
"sha256:faad39fdbe2c2ccda9846cd21581063086330efafa47d87afea4073a08128656"
],
"version": "==2019.12.20"
}, },
"requirements-detector": { "requirements-detector": {
"hashes": [ "hashes": [
@ -1167,6 +1226,13 @@
], ],
"version": "==1.31.0" "version": "==1.31.0"
}, },
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
},
"typed-ast": { "typed-ast": {
"hashes": [ "hashes": [
"sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161",
@ -1190,7 +1256,7 @@
"sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
], ],
"markers": "implementation_name == 'cpython'", "markers": "implementation_name == 'cpython' and python_version < '3.8'",
"version": "==1.4.0" "version": "==1.4.0"
}, },
"unittest-xml-reporting": { "unittest-xml-reporting": {

View File

@ -1,23 +0,0 @@
FROM python:3.7-slim-buster as locker
COPY ./Pipfile /app/
COPY ./Pipfile.lock /app/
WORKDIR /app/
RUN pip install pipenv && \
pipenv lock -r > requirements.txt && \
pipenv lock -rd > requirements-dev.txt
FROM python:3.7-slim-buster
COPY --from=locker /app/requirements.txt /app/
COPY --from=locker /app/requirements-dev.txt /app/
WORKDIR /app/
RUN apt-get update && \
apt-get install -y --no-install-recommends postgresql-client-11 && \
rm -rf /var/lib/apt/ && \
pip install -r requirements.txt --no-cache-dir && \
adduser --system --no-create-home --uid 1000 --group --home /app passbook

View File

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

View File

@ -21,7 +21,7 @@ services:
labels: labels:
- traefik.enable=false - traefik.enable=false
server: server:
image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest} image: beryju/passbook:${SERVER_TAG:-latest}
command: command:
- uwsgi - uwsgi
- uwsgi.ini - uwsgi.ini
@ -40,7 +40,7 @@ services:
- traefik.docker.network=internal - traefik.docker.network=internal
- traefik.frontend.rule=PathPrefix:/ - traefik.frontend.rule=PathPrefix:/
worker: worker:
image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest} image: beryju/passbook:${SERVER_TAG:-latest}
command: command:
- celery - celery
- worker - worker
@ -60,7 +60,7 @@ services:
- PASSBOOK_POSTGRESQL__HOST=postgresql - PASSBOOK_POSTGRESQL__HOST=postgresql
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword} - PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
static: static:
image: docker.beryju.org/passbook/static:latest image: beryju/passbook-static:latest
networks: networks:
- internal - internal
labels: labels:

14
docs/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM python:3.7-slim-buster as builder
WORKDIR /mkdocs
RUN pip install mkdocs mkdocs-material
COPY docs/ docs
COPY mkdocs.yml .
RUN mkdocs build
FROM nginx
COPY --from=builder /mkdocs/site /usr/share/nginx/html

23
docs/factors.md Normal file
View File

@ -0,0 +1,23 @@
# Factors
A factor represents a single authenticating factor for a user. Common examples of this would be a password or an OTP. These factors can be combined in any order, and can be dynamically enabled using policies.
## Password Factor
This is the standard Password Factor. It allows you to select which Backend the password is checked with. here you can also specify which Policies are used to check the password. You can also specify which Factors a User has to pass to recover their account.
## Dummy Factor
This factor waits a random amount of time. Mostly used for debugging.
## E-Mail Factor
This factor is mostly for recovery, and used in conjunction with the Password Factor.
## OTP Factor
This is your typical One-Time Password implementation, compatible with Authy and Google Authenticator. You can enfore this Factor so that every user has to configure it, or leave it optional.
## Captcha Factor
While this factor doesn't really authenticate a user, it is part of the Authentication Flow. passbook uses Google's reCaptcha implementation.

55
docs/images/logo.svg Normal file
View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#57565C;" d="M407,512H105C47.103,512,0,464.897,0,407V105C0,47.103,47.103,0,105,0h302
c57.897,0,105,47.103,105,105v302C512,464.897,464.897,512,407,512z"/>
<path style="fill:#3E3D42;" d="M407,0H256v512h151c57.897,0,105-47.103,105-105V105C512,47.103,464.897,0,407,0z"/>
<rect x="91" y="141" style="fill:#00C3FF;" width="330" height="44"/>
<rect x="256" y="141" style="fill:#00AAF0;" width="165" height="44"/>
<rect x="91" y="176" style="fill:#FFDC40;" width="330" height="44"/>
<rect x="256" y="176" style="fill:#FFAB15;" width="165" height="44"/>
<rect x="91" y="206" style="fill:#87E694;" width="330" height="44"/>
<rect x="256" y="206" style="fill:#66CC70;" width="165" height="44"/>
<path style="fill:#F2F2F2;" d="M421,381c0,8.284-6.716,15-15,15H106c-8.284,0-15-6.716-15-15v-85h89.997
c9.31,0,17.688,4.938,21.868,12.888C213.277,328.695,233.638,341,256,341s42.723-12.305,53.135-32.111
c4.18-7.95,12.559-12.889,21.868-12.889H421V381z"/>
<path style="fill:#FF6849;" d="M421,266h-89.997c-20.487,0-39.041,11.085-48.423,28.929C277.369,304.842,267.185,311,256,311
s-21.369-6.158-26.58-16.071C220.038,277.085,201.484,266,180.997,266H91v-30h330V266z"/>
<path style="fill:#F2F2F2;" d="M421,146H91v-15c0-8.284,6.716-15,15-15h300c8.284,0,15,6.716,15,15V146z"/>
<path style="fill:#E5E5E5;" d="M331.003,296c-9.31,0-17.688,4.938-21.868,12.889C298.723,328.695,278.362,341,256,341v55h150
c8.284,0,15-6.716,15-15v-85H331.003z"/>
<path style="fill:#FD4B2D;" d="M256,236v75c11.185,0,21.369-6.158,26.58-16.071C291.962,277.085,310.516,266,331.003,266H421v-30
H256z"/>
<path style="fill:#E5E5E5;" d="M406,116H256v30h165v-15C421,122.716,414.284,116,406,116z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

31
docs/index.md Executable file
View File

@ -0,0 +1,31 @@
# Welcome
Welcome to the passbook Documentation. passbook is an open-source Identity Provider and Usermanagement software. It can be used as a central directory for users or customers and it can integrate with your existing Directory.
passbook can also be used as part of an Application to facilitate User Enrollment, Password recovery and Social Login.
passbook uses the following Terminology:
### Policy
A Policy is at a base level a yes/no gate. It will either evaluate to True or False depending on the Policy Kind and settings. For example, a "Group Membership Policy" evaluates to True if the User is member of the specified Group and False if not. This can be used to conditionally apply Factors and grant/deny access.
### Provider
A Provider is a way for other Applications to authenticate against passbook. Common Providers are OpenID Connect (OIDC) and SAML.
### Source
Sources are ways to get users into passbook. This might be an LDAP Connection to import Users from Active Directory, or an OAuth2 Connection to allow Social Logins.
### Application
An application links together Policies with a Provider, allowing you to control access. It also holds Information like UI Name, Icon and more.
### Factors
Factors represent Authentication Factors, like a Password or OTP. These Factors can be dynamically enabled using policies. This allows you to, for example, force users from a certain IP ranges to complete a Captcha to authenticate.
### Property Mappings
Property Mappings allow you to make Information available for external Applications. For example, if you want to login to AWS with passbook, you'd use Property Mappings to set the User's Roles based on their Groups.

View File

@ -0,0 +1,26 @@
# docker-compose
This installation Method is for test-setups and small-scale productive setups.
## Prerequisites
- docker
- docker-compose
## Install
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). Place it in a directory of your choice.
passbook needs to know it's primary URL to create links in E-Mails and set cookies, so you have to run the following command:
```
export PASSBOOK_DOMAIN=domain.tld # this can be any domain or IP, it just needs to point to passbook.
```
The compose file references the current latest version, which can be overridden with the `SERVER_TAG` Environment variable.
If you plan to use this setup for production, it is also advised to change the PostgreSQL Password by setting `PG_PASS` to a password of your choice.
Now you can pull the Docker images needed by running `docker-compose pull`. After this has finished, run `docker-compose up -d` to start passbook.
passbook will then be reachable on Port 80. You can optionally configure the packaged traefik to use Let's Encrypt for TLS Encryption.

6
docs/installation/install.md Executable file
View File

@ -0,0 +1,6 @@
# Installation
There are two supported ways to install passbook:
- [docker-compose](docker-compose.md) for test- or small productive setups
- [Kubernetes](./kubernetes.md) for larger Productive setups

View File

@ -0,0 +1,3 @@
# Kubernetes
For a mid to high-load Installation, Kubernetes is recommended. passbook is installed using a helm-chart.

View File

@ -0,0 +1,59 @@
# GitLab Integration
## What is GitLab
From https://about.gitlab.com/what-is-gitlab/
```
GitLab is a complete DevOps platform, delivered as a single application. This makes GitLab unique and makes Concurrent DevOps possible, unlocking your organization from the constraints of a pieced together toolchain. Join us for a live Q&A to learn how GitLab can give you unmatched visibility and higher levels of efficiency in a single application across the DevOps lifecycle.
```
## Preparation
The following placeholders will be used:
- `gitlab.company` is the FQDN of the GitLab Install
- `passbook.company` is the FQDN of the passbook Install
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
- ACS URL: `https://gitlab.company/users/auth/saml/callback`
- Audience: `https://gitlab.company`
- Issuer: `https://gitlab.company`
You can of course use a custom Signing Certificate, and adjust the Assertion Length. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
## GitLab Configuration
Paste the following block in your `gitlab.rb` file, after replacing the placeholder values from above. The file is located in `/etc/gitlab`.
```ruby
gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
gitlab_rails['omniauth_sync_email_from_provider'] = 'saml'
gitlab_rails['omniauth_sync_profile_from_provider'] = ['saml']
gitlab_rails['omniauth_sync_profile_attributes'] = ['email']
gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'saml'
gitlab_rails['omniauth_block_auto_created_users'] = false
gitlab_rails['omniauth_auto_link_saml_user'] = true
gitlab_rails['omniauth_providers'] = [
{
name: 'saml',
args: {
assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback',
idp_cert_fingerprint: '4E:1E:CD:67:4A:67:5A:E9:6A:D0:3C:E6:DD:7A:F2:44:2E:76:00:6A',
idp_sso_target_url: 'https://passbook.company/application/saml/<passbook application slug>/login/',
issuer: 'https://gitlab.company',
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
attribute_statements: {
email: ['urn:oid:1.3.6.1.4.1.5923.1.1.1.6'],
first_name: ['urn:oid:2.5.4.3'],
nickname: ['urn:oid:2.16.840.1.113730.3.1.241']
}
},
label: 'passbook'
}
]
```
Afterwards, either run `gitlab-ctl reconfigure` if you're running GitLab Omnibus, or restart the container if you're using the container.

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View File

@ -0,0 +1,28 @@
# Harbor Integration
## What is Harbor
From https://goharbor.io
```
Harbor is an open source container image registry that secures images with role-based access control, scans images for vulnerabilities, and signs images as trusted. A CNCF Incubating project, Harbor delivers compliance, performance, and interoperability to help you consistently and securely manage images across cloud native compute platforms like Kubernetes and Docker.
```
## Preparation
The following placeholders will be used:
- `harbor.company` is the FQDN of the Harbor Install
- `passbook.company` is the FQDN of the passbook Install
Create an application in passbook. Create an OpenID Provider with the following Parameters:
- Client Type: `Confidential`
- Response types: `code (Authorization Code Flow)`
- JWT Algorithm: `RS256`
- Redirect URIs: `https://harbor.company/c/oidc/callback`
- Scopes: `openid`
## Harbor
![](./harbor.png)

View File

@ -0,0 +1,29 @@
# Rancher Integration
## What is Rancher
From https://rancher.com/products/rancher
```
An Enterprise Platform for Managing Kubernetes Everywhere
Rancher is a platform built to address the needs of the DevOps teams deploying applications with Kubernetes, and the IT staff responsible for delivering an enterprise-critical service.
```
## Preparation
The following placeholders will be used:
- `rancher.company` is the FQDN of the Rancher Install
- `passbook.company` is the FQDN of the passbook Install
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
- ACS URL: `https://rancher.company/v1-saml/adfs/saml/acs`
- Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
- Issuer: `passbook`
You can of course use a custom Signing Certificate, and adjust the Assertion Length.
## Rancher
![](./rancher.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

View File

@ -0,0 +1,42 @@
# Sentry Integration
## What is Sentry
From https://sentry.io
```
Sentry provides self-hosted and cloud-based error monitoring that helps all software
teams discover, triage, and prioritize errors in real-time.
One million developers at over fifty thousand companies already ship
better software faster with Sentry. Wont you join them?
```
## Preparation
The following placeholders will be used:
- `sentry.company` is the FQDN of the Sentry Install
- `passbook.company` is the FQDN of the passbook Install
Create an application in passbook. Create an OpenID Provider with the following Parameters:
- Client Type: `Confidential`
- Response types: `code (Authorization Code Flow)`
- JWT Algorithm: `RS256`
- Redirect URIs: `https://sentry.company/auth/sso/`
- Scopes: `openid email`
## Sentry
**This guide assumes you've installed Sentry using [getsentry/onpremise](https://github.com/getsentry/onpremise)**
- Add `sentry-auth-oidc` to `onpremise/sentry/requirements.txt` (Create the file if it doesn't exist yet)
- Add the following block to your `onpremise/sentry/sentry.conf.py`:
```
OIDC_ISSUER = "passbook"
OIDC_CLIENT_ID = "<Client ID from passbook>"
OIDC_CLIENT_SECRET = "<Client Secret from passbook>"
OIDC_SCOPE = "openid email"
OIDC_DOMAIN = "https://passbook.company/application/oidc/"
```

33
docs/k8s/deployment.yml Normal file
View File

@ -0,0 +1,33 @@
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: passbook-docs
namespace: prod-passbook-docs
labels:
app.kubernetes.io/name: passbook-docs
app.kubernetes.io/managed-by: passbook-docs
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: passbook-docs
template:
metadata:
labels:
app.kubernetes.io/name: passbook-docs
spec:
containers:
- name: passbook-docs
image: "beryju/passbook-docs:latest"
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
limits:
cpu: 10m
memory: 20Mi
requests:
cpu: 10m
memory: 20Mi

21
docs/k8s/ingress.yml Normal file
View File

@ -0,0 +1,21 @@
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
labels:
app.kubernetes.io/name: passbook-docs
name: passbook-docs
namespace: prod-passbook-docs
spec:
rules:
- host: docs.passbook.beryju.org
http:
paths:
- backend:
serviceName: passbook-docs-http
servicePort: http
path: /
tls:
- hosts:
- docs.passbook.beryju.org
secretName: passbook-docs-acme

17
docs/k8s/service.yml Normal file
View File

@ -0,0 +1,17 @@
---
apiVersion: v1
kind: Service
metadata:
name: passbook-docs-http
namespace: prod-passbook-docs
labels:
app.kubernetes.io/name: passbook-docs
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: passbook-docs

68
docs/policies.md Normal file
View File

@ -0,0 +1,68 @@
# Policies
## Kinds
There are two different Kind of policies, a Standard Policy and a Password Policy. Normal Policies just evaluate to True or False, and can be used everywhere. Password Policies apply when a Password is set (during User enrollment, Recovery or anywhere else). These policies can be used to apply Password Rules like length, etc. The can also be used to expire passwords after a certain amount of time.
## Standard Policies
---
### Group-Membership Policy
This policy evaluates to True if the current user is a Member of the selected group.
### Reputation Policy
passbook keeps track of failed login attempts by Source IP and Attempted Username. These values are saved as scores. Each failed login decreases the Score for the Client IP as well as the targeted Username by one.
This policy can be used to for example prompt Clients with a low score to pass a Captcha before they can continue.
### Field matcher Policy
This policy allows you to evaluate arbitrary comparisons against the User instance. Currently supported fields are:
- Username
- E-Mail
- Name
- Is_active
- Date joined
Any of the following operations are supported:
- Starts with
- Ends with
- Contains
- Regexp (standard Python engine)
- Exact
### SSO Policy
This policy evaluates to True if the current Authentication Flow has been initiated through an external Source, like OAuth and SAML.
### Webhook Policy
This policy allows you to send an arbitrary HTTP Request to any URL. You can then use JSONPath to extract the result you need.
## Password Policies
---
### Password Policy
This Policy allows you to specify Password rules, like Length and required Characters.
The following rules can be set:
- Minimum amount of Uppercase Characters
- Minimum amount of Lowercase Characters
- Minimum amount of Symbols Characters
- Minimum Length
- Symbol charset (define which characters are counted as symbols)
### Have I Been Pwned Policy
This Policy checks the hashed Password against the [Have I Been Pwned](https://haveibeenpwned.com/) API. This only sends the first 5 characters of the hashed password. The remaining comparison is done within passbook.
### Password-Expiry Policy
This policy can enforce regular password rotation by expiring set Passwords after a finite amount of time. This forces users to set a new password.

21
docs/property-mappings.md Normal file
View File

@ -0,0 +1,21 @@
# Property Mappings
Property Mappings allow you to pass information to external Applications. For example, pass the current user's Groups as a SAML Parameter. Property Mappings are also used to map Source fields to passbook fields, for example when using LDAP.
## SAML Property Mapping
SAML Property Mappings allow you embed Information into the SAML AuthN Request. THis Information can then be used by the Application to assign permissions for example.
You can find examples [here](integrations/)
## LDAP Property Mapping
LDAP Property Mappings are used when you define a LDAP Source. These Mappings define which LDAP Property maps to which passbook Property. By default, these mappings are created:
- Autogenerated LDAP Mapping: givenName -> first_name
- Autogenerated LDAP Mapping: mail -> email
- Autogenerated LDAP Mapping: name -> name
- Autogenerated LDAP Mapping: sAMAccountName -> username
- Autogenerated LDAP Mapping: sn -> last_name
These are configured for the most common LDAP Setups.

23
docs/providers.md Normal file
View File

@ -0,0 +1,23 @@
# Providers
Providers allow external Applications to authenticate against passbook and use its User Information.
## OpenID Provider
This provider uses the commonly used OpenID Connect variation of OAuth2.
## OAuth2 Provider
This provider is slightly different than the OpenID Provider. While it uses the same basic OAuth2 Protocol, it provides a GitHub-compatible Endpoint. This allows you to integrate Applications, which don't support Custom OpenID Providers.
The API exposes Username, E-Mail, Name and Groups in a GitHub-compatible format.
## SAML Provider
This provider allows you to integrate Enterprise Software using the SAML2 Protocol. It supports signed Requests. This Provider also has [Property Mappings](property-mappings.md#saml-property-mapping), which allows you to expose Vendor-specific Fields.
Default fields are:
- `eduPersonPrincipalName`: User's E-Mail
- `cn`: User's Full Name
- `mail`: User's E-Mail
- `displayName`: User's Username
- `uid`: User Unique Identifier

39
docs/sources.md Normal file
View File

@ -0,0 +1,39 @@
# Sources
Sources allow you to connect passbook to an existing User directory. They can also be used for Social-Login, using external Providers like Facebook, Twitter, etc.
## Generic OAuth Source
**All Integration-specific Sources are documented in the Integrations Section**
This source allows users to enroll themselves with an External OAuth-based Identity Provider. The Generic Provider expects the Endpoint to return OpenID-Connect compatible Information. Vendor specific Implementations have their own OAuth Source.
- Policies: Allow/Forbid Users from linking their Accounts with this Provider
- Request Token URL: This field is used for OAuth v1 Implementations and will be provided by the Provider.
- Authorization URL: This value will be provided by the Provider.
- Access Token URL: This value will be provided by the Provider.
- Profile URL: This URL is called by passbook to retrieve User information upon successful authentication.
- Consumer key/Consumer secret: These values will be provided by the Provider.
## SAML Source
This source allows passbook to act as a SAML Service Provider. Just like the SAML Provider, it supports signed Requests. Vendor specific documentation can be found in the Integrations Section
## LDAP Source
This source allows you to import Users and Groups from an LDAP Server
- Server URI: URI to your LDAP Server/Domain Controller
- Bind CN: CN to bind as, this can also be a UPN in the format of `user@domain.tld`
- Bind password: Password used during the bind process
- Enable Start TLS: Enables StartTLS functionality. To use SSL instead, use port `636`
- Base DN: Base DN used for all LDAP queries
- Addition User DN: Prepended to Base DN for User-queries.
- Addition Group DN: Prepended to Base DN for Group-queries.
- User object filter: Consider Objects matching this filter to be Users.
- Group object filter: Consider Objects matching this filter to be Groups.
- User group membership field: Field which contains Groups of user.
- Object uniqueness field: Field which contains a unique Identifier.
- Sync groups: Enable/disable Group synchronization. Groups are synced in the background every 5 minutes.
- Sync parent group: Optionally set this Group as parent Group for all synced Groups (allows you to, for example, import AD Groups under a root `imported-from-ad` group.)
- Property mappings: Define which LDAP Properties map to which passbook Properties. The default set of Property Mappings is generated for Active Directory. See also [LDAP Property Mappings](property-mappings.md#ldap-property-mapping)

View File

@ -6,4 +6,4 @@ dependencies:
repository: https://kubernetes-charts.storage.googleapis.com/ repository: https://kubernetes-charts.storage.googleapis.com/
version: 9.5.1 version: 9.5.1
digest: sha256:f18b5dc8d0be13d584407405c60d10b6b84d25f7fa8aaa3dd0e5385c38f5c516 digest: sha256:f18b5dc8d0be13d584407405c60d10b6b84d25f7fa8aaa3dd0e5385c38f5c516
generated: "2019-11-07T10:23:07.259176+01:00" generated: "2019-12-14T13:33:48.4341939Z"

View File

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

View File

@ -21,7 +21,7 @@ spec:
spec: spec:
containers: containers:
- name: {{ .Chart.Name }}-static - name: {{ .Chart.Name }}-static
image: "docker.beryju.org/passbook/static:{{ .Values.image.tag }}" image: "beryju/passbook-static:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
ports: ports:
- name: http - name: http
@ -31,13 +31,13 @@ spec:
initialDelaySeconds: 10 initialDelaySeconds: 10
timeoutSeconds: 5 timeoutSeconds: 5
httpGet: httpGet:
path: /_/healthz path: /-/ping
port: http port: http
readinessProbe: readinessProbe:
initialDelaySeconds: 10 initialDelaySeconds: 10
timeoutSeconds: 5 timeoutSeconds: 5
httpGet: httpGet:
path: /_/healthz path: /-/ping
port: http port: http
resources: resources:
requests: requests:

View File

@ -26,7 +26,7 @@ spec:
name: {{ include "passbook.fullname" . }}-config name: {{ include "passbook.fullname" . }}-config
initContainers: initContainers:
- name: passbook-database-migrations - name: passbook-database-migrations
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}" image: "beryju/passbook:{{ .Values.image.tag }}"
command: command:
- ./manage.py - ./manage.py
args: args:
@ -56,7 +56,7 @@ spec:
key: postgresql-password key: postgresql-password
containers: containers:
- name: {{ .Chart.Name }} - name: {{ .Chart.Name }}
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}" image: "beryju/passbook:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: command:
- uwsgi - uwsgi

View File

@ -26,7 +26,7 @@ spec:
name: {{ include "passbook.fullname" . }}-config name: {{ include "passbook.fullname" . }}-config
containers: containers:
- name: {{ .Chart.Name }} - name: {{ .Chart.Name }}
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}" image: "beryju/passbook:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: command:
- celery - celery

View File

@ -2,7 +2,7 @@
# This is a YAML-formatted file. # This is a YAML-formatted file.
# Declare variables to be passed into your templates. # Declare variables to be passed into your templates.
image: image:
tag: 0.7.4-beta tag: 0.7.8-beta
nameOverride: "" nameOverride: ""

31
mkdocs.yml Normal file
View File

@ -0,0 +1,31 @@
site_name: passbook Docs
site_url: https://docs.passbook.beryju.org
copyright: "Copyright &copy; 2019 - 2020 BeryJu.org"
nav:
- Home: index.md
- Installation:
- Installation: installation/install.md
- docker-compose: installation/docker-compose.md
- Kubernetes: installation/kubernetes.md
- Sources: sources.md
- Providers: providers.md
- Property Mappings: property-mappings.md
- Factors: factors.md
- Policies: policies.md
- Integrations:
- as Provider:
- GitLab: integrations/services/gitlab/index.md
- Rancher: integrations/services/rancher/index.md
- Harbor: integrations/services/harbor/index.md
- Sentry: integrations/services/sentry/index.md
repo_name: "BeryJu.org/passbook"
repo_url: https://github.com/BeryJu/passbook
theme:
name: "material"
logo: "images/logo.svg"
markdown_extensions:
- toc:
permalink: "¶"

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = '0.7.4-beta' __version__ = "0.7.8-beta"

View File

@ -5,7 +5,7 @@ from django.apps import AppConfig
class PassbookAdminConfig(AppConfig): class PassbookAdminConfig(AppConfig):
"""passbook admin app config""" """passbook admin app config"""
name = 'passbook.admin' name = "passbook.admin"
label = 'passbook_admin' label = "passbook_admin"
mountpoint = 'administration/' mountpoint = "administration/"
verbose_name = 'passbook Admin' verbose_name = "passbook Admin"

View File

@ -16,7 +16,7 @@ class YAMLField(forms.CharField):
"""Django's JSON Field converted to YAML""" """Django's JSON Field converted to YAML"""
default_error_messages = { default_error_messages = {
'invalid': _("'%(value)s' value must be valid YAML."), "invalid": _("'%(value)s' value must be valid YAML."),
} }
widget = forms.Textarea widget = forms.Textarea
@ -31,9 +31,7 @@ class YAMLField(forms.CharField):
converted = yaml.safe_load(value) converted = yaml.safe_load(value)
except yaml.YAMLError: except yaml.YAMLError:
raise forms.ValidationError( raise forms.ValidationError(
self.error_messages['invalid'], self.error_messages["invalid"], code="invalid", params={"value": value},
code='invalid',
params={'value': value},
) )
if isinstance(converted, str): if isinstance(converted, str):
return YAMLString(converted) return YAMLString(converted)

View File

@ -1,4 +1,4 @@
"""p2 form helpers""" """passbook form helpers"""
from django import forms from django import forms
from passbook.admin.fields import YAMLField from passbook.admin.fields import YAMLField
@ -9,29 +9,32 @@ class TagModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Check if we have an instance, load tags otherwise use an empty dict # Check if we have an instance, load tags otherwise use an empty dict
instance = kwargs.get('instance', None) instance = kwargs.get("instance", None)
tags = instance.tags if instance else {} tags = instance.tags if instance else {}
# Make sure all predefined tags exist in tags, and set default if they don't # Make sure all predefined tags exist in tags, and set default if they don't
predefined_tags = self._meta.model().get_predefined_tags() # pylint: disable=no-member predefined_tags = (
self._meta.model().get_predefined_tags() # pylint: disable=no-member
)
for key, value in predefined_tags.items(): for key, value in predefined_tags.items():
if key not in tags: if key not in tags:
tags[key] = value tags[key] = value
# Format JSON # Format JSON
kwargs['initial']['tags'] = tags kwargs["initial"]["tags"] = tags
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def clean_tags(self): def clean_tags(self):
"""Make sure all required tags are set""" """Make sure all required tags are set"""
if hasattr(self.instance, 'get_required_keys') and hasattr(self.instance, 'tags'): if hasattr(self.instance, "get_required_keys") and hasattr(
self.instance, "tags"
):
for key in self.instance.get_required_keys(): for key in self.instance.get_required_keys():
if key not in self.cleaned_data.get('tags'): if key not in self.cleaned_data.get("tags"):
raise forms.ValidationError("Tag %s missing." % key) raise forms.ValidationError("Tag %s missing." % key)
return self.cleaned_data.get('tags') return self.cleaned_data.get("tags")
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class TagModelFormMeta: class TagModelFormMeta:
"""Base Meta class that uses the YAMLField""" """Base Meta class that uses the YAMLField"""
field_classes = { field_classes = {"tags": YAMLField}
'tags': YAMLField
}

View File

@ -1,7 +1,7 @@
"""passbook core source form fields""" """passbook core source form fields"""
# from django import forms # from django import forms
SOURCE_FORM_FIELDS = ['name', 'slug', 'enabled', 'policies'] SOURCE_FORM_FIELDS = ["name", "slug", "enabled", "policies"]
SOURCE_SERIALIZER_FIELDS = ['pk', 'name', 'slug', 'enabled', 'policies'] SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled", "policies"]
# class SourceForm(forms.Form) # class SourceForm(forms.Form)

View File

@ -12,10 +12,10 @@ class UserForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = ['username', 'name', 'email', 'is_staff', 'is_active', 'attributes'] fields = ["username", "name", "email", "is_staff", "is_active", "attributes"]
widgets = { widgets = {
'name': forms.TextInput, "name": forms.TextInput,
} }
field_classes = { field_classes = {
'attributes': YAMLField, "attributes": YAMLField,
} }

View File

@ -11,15 +11,16 @@ def impersonate(get_response):
# User is superuser and has __impersonate ID set # User is superuser and has __impersonate ID set
if request.user.is_superuser and "__impersonate" in request.GET: if request.user.is_superuser and "__impersonate" in request.GET:
request.session['impersonate_id'] = request.GET["__impersonate"] request.session["impersonate_id"] = request.GET["__impersonate"]
# user wants to stop impersonation # user wants to stop impersonation
elif "__unimpersonate" in request.GET and 'impersonate_id' in request.session: elif "__unimpersonate" in request.GET and "impersonate_id" in request.session:
del request.session['impersonate_id'] del request.session["impersonate_id"]
# Actually impersonate user # Actually impersonate user
if request.user.is_superuser and 'impersonate_id' in request.session: if request.user.is_superuser and "impersonate_id" in request.session:
request.user = User.objects.get(pk=request.session['impersonate_id']) request.user = User.objects.get(pk=request.session["impersonate_id"])
response = get_response(request) response = get_response(request)
return response return response
return middleware return middleware

View File

@ -1,5 +1,5 @@
"""passbook admin settings""" """passbook admin settings"""
MIDDLEWARE = [ MIDDLEWARE = [
'passbook.admin.middleware.impersonate', "passbook.admin.middleware.impersonate",
] ]

View File

@ -10,10 +10,11 @@ from passbook.lib.utils.template import render_to_string
register = template.Library() register = template.Library()
LOGGER = get_logger() LOGGER = get_logger()
@register.simple_tag() @register.simple_tag()
def get_links(model_instance): def get_links(model_instance):
"""Find all link_ methods on an object instance, run them and return as dict""" """Find all link_ methods on an object instance, run them and return as dict"""
prefix = 'link_' prefix = "link_"
links = {} links = {}
if not isinstance(model_instance, Model): if not isinstance(model_instance, Model):
@ -21,9 +22,11 @@ def get_links(model_instance):
return links return links
try: try:
for name, method in inspect.getmembers(model_instance, predicate=inspect.ismethod): for name, method in inspect.getmembers(
model_instance, predicate=inspect.ismethod
):
if name.startswith(prefix): if name.startswith(prefix):
human_name = name.replace(prefix, '').replace('_', ' ').capitalize() human_name = name.replace(prefix, "").replace("_", " ").capitalize()
link = method() link = method()
if link: if link:
links[human_name] = link links[human_name] = link
@ -36,7 +39,7 @@ def get_links(model_instance):
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def get_htmls(context, model_instance): def get_htmls(context, model_instance):
"""Find all html_ methods on an object instance, run them and return as dict""" """Find all html_ methods on an object instance, run them and return as dict"""
prefix = 'html_' prefix = "html_"
htmls = [] htmls = []
if not isinstance(model_instance, Model): if not isinstance(model_instance, Model):
@ -44,9 +47,11 @@ def get_htmls(context, model_instance):
return htmls return htmls
try: try:
for name, method in inspect.getmembers(model_instance, predicate=inspect.ismethod): for name, method in inspect.getmembers(
model_instance, predicate=inspect.ismethod
):
if name.startswith(prefix): if name.startswith(prefix):
template, _context = method(context.get('request')) template, _context = method(context.get("request"))
htmls.append(render_to_string(template, _context)) htmls.append(render_to_string(template, _context))
except NotImplementedError: except NotImplementedError:
pass pass

View File

@ -1,82 +1,157 @@
"""passbook URL Configuration""" """passbook URL Configuration"""
from django.urls import path from django.urls import path
from passbook.admin.views import (applications, audit, debug, factors, groups, from passbook.admin.views import (
invitations, overview, policy, applications,
property_mapping, providers, sources, users) audit,
debug,
factors,
groups,
invitations,
overview,
policy,
property_mapping,
providers,
sources,
users,
)
urlpatterns = [ urlpatterns = [
path('', overview.AdministrationOverviewView.as_view(), name='overview'), path("", overview.AdministrationOverviewView.as_view(), name="overview"),
# Applications # Applications
path('applications/', applications.ApplicationListView.as_view(), path(
name='applications'), "applications/", applications.ApplicationListView.as_view(), name="applications"
path('applications/create/', applications.ApplicationCreateView.as_view(), ),
name='application-create'), path(
path('applications/<uuid:pk>/update/', "applications/create/",
applications.ApplicationUpdateView.as_view(), name='application-update'), applications.ApplicationCreateView.as_view(),
path('applications/<uuid:pk>/delete/', name="application-create",
applications.ApplicationDeleteView.as_view(), name='application-delete'), ),
path(
"applications/<uuid:pk>/update/",
applications.ApplicationUpdateView.as_view(),
name="application-update",
),
path(
"applications/<uuid:pk>/delete/",
applications.ApplicationDeleteView.as_view(),
name="application-delete",
),
# Sources # Sources
path('sources/', sources.SourceListView.as_view(), name='sources'), path("sources/", sources.SourceListView.as_view(), name="sources"),
path('sources/create/', sources.SourceCreateView.as_view(), name='source-create'), path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
path('sources/<uuid:pk>/update/', sources.SourceUpdateView.as_view(), name='source-update'), path(
path('sources/<uuid:pk>/delete/', sources.SourceDeleteView.as_view(), name='source-delete'), "sources/<uuid:pk>/update/",
sources.SourceUpdateView.as_view(),
name="source-update",
),
path(
"sources/<uuid:pk>/delete/",
sources.SourceDeleteView.as_view(),
name="source-delete",
),
# Policies # Policies
path('policies/', policy.PolicyListView.as_view(), name='policies'), path("policies/", policy.PolicyListView.as_view(), name="policies"),
path('policies/create/', policy.PolicyCreateView.as_view(), name='policy-create'), path("policies/create/", policy.PolicyCreateView.as_view(), name="policy-create"),
path('policies/<uuid:pk>/update/', policy.PolicyUpdateView.as_view(), name='policy-update'), path(
path('policies/<uuid:pk>/delete/', policy.PolicyDeleteView.as_view(), name='policy-delete'), "policies/<uuid:pk>/update/",
path('policies/<uuid:pk>/test/', policy.PolicyTestView.as_view(), name='policy-test'), policy.PolicyUpdateView.as_view(),
name="policy-update",
),
path(
"policies/<uuid:pk>/delete/",
policy.PolicyDeleteView.as_view(),
name="policy-delete",
),
path(
"policies/<uuid:pk>/test/", policy.PolicyTestView.as_view(), name="policy-test"
),
# Providers # Providers
path('providers/', providers.ProviderListView.as_view(), name='providers'), path("providers/", providers.ProviderListView.as_view(), name="providers"),
path('providers/create/', path(
providers.ProviderCreateView.as_view(), name='provider-create'), "providers/create/",
path('providers/<int:pk>/update/', providers.ProviderCreateView.as_view(),
providers.ProviderUpdateView.as_view(), name='provider-update'), name="provider-create",
path('providers/<int:pk>/delete/', ),
providers.ProviderDeleteView.as_view(), name='provider-delete'), path(
"providers/<int:pk>/update/",
providers.ProviderUpdateView.as_view(),
name="provider-update",
),
path(
"providers/<int:pk>/delete/",
providers.ProviderDeleteView.as_view(),
name="provider-delete",
),
# Factors # Factors
path('factors/', factors.FactorListView.as_view(), name='factors'), path("factors/", factors.FactorListView.as_view(), name="factors"),
path('factors/create/', path("factors/create/", factors.FactorCreateView.as_view(), name="factor-create"),
factors.FactorCreateView.as_view(), name='factor-create'), path(
path('factors/<uuid:pk>/update/', "factors/<uuid:pk>/update/",
factors.FactorUpdateView.as_view(), name='factor-update'), factors.FactorUpdateView.as_view(),
path('factors/<uuid:pk>/delete/', name="factor-update",
factors.FactorDeleteView.as_view(), name='factor-delete'), ),
path(
"factors/<uuid:pk>/delete/",
factors.FactorDeleteView.as_view(),
name="factor-delete",
),
# Factors # Factors
path('property-mappings/', property_mapping.PropertyMappingListView.as_view(), path(
name='property-mappings'), "property-mappings/",
path('property-mappings/create/', property_mapping.PropertyMappingListView.as_view(),
property_mapping.PropertyMappingCreateView.as_view(), name='property-mapping-create'), name="property-mappings",
path('property-mappings/<uuid:pk>/update/', ),
property_mapping.PropertyMappingUpdateView.as_view(), name='property-mapping-update'), path(
path('property-mappings/<uuid:pk>/delete/', "property-mappings/create/",
property_mapping.PropertyMappingDeleteView.as_view(), name='property-mapping-delete'), property_mapping.PropertyMappingCreateView.as_view(),
name="property-mapping-create",
),
path(
"property-mappings/<uuid:pk>/update/",
property_mapping.PropertyMappingUpdateView.as_view(),
name="property-mapping-update",
),
path(
"property-mappings/<uuid:pk>/delete/",
property_mapping.PropertyMappingDeleteView.as_view(),
name="property-mapping-delete",
),
# Invitations # Invitations
path('invitations/', invitations.InvitationListView.as_view(), name='invitations'), path("invitations/", invitations.InvitationListView.as_view(), name="invitations"),
path('invitations/create/', path(
invitations.InvitationCreateView.as_view(), name='invitation-create'), "invitations/create/",
path('invitations/<uuid:pk>/delete/', invitations.InvitationCreateView.as_view(),
invitations.InvitationDeleteView.as_view(), name='invitation-delete'), name="invitation-create",
),
path(
"invitations/<uuid:pk>/delete/",
invitations.InvitationDeleteView.as_view(),
name="invitation-delete",
),
# Users # Users
path('users/', users.UserListView.as_view(), path("users/", users.UserListView.as_view(), name="users"),
name='users'), path("users/create/", users.UserCreateView.as_view(), name="user-create"),
path('users/create/', users.UserCreateView.as_view(), name='user-create'), path("users/<int:pk>/update/", users.UserUpdateView.as_view(), name="user-update"),
path('users/<int:pk>/update/', path("users/<int:pk>/delete/", users.UserDeleteView.as_view(), name="user-delete"),
users.UserUpdateView.as_view(), name='user-update'), path(
path('users/<int:pk>/delete/', "users/<int:pk>/reset/",
users.UserDeleteView.as_view(), name='user-delete'), users.UserPasswordResetView.as_view(),
path('users/<int:pk>/reset/', name="user-password-reset",
users.UserPasswordResetView.as_view(), name='user-password-reset'), ),
# Groups # Groups
path('group/', groups.GroupListView.as_view(), name='group'), path("group/", groups.GroupListView.as_view(), name="group"),
path('group/create/', groups.GroupCreateView.as_view(), name='group-create'), path("group/create/", groups.GroupCreateView.as_view(), name="group-create"),
path('group/<uuid:pk>/update/', groups.GroupUpdateView.as_view(), name='group-update'), path(
path('group/<uuid:pk>/delete/', groups.GroupDeleteView.as_view(), name='group-delete'), "group/<uuid:pk>/update/", groups.GroupUpdateView.as_view(), name="group-update"
),
path(
"group/<uuid:pk>/delete/", groups.GroupDeleteView.as_view(), name="group-delete"
),
# Audit Log # Audit Log
path('audit/', audit.EventListView.as_view(), name='audit-log'), path("audit/", audit.EventListView.as_view(), name="audit-log"),
# Groups # Groups
path('groups/', groups.GroupListView.as_view(), name='groups'), path("groups/", groups.GroupListView.as_view(), name="groups"),
# Debug # Debug
path('debug/request/', debug.DebugRequestView.as_view(), name='debug-request'), path("debug/request/", debug.DebugRequestView.as_view(), name="debug-request"),
] ]

View File

@ -1,8 +1,9 @@
"""passbook Application administration""" """passbook Application administration"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -18,55 +19,61 @@ class ApplicationListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all applications""" """Show list of all applications"""
model = Application model = Application
permission_required = 'passbook_core.view_application' permission_required = "passbook_core.view_application"
ordering = 'name' ordering = "name"
paginate_by = 40 paginate_by = 40
template_name = 'administration/application/list.html' template_name = "administration/application/list.html"
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_subclasses() return super().get_queryset().select_subclasses()
class ApplicationCreateView(SuccessMessageMixin, LoginRequiredMixin, class ApplicationCreateView(
DjangoPermissionRequiredMixin, CreateAssignPermView): SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new Application""" """Create new Application"""
model = Application model = Application
form_class = ApplicationForm form_class = ApplicationForm
permission_required = 'passbook_core.add_application' permission_required = "passbook_core.add_application"
template_name = 'generic/create.html' template_name = "generic/create.html"
success_url = reverse_lazy('passbook_admin:applications') success_url = reverse_lazy("passbook_admin:applications")
success_message = _('Successfully created Application') success_message = _("Successfully created Application")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['type'] = 'Application' kwargs["type"] = "Application"
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class ApplicationUpdateView(SuccessMessageMixin, LoginRequiredMixin, class ApplicationUpdateView(
PermissionRequiredMixin, UpdateView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update application""" """Update application"""
model = Application model = Application
form_class = ApplicationForm form_class = ApplicationForm
permission_required = 'passbook_core.change_application' permission_required = "passbook_core.change_application"
template_name = 'generic/update.html' template_name = "generic/update.html"
success_url = reverse_lazy('passbook_admin:applications') success_url = reverse_lazy("passbook_admin:applications")
success_message = _('Successfully updated Application') success_message = _("Successfully updated Application")
class ApplicationDeleteView(SuccessMessageMixin, LoginRequiredMixin, class ApplicationDeleteView(
PermissionRequiredMixin, DeleteView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete application""" """Delete application"""
model = Application model = Application
permission_required = 'passbook_core.delete_application' permission_required = "passbook_core.delete_application"
template_name = 'generic/delete.html' template_name = "generic/delete.html"
success_url = reverse_lazy('passbook_admin:applications') success_url = reverse_lazy("passbook_admin:applications")
success_message = _('Successfully deleted Application') success_message = _("Successfully deleted Application")
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)

View File

@ -9,10 +9,10 @@ class EventListView(PermissionListMixin, ListView):
"""Show list of all invitations""" """Show list of all invitations"""
model = Event model = Event
template_name = 'administration/audit/list.html' template_name = "administration/audit/list.html"
permission_required = 'passbook_audit.view_event' permission_required = "passbook_audit.view_event"
ordering = '-created' ordering = "-created"
paginate_by = 10 paginate_by = 10
def get_queryset(self): def get_queryset(self):
return Event.objects.all().order_by('-created') return Event.objects.all().order_by("-created")

View File

@ -6,10 +6,10 @@ from django.views.generic import TemplateView
class DebugRequestView(LoginRequiredMixin, TemplateView): class DebugRequestView(LoginRequiredMixin, TemplateView):
"""Show debug info about request""" """Show debug info about request"""
template_name = 'administration/debug/request.html' template_name = "administration/debug/request.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['request_dict'] = {} kwargs["request_dict"] = {}
for key in dir(self.request): for key in dir(self.request):
kwargs['request_dict'][key] = getattr(self.request, key) kwargs["request_dict"][key] = getattr(self.request, key)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -1,8 +1,9 @@
"""passbook Factor administration""" """passbook Factor administration"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -18,62 +19,69 @@ from passbook.lib.views import CreateAssignPermView
def all_subclasses(cls): def all_subclasses(cls):
"""Recursively return all subclassess of cls""" """Recursively return all subclassess of cls"""
return set(cls.__subclasses__()).union( return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)]) [s for c in cls.__subclasses__() for s in all_subclasses(c)]
)
class FactorListView(LoginRequiredMixin, PermissionListMixin, ListView): class FactorListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all factors""" """Show list of all factors"""
model = Factor model = Factor
template_name = 'administration/factor/list.html' template_name = "administration/factor/list.html"
permission_required = 'passbook_core.view_factor' permission_required = "passbook_core.view_factor"
ordering = 'order' ordering = "order"
paginate_by = 40 paginate_by = 40
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['types'] = { kwargs["types"] = {
x.__name__: x._meta.verbose_name for x in all_subclasses(Factor)} x.__name__: x._meta.verbose_name for x in all_subclasses(Factor)
}
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_subclasses() return super().get_queryset().select_subclasses()
class FactorCreateView(SuccessMessageMixin, LoginRequiredMixin, class FactorCreateView(
DjangoPermissionRequiredMixin, CreateAssignPermView): SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new Factor""" """Create new Factor"""
model = Factor model = Factor
template_name = 'generic/create.html' template_name = "generic/create.html"
permission_required = 'passbook_core.add_factor' permission_required = "passbook_core.add_factor"
success_url = reverse_lazy('passbook_admin:factors') success_url = reverse_lazy("passbook_admin:factors")
success_message = _('Successfully created Factor') success_message = _("Successfully created Factor")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
factor_type = self.request.GET.get('type') factor_type = self.request.GET.get("type")
model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type) model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
kwargs['type'] = model._meta.verbose_name kwargs["type"] = model._meta.verbose_name
return kwargs return kwargs
def get_form_class(self): def get_form_class(self):
factor_type = self.request.GET.get('type') factor_type = self.request.GET.get("type")
model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type) model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
if not model: if not model:
raise Http404 raise Http404
return path_to_class(model.form) return path_to_class(model.form)
class FactorUpdateView(SuccessMessageMixin, LoginRequiredMixin, class FactorUpdateView(
PermissionRequiredMixin, UpdateView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update factor""" """Update factor"""
model = Factor model = Factor
permission_required = 'passbook_core.update_application' permission_required = "passbook_core.update_application"
template_name = 'generic/update.html' template_name = "generic/update.html"
success_url = reverse_lazy('passbook_admin:factors') success_url = reverse_lazy("passbook_admin:factors")
success_message = _('Successfully updated Factor') success_message = _("Successfully updated Factor")
def get_form_class(self): def get_form_class(self):
form_class_path = self.get_object().form form_class_path = self.get_object().form
@ -81,21 +89,26 @@ class FactorUpdateView(SuccessMessageMixin, LoginRequiredMixin,
return form_class return form_class
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return (
Factor.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
class FactorDeleteView(SuccessMessageMixin, LoginRequiredMixin, class FactorDeleteView(
PermissionRequiredMixin, DeleteView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete factor""" """Delete factor"""
model = Factor model = Factor
template_name = 'generic/delete.html' template_name = "generic/delete.html"
permission_required = 'passbook_core.delete_factor' permission_required = "passbook_core.delete_factor"
success_url = reverse_lazy('passbook_admin:factors') success_url = reverse_lazy("passbook_admin:factors")
success_message = _('Successfully deleted Factor') success_message = _("Successfully deleted Factor")
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return (
Factor.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)

View File

@ -1,8 +1,9 @@
"""passbook Group administration""" """passbook Group administration"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -18,40 +19,45 @@ class GroupListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all groups""" """Show list of all groups"""
model = Group model = Group
permission_required = 'passbook_core.view_group' permission_required = "passbook_core.view_group"
ordering = 'name' ordering = "name"
paginate_by = 40 paginate_by = 40
template_name = 'administration/group/list.html' template_name = "administration/group/list.html"
class GroupCreateView(SuccessMessageMixin, LoginRequiredMixin, class GroupCreateView(
DjangoPermissionRequiredMixin, CreateAssignPermView): SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new Group""" """Create new Group"""
model = Group model = Group
form_class = GroupForm form_class = GroupForm
permission_required = 'passbook_core.add_group' permission_required = "passbook_core.add_group"
template_name = 'generic/create.html' template_name = "generic/create.html"
success_url = reverse_lazy('passbook_admin:groups') success_url = reverse_lazy("passbook_admin:groups")
success_message = _('Successfully created Group') success_message = _("Successfully created Group")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['type'] = 'Group' kwargs["type"] = "Group"
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class GroupUpdateView(SuccessMessageMixin, LoginRequiredMixin, class GroupUpdateView(
PermissionRequiredMixin, UpdateView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update group""" """Update group"""
model = Group model = Group
form_class = GroupForm form_class = GroupForm
permission_required = 'passbook_core.change_group' permission_required = "passbook_core.change_group"
template_name = 'generic/update.html' template_name = "generic/update.html"
success_url = reverse_lazy('passbook_admin:groups') success_url = reverse_lazy("passbook_admin:groups")
success_message = _('Successfully updated Group') success_message = _("Successfully updated Group")
class GroupDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView): class GroupDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView):
@ -59,9 +65,9 @@ class GroupDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView):
model = Group model = Group
template_name = 'generic/delete.html' template_name = "generic/delete.html"
success_url = reverse_lazy('passbook_admin:groups') success_url = reverse_lazy("passbook_admin:groups")
success_message = _('Successfully deleted Group') success_message = _("Successfully deleted Group")
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)

View File

@ -1,8 +1,9 @@
"""passbook Invitation administration""" """passbook Invitation administration"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -20,47 +21,49 @@ class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all invitations""" """Show list of all invitations"""
model = Invitation model = Invitation
permission_required = 'passbook_core.view_invitation' permission_required = "passbook_core.view_invitation"
template_name = 'administration/invitation/list.html' template_name = "administration/invitation/list.html"
class InvitationCreateView(SuccessMessageMixin, LoginRequiredMixin, class InvitationCreateView(
DjangoPermissionRequiredMixin, CreateAssignPermView): SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new Invitation""" """Create new Invitation"""
model = Invitation model = Invitation
form_class = InvitationForm form_class = InvitationForm
permission_required = 'passbook_core.add_invitation' permission_required = "passbook_core.add_invitation"
template_name = 'generic/create.html' template_name = "generic/create.html"
success_url = reverse_lazy('passbook_admin:invitations') success_url = reverse_lazy("passbook_admin:invitations")
success_message = _('Successfully created Invitation') success_message = _("Successfully created Invitation")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['type'] = 'Invitation' kwargs["type"] = "Invitation"
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def form_valid(self, form): def form_valid(self, form):
obj = form.save(commit=False) obj = form.save(commit=False)
obj.created_by = self.request.user obj.created_by = self.request.user
obj.save() obj.save()
invitation_created.send( invitation_created.send(sender=self, request=self.request, invitation=obj)
sender=self,
request=self.request,
invitation=obj)
return HttpResponseRedirect(self.success_url) return HttpResponseRedirect(self.success_url)
class InvitationDeleteView(SuccessMessageMixin, LoginRequiredMixin, class InvitationDeleteView(
PermissionRequiredMixin, DeleteView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete invitation""" """Delete invitation"""
model = Invitation model = Invitation
permission_required = 'passbook_core.delete_invitation' permission_required = "passbook_core.delete_invitation"
template_name = 'generic/delete.html' template_name = "generic/delete.html"
success_url = reverse_lazy('passbook_admin:invitations') success_url = reverse_lazy("passbook_admin:invitations")
success_message = _('Successfully deleted Invitation') success_message = _("Successfully deleted Invitation")
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)

View File

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

View File

@ -1,8 +1,9 @@
"""passbook Policy administration""" """passbook Policy administration"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -22,49 +23,54 @@ class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all policies""" """Show list of all policies"""
model = Policy model = Policy
permission_required = 'passbook_core.view_policy' permission_required = "passbook_core.view_policy"
template_name = 'administration/policy/list.html' template_name = "administration/policy/list.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['types'] = { kwargs["types"] = {
x.__name__: x._meta.verbose_name for x in Policy.__subclasses__()} x.__name__: x._meta.verbose_name for x in Policy.__subclasses__()
}
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def get_queryset(self): def get_queryset(self):
return super().get_queryset().order_by('order').select_subclasses() return super().get_queryset().order_by("order").select_subclasses()
class PolicyCreateView(SuccessMessageMixin, LoginRequiredMixin, class PolicyCreateView(
DjangoPermissionRequiredMixin, CreateAssignPermView): SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new Policy""" """Create new Policy"""
model = Policy model = Policy
permission_required = 'passbook_core.add_policy' permission_required = "passbook_core.add_policy"
template_name = 'generic/create.html' template_name = "generic/create.html"
success_url = reverse_lazy('passbook_admin:policies') success_url = reverse_lazy("passbook_admin:policies")
success_message = _('Successfully created Policy') success_message = _("Successfully created Policy")
def get_form_class(self): def get_form_class(self):
policy_type = self.request.GET.get('type') policy_type = self.request.GET.get("type")
model = next(x for x in Policy.__subclasses__() model = next(x for x in Policy.__subclasses__() if x.__name__ == policy_type)
if x.__name__ == policy_type)
if not model: if not model:
raise Http404 raise Http404
return path_to_class(model.form) return path_to_class(model.form)
class PolicyUpdateView(SuccessMessageMixin, LoginRequiredMixin, class PolicyUpdateView(
PermissionRequiredMixin, UpdateView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update policy""" """Update policy"""
model = Policy model = Policy
permission_required = 'passbook_core.change_policy' permission_required = "passbook_core.change_policy"
template_name = 'generic/update.html' template_name = "generic/update.html"
success_url = reverse_lazy('passbook_admin:policies') success_url = reverse_lazy("passbook_admin:policies")
success_message = _('Successfully updated Policy') success_message = _("Successfully updated Policy")
def get_form_class(self): def get_form_class(self):
form_class_path = self.get_object().form form_class_path = self.get_object().form
@ -72,22 +78,27 @@ class PolicyUpdateView(SuccessMessageMixin, LoginRequiredMixin,
return form_class return form_class
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return (
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
class PolicyDeleteView(SuccessMessageMixin, LoginRequiredMixin, class PolicyDeleteView(
PermissionRequiredMixin, DeleteView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete policy""" """Delete policy"""
model = Policy model = Policy
permission_required = 'passbook_core.delete_policy' permission_required = "passbook_core.delete_policy"
template_name = 'generic/delete.html' template_name = "generic/delete.html"
success_url = reverse_lazy('passbook_admin:policies') success_url = reverse_lazy("passbook_admin:policies")
success_message = _('Successfully deleted Policy') success_message = _("Successfully deleted Policy")
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return (
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)
@ -99,15 +110,17 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo
model = Policy model = Policy
form_class = PolicyTestForm form_class = PolicyTestForm
permission_required = 'passbook_core.view_policy' permission_required = "passbook_core.view_policy"
template_name = 'administration/policy/test.html' template_name = "administration/policy/test.html"
object = None object = None
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return (
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['policy'] = self.get_object() kwargs["policy"] = self.get_object()
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
@ -116,13 +129,13 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo
def form_valid(self, form): def form_valid(self, form):
policy = self.get_object() policy = self.get_object()
user = form.cleaned_data.get('user') user = form.cleaned_data.get("user")
policy_engine = PolicyEngine([policy], user, self.request) policy_engine = PolicyEngine([policy], user, self.request)
policy_engine.use_cache = False policy_engine.use_cache = False
policy_engine.build() policy_engine.build()
result = policy_engine.passing result = policy_engine.passing
if result: if result:
messages.success(self.request, _('User successfully passed policy.')) messages.success(self.request, _("User successfully passed policy."))
else: else:
messages.error(self.request, _("User didn't pass policy.")) messages.error(self.request, _("User didn't pass policy."))
return self.render_to_response(self.get_context_data(form=form, result=result)) return self.render_to_response(self.get_context_data(form=form, result=result))

View File

@ -1,8 +1,9 @@
"""passbook PropertyMapping administration""" """passbook PropertyMapping administration"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -18,65 +19,78 @@ from passbook.lib.views import CreateAssignPermView
def all_subclasses(cls): def all_subclasses(cls):
"""Recursively return all subclassess of cls""" """Recursively return all subclassess of cls"""
return set(cls.__subclasses__()).union( return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)]) [s for c in cls.__subclasses__() for s in all_subclasses(c)]
)
class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView): class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all property_mappings""" """Show list of all property_mappings"""
model = PropertyMapping model = PropertyMapping
permission_required = 'passbook_core.view_propertymapping' permission_required = "passbook_core.view_propertymapping"
template_name = 'administration/property_mapping/list.html' template_name = "administration/property_mapping/list.html"
ordering = 'name' ordering = "name"
paginate_by = 40 paginate_by = 40
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['types'] = { kwargs["types"] = {
x.__name__: x._meta.verbose_name for x in all_subclasses(PropertyMapping)} x.__name__: x._meta.verbose_name for x in all_subclasses(PropertyMapping)
}
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_subclasses() return super().get_queryset().select_subclasses()
class PropertyMappingCreateView(SuccessMessageMixin, LoginRequiredMixin, class PropertyMappingCreateView(
DjangoPermissionRequiredMixin, CreateAssignPermView): SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new PropertyMapping""" """Create new PropertyMapping"""
model = PropertyMapping model = PropertyMapping
permission_required = 'passbook_core.add_propertymapping' permission_required = "passbook_core.add_propertymapping"
template_name = 'generic/create.html' template_name = "generic/create.html"
success_url = reverse_lazy('passbook_admin:property-mappings') success_url = reverse_lazy("passbook_admin:property-mappings")
success_message = _('Successfully created Property Mapping') success_message = _("Successfully created Property Mapping")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
property_mapping_type = self.request.GET.get('type') property_mapping_type = self.request.GET.get("type")
model = next(x for x in all_subclasses(PropertyMapping) model = next(
if x.__name__ == property_mapping_type) x
kwargs['type'] = model._meta.verbose_name for x in all_subclasses(PropertyMapping)
if x.__name__ == property_mapping_type
)
kwargs["type"] = model._meta.verbose_name
return kwargs return kwargs
def get_form_class(self): def get_form_class(self):
property_mapping_type = self.request.GET.get('type') property_mapping_type = self.request.GET.get("type")
model = next(x for x in all_subclasses(PropertyMapping) model = next(
if x.__name__ == property_mapping_type) x
for x in all_subclasses(PropertyMapping)
if x.__name__ == property_mapping_type
)
if not model: if not model:
raise Http404 raise Http404
return path_to_class(model.form) return path_to_class(model.form)
class PropertyMappingUpdateView(SuccessMessageMixin, LoginRequiredMixin, class PropertyMappingUpdateView(
PermissionRequiredMixin, UpdateView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update property_mapping""" """Update property_mapping"""
model = PropertyMapping model = PropertyMapping
permission_required = 'passbook_core.change_propertymapping' permission_required = "passbook_core.change_propertymapping"
template_name = 'generic/update.html' template_name = "generic/update.html"
success_url = reverse_lazy('passbook_admin:property-mappings') success_url = reverse_lazy("passbook_admin:property-mappings")
success_message = _('Successfully updated Property Mapping') success_message = _("Successfully updated Property Mapping")
def get_form_class(self): def get_form_class(self):
form_class_path = self.get_object().form form_class_path = self.get_object().form
@ -84,22 +98,31 @@ class PropertyMappingUpdateView(SuccessMessageMixin, LoginRequiredMixin,
return form_class return form_class
def get_object(self, queryset=None): def get_object(self, queryset=None):
return PropertyMapping.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return (
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
class PropertyMappingDeleteView(SuccessMessageMixin, LoginRequiredMixin, class PropertyMappingDeleteView(
PermissionRequiredMixin, DeleteView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete property_mapping""" """Delete property_mapping"""
model = PropertyMapping model = PropertyMapping
permission_required = 'passbook_core.delete_propertymapping' permission_required = "passbook_core.delete_propertymapping"
template_name = 'generic/delete.html' template_name = "generic/delete.html"
success_url = reverse_lazy('passbook_admin:property-mappings') success_url = reverse_lazy("passbook_admin:property-mappings")
success_message = _('Successfully deleted Property Mapping') success_message = _("Successfully deleted Property Mapping")
def get_object(self, queryset=None): def get_object(self, queryset=None):
return PropertyMapping.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return (
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)

View File

@ -1,8 +1,9 @@
"""passbook Provider administration""" """passbook Provider administration"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -19,48 +20,55 @@ class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all providers""" """Show list of all providers"""
model = Provider model = Provider
permission_required = 'passbook_core.add_provider' permission_required = "passbook_core.add_provider"
template_name = 'administration/provider/list.html' template_name = "administration/provider/list.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['types'] = { kwargs["types"] = {
x.__name__: x._meta.verbose_name for x in Provider.__subclasses__()} x.__name__: x._meta.verbose_name for x in Provider.__subclasses__()
}
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_subclasses() return super().get_queryset().select_subclasses()
class ProviderCreateView(SuccessMessageMixin, LoginRequiredMixin, class ProviderCreateView(
DjangoPermissionRequiredMixin, CreateAssignPermView): SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new Provider""" """Create new Provider"""
model = Provider model = Provider
permission_required = 'passbook_core.add_provider' permission_required = "passbook_core.add_provider"
template_name = 'generic/create.html' template_name = "generic/create.html"
success_url = reverse_lazy('passbook_admin:providers') success_url = reverse_lazy("passbook_admin:providers")
success_message = _('Successfully created Provider') success_message = _("Successfully created Provider")
def get_form_class(self): def get_form_class(self):
provider_type = self.request.GET.get('type') provider_type = self.request.GET.get("type")
model = next(x for x in Provider.__subclasses__() model = next(
if x.__name__ == provider_type) x for x in Provider.__subclasses__() if x.__name__ == provider_type
)
if not model: if not model:
raise Http404 raise Http404
return path_to_class(model.form) return path_to_class(model.form)
class ProviderUpdateView(SuccessMessageMixin, LoginRequiredMixin, class ProviderUpdateView(
PermissionRequiredMixin, UpdateView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update provider""" """Update provider"""
model = Provider model = Provider
permission_required = 'passbook_core.change_provider' permission_required = "passbook_core.change_provider"
template_name = 'generic/update.html' template_name = "generic/update.html"
success_url = reverse_lazy('passbook_admin:providers') success_url = reverse_lazy("passbook_admin:providers")
success_message = _('Successfully updated Provider') success_message = _("Successfully updated Provider")
def get_form_class(self): def get_form_class(self):
form_class_path = self.get_object().form form_class_path = self.get_object().form
@ -68,22 +76,31 @@ class ProviderUpdateView(SuccessMessageMixin, LoginRequiredMixin,
return form_class return form_class
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Provider.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return (
Provider.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
class ProviderDeleteView(SuccessMessageMixin, LoginRequiredMixin, class ProviderDeleteView(
PermissionRequiredMixin, DeleteView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete provider""" """Delete provider"""
model = Provider model = Provider
permission_required = 'passbook_core.delete_provider' permission_required = "passbook_core.delete_provider"
template_name = 'generic/delete.html' template_name = "generic/delete.html"
success_url = reverse_lazy('passbook_admin:providers') success_url = reverse_lazy("passbook_admin:providers")
success_message = _('Successfully deleted Provider') success_message = _("Successfully deleted Provider")
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Provider.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return (
Provider.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)

View File

@ -1,8 +1,9 @@
"""passbook Source administration""" """passbook Source administration"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -18,55 +19,63 @@ from passbook.lib.views import CreateAssignPermView
def all_subclasses(cls): def all_subclasses(cls):
"""Recursively return all subclassess of cls""" """Recursively return all subclassess of cls"""
return set(cls.__subclasses__()).union( return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)]) [s for c in cls.__subclasses__() for s in all_subclasses(c)]
)
class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView): class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all sources""" """Show list of all sources"""
model = Source model = Source
permission_required = 'passbook_core.view_source' permission_required = "passbook_core.view_source"
ordering = 'name' ordering = "name"
paginate_by = 40 paginate_by = 40
template_name = 'administration/source/list.html' template_name = "administration/source/list.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['types'] = { kwargs["types"] = {
x.__name__: x._meta.verbose_name for x in all_subclasses(Source)} x.__name__: x._meta.verbose_name for x in all_subclasses(Source)
}
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_subclasses() return super().get_queryset().select_subclasses()
class SourceCreateView(SuccessMessageMixin, LoginRequiredMixin, class SourceCreateView(
DjangoPermissionRequiredMixin, CreateAssignPermView): SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new Source""" """Create new Source"""
model = Source model = Source
permission_required = 'passbook_core.add_source' permission_required = "passbook_core.add_source"
template_name = 'generic/create.html' template_name = "generic/create.html"
success_url = reverse_lazy('passbook_admin:sources') success_url = reverse_lazy("passbook_admin:sources")
success_message = _('Successfully created Source') success_message = _("Successfully created Source")
def get_form_class(self): def get_form_class(self):
source_type = self.request.GET.get('type') source_type = self.request.GET.get("type")
model = next(x for x in all_subclasses(Source) if x.__name__ == source_type) model = next(x for x in all_subclasses(Source) if x.__name__ == source_type)
if not model: if not model:
raise Http404 raise Http404
return path_to_class(model.form) return path_to_class(model.form)
class SourceUpdateView(SuccessMessageMixin, LoginRequiredMixin, class SourceUpdateView(
PermissionRequiredMixin, UpdateView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update source""" """Update source"""
model = Source model = Source
permission_required = 'passbook_core.change_source' permission_required = "passbook_core.change_source"
template_name = 'generic/update.html' template_name = "generic/update.html"
success_url = reverse_lazy('passbook_admin:sources') success_url = reverse_lazy("passbook_admin:sources")
success_message = _('Successfully updated Source') success_message = _("Successfully updated Source")
def get_form_class(self): def get_form_class(self):
form_class_path = self.get_object().form form_class_path = self.get_object().form
@ -74,22 +83,27 @@ class SourceUpdateView(SuccessMessageMixin, LoginRequiredMixin,
return form_class return form_class
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Source.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return (
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
class SourceDeleteView(SuccessMessageMixin, LoginRequiredMixin, class SourceDeleteView(
PermissionRequiredMixin, DeleteView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete source""" """Delete source"""
model = Source model = Source
permission_required = 'passbook_core.delete_source' permission_required = "passbook_core.delete_source"
template_name = 'generic/delete.html' template_name = "generic/delete.html"
success_url = reverse_lazy('passbook_admin:sources') success_url = reverse_lazy("passbook_admin:sources")
success_message = _('Successfully deleted Source') success_message = _("Successfully deleted Source")
def get_object(self, queryset=None): def get_object(self, queryset=None):
return Source.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first() return (
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)

View File

@ -1,8 +1,9 @@
"""passbook User administration""" """passbook User administration"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
@ -19,50 +20,56 @@ class UserListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all users""" """Show list of all users"""
model = User model = User
permission_required = 'passbook_core.view_user' permission_required = "passbook_core.view_user"
ordering = 'username' ordering = "username"
paginate_by = 40 paginate_by = 40
template_name = 'administration/user/list.html' template_name = "administration/user/list.html"
class UserCreateView(SuccessMessageMixin, LoginRequiredMixin, class UserCreateView(
DjangoPermissionRequiredMixin, CreateAssignPermView): SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create user""" """Create user"""
model = User model = User
form_class = UserForm form_class = UserForm
permission_required = 'passbook_core.add_user' permission_required = "passbook_core.add_user"
template_name = 'generic/create.html' template_name = "generic/create.html"
success_url = reverse_lazy('passbook_admin:users') success_url = reverse_lazy("passbook_admin:users")
success_message = _('Successfully created User') success_message = _("Successfully created User")
class UserUpdateView(SuccessMessageMixin, LoginRequiredMixin, class UserUpdateView(
PermissionRequiredMixin, UpdateView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update user""" """Update user"""
model = User model = User
form_class = UserForm form_class = UserForm
permission_required = 'passbook_core.change_user' permission_required = "passbook_core.change_user"
context_object_name = 'object' # By default the object's name # By default the object's name is user which is used by other checks
# is user which is used by other checks context_object_name = "object"
template_name = 'generic/update.html' template_name = "generic/update.html"
success_url = reverse_lazy('passbook_admin:users') success_url = reverse_lazy("passbook_admin:users")
success_message = _('Successfully updated User') success_message = _("Successfully updated User")
class UserDeleteView(SuccessMessageMixin, LoginRequiredMixin, class UserDeleteView(
PermissionRequiredMixin, DeleteView): SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete user""" """Delete user"""
model = User model = User
permission_required = 'passbook_core.delete_user' permission_required = "passbook_core.delete_user"
template_name = 'generic/delete.html' template_name = "generic/delete.html"
success_url = reverse_lazy('passbook_admin:users') success_url = reverse_lazy("passbook_admin:users")
success_message = _('Successfully deleted User') success_message = _("Successfully deleted User")
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)
@ -73,13 +80,16 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
"""Get Password reset link for user""" """Get Password reset link for user"""
model = User model = User
permission_required = 'passbook_core.reset_user_password' permission_required = "passbook_core.reset_user_password"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Create nonce for user and return link""" """Create nonce for user and return link"""
super().get(request, *args, **kwargs) super().get(request, *args, **kwargs)
nonce = Nonce.objects.create(user=self.object) nonce = Nonce.objects.create(user=self.object)
link = request.build_absolute_uri(reverse( link = request.build_absolute_uri(
'passbook_core:auth-password-reset', kwargs={'nonce': nonce.uuid})) reverse("passbook_core:auth-password-reset", kwargs={"nonce": nonce.uuid})
messages.success(request, _('Password reset link: <pre>%(link)s</pre>' % {'link': link})) )
return redirect('passbook_admin:users') messages.success(
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})
)
return redirect("passbook_admin:users")

View File

@ -6,7 +6,7 @@ from django.apps import AppConfig
class PassbookAPIConfig(AppConfig): class PassbookAPIConfig(AppConfig):
"""passbook API Config""" """passbook API Config"""
name = 'passbook.api' name = "passbook.api"
label = 'passbook_api' label = "passbook_api"
mountpoint = 'api/' mountpoint = "api/"
verbose_name = 'passbook API' verbose_name = "passbook API"

View File

@ -9,13 +9,13 @@ class CustomObjectPermissions(DjangoObjectPermissions):
"""Similar to `DjangoObjectPermissions`, but adding 'view' permissions.""" """Similar to `DjangoObjectPermissions`, but adding 'view' permissions."""
perms_map = { perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'], "GET": ["%(app_label)s.view_%(model_name)s"],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'], "OPTIONS": ["%(app_label)s.view_%(model_name)s"],
'HEAD': ['%(app_label)s.view_%(model_name)s'], "HEAD": ["%(app_label)s.view_%(model_name)s"],
'POST': ['%(app_label)s.add_%(model_name)s'], "POST": ["%(app_label)s.add_%(model_name)s"],
'PUT': ['%(app_label)s.change_%(model_name)s'], "PUT": ["%(app_label)s.change_%(model_name)s"],
'PATCH': ['%(app_label)s.change_%(model_name)s'], "PATCH": ["%(app_label)s.change_%(model_name)s"],
'DELETE': ['%(app_label)s.delete_%(model_name)s'], "DELETE": ["%(app_label)s.delete_%(model_name)s"],
} }

View File

@ -5,6 +5,6 @@ from passbook.api.v1.urls import urlpatterns as v1_urls
from passbook.api.v2.urls import urlpatterns as v2_urls from passbook.api.v2.urls import urlpatterns as v2_urls
urlpatterns = [ urlpatterns = [
path('v1/', include(v1_urls)), path("v1/", include(v1_urls)),
path('v2/', include(v2_urls)), path("v2/", include(v2_urls)),
] ]

View File

@ -7,16 +7,16 @@ from oauth2_provider.views.mixins import ScopedResourceMixin
class OpenIDUserInfoView(ScopedResourceMixin, View): class OpenIDUserInfoView(ScopedResourceMixin, View):
"""Passbook v1 OpenID API""" """Passbook v1 OpenID API"""
required_scopes = ['openid:userinfo'] required_scopes = ["openid:userinfo"]
def get(self, request, *args, **kwargs): def get(self, request, *_, **__):
"""Passbook v1 OpenID API""" """Passbook v1 OpenID API"""
payload = { payload = {
'sub': request.user.uuid.int, "sub": request.user.uuid.int,
'name': request.user.get_full_name(), "name": request.user.get_full_name(),
'given_name': request.user.name, "given_name": request.user.name,
'family_name': '', "family_name": "",
'preferred_username': request.user.username, "preferred_username": request.user.username,
'email': request.user.email, "email": request.user.email,
} }
return JsonResponse(payload) return JsonResponse(payload)

View File

@ -3,6 +3,4 @@ from django.urls import path
from passbook.api.v1.openid import OpenIDUserInfoView from passbook.api.v1.openid import OpenIDUserInfoView
urlpatterns = [ urlpatterns = [path("openid/", OpenIDUserInfoView.as_view(), name="openid")]
path('openid/', OpenIDUserInfoView.as_view(), name='openid')
]

View File

@ -34,70 +34,73 @@ from passbook.policies.webhook.api import WebhookPolicyViewSet
from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet
from passbook.providers.oauth.api import OAuth2ProviderViewSet from passbook.providers.oauth.api import OAuth2ProviderViewSet
from passbook.providers.oidc.api import OpenIDProviderViewSet from passbook.providers.oidc.api import OpenIDProviderViewSet
from passbook.providers.saml.api import (SAMLPropertyMappingViewSet, from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet
SAMLProviderViewSet) from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
from passbook.sources.ldap.api import (LDAPPropertyMappingViewSet,
LDAPSourceViewSet)
from passbook.sources.oauth.api import OAuthSourceViewSet from passbook.sources.oauth.api import OAuthSourceViewSet
LOGGER = get_logger() LOGGER = get_logger()
router = routers.DefaultRouter() router = routers.DefaultRouter()
for _passbook_app in get_apps(): for _passbook_app in get_apps():
if hasattr(_passbook_app, 'api_mountpoint'): if hasattr(_passbook_app, "api_mountpoint"):
for prefix, viewset in _passbook_app.api_mountpoint: for prefix, viewset in _passbook_app.api_mountpoint:
router.register(prefix, viewset) router.register(prefix, viewset)
LOGGER.debug("Mounted API URLs", app_name=_passbook_app.name) LOGGER.debug("Mounted API URLs", app_name=_passbook_app.name)
router.register('core/applications', ApplicationViewSet) router.register("core/applications", ApplicationViewSet)
router.register('core/invitations', InvitationViewSet) router.register("core/invitations", InvitationViewSet)
router.register('core/groups', GroupViewSet) router.register("core/groups", GroupViewSet)
router.register('core/users', UserViewSet) router.register("core/users", UserViewSet)
router.register('audit/events', EventViewSet) router.register("audit/events", EventViewSet)
router.register('sources/all', SourceViewSet) router.register("sources/all", SourceViewSet)
router.register('sources/ldap', LDAPSourceViewSet) router.register("sources/ldap", LDAPSourceViewSet)
router.register('sources/oauth', OAuthSourceViewSet) router.register("sources/oauth", OAuthSourceViewSet)
router.register('policies/all', PolicyViewSet) router.register("policies/all", PolicyViewSet)
router.register('policies/passwordexpiry', PasswordExpiryPolicyViewSet) router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
router.register('policies/groupmembership', GroupMembershipPolicyViewSet) router.register("policies/groupmembership", GroupMembershipPolicyViewSet)
router.register('policies/haveibeenpwned', HaveIBeenPwendPolicyViewSet) router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
router.register('policies/fieldmatcher', FieldMatcherPolicyViewSet) router.register("policies/fieldmatcher", FieldMatcherPolicyViewSet)
router.register('policies/password', PasswordPolicyViewSet) router.register("policies/password", PasswordPolicyViewSet)
router.register('policies/reputation', ReputationPolicyViewSet) router.register("policies/reputation", ReputationPolicyViewSet)
router.register('policies/ssologin', SSOLoginPolicyViewSet) router.register("policies/ssologin", SSOLoginPolicyViewSet)
router.register('policies/webhook', WebhookPolicyViewSet) router.register("policies/webhook", WebhookPolicyViewSet)
router.register('providers/all', ProviderViewSet) router.register("providers/all", ProviderViewSet)
router.register('providers/applicationgateway', ApplicationGatewayProviderViewSet) router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet)
router.register('providers/oauth', OAuth2ProviderViewSet) router.register("providers/oauth", OAuth2ProviderViewSet)
router.register('providers/openid', OpenIDProviderViewSet) router.register("providers/openid", OpenIDProviderViewSet)
router.register('providers/saml', SAMLProviderViewSet) router.register("providers/saml", SAMLProviderViewSet)
router.register('propertymappings/all', PropertyMappingViewSet) router.register("propertymappings/all", PropertyMappingViewSet)
router.register('propertymappings/ldap', LDAPPropertyMappingViewSet) router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
router.register('propertymappings/saml', SAMLPropertyMappingViewSet) router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
router.register('factors/all', FactorViewSet) router.register("factors/all", FactorViewSet)
router.register('factors/captcha', CaptchaFactorViewSet) router.register("factors/captcha", CaptchaFactorViewSet)
router.register('factors/dummy', DummyFactorViewSet) router.register("factors/dummy", DummyFactorViewSet)
router.register('factors/email', EmailFactorViewSet) router.register("factors/email", EmailFactorViewSet)
router.register('factors/otp', OTPFactorViewSet) router.register("factors/otp", OTPFactorViewSet)
router.register('factors/password', PasswordFactorViewSet) router.register("factors/password", PasswordFactorViewSet)
info = openapi.Info( info = openapi.Info(
title="passbook API", title="passbook API",
default_version='v2', default_version="v2",
# description="Test description", # description="Test description",
# terms_of_service="https://www.google.com/policies/terms/", # terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="hello@beryju.org"), contact=openapi.Contact(email="hello@beryju.org"),
license=openapi.License(name="MIT License"), license=openapi.License(name="MIT License"),
) )
SchemaView = get_schema_view( SchemaView = get_schema_view(
info, info, public=True, permission_classes=(CustomObjectPermissions,),
public=True,
permission_classes=(CustomObjectPermissions,),
) )
urlpatterns = [ urlpatterns = [
url(r'^swagger(?P<format>\.json|\.yaml)$', url(
SchemaView.without_ui(cache_timeout=0), name='schema-json'), r"^swagger(?P<format>\.json|\.yaml)$",
path('swagger/', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), SchemaView.without_ui(cache_timeout=0),
path('redoc/', SchemaView.with_ui('redoc', cache_timeout=0), name='schema-redoc'), name="schema-json",
),
path(
"swagger/",
SchemaView.with_ui("swagger", cache_timeout=0),
name="schema-swagger-ui",
),
path("redoc/", SchemaView.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
] + router.urls ] + router.urls

View File

@ -2,4 +2,4 @@
from passbook.lib.admin import admin_autoregister from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_audit') admin_autoregister("passbook_audit")

View File

@ -11,7 +11,16 @@ class EventSerializer(ModelSerializer):
class Meta: class Meta:
model = Event model = Event
fields = ['pk', 'user', 'action', 'date', 'app', 'context', 'request_ip', 'created', ] fields = [
"pk",
"user",
"action",
"date",
"app",
"context",
"request_ip",
"created",
]
class EventViewSet(ReadOnlyModelViewSet): class EventViewSet(ReadOnlyModelViewSet):

View File

@ -7,10 +7,10 @@ from django.apps import AppConfig
class PassbookAuditConfig(AppConfig): class PassbookAuditConfig(AppConfig):
"""passbook audit app""" """passbook audit app"""
name = 'passbook.audit' name = "passbook.audit"
label = 'passbook_audit' label = "passbook_audit"
verbose_name = 'passbook Audit' verbose_name = "passbook Audit"
mountpoint = 'audit/' mountpoint = "audit/"
def ready(self): def ready(self):
import_module('passbook.audit.signals') import_module("passbook.audit.signals")

View File

@ -18,20 +18,55 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='AuditEntry', name="AuditEntry",
fields=[ fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('action', models.TextField(choices=[('login', 'login'), ('login_failed', 'login_failed'), ('logout', 'logout'), ('authorize_application', 'authorize_application'), ('suspicious_request', 'suspicious_request'), ('sign_up', 'sign_up'), ('password_reset', 'password_reset'), ('invitation_created', 'invitation_created'), ('invitation_used', 'invitation_used')])), "uuid",
('date', models.DateTimeField(auto_now_add=True)), models.UUIDField(
('app', models.TextField()), default=uuid.uuid4,
('context', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)), editable=False,
('request_ip', models.GenericIPAddressField()), primary_key=True,
('created', models.DateTimeField(auto_now_add=True)), serialize=False,
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), ),
),
(
"action",
models.TextField(
choices=[
("login", "login"),
("login_failed", "login_failed"),
("logout", "logout"),
("authorize_application", "authorize_application"),
("suspicious_request", "suspicious_request"),
("sign_up", "sign_up"),
("password_reset", "password_reset"),
("invitation_created", "invitation_created"),
("invitation_used", "invitation_used"),
]
),
),
("date", models.DateTimeField(auto_now_add=True)),
("app", models.TextField()),
(
"context",
django.contrib.postgres.fields.jsonb.JSONField(
blank=True, default=dict
),
),
("request_ip", models.GenericIPAddressField()),
("created", models.DateTimeField(auto_now_add=True)),
(
"user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
], ],
options={ options={
'verbose_name': 'Audit Entry', "verbose_name": "Audit Entry",
'verbose_name_plural': 'Audit Entries', "verbose_name_plural": "Audit Entries",
}, },
), ),
] ]

View File

@ -8,12 +8,9 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('passbook_audit', '0001_initial'), ("passbook_audit", "0001_initial"),
] ]
operations = [ operations = [
migrations.RenameModel( migrations.RenameModel(old_name="AuditEntry", new_name="Event",),
old_name='AuditEntry',
new_name='Event',
),
] ]

View File

@ -0,0 +1,40 @@
# Generated by Django 2.2.8 on 2019-12-05 14:07
from django.db import migrations, models
import passbook.audit.models
class Migration(migrations.Migration):
dependencies = [
("passbook_audit", "0002_auto_20191028_0829"),
]
operations = [
migrations.AlterModelOptions(
name="event",
options={
"verbose_name": "Audit Event",
"verbose_name_plural": "Audit Events",
},
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("LOGIN", "login"),
("LOGIN_FAILED", "login_failed"),
("LOGOUT", "logout"),
("AUTHORIZE_APPLICATION", "authorize_application"),
("SUSPICIOUS_REQUEST", "suspicious_request"),
("SIGN_UP", "sign_up"),
("PASSWORD_RESET", "password_reset"),
("INVITE_CREATED", "invitation_created"),
("INVITE_USED", "invitation_used"),
("CUSTOM", "custom"),
]
),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.8 on 2019-12-05 15:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_audit", "0003_auto_20191205_1407"),
]
operations = [
migrations.RemoveField(model_name="event", name="request_ip",),
migrations.AddField(
model_name="event",
name="client_ip",
field=models.GenericIPAddressField(null=True),
),
]

View File

@ -1,75 +1,137 @@
"""passbook audit models""" """passbook audit models"""
from enum import Enum
from uuid import UUID
from inspect import getmodule, stack
from typing import Optional, Dict, Any
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.http import HttpRequest
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from ipware import get_client_ip from guardian.shortcuts import get_anonymous_user
from structlog import get_logger from structlog import get_logger
from passbook.lib.models import UUIDModel from passbook.lib.models import UUIDModel
from passbook.lib.utils.http import get_client_ip
LOGGER = get_logger() LOGGER = get_logger()
def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
"""clean source of all Models that would interfere with the JSONField.
Models are replaced with a dictionary of {
app: str,
name: str,
pk: Any
}"""
for key, value in source.items():
if isinstance(value, dict):
source[key] = sanitize_dict(value)
elif isinstance(value, models.Model):
model_content_type = ContentType.objects.get_for_model(value)
source[key] = sanitize_dict(
{
"app": model_content_type.app_label,
"name": model_content_type.model,
"pk": value.pk,
}
)
elif isinstance(value, UUID):
source[key] = value.hex
return source
class EventAction(Enum):
"""All possible actions to save into the audit log"""
LOGIN = "login"
LOGIN_FAILED = "login_failed"
LOGOUT = "logout"
AUTHORIZE_APPLICATION = "authorize_application"
SUSPICIOUS_REQUEST = "suspicious_request"
SIGN_UP = "sign_up"
PASSWORD_RESET = "password_reset" # noqa # nosec
INVITE_CREATED = "invitation_created"
INVITE_USED = "invitation_used"
CUSTOM = "custom"
@staticmethod
def as_choices():
"""Generate choices of actions used for database"""
return tuple((x, y.value) for x, y in EventAction.__members__.items())
class Event(UUIDModel): class Event(UUIDModel):
"""An individual audit log event""" """An individual audit log event"""
ACTION_LOGIN = 'login' user = models.ForeignKey(
ACTION_LOGIN_FAILED = 'login_failed' settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
ACTION_LOGOUT = 'logout'
ACTION_AUTHORIZE_APPLICATION = 'authorize_application'
ACTION_SUSPICIOUS_REQUEST = 'suspicious_request'
ACTION_SIGN_UP = 'sign_up'
ACTION_PASSWORD_RESET = 'password_reset' # noqa # nosec
ACTION_INVITE_CREATED = 'invitation_created'
ACTION_INVITE_USED = 'invitation_used'
ACTIONS = (
(ACTION_LOGIN, ACTION_LOGIN),
(ACTION_LOGIN_FAILED, ACTION_LOGIN_FAILED),
(ACTION_LOGOUT, ACTION_LOGOUT),
(ACTION_AUTHORIZE_APPLICATION, ACTION_AUTHORIZE_APPLICATION),
(ACTION_SUSPICIOUS_REQUEST, ACTION_SUSPICIOUS_REQUEST),
(ACTION_SIGN_UP, ACTION_SIGN_UP),
(ACTION_PASSWORD_RESET, ACTION_PASSWORD_RESET),
(ACTION_INVITE_CREATED, ACTION_INVITE_CREATED),
(ACTION_INVITE_USED, ACTION_INVITE_USED),
) )
action = models.TextField(choices=EventAction.as_choices())
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)
action = models.TextField(choices=ACTIONS)
date = models.DateTimeField(auto_now_add=True) date = models.DateTimeField(auto_now_add=True)
app = models.TextField() app = models.TextField()
context = JSONField(default=dict, blank=True) context = JSONField(default=dict, blank=True)
request_ip = models.GenericIPAddressField() client_ip = models.GenericIPAddressField(null=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
@staticmethod @staticmethod
def create(action, request, **kwargs): def _get_app_from_request(request: HttpRequest) -> str:
"""Create Event from arguments""" if not isinstance(request, HttpRequest):
client_ip, _ = get_client_ip(request) return ""
if not hasattr(request, 'user'): return request.resolver_match.app_name
user = None
else: @staticmethod
user = request.user def new(
if isinstance(user, AnonymousUser): action: EventAction,
user = kwargs.get('user', None) app: Optional[str] = None,
entry = Event.objects.create( _inspect_offset: int = 1,
action=action, **kwargs,
user=user, ) -> "Event":
# User 255.255.255.255 as fallback if IP cannot be determined """Create new Event instance from arguments. Instance is NOT saved."""
request_ip=client_ip or '255.255.255.255', if not isinstance(action, EventAction):
context=kwargs) raise ValueError(
LOGGER.debug("Created Audit entry", action=action, f"action must be EventAction instance but was {type(action)}"
user=user, from_ip=client_ip, context=kwargs) )
return entry if not app:
app = getmodule(stack()[_inspect_offset][0]).__name__
cleaned_kwargs = sanitize_dict(kwargs)
event = Event(action=action.value, app=app, context=cleaned_kwargs)
LOGGER.debug("Created Audit event", action=action, context=cleaned_kwargs)
return event
def from_http(
self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None
) -> "Event":
"""Add data from a Django-HttpRequest, allowing the creation of
Events independently from requests.
`user` arguments optionally overrides user from requests."""
if hasattr(request, "user"):
if isinstance(request.user, AnonymousUser):
self.user = get_anonymous_user()
else:
self.user = request.user
if user:
self.user = user
# User 255.255.255.255 as fallback if IP cannot be determined
self.client_ip = get_client_ip(request) or "255.255.255.255"
# If there's no app set, we get it from the requests too
if not self.app:
self.app = Event._get_app_from_request(request)
self.save()
return self
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self._state.adding: if not self._state.adding:
raise ValidationError("you may not edit an existing %s" % self._meta.model_name) raise ValidationError(
super().save(*args, **kwargs) "you may not edit an existing %s" % self._meta.model_name
)
return super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _('Audit Entry') verbose_name = _("Audit Event")
verbose_name_plural = _('Audit Entries') verbose_name_plural = _("Audit Events")

View File

@ -2,34 +2,44 @@
from django.contrib.auth.signals import user_logged_in, user_logged_out from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.dispatch import receiver from django.dispatch import receiver
from passbook.audit.models import Event from passbook.audit.models import Event, EventAction
from passbook.core.signals import (invitation_created, invitation_used, from passbook.core.signals import invitation_created, invitation_used, user_signed_up
user_signed_up)
@receiver(user_logged_in) @receiver(user_logged_in)
def on_user_logged_in(sender, request, user, **kwargs): # pylint: disable=unused-argument
def on_user_logged_in(sender, request, user, **_):
"""Log successful login""" """Log successful login"""
Event.create(Event.ACTION_LOGIN, request) Event.new(EventAction.LOGIN).from_http(request)
@receiver(user_logged_out) @receiver(user_logged_out)
def on_user_logged_out(sender, request, user, **kwargs): # pylint: disable=unused-argument
def on_user_logged_out(sender, request, user, **_):
"""Log successfully logout""" """Log successfully logout"""
Event.create(Event.ACTION_LOGOUT, request) Event.new(EventAction.LOGOUT).from_http(request)
@receiver(user_signed_up) @receiver(user_signed_up)
def on_user_signed_up(sender, request, user, **kwargs): # pylint: disable=unused-argument
def on_user_signed_up(sender, request, user, **_):
"""Log successfully signed up""" """Log successfully signed up"""
Event.create(Event.ACTION_SIGN_UP, request) Event.new(EventAction.SIGN_UP).from_http(request)
@receiver(invitation_created) @receiver(invitation_created)
def on_invitation_created(sender, request, invitation, **kwargs): # pylint: disable=unused-argument
def on_invitation_created(sender, request, invitation, **_):
"""Log Invitation creation""" """Log Invitation creation"""
Event.create(Event.ACTION_INVITE_CREATED, request, Event.new(
invitation_uuid=invitation.uuid.hex) EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex
).from_http(request)
@receiver(invitation_used) @receiver(invitation_used)
def on_invitation_used(sender, request, invitation, **kwargs): # pylint: disable=unused-argument
def on_invitation_used(sender, request, invitation, **_):
"""Log Invitation usage""" """Log Invitation usage"""
Event.create(Event.ACTION_INVITE_USED, request, Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http(
invitation_uuid=invitation.uuid.hex) request
)

View File

View File

@ -0,0 +1,33 @@
"""audit event tests"""
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from passbook.audit.models import Event, EventAction
from passbook.core.models import Policy
class TestAuditEvent(TestCase):
"""Test Audit Event"""
def test_new_with_model(self):
"""Create a new Event passing a model as kwarg"""
event = Event.new(EventAction.CUSTOM, test={"model": get_anonymous_user()})
event.save() # We save to ensure nothing is un-saveable
model_content_type = ContentType.objects.get_for_model(get_anonymous_user())
self.assertEqual(
event.context.get("test").get("model").get("app"),
model_content_type.app_label,
)
def test_new_with_uuid_model(self):
"""Create a new Event passing a model (with UUID PK) as kwarg"""
temp_model = Policy.objects.create()
event = Event.new(EventAction.CUSTOM, model=temp_model)
event.save() # We save to ensure nothing is un-saveable
model_content_type = ContentType.objects.get_for_model(temp_model)
self.assertEqual(
event.context.get("model").get("app"), model_content_type.app_label
)
self.assertEqual(event.context.get("model").get("pk"), temp_model.pk.hex)

View File

@ -2,4 +2,4 @@
from passbook.lib.admin import admin_autoregister from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_core') admin_autoregister("passbook_core")

View File

@ -11,8 +11,16 @@ class ApplicationSerializer(ModelSerializer):
class Meta: class Meta:
model = Application model = Application
fields = ['pk', 'name', 'slug', 'launch_url', 'icon_url', fields = [
'provider', 'policies', 'skip_authorization'] "pk",
"name",
"slug",
"launch_url",
"icon_url",
"provider",
"policies",
"skip_authorization",
]
class ApplicationViewSet(ModelViewSet): class ApplicationViewSet(ModelViewSet):

View File

@ -8,16 +8,16 @@ from passbook.core.models import Factor
class FactorSerializer(ModelSerializer): class FactorSerializer(ModelSerializer):
"""Factor Serializer""" """Factor Serializer"""
__type__ = SerializerMethodField(method_name='get_type') __type__ = SerializerMethodField(method_name="get_type")
def get_type(self, obj): def get_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object""" """Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace('factor', '') return obj._meta.object_name.lower().replace("factor", "")
class Meta: class Meta:
model = Factor model = Factor
fields = ['pk', 'name', 'slug', 'order', 'enabled', '__type__'] fields = ["pk", "name", "slug", "order", "enabled", "__type__"]
class FactorViewSet(ReadOnlyModelViewSet): class FactorViewSet(ReadOnlyModelViewSet):

View File

@ -11,7 +11,7 @@ class GroupSerializer(ModelSerializer):
class Meta: class Meta:
model = Group model = Group
fields = ['pk', 'name', 'parent', 'user_set', 'attributes'] fields = ["pk", "name", "parent", "user_set", "attributes"]
class GroupViewSet(ModelViewSet): class GroupViewSet(ModelViewSet):

View File

@ -11,7 +11,13 @@ class InvitationSerializer(ModelSerializer):
class Meta: class Meta:
model = Invitation model = Invitation
fields = ['pk', 'expires', 'fixed_username', 'fixed_email', 'needs_confirmation'] fields = [
"pk",
"expires",
"fixed_username",
"fixed_email",
"needs_confirmation",
]
class InvitationViewSet(ModelViewSet): class InvitationViewSet(ModelViewSet):

View File

@ -9,16 +9,16 @@ from passbook.policies.forms import GENERAL_FIELDS
class PolicySerializer(ModelSerializer): class PolicySerializer(ModelSerializer):
"""Policy Serializer""" """Policy Serializer"""
__type__ = SerializerMethodField(method_name='get_type') __type__ = SerializerMethodField(method_name="get_type")
def get_type(self, obj): def get_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object""" """Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace('policy', '') return obj._meta.object_name.lower().replace("policy", "")
class Meta: class Meta:
model = Policy model = Policy
fields = ['pk'] + GENERAL_FIELDS + ['__type__'] fields = ["pk"] + GENERAL_FIELDS + ["__type__"]
class PolicyViewSet(ReadOnlyModelViewSet): class PolicyViewSet(ReadOnlyModelViewSet):

View File

@ -8,16 +8,16 @@ from passbook.core.models import PropertyMapping
class PropertyMappingSerializer(ModelSerializer): class PropertyMappingSerializer(ModelSerializer):
"""PropertyMapping Serializer""" """PropertyMapping Serializer"""
__type__ = SerializerMethodField(method_name='get_type') __type__ = SerializerMethodField(method_name="get_type")
def get_type(self, obj): def get_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object""" """Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace('propertymapping', '') return obj._meta.object_name.lower().replace("propertymapping", "")
class Meta: class Meta:
model = PropertyMapping model = PropertyMapping
fields = ['pk', 'name', '__type__'] fields = ["pk", "name", "__type__"]
class PropertyMappingViewSet(ReadOnlyModelViewSet): class PropertyMappingViewSet(ReadOnlyModelViewSet):

View File

@ -8,16 +8,16 @@ from passbook.core.models import Provider
class ProviderSerializer(ModelSerializer): class ProviderSerializer(ModelSerializer):
"""Provider Serializer""" """Provider Serializer"""
__type__ = SerializerMethodField(method_name='get_type') __type__ = SerializerMethodField(method_name="get_type")
def get_type(self, obj): def get_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object""" """Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace('provider', '') return obj._meta.object_name.lower().replace("provider", "")
class Meta: class Meta:
model = Provider model = Provider
fields = ['pk', 'property_mappings', '__type__'] fields = ["pk", "property_mappings", "__type__"]
class ProviderViewSet(ReadOnlyModelViewSet): class ProviderViewSet(ReadOnlyModelViewSet):

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