Compare commits

..

44 Commits

Author SHA1 Message Date
f124314eab new release: 0.7.12-beta 2020-01-02 20:22:44 +01:00
684e4ffdcf providers/app_gw: fix formatting 2020-01-02 20:22:36 +01:00
d9ff5c69c8 providers/app_gw: fix assignment of response_types 2020-01-02 20:20:10 +01:00
8142e3df45 providers/oidc: fix application property of wrong object being used 2020-01-02 20:19:53 +01:00
73920899de static: use current pixie image 2020-01-02 20:09:30 +01:00
13666965a7 actions: fix build over gatekeeper 2020-01-02 16:55:30 +01:00
86f16e2781 providers/oidc: fix incorrectly sorted imports 2020-01-02 16:42:52 +01:00
2ed8e72c62 new release: 0.7.11-beta 2020-01-02 16:38:11 +01:00
edeed18ae8 providers/oidc: fix error when using with app_gw 2020-01-02 16:38:01 +01:00
d24133d8a2 core: fix _redirect_with_qs appending an array to the URL 2020-01-02 16:14:56 +01:00
b9733e56aa providers/app_gw: fix passbook domain being empty 2020-01-02 16:09:17 +01:00
cd34413914 providers/app_gw: separate host field into external_ and internal_ 2020-01-02 16:09:04 +01:00
c3a4a76d43 providers/app_gw: fix Client's response_type not being set 2020-01-02 16:06:32 +01:00
a59a29b256 actions: also build gatekeeper on release 2020-01-02 15:55:39 +01:00
dce1edbe53 new release: 0.7.10-beta 2020-01-02 14:54:52 +01:00
264d43827a actions: create release based on version number, not tag name 2020-01-02 14:46:44 +01:00
6207226bdf new release: 0.7.9-beta 2020-01-02 14:09:58 +01:00
ebf33f39c9 actions: fix missing backslash for dockerbuild 2020-01-02 14:09:42 +01:00
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
322 changed files with 5453 additions and 3538 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.7.5-beta
current_version = 0.7.12-beta
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -19,7 +19,7 @@ values =
[bumpversion:file:helm/Chart.yaml]
[bumpversion:file:.gitlab-ci.yml]
[bumpversion:file:.github/workflows/release.yml]
[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

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

@ -0,0 +1,89 @@
name: passbook-release
on:
release
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.12-beta
-t beryju/passbook:latest
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook:0.7.12-beta
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook:latest
build-gatekeeper:
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: |
cd gatekeeper
docker build \
--no-cache \
-t beryju/passbook-gatekeeper:0.7.12-beta \
-t beryju/passbook-gatekeeper:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-gatekeeper:0.7.12-beta
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-gatekeeper: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.12-beta
-t beryju/passbook-static:latest
-f static.Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-static:0.7.12-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"

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

@ -0,0 +1,60 @@
on:
push:
tags:
- 'version/*'
name: passbook-version-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: Extract verison number
id: get_version
uses: actions/github-script@0.2.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
- 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 ${{ steps.get_version.outputs.result }}
draft: false
prerelease: false
- name: Upload packaged Helm Chart
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

View File

@ -1,160 +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:
- ./scripts/coverage.sh
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.5-beta
only:
- tags
- /^version/.*$/
build-docs:
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/docs/Dockerfile --destination docker.beryju.org/passbook/docs:latest --destination docker.beryju.org/passbook/docs:0.7.5-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.5-beta
only:
- tags
- /^version/.*$/
# running collectstatic fully initialises django, hence we need that databases
services:
- postgres:latest
- redis:latest
package-helm:
image: debian:stretch-slim
stage: package
before_script:
- apt update && apt install -y curl
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
script:
- helm dependency update helm
- helm package helm
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.5-beta
- sentry-cli releases set-commits --auto passbook@0.7.5-beta
only:
- tags
- /^version/.*$/

View File

@ -1,6 +1,6 @@
[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
extension-pkg-whitelist=lxml
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 ./manage.py /app/

View File

@ -23,7 +23,7 @@ django-rest-framework = "*"
django-storages = "*"
djangorestframework-guardian = "*"
drf-yasg = "*"
kombu = "==4.5.0"
kombu = "*"
ldap3 = "*"
lxml = "*"
oauthlib = "*"
@ -51,8 +51,11 @@ bumpversion = "*"
colorama = "*"
coverage = "*"
django-debug-toolbar = "*"
isort = "*"
prospector = "*"
pylint = "==2.3.1"
pylint = "*"
pylint-django = "*"
unittest-xml-reporting = "*"
black = "*"
[pipenv]
allow_prereleases = true

337
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "865b57ef5ef326de114d39d8505f60f19b5f7e42a50d988ea3fc9dfc9b9371ec"
"sha256": "138816efaba5be0b175cfd5b5e6a0b58e5ba551567f0efb441740344da3986d8"
},
"pipfile-spec": 6,
"requires": {
@ -46,26 +46,26 @@
},
"boto3": {
"hashes": [
"sha256:d280f2bf7dc373e8aeab296f81aadefabf8780ff8c8ad27cdc36f8f112ca95ed",
"sha256:edbf4636e700c46e49f555ac87ab48b8c385fde604528db15fc5189d5a73dc72"
"sha256:982823e7c992d27e5954c81db93238ffc42c7a1210d863b4f5e048fdc088040e",
"sha256:f05ee90a738c2f1ec8088121030229f26ef6a809fb9a1338de2118fd088dd99a"
],
"index": "pypi",
"version": "==1.10.33"
"version": "==1.10.45"
},
"botocore": {
"hashes": [
"sha256:4861785b52b0b3f97da91613c31f8e501f12517c9c79482b44efbdb56b69aefc",
"sha256:9cc87d7906693c9c8fe862c574a1bebbe22a0475d6991e9b7251bc93cb1954d9"
"sha256:88ee646f7a0fe6a418681c6f119a590fae23d8439c48c2aec6878f7f89430b1f",
"sha256:f48ba1ef04b25323c1d27fa6399795baa0ca9d316911b87be4d33acda5cef07c"
],
"version": "==1.13.33"
"version": "==1.13.45"
},
"celery": {
"hashes": [
"sha256:4c4532aa683f170f40bd76f928b70bc06ff171a959e06e71bf35f2f9d6031ef9",
"sha256:528e56767ae7e43a16cfef24ee1062491f5754368d38fcfffa861cdb9ef219be"
"sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f",
"sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2"
],
"index": "pypi",
"version": "==4.3.0"
"version": "==4.4.0"
},
"certifi": {
"hashes": [
@ -169,19 +169,19 @@
},
"django": {
"hashes": [
"sha256:a4ad4f6f9c6a4b7af7e2deec8d0cbff28501852e5010d6c2dc695d3d1fae7ca0",
"sha256:fa98ec9cc9bf5d72a08ebf3654a9452e761fbb8566e3f80de199cbc15477e891"
"sha256:662a1ff78792e3fd77f16f71b1f31149489434de4b62a74895bd5d6534e635a5",
"sha256:687c37153486cf26c3fdcbdd177ef16de38dc3463f094b5f9c9955d91f277b14"
],
"index": "pypi",
"version": "==2.2.8"
"version": "==2.2.9"
},
"django-cors-middleware": {
"hashes": [
"sha256:85904a3401e7bc0c86502ff2b01d726917af3aaa7dafb77799b27ace637e8c92",
"sha256:bca8888ed33a94ba5472bde37ed71ec3d08231d6817fd4d799296b016073da95"
"sha256:5bbdea85e22909d596e26f6e0dbc174d5521429fa3943ae02a2c6c48e76c88c7",
"sha256:856dbe4d7aae65844ccc68acb49c6da7dbf7cbacaf5bcf37019f4c0c60b3be84"
],
"index": "pypi",
"version": "==1.4.0"
"version": "==1.5.0"
},
"django-dbbackup": {
"hashes": [
@ -208,11 +208,11 @@
},
"django-model-utils": {
"hashes": [
"sha256:3f130a262e45d73e0950d2be76af4bf4ee86804dd60e5f90afc5cd948fcfe760",
"sha256:682f58c1de330cedcda58cc85d5232c5b47a9e2cb67bef4541fb43fdaeb18e96"
"sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c",
"sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061"
],
"index": "pypi",
"version": "==3.2.0"
"version": "==4.0.0"
},
"django-oauth-toolkit": {
"hashes": [
@ -230,19 +230,19 @@
},
"django-otp": {
"hashes": [
"sha256:1b6025bbbd2517b7c246828b1d11c83d53567904836ae6d57bc0058f3cd18b50",
"sha256:76a698466178ce40473726ffd8c33f68d1c47f27c53f67fa4aeeb6fdde74d37b"
"sha256:1f16c2b93fe484706ff16ac6f5e64ecc73dd240318c333e0560384ba548d3837",
"sha256:cd4975539be478417033561e9832a1a69a583189f680e92a649f412c661f90aa"
],
"index": "pypi",
"version": "==0.7.4"
"version": "==0.7.5"
},
"django-prometheus": {
"hashes": [
"sha256:60f331788f9846891e9ea8d7ccd2928b1042e2e99c8d673f97e2b85f5bc20112",
"sha256:bb2d4f8acd681fa5787df77e7482391017f0090c70473bccd2aa7cad327800ad"
"sha256:f0657d4b887309086b71b55f6aa4a95f967b35fe115128b501f95422c423b12c",
"sha256:f645016ae5270ac2025a70788cd2bd636244a0c5705b323cc086994bf828181e"
],
"index": "pypi",
"version": "==1.1.0"
"version": "==2.0.0.dev124"
},
"django-recaptcha": {
"hashes": [
@ -254,11 +254,11 @@
},
"django-redis": {
"hashes": [
"sha256:af0b393864e91228dd30d8c85b5c44d670b5524cb161b7f9e41acc98b6e5ace7",
"sha256:f46115577063d00a890867c6964ba096057f07cb756e78e0503b89cd18e4e083"
"sha256:a5b1e3ffd3198735e6c529d9bdf38ca3fcb3155515249b98dc4d966b8ddf9d2b",
"sha256:e1aad4cc5bd743d8d0b13d5cae0cef5410eaace33e83bff5fc3a139ad8db50b4"
],
"index": "pypi",
"version": "==4.10.0"
"version": "==4.11.0"
},
"django-rest-framework": {
"hashes": [
@ -277,10 +277,10 @@
},
"djangorestframework": {
"hashes": [
"sha256:5488aed8f8df5ec1d70f04b2114abc52ae6729748a176c453313834a9ee179c8",
"sha256:dc81cbf9775c6898a580f6f1f387c4777d12bd87abf0f5406018d32ccae71090"
"sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4",
"sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f"
],
"version": "==3.10.3"
"version": "==3.11.0"
},
"djangorestframework-guardian": {
"hashes": [
@ -328,11 +328,11 @@
},
"importlib-metadata": {
"hashes": [
"sha256:3a8b2dfd0a2c6a3636e7c016a7e54ae04b997d30e69d5eacdca7a6c2221a1402",
"sha256:41e688146d000891f32b1669e8573c57e39e5060e7f5f647aa617cd9a9568278"
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
],
"markers": "python_version < '3.8'",
"version": "==1.2.0"
"version": "==1.3.0"
},
"inflection": {
"hashes": [
@ -369,11 +369,11 @@
},
"kombu": {
"hashes": [
"sha256:389ba09e03b15b55b1a7371a441c894fd8121d174f5583bbbca032b9ea8c9edd",
"sha256:7b92303af381ef02fad6899fd5f5a9a96031d781356cd8e505fa54ae5ddee181"
"sha256:2a9e7adff14d046c9996752b2c48b6d9185d0b992106d5160e1a179907a5d4ac",
"sha256:67b32ccb6fea030f8799f8fd50dd08e03a4b99464ebc4952d71d8747b1a52ad1"
],
"index": "pypi",
"version": "==4.5.0"
"version": "==4.6.7"
},
"ldap3": {
"hashes": [
@ -450,10 +450,10 @@
},
"more-itertools": {
"hashes": [
"sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
"sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
],
"version": "==8.0.0"
"version": "==8.0.2"
},
"oauthlib": {
"hashes": [
@ -625,10 +625,10 @@
},
"pyparsing": {
"hashes": [
"sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f",
"sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"
"sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
"sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
],
"version": "==2.4.5"
"version": "==2.4.6"
},
"pyrsistent": {
"hashes": [
@ -638,11 +638,11 @@
},
"python-dateutil": {
"hashes": [
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"markers": "python_version >= '2.7'",
"version": "==2.8.0"
"version": "==2.8.1"
},
"pytz": {
"hashes": [
@ -685,20 +685,20 @@
},
"pyyaml": {
"hashes": [
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
"sha256:21a8e19e2007a4047ffabbd8f0ee32c0dabae3b7f4b6c645110ae53e7714b470",
"sha256:74ad685bfb065f4bdd36d24aa97092f04bcbb1179b5ffdd3d5f994023fb8c292",
"sha256:79c3ba1da22e61c2a71aaa382c57518ab492278c8974c40187b900b50f3e0282",
"sha256:94ad913ab3fd967d14ecffda8182d7d0e1f7dd919b352773c492ec51890d3224",
"sha256:998db501e3a627c3e5678d6505f0e182d1529545df289db036cdc717f35d8058",
"sha256:9b69d4645bff5820713e8912bc61c4277dc127a6f8c197b52b6436503c42600f",
"sha256:9da13b536533518343a04f3c6564782ec8a13c705310b26b4832d77fa4d92a47",
"sha256:a76159f13b47fb44fb2acac8fef798a1940dd31b4acec6f4560bd11b2d92d31b",
"sha256:a9e9175c1e47a089a2b45d9e2afc6aae1f1f725538c32eec761894a42ba1227f",
"sha256:ea51ce7b96646ecd3bb12c2702e570c2bd7dd4d9f146db7fa83c5008ede35f66",
"sha256:ffbaaa05de60fc444eda3f6300d1af27d965b09b67f1fb4ebcc88dd0fb4ab1b4"
],
"index": "pypi",
"version": "==5.2"
"version": "==5.3b1"
},
"qrcode": {
"hashes": [
@ -752,6 +752,7 @@
"sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5",
"sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070",
"sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c",
"sha256:be018933c2f4ee7de55e7bd7d0d801b3dfb09d21dad0cce8a97995fd3e44be30",
"sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947",
"sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc",
"sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973",
@ -770,11 +771,11 @@
},
"sentry-sdk": {
"hashes": [
"sha256:a7c2c8d3f53b6b57454830cd6a4b73d272f1ba91952f59e6545b3cf885f3c22f",
"sha256:bfc486af718c268cf49ff43d6334ed4db7333ace420240b630acdd8f8a3a8f60"
"sha256:05285942901d38c7ce2498aba50d8e87b361fc603281a5902dda98f3f8c5e145",
"sha256:c6b919623e488134a728f16326c6f0bcdab7e3f59e7f4c472a90eea4d6d8fe82"
],
"index": "pypi",
"version": "==0.13.4"
"version": "==0.13.5"
},
"service-identity": {
"hashes": [
@ -824,11 +825,10 @@
},
"uritemplate": {
"hashes": [
"sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd",
"sha256:1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd",
"sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d"
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
],
"version": "==3.0.0"
"version": "==3.0.1"
},
"urllib3": {
"extras": [
@ -858,6 +858,13 @@
}
},
"develop": {
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"asgiref": {
"hashes": [
"sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
@ -867,10 +874,17 @@
},
"astroid": {
"hashes": [
"sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4",
"sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4"
"sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a",
"sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"
],
"version": "==2.2.5"
"version": "==2.3.3"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
},
"autopep8": {
"hashes": [
@ -887,6 +901,14 @@
"index": "pypi",
"version": "==1.6.2"
},
"black": {
"hashes": [
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
],
"index": "pypi",
"version": "==19.10b0"
},
"bumpversion": {
"hashes": [
"sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e",
@ -895,59 +917,65 @@
"index": "pypi",
"version": "==0.5.3"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"colorama": {
"hashes": [
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
"sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff",
"sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
],
"index": "pypi",
"version": "==0.4.1"
"version": "==0.4.3"
},
"coverage": {
"hashes": [
"sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6",
"sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650",
"sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5",
"sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d",
"sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351",
"sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755",
"sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef",
"sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca",
"sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca",
"sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9",
"sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc",
"sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5",
"sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f",
"sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe",
"sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888",
"sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5",
"sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce",
"sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5",
"sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e",
"sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e",
"sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9",
"sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437",
"sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1",
"sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c",
"sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24",
"sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47",
"sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2",
"sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28",
"sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c",
"sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7",
"sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0",
"sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"
"sha256:0101888bd1592a20ccadae081ba10e8b204d20235d18d05c6f7d5e904a38fc10",
"sha256:04b961862334687549eb91cd5178a6fbe977ad365bddc7c60f2227f2f9880cf4",
"sha256:1ca43dbd739c0fc30b0a3637a003a0d2c7edc1dd618359d58cc1e211742f8bd1",
"sha256:1cbb88b34187bdb841f2599770b7e6ff8e259dc3bb64fc7893acf44998acf5f8",
"sha256:232f0b52a5b978288f0bbc282a6c03fe48cd19a04202df44309919c142b3bb9c",
"sha256:24bcfa86fd9ce86b73a8368383c39d919c497a06eebb888b6f0c12f13e920b1a",
"sha256:25b8f60b5c7da71e64c18888f3067d5b6f1334b9681876b2fb41eea26de881ae",
"sha256:2714160a63da18aed9340c70ed514973971ee7e665e6b336917ff4cca81a25b1",
"sha256:2ca2cd5264e84b2cafc73f0045437f70c6378c0d7dbcddc9ee3fe192c1e29e5d",
"sha256:2cc707fc9aad2592fc686d63ef72dc0031fc98b6fb921d2f5395d9ab84fbc3ef",
"sha256:348630edea485f4228233c2f310a598abf8afa5f8c716c02a9698089687b6085",
"sha256:40fbfd6b044c9db13aeec1daf5887d322c710d811f944011757526ef6e323fd9",
"sha256:46c9c6a1d1190c0b75ec7c0f339088309952b82ae8d67a79ff1319eb4e749b96",
"sha256:591506e088901bdc25620c37aec885e82cc896528f28c57e113751e3471fc314",
"sha256:5ac71bba1e07eab403b082c4428f868c1c9e26a21041436b4905c4c3d4e49b08",
"sha256:5f622f19abda4e934938e24f1d67599249abc201844933a6f01aaa8663094489",
"sha256:65bead1ac8c8930cf92a1ccaedcce19a57298547d5d1db5c9d4d068a0675c38b",
"sha256:7362a7f829feda10c7265b553455de596b83d1623b3d436b6d3c51c688c57bf6",
"sha256:7f2675750c50151f806070ec11258edf4c328340916c53bac0adbc465abd6b1e",
"sha256:960d7f42277391e8b1c0b0ae427a214e1b31a1278de6b73f8807b20c2e913bba",
"sha256:a50b0888d8a021a3342d36a6086501e30de7d840ab68fca44913e97d14487dc1",
"sha256:b7dbc5e8c39ea3ad3db22715f1b5401cd698a621218680c6daf42c2f9d36e205",
"sha256:bb3d29df5d07d5399d58a394d0ef50adf303ab4fbf66dfd25b9ef258effcb692",
"sha256:c0fff2733f7c2950f58a4fd09b5db257b00c6fec57bf3f68c5bae004d804b407",
"sha256:c792d3707a86c01c02607ae74364854220fb3e82735f631cd0a345dea6b4cee5",
"sha256:c90bda74e16bcd03861b09b1d37c0a4158feda5d5a036bb2d6e58de6ff65793e",
"sha256:cfce79ce41cc1a1dc7fc85bb41eeeb32d34a4cf39a645c717c0550287e30ff06",
"sha256:eeafb646f374988c22c8e6da5ab9fb81367ecfe81c70c292623373d2a021b1a1",
"sha256:f425f50a6dd807cb9043d15a4fcfba3b5874a54d9587ccbb748899f70dc18c47",
"sha256:fcd4459fe35a400b8f416bc57906862693c9f88b66dc925e7f2a933e77f6b18b",
"sha256:ff3936dd5feaefb4f91c8c1f50a06c588b5dc69fba4f7d9c79a6617ad80bb7df"
],
"index": "pypi",
"version": "==4.5.4"
"version": "==5.0.1"
},
"django": {
"hashes": [
"sha256:a4ad4f6f9c6a4b7af7e2deec8d0cbff28501852e5010d6c2dc695d3d1fae7ca0",
"sha256:fa98ec9cc9bf5d72a08ebf3654a9452e761fbb8566e3f80de199cbc15477e891"
"sha256:662a1ff78792e3fd77f16f71b1f31149489434de4b62a74895bd5d6534e635a5",
"sha256:687c37153486cf26c3fdcbdd177ef16de38dc3463f094b5f9c9955d91f277b14"
],
"index": "pypi",
"version": "==2.2.8"
"version": "==2.2.9"
},
"django-debug-toolbar": {
"hashes": [
@ -982,7 +1010,6 @@
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
],
"index": "pypi",
"version": "==4.3.21"
},
"lazy-object-proxy": {
@ -1018,6 +1045,13 @@
],
"version": "==0.6.1"
},
"pathspec": {
"hashes": [
"sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424",
"sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"
],
"version": "==0.7.0"
},
"pbr": {
"hashes": [
"sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b",
@ -1034,10 +1068,10 @@
},
"prospector": {
"hashes": [
"sha256:aba551e53dc1a5a432afa67385eaa81d7b4cf4c162dc1a4d0ee00b3a0712ad90"
"sha256:ea910794b53cfefcb5dfb6b4eb0323e42d1a88132e165b85b016cc7f0b6ae635"
],
"index": "pypi",
"version": "==1.1.7"
"version": "==1.2.0"
},
"pycodestyle": {
"hashes": [
@ -1048,25 +1082,25 @@
},
"pydocstyle": {
"hashes": [
"sha256:04c84e034ebb56eb6396c820442b8c4499ac5eb94a3bda88951ac3dc519b6058",
"sha256:66aff87ffe34b1e49bff2dd03a88ce6843be2f3346b0c9814410d34987fbab59"
"sha256:4167fe954b8f27ebbbef2fbcf73c6e8ad1e7bb31488fce44a69fdfc4b0cd0fae",
"sha256:a0de36e549125d0a16a72a8c8c6c9ba267750656e72e466e994c222f1b6e92cb"
],
"version": "==4.0.1"
"version": "==5.0.1"
},
"pyflakes": {
"hashes": [
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
],
"version": "==1.6.0"
"version": "==2.1.1"
},
"pylint": {
"hashes": [
"sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09",
"sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1"
"sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd",
"sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"
],
"index": "pypi",
"version": "==2.3.1"
"version": "==2.4.4"
},
"pylint-celery": {
"hashes": [
@ -1076,11 +1110,11 @@
},
"pylint-django": {
"hashes": [
"sha256:75c69d1ec2275918c37f175976da20e2f1e1e62e067098a685cd263ffa833dfd",
"sha256:c7cb6384ea7b33ea77052a5ae07358c10d377807390ef27b2e6ff997303fadb7"
"sha256:9bdb0e022b19881218a25ffb8ad05e83b83bc5cdbc58e5ee8ffbe99965193f6c",
"sha256:9eea6a026eaa5ecfad5fed7a33faf77ef55a43cc78afbcaf2f6ddd071156b3f8"
],
"index": "pypi",
"version": "==2.0.10"
"version": "==2.0.12"
},
"pylint-flask": {
"hashes": [
@ -1104,20 +1138,46 @@
},
"pyyaml": {
"hashes": [
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
"sha256:21a8e19e2007a4047ffabbd8f0ee32c0dabae3b7f4b6c645110ae53e7714b470",
"sha256:74ad685bfb065f4bdd36d24aa97092f04bcbb1179b5ffdd3d5f994023fb8c292",
"sha256:79c3ba1da22e61c2a71aaa382c57518ab492278c8974c40187b900b50f3e0282",
"sha256:94ad913ab3fd967d14ecffda8182d7d0e1f7dd919b352773c492ec51890d3224",
"sha256:998db501e3a627c3e5678d6505f0e182d1529545df289db036cdc717f35d8058",
"sha256:9b69d4645bff5820713e8912bc61c4277dc127a6f8c197b52b6436503c42600f",
"sha256:9da13b536533518343a04f3c6564782ec8a13c705310b26b4832d77fa4d92a47",
"sha256:a76159f13b47fb44fb2acac8fef798a1940dd31b4acec6f4560bd11b2d92d31b",
"sha256:a9e9175c1e47a089a2b45d9e2afc6aae1f1f725538c32eec761894a42ba1227f",
"sha256:ea51ce7b96646ecd3bb12c2702e570c2bd7dd4d9f146db7fa83c5008ede35f66",
"sha256:ffbaaa05de60fc444eda3f6300d1af27d965b09b67f1fb4ebcc88dd0fb4ab1b4"
],
"index": "pypi",
"version": "==5.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": {
"hashes": [
@ -1166,6 +1226,13 @@
],
"version": "==1.31.0"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
},
"typed-ast": {
"hashes": [
"sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161",
@ -1189,7 +1256,7 @@
"sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
],
"markers": "implementation_name == 'cpython'",
"markers": "implementation_name == 'cpython' and python_version < '3.8'",
"version": "==1.4.0"
},
"unittest-xml-reporting": {

View File

@ -1,5 +1,7 @@
# passbook
![](https://github.com/BeryJu/passbook/workflows/passbook-ci/badge.svg)
## Quick instance
```

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

View File

@ -9,7 +9,7 @@ This installation Method is for test-setups and small-scale productive setups.
## Install
Download the latest `docker-compose.yml` from [here](https://git.beryju.org/BeryJu.org/passbook/raw/master/docker-compose.yml). Place it in a directory of your choice.
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:

View File

@ -19,7 +19,7 @@ spec:
spec:
containers:
- name: passbook-docs
image: "docker.beryju.org/passbook/docs:latest"
image: "beryju/passbook-docs:latest"
ports:
- name: http
containerPort: 80

View File

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

View File

@ -21,7 +21,7 @@ spec:
spec:
containers:
- name: {{ .Chart.Name }}-static
image: "docker.beryju.org/passbook/static:{{ .Values.image.tag }}"
image: "beryju/passbook-static:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
ports:
- name: http

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ nav:
- Sentry: integrations/services/sentry/index.md
repo_name: "BeryJu.org/passbook"
repo_url: https://git.beryju.org/BeryJu.org/passbook
repo_url: https://github.com/BeryJu/passbook
theme:
name: "material"
logo: "images/logo.svg"

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = '0.7.5-beta'
__version__ = "0.7.12-beta"

View File

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

View File

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

View File

@ -9,29 +9,32 @@ class TagModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
# 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 {}
# 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():
if key not in tags:
tags[key] = value
# Format JSON
kwargs['initial']['tags'] = tags
kwargs["initial"]["tags"] = tags
super().__init__(*args, **kwargs)
def clean_tags(self):
"""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():
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)
return self.cleaned_data.get('tags')
return self.cleaned_data.get("tags")
# pylint: disable=too-few-public-methods
class TagModelFormMeta:
"""Base Meta class that uses the YAMLField"""
field_classes = {
'tags': YAMLField
}
field_classes = {"tags": YAMLField}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
"""passbook admin settings"""
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()
LOGGER = get_logger()
@register.simple_tag()
def get_links(model_instance):
"""Find all link_ methods on an object instance, run them and return as dict"""
prefix = 'link_'
prefix = "link_"
links = {}
if not isinstance(model_instance, Model):
@ -21,9 +22,11 @@ def get_links(model_instance):
return links
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):
human_name = name.replace(prefix, '').replace('_', ' ').capitalize()
human_name = name.replace(prefix, "").replace("_", " ").capitalize()
link = method()
if link:
links[human_name] = link
@ -36,7 +39,7 @@ def get_links(model_instance):
@register.simple_tag(takes_context=True)
def get_htmls(context, model_instance):
"""Find all html_ methods on an object instance, run them and return as dict"""
prefix = 'html_'
prefix = "html_"
htmls = []
if not isinstance(model_instance, Model):
@ -44,9 +47,11 @@ def get_htmls(context, model_instance):
return htmls
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):
template, _context = method(context.get('request'))
template, _context = method(context.get("request"))
htmls.append(render_to_string(template, _context))
except NotImplementedError:
pass

View File

@ -1,82 +1,157 @@
"""passbook URL Configuration"""
from django.urls import path
from passbook.admin.views import (applications, audit, debug, factors, groups,
invitations, overview, policy,
property_mapping, providers, sources, users)
from passbook.admin.views import (
applications,
audit,
debug,
factors,
groups,
invitations,
overview,
policy,
property_mapping,
providers,
sources,
users,
)
urlpatterns = [
path('', overview.AdministrationOverviewView.as_view(), name='overview'),
path("", overview.AdministrationOverviewView.as_view(), name="overview"),
# Applications
path('applications/', applications.ApplicationListView.as_view(),
name='applications'),
path('applications/create/', applications.ApplicationCreateView.as_view(),
name='application-create'),
path('applications/<uuid:pk>/update/',
applications.ApplicationUpdateView.as_view(), name='application-update'),
path('applications/<uuid:pk>/delete/',
applications.ApplicationDeleteView.as_view(), name='application-delete'),
path(
"applications/", applications.ApplicationListView.as_view(), name="applications"
),
path(
"applications/create/",
applications.ApplicationCreateView.as_view(),
name="application-create",
),
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
path('sources/', sources.SourceListView.as_view(), name='sources'),
path('sources/create/', sources.SourceCreateView.as_view(), name='source-create'),
path('sources/<uuid:pk>/update/', sources.SourceUpdateView.as_view(), name='source-update'),
path('sources/<uuid:pk>/delete/', sources.SourceDeleteView.as_view(), name='source-delete'),
path("sources/", sources.SourceListView.as_view(), name="sources"),
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
path(
"sources/<uuid:pk>/update/",
sources.SourceUpdateView.as_view(),
name="source-update",
),
path(
"sources/<uuid:pk>/delete/",
sources.SourceDeleteView.as_view(),
name="source-delete",
),
# Policies
path('policies/', policy.PolicyListView.as_view(), name='policies'),
path('policies/create/', policy.PolicyCreateView.as_view(), name='policy-create'),
path('policies/<uuid:pk>/update/', 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'),
path("policies/", policy.PolicyListView.as_view(), name="policies"),
path("policies/create/", policy.PolicyCreateView.as_view(), name="policy-create"),
path(
"policies/<uuid:pk>/update/",
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
path('providers/', providers.ProviderListView.as_view(), name='providers'),
path('providers/create/',
providers.ProviderCreateView.as_view(), name='provider-create'),
path('providers/<int:pk>/update/',
providers.ProviderUpdateView.as_view(), name='provider-update'),
path('providers/<int:pk>/delete/',
providers.ProviderDeleteView.as_view(), name='provider-delete'),
path("providers/", providers.ProviderListView.as_view(), name="providers"),
path(
"providers/create/",
providers.ProviderCreateView.as_view(),
name="provider-create",
),
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
path('factors/', factors.FactorListView.as_view(), name='factors'),
path('factors/create/',
factors.FactorCreateView.as_view(), name='factor-create'),
path('factors/<uuid:pk>/update/',
factors.FactorUpdateView.as_view(), name='factor-update'),
path('factors/<uuid:pk>/delete/',
factors.FactorDeleteView.as_view(), name='factor-delete'),
path("factors/", factors.FactorListView.as_view(), name="factors"),
path("factors/create/", factors.FactorCreateView.as_view(), name="factor-create"),
path(
"factors/<uuid:pk>/update/",
factors.FactorUpdateView.as_view(),
name="factor-update",
),
path(
"factors/<uuid:pk>/delete/",
factors.FactorDeleteView.as_view(),
name="factor-delete",
),
# Factors
path('property-mappings/', property_mapping.PropertyMappingListView.as_view(),
name='property-mappings'),
path('property-mappings/create/',
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'),
path(
"property-mappings/",
property_mapping.PropertyMappingListView.as_view(),
name="property-mappings",
),
path(
"property-mappings/create/",
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
path('invitations/', invitations.InvitationListView.as_view(), name='invitations'),
path('invitations/create/',
invitations.InvitationCreateView.as_view(), name='invitation-create'),
path('invitations/<uuid:pk>/delete/',
invitations.InvitationDeleteView.as_view(), name='invitation-delete'),
path("invitations/", invitations.InvitationListView.as_view(), name="invitations"),
path(
"invitations/create/",
invitations.InvitationCreateView.as_view(),
name="invitation-create",
),
path(
"invitations/<uuid:pk>/delete/",
invitations.InvitationDeleteView.as_view(),
name="invitation-delete",
),
# Users
path('users/', users.UserListView.as_view(),
name='users'),
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>/delete/',
users.UserDeleteView.as_view(), name='user-delete'),
path('users/<int:pk>/reset/',
users.UserPasswordResetView.as_view(), name='user-password-reset'),
path("users/", users.UserListView.as_view(), name="users"),
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>/delete/", users.UserDeleteView.as_view(), name="user-delete"),
path(
"users/<int:pk>/reset/",
users.UserPasswordResetView.as_view(),
name="user-password-reset",
),
# Groups
path('group/', groups.GroupListView.as_view(), name='group'),
path('group/create/', groups.GroupCreateView.as_view(), name='group-create'),
path('group/<uuid:pk>/update/', groups.GroupUpdateView.as_view(), name='group-update'),
path('group/<uuid:pk>/delete/', groups.GroupDeleteView.as_view(), name='group-delete'),
path("group/", groups.GroupListView.as_view(), name="group"),
path("group/create/", groups.GroupCreateView.as_view(), name="group-create"),
path(
"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
path('audit/', audit.EventListView.as_view(), name='audit-log'),
path("audit/", audit.EventListView.as_view(), name="audit-log"),
# Groups
path('groups/', groups.GroupListView.as_view(), name='groups'),
path("groups/", groups.GroupListView.as_view(), name="groups"),
# 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"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \
PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
@ -18,55 +19,61 @@ class ApplicationListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all applications"""
model = Application
permission_required = 'passbook_core.view_application'
ordering = 'name'
permission_required = "passbook_core.view_application"
ordering = "name"
paginate_by = 40
template_name = 'administration/application/list.html'
template_name = "administration/application/list.html"
def get_queryset(self):
return super().get_queryset().select_subclasses()
class ApplicationCreateView(SuccessMessageMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, CreateAssignPermView):
class ApplicationCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new Application"""
model = Application
form_class = ApplicationForm
permission_required = 'passbook_core.add_application'
permission_required = "passbook_core.add_application"
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:applications')
success_message = _('Successfully created Application')
template_name = "generic/create.html"
success_url = reverse_lazy("passbook_admin:applications")
success_message = _("Successfully created Application")
def get_context_data(self, **kwargs):
kwargs['type'] = 'Application'
kwargs["type"] = "Application"
return super().get_context_data(**kwargs)
class ApplicationUpdateView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, UpdateView):
class ApplicationUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update application"""
model = Application
form_class = ApplicationForm
permission_required = 'passbook_core.change_application'
permission_required = "passbook_core.change_application"
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:applications')
success_message = _('Successfully updated Application')
template_name = "generic/update.html"
success_url = reverse_lazy("passbook_admin:applications")
success_message = _("Successfully updated Application")
class ApplicationDeleteView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, DeleteView):
class ApplicationDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete application"""
model = Application
permission_required = 'passbook_core.delete_application'
permission_required = "passbook_core.delete_application"
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:applications')
success_message = _('Successfully deleted Application')
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:applications")
success_message = _("Successfully deleted Application")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)

View File

@ -9,10 +9,10 @@ class EventListView(PermissionListMixin, ListView):
"""Show list of all invitations"""
model = Event
template_name = 'administration/audit/list.html'
permission_required = 'passbook_audit.view_event'
ordering = '-created'
template_name = "administration/audit/list.html"
permission_required = "passbook_audit.view_event"
ordering = "-created"
paginate_by = 10
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):
"""Show debug info about request"""
template_name = 'administration/debug/request.html'
template_name = "administration/debug/request.html"
def get_context_data(self, **kwargs):
kwargs['request_dict'] = {}
kwargs["request_dict"] = {}
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)

View File

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

View File

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

View File

@ -1,8 +1,9 @@
"""passbook Invitation administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \
PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
@ -20,47 +21,49 @@ class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all invitations"""
model = Invitation
permission_required = 'passbook_core.view_invitation'
template_name = 'administration/invitation/list.html'
permission_required = "passbook_core.view_invitation"
template_name = "administration/invitation/list.html"
class InvitationCreateView(SuccessMessageMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, CreateAssignPermView):
class InvitationCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new Invitation"""
model = Invitation
form_class = InvitationForm
permission_required = 'passbook_core.add_invitation'
permission_required = "passbook_core.add_invitation"
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:invitations')
success_message = _('Successfully created Invitation')
template_name = "generic/create.html"
success_url = reverse_lazy("passbook_admin:invitations")
success_message = _("Successfully created Invitation")
def get_context_data(self, **kwargs):
kwargs['type'] = 'Invitation'
kwargs["type"] = "Invitation"
return super().get_context_data(**kwargs)
def form_valid(self, form):
obj = form.save(commit=False)
obj.created_by = self.request.user
obj.save()
invitation_created.send(
sender=self,
request=self.request,
invitation=obj)
invitation_created.send(sender=self, request=self.request, invitation=obj)
return HttpResponseRedirect(self.success_url)
class InvitationDeleteView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, DeleteView):
class InvitationDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete invitation"""
model = Invitation
permission_required = 'passbook_core.delete_invitation'
permission_required = "passbook_core.delete_invitation"
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:invitations')
success_message = _('Successfully deleted Invitation')
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:invitations")
success_message = _("Successfully deleted Invitation")
def delete(self, request, *args, **kwargs):
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.admin.mixins import AdminRequiredMixin
from passbook.core.models import (Application, Factor, Invitation, Policy,
Provider, Source, User)
from passbook.core.models import (
Application,
Factor,
Invitation,
Policy,
Provider,
Source,
User,
)
from passbook.root.celery import CELERY_APP
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
"""Overview View"""
template_name = 'administration/overview.html'
template_name = "administration/overview.html"
def post(self, *args, **kwargs):
"""Handle post (clear cache from modal)"""
if 'clear' in self.request.POST:
if "clear" in self.request.POST:
cache.clear()
return redirect(reverse('passbook_core:auth-login'))
return redirect(reverse("passbook_core:auth-login"))
return self.get(*args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['application_count'] = len(Application.objects.all())
kwargs['policy_count'] = len(Policy.objects.all())
kwargs['user_count'] = len(User.objects.all())
kwargs['provider_count'] = len(Provider.objects.all())
kwargs['source_count'] = len(Source.objects.all())
kwargs['factor_count'] = len(Factor.objects.all())
kwargs['invitation_count'] = len(Invitation.objects.all())
kwargs['version'] = __version__
kwargs['worker_count'] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs['providers_without_application'] = Provider.objects.filter(application=None)
kwargs['policies_without_attachment'] = len(Policy.objects.filter(policymodel__isnull=True))
kwargs['cached_policies'] = len(cache.keys('policy_*'))
kwargs["application_count"] = len(Application.objects.all())
kwargs["policy_count"] = len(Policy.objects.all())
kwargs["user_count"] = len(User.objects.all())
kwargs["provider_count"] = len(Provider.objects.all())
kwargs["source_count"] = len(Source.objects.all())
kwargs["factor_count"] = len(Factor.objects.all())
kwargs["invitation_count"] = len(Invitation.objects.all())
kwargs["version"] = __version__
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs["providers_without_application"] = Provider.objects.filter(
application=None
)
kwargs["policies_without_attachment"] = len(
Policy.objects.filter(policymodel__isnull=True)
)
kwargs["cached_policies"] = len(cache.keys("policy_*"))
return super().get_context_data(**kwargs)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,13 +9,13 @@ class CustomObjectPermissions(DjangoObjectPermissions):
"""Similar to `DjangoObjectPermissions`, but adding 'view' permissions."""
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
"GET": ["%(app_label)s.view_%(model_name)s"],
"OPTIONS": ["%(app_label)s.view_%(model_name)s"],
"HEAD": ["%(app_label)s.view_%(model_name)s"],
"POST": ["%(app_label)s.add_%(model_name)s"],
"PUT": ["%(app_label)s.change_%(model_name)s"],
"PATCH": ["%(app_label)s.change_%(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
urlpatterns = [
path('v1/', include(v1_urls)),
path('v2/', include(v2_urls)),
path("v1/", include(v1_urls)),
path("v2/", include(v2_urls)),
]

View File

@ -7,16 +7,16 @@ from oauth2_provider.views.mixins import ScopedResourceMixin
class OpenIDUserInfoView(ScopedResourceMixin, View):
"""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"""
payload = {
'sub': request.user.uuid.int,
'name': request.user.get_full_name(),
'given_name': request.user.name,
'family_name': '',
'preferred_username': request.user.username,
'email': request.user.email,
"sub": request.user.uuid.int,
"name": request.user.get_full_name(),
"given_name": request.user.name,
"family_name": "",
"preferred_username": request.user.username,
"email": request.user.email,
}
return JsonResponse(payload)

View File

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

View File

@ -2,4 +2,4 @@
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:
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):

View File

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

View File

@ -18,20 +18,55 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='AuditEntry',
name="AuditEntry",
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')])),
('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)),
(
"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"),
]
),
),
("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={
'verbose_name': 'Audit Entry',
'verbose_name_plural': 'Audit Entries',
"verbose_name": "Audit Entry",
"verbose_name_plural": "Audit Entries",
},
),
]

View File

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

View File

@ -8,17 +8,33 @@ import passbook.audit.models
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0002_auto_20191028_0829'),
("passbook_audit", "0002_auto_20191028_0829"),
]
operations = [
migrations.AlterModelOptions(
name='event',
options={'verbose_name': 'Audit Event', 'verbose_name_plural': 'Audit Events'},
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')]),
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

@ -6,17 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0003_auto_20191205_1407'),
("passbook_audit", "0003_auto_20191205_1407"),
]
operations = [
migrations.RemoveField(
model_name='event',
name='request_ip',
),
migrations.RemoveField(model_name="event", name="request_ip",),
migrations.AddField(
model_name='event',
name='client_ip',
model_name="event",
name="client_ip",
field=models.GenericIPAddressField(null=True),
),
]

View File

@ -1,12 +1,14 @@
"""passbook audit models"""
from enum import Enum
from uuid import UUID
from inspect import getmodule, stack
from typing import Optional
from typing import Optional, Dict, Any
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.http import HttpRequest
from django.utils.translation import gettext as _
@ -18,19 +20,44 @@ from passbook.lib.utils.http import get_client_ip
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'
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():
@ -41,7 +68,9 @@ class EventAction(Enum):
class Event(UUIDModel):
"""An individual audit log event"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
)
action = models.TextField(choices=EventAction.as_choices())
date = models.DateTimeField(auto_now_add=True)
app = models.TextField()
@ -56,28 +85,31 @@ class Event(UUIDModel):
return request.resolver_match.app_name
@staticmethod
def new(action: EventAction,
app: Optional[str] = None,
_inspect_offset: int = 1,
**kwargs) -> 'Event':
def new(
action: EventAction,
app: Optional[str] = None,
_inspect_offset: int = 1,
**kwargs,
) -> "Event":
"""Create new Event instance from arguments. Instance is NOT saved."""
if not isinstance(action, EventAction):
raise ValueError(f"action must be EventAction instance but was {type(action)}")
raise ValueError(
f"action must be EventAction instance but was {type(action)}"
)
if not app:
app = getmodule(stack()[_inspect_offset][0]).__name__
event = Event(
action=action.value,
app=app,
context=kwargs)
LOGGER.debug("Created Audit event", action=action, context=kwargs)
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':
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 hasattr(request, "user"):
if isinstance(request.user, AnonymousUser):
self.user = get_anonymous_user()
else:
@ -85,7 +117,7 @@ class Event(UUIDModel):
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'
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)
@ -94,10 +126,12 @@ class Event(UUIDModel):
def save(self, *args, **kwargs):
if not self._state.adding:
raise ValidationError("you may not edit an existing %s" % self._meta.model_name)
raise ValidationError(
"you may not edit an existing %s" % self._meta.model_name
)
return super().save(*args, **kwargs)
class Meta:
verbose_name = _('Audit Event')
verbose_name_plural = _('Audit Events')
verbose_name = _("Audit Event")
verbose_name_plural = _("Audit Events")

View File

@ -3,31 +3,43 @@ from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.dispatch import receiver
from passbook.audit.models import Event, EventAction
from passbook.core.signals import (invitation_created, invitation_used,
user_signed_up)
from passbook.core.signals import invitation_created, invitation_used, user_signed_up
@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"""
Event.new(EventAction.LOGIN).from_http(request)
@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"""
Event.new(EventAction.LOGOUT).from_http(request)
@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"""
Event.new(EventAction.SIGN_UP).from_http(request)
@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"""
Event.new(EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex).from_http(request)
Event.new(
EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex
).from_http(request)
@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"""
Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http(request)
Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http(
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
admin_autoregister('passbook_core')
admin_autoregister("passbook_core")

View File

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

View File

@ -8,16 +8,16 @@ from passbook.core.models import Factor
class FactorSerializer(ModelSerializer):
"""Factor Serializer"""
__type__ = SerializerMethodField(method_name='get_type')
__type__ = SerializerMethodField(method_name="get_type")
def get_type(self, obj):
"""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:
model = Factor
fields = ['pk', 'name', 'slug', 'order', 'enabled', '__type__']
fields = ["pk", "name", "slug", "order", "enabled", "__type__"]
class FactorViewSet(ReadOnlyModelViewSet):

View File

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

View File

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

View File

@ -9,16 +9,16 @@ from passbook.policies.forms import GENERAL_FIELDS
class PolicySerializer(ModelSerializer):
"""Policy Serializer"""
__type__ = SerializerMethodField(method_name='get_type')
__type__ = SerializerMethodField(method_name="get_type")
def get_type(self, obj):
"""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:
model = Policy
fields = ['pk'] + GENERAL_FIELDS + ['__type__']
fields = ["pk"] + GENERAL_FIELDS + ["__type__"]
class PolicyViewSet(ReadOnlyModelViewSet):

View File

@ -8,16 +8,16 @@ from passbook.core.models import PropertyMapping
class PropertyMappingSerializer(ModelSerializer):
"""PropertyMapping Serializer"""
__type__ = SerializerMethodField(method_name='get_type')
__type__ = SerializerMethodField(method_name="get_type")
def get_type(self, obj):
"""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:
model = PropertyMapping
fields = ['pk', 'name', '__type__']
fields = ["pk", "name", "__type__"]
class PropertyMappingViewSet(ReadOnlyModelViewSet):

View File

@ -8,16 +8,16 @@ from passbook.core.models import Provider
class ProviderSerializer(ModelSerializer):
"""Provider Serializer"""
__type__ = SerializerMethodField(method_name='get_type')
__type__ = SerializerMethodField(method_name="get_type")
def get_type(self, obj):
"""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:
model = Provider
fields = ['pk', 'property_mappings', '__type__']
fields = ["pk", "property_mappings", "__type__"]
class ProviderViewSet(ReadOnlyModelViewSet):

View File

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

View File

@ -11,7 +11,7 @@ class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = ['pk', 'username', 'name', 'email']
fields = ["pk", "username", "name", "email"]
class UserViewSet(ModelViewSet):

View File

@ -5,7 +5,7 @@ from django.apps import AppConfig
class PassbookCoreConfig(AppConfig):
"""passbook core app config"""
name = 'passbook.core'
label = 'passbook_core'
verbose_name = 'passbook Core'
mountpoint = ''
name = "passbook.core"
label = "passbook_core"
verbose_name = "passbook Core"
mountpoint = ""

View File

@ -9,21 +9,29 @@ from passbook.core.models import Application, Provider
class ApplicationForm(forms.ModelForm):
"""Application Form"""
provider = forms.ModelChoiceField(queryset=Provider.objects.all().select_subclasses(),
required=False)
provider = forms.ModelChoiceField(
queryset=Provider.objects.all().select_subclasses(), required=False
)
class Meta:
model = Application
fields = ['name', 'slug', 'launch_url', 'icon_url',
'provider', 'policies', 'skip_authorization']
fields = [
"name",
"slug",
"launch_url",
"icon_url",
"provider",
"policies",
"skip_authorization",
]
widgets = {
'name': forms.TextInput(),
'launch_url': forms.TextInput(),
'icon_url': forms.TextInput(),
'policies': FilteredSelectMultiple(_('policies'), False)
"name": forms.TextInput(),
"launch_url": forms.TextInput(),
"icon_url": forms.TextInput(),
"policies": FilteredSelectMultiple(_("policies"), False),
}
labels = {
'launch_url': _('Launch URL'),
'icon_url': _('Icon URL'),
"launch_url": _("Launch URL"),
"icon_url": _("Icon URL"),
}

View File

@ -11,55 +11,64 @@ from passbook.lib.utils.ui import human_list
LOGGER = get_logger()
class LoginForm(forms.Form):
"""Allow users to login"""
title = _('Log in to your account')
title = _("Log in to your account")
uid_field = forms.CharField()
remember_me = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if CONFIG.y('passbook.uid_fields') == ['e-mail']:
self.fields['uid_field'] = forms.EmailField()
self.fields['uid_field'].widget.attrs = {
'placeholder': _(human_list([x.title() for x in CONFIG.y('passbook.uid_fields')]))
if CONFIG.y("passbook.uid_fields") == ["e-mail"]:
self.fields["uid_field"] = forms.EmailField()
self.fields["uid_field"].widget.attrs = {
"placeholder": _(
human_list([x.title() for x in CONFIG.y("passbook.uid_fields")])
)
}
def clean_uid_field(self):
"""Validate uid_field after EmailValidator if 'email' is the only selected uid_fields"""
if CONFIG.y('passbook.uid_fields') == ['email']:
validate_email(self.cleaned_data.get('uid_field'))
return self.cleaned_data.get('uid_field')
if CONFIG.y("passbook.uid_fields") == ["email"]:
validate_email(self.cleaned_data.get("uid_field"))
return self.cleaned_data.get("uid_field")
class SignUpForm(forms.Form):
"""SignUp Form"""
title = _('Sign Up')
name = forms.CharField(label=_('Name'),
widget=forms.TextInput(attrs={'placeholder': _('Name')}))
username = forms.CharField(label=_('Username'),
widget=forms.TextInput(attrs={'placeholder': _('Username')}))
email = forms.EmailField(label=_('E-Mail'),
widget=forms.TextInput(attrs={'placeholder': _('E-Mail')}))
password = forms.CharField(label=_('Password'),
widget=forms.PasswordInput(attrs={'placeholder': _('Password')}))
password_repeat = forms.CharField(label=_('Repeat Password'),
widget=forms.PasswordInput(attrs={
'placeholder': _('Repeat Password')
}))
title = _("Sign Up")
name = forms.CharField(
label=_("Name"), widget=forms.TextInput(attrs={"placeholder": _("Name")})
)
username = forms.CharField(
label=_("Username"),
widget=forms.TextInput(attrs={"placeholder": _("Username")}),
)
email = forms.EmailField(
label=_("E-Mail"), widget=forms.TextInput(attrs={"placeholder": _("E-Mail")})
)
password = forms.CharField(
label=_("Password"),
widget=forms.PasswordInput(attrs={"placeholder": _("Password")}),
)
password_repeat = forms.CharField(
label=_("Repeat Password"),
widget=forms.PasswordInput(attrs={"placeholder": _("Repeat Password")}),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# All fields which have initial data supplied are set to read only
if 'initial' in kwargs:
for field in kwargs.get('initial').keys():
self.fields[field].widget.attrs['readonly'] = 'readonly'
if "initial" in kwargs:
for field in kwargs.get("initial").keys():
self.fields[field].widget.attrs["readonly"] = "readonly"
def clean_username(self):
"""Check if username is used already"""
username = self.cleaned_data.get('username')
username = self.cleaned_data.get("username")
if User.objects.filter(username=username).exists():
LOGGER.warning("Username %s already exists", username)
raise ValidationError(_("Username already exists"))
@ -67,7 +76,7 @@ class SignUpForm(forms.Form):
def clean_email(self):
"""Check if email is already used in django or other auth sources"""
email = self.cleaned_data.get('email')
email = self.cleaned_data.get("email")
# Check if user exists already, error early
if User.objects.filter(email=email).exists():
LOGGER.debug("email %s exists in django", email)
@ -76,8 +85,8 @@ class SignUpForm(forms.Form):
def clean_password_repeat(self):
"""Check if Password adheres to filter and if passwords matche"""
password = self.cleaned_data.get('password')
password_repeat = self.cleaned_data.get('password_repeat')
password = self.cleaned_data.get("password")
password_repeat = self.cleaned_data.get("password_repeat")
if password != password_repeat:
raise ValidationError(_("Passwords don't match"))
return self.cleaned_data.get('password_repeat')
return self.cleaned_data.get("password_repeat")

View File

@ -9,24 +9,29 @@ class GroupForm(forms.ModelForm):
"""Group Form"""
members = forms.ModelMultipleChoiceField(
User.objects.all(), required=False, widget=FilteredSelectMultiple('users', False))
User.objects.all(),
required=False,
widget=FilteredSelectMultiple("users", False),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
self.initial['members'] = self.instance.user_set.values_list('pk', flat=True)
self.initial["members"] = self.instance.user_set.values_list(
"pk", flat=True
)
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
if instance.pk:
instance.user_set.clear()
instance.user_set.add(*self.cleaned_data['members'])
instance.user_set.add(*self.cleaned_data["members"])
return instance
class Meta:
model = Group
fields = ['name', 'parent', 'members', 'attributes']
fields = ["name", "parent", "members", "attributes"]
widgets = {
'name': forms.TextInput(),
"name": forms.TextInput(),
}

View File

@ -12,27 +12,27 @@ class InvitationForm(forms.ModelForm):
def clean_fixed_username(self):
"""Check if username is already used"""
username = self.cleaned_data.get('fixed_username')
username = self.cleaned_data.get("fixed_username")
if User.objects.filter(username=username).exists():
raise ValidationError(_('Username is already in use.'))
raise ValidationError(_("Username is already in use."))
return username
def clean_fixed_email(self):
"""Check if email is already used"""
email = self.cleaned_data.get('fixed_email')
email = self.cleaned_data.get("fixed_email")
if User.objects.filter(email=email).exists():
raise ValidationError(_('E-Mail is already in use.'))
raise ValidationError(_("E-Mail is already in use."))
return email
class Meta:
model = Invitation
fields = ['expires', 'fixed_username', 'fixed_email', 'needs_confirmation']
fields = ["expires", "fixed_username", "fixed_email", "needs_confirmation"]
labels = {
'fixed_username': "Force user's username (optional)",
'fixed_email': "Force user's email (optional)",
"fixed_username": "Force user's username (optional)",
"fixed_email": "Force user's email (optional)",
}
widgets = {
'fixed_username': forms.TextInput(),
'fixed_email': forms.TextInput(),
"fixed_username": forms.TextInput(),
"fixed_email": forms.TextInput(),
}

View File

@ -13,10 +13,8 @@ class DebugPolicyForm(forms.ModelForm):
class Meta:
model = DebugPolicy
fields = GENERAL_FIELDS + ['result', 'wait_min', 'wait_max']
fields = GENERAL_FIELDS + ["result", "wait_min", "wait_max"]
widgets = {
'name': forms.TextInput(),
}
labels = {
'result': _('Allow user')
"name": forms.TextInput(),
}
labels = {"result": _("Allow user")}

View File

@ -13,29 +13,30 @@ class UserDetailForm(forms.ModelForm):
class Meta:
model = User
fields = ['username', 'name', 'email']
widgets = {
'name': forms.TextInput
}
fields = ["username", "name", "email"]
widgets = {"name": forms.TextInput}
class PasswordChangeForm(forms.Form):
"""Form to update password"""
password = forms.CharField(label=_('Password'),
widget=forms.PasswordInput(attrs={
'placeholder': _('New Password'),
'autocomplete': 'new-password'
}))
password_repeat = forms.CharField(label=_('Repeat Password'),
widget=forms.PasswordInput(attrs={
'placeholder': _('Repeat Password'),
'autocomplete': 'new-password'
}))
password = forms.CharField(
label=_("Password"),
widget=forms.PasswordInput(
attrs={"placeholder": _("New Password"), "autocomplete": "new-password"}
),
)
password_repeat = forms.CharField(
label=_("Repeat Password"),
widget=forms.PasswordInput(
attrs={"placeholder": _("Repeat Password"), "autocomplete": "new-password"}
),
)
def clean_password_repeat(self):
"""Check if Password adheres to filter and if passwords matche"""
password = self.cleaned_data.get('password')
password_repeat = self.cleaned_data.get('password_repeat')
password = self.cleaned_data.get("password")
password_repeat = self.cleaned_data.get("password_repeat")
if password != password_repeat:
raise ValidationError(_("Passwords don't match"))
return self.cleaned_data.get('password_repeat')
return self.cleaned_data.get("password_repeat")

View File

@ -18,206 +18,433 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0011_update_proxy_permissions'),
("auth", "0011_update_proxy_permissions"),
]
operations = [
migrations.CreateModel(
name='User',
name="User",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
('name', models.TextField()),
('password_change_date', models.DateTimeField(auto_now_add=True)),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=30, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="email address"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
("uuid", models.UUIDField(default=uuid.uuid4, editable=False)),
("name", models.TextField()),
("password_change_date", models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
"verbose_name": "user",
"verbose_name_plural": "users",
"abstract": False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
managers=[("objects", django.contrib.auth.models.UserManager()),],
),
migrations.CreateModel(
name='Policy',
name="Policy",
fields=[
('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField(blank=True, null=True)),
('action', models.CharField(choices=[('allow', 'allow'), ('deny', 'deny')], max_length=20)),
('negate', models.BooleanField(default=False)),
('order', models.IntegerField(default=0)),
('timeout', models.IntegerField(default=30)),
("created", models.DateTimeField(auto_now_add=True)),
("last_updated", models.DateTimeField(auto_now=True)),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.TextField(blank=True, null=True)),
(
"action",
models.CharField(
choices=[("allow", "allow"), ("deny", "deny")], max_length=20
),
),
("negate", models.BooleanField(default=False)),
("order", models.IntegerField(default=0)),
("timeout", models.IntegerField(default=30)),
],
options={"abstract": False,},
),
migrations.CreateModel(
name="PolicyModel",
fields=[
("created", models.DateTimeField(auto_now_add=True)),
("last_updated", models.DateTimeField(auto_now=True)),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"policies",
models.ManyToManyField(blank=True, to="passbook_core.Policy"),
),
],
options={"abstract": False,},
),
migrations.CreateModel(
name="PropertyMapping",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.TextField()),
],
options={
'abstract': False,
"verbose_name": "Property Mapping",
"verbose_name_plural": "Property Mappings",
},
),
migrations.CreateModel(
name='PolicyModel',
name="DebugPolicy",
fields=[
('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('policies', models.ManyToManyField(blank=True, to='passbook_core.Policy')),
(
"policy_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="passbook_core.Policy",
),
),
("result", models.BooleanField(default=False)),
("wait_min", models.IntegerField(default=5)),
("wait_max", models.IntegerField(default=30)),
],
options={
'abstract': False,
"verbose_name": "Debug Policy",
"verbose_name_plural": "Debug Policies",
},
bases=("passbook_core.policy",),
),
migrations.CreateModel(
name='PropertyMapping',
name="Factor",
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField()),
(
"policymodel_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="passbook_core.PolicyModel",
),
),
("name", models.TextField()),
("slug", models.SlugField(unique=True)),
("order", models.IntegerField()),
("enabled", models.BooleanField(default=True)),
],
options={
'verbose_name': 'Property Mapping',
'verbose_name_plural': 'Property Mappings',
},
options={"abstract": False,},
bases=("passbook_core.policymodel",),
),
migrations.CreateModel(
name='DebugPolicy',
name="Source",
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('result', models.BooleanField(default=False)),
('wait_min', models.IntegerField(default=5)),
('wait_max', models.IntegerField(default=30)),
(
"policymodel_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="passbook_core.PolicyModel",
),
),
("name", models.TextField()),
("slug", models.SlugField()),
("enabled", models.BooleanField(default=True)),
],
options={
'verbose_name': 'Debug Policy',
'verbose_name_plural': 'Debug Policies',
},
bases=('passbook_core.policy',),
options={"abstract": False,},
bases=("passbook_core.policymodel",),
),
migrations.CreateModel(
name='Factor',
name="Provider",
fields=[
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField(unique=True)),
('order', models.IntegerField()),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='Source',
fields=[
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField()),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('property_mappings', models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"property_mappings",
models.ManyToManyField(
blank=True, default=None, to="passbook_core.PropertyMapping"
),
),
],
),
migrations.CreateModel(
name='Nonce',
name="Nonce",
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.DateTimeField(default=passbook.core.models.default_nonce_duration)),
('expiring', models.BooleanField(default=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"expires",
models.DateTimeField(
default=passbook.core.models.default_nonce_duration
),
),
("expiring", models.BooleanField(default=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={"verbose_name": "Nonce", "verbose_name_plural": "Nonces",},
),
migrations.CreateModel(
name="Invitation",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("expires", models.DateTimeField(blank=True, default=None, null=True)),
("fixed_username", models.TextField(blank=True, default=None)),
("fixed_email", models.TextField(blank=True, default=None)),
("needs_confirmation", models.BooleanField(default=True)),
(
"created_by",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
'verbose_name': 'Nonce',
'verbose_name_plural': 'Nonces',
"verbose_name": "Invitation",
"verbose_name_plural": "Invitations",
},
),
migrations.CreateModel(
name='Invitation',
name="Group",
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.DateTimeField(blank=True, default=None, null=True)),
('fixed_username', models.TextField(blank=True, default=None)),
('fixed_email', models.TextField(blank=True, default=None)),
('needs_confirmation', models.BooleanField(default=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=80, verbose_name="name")),
(
"tags",
django.contrib.postgres.fields.jsonb.JSONField(
blank=True, default=dict
),
),
(
"parent",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="children",
to="passbook_core.Group",
),
),
],
options={
'verbose_name': 'Invitation',
'verbose_name_plural': 'Invitations',
},
),
migrations.CreateModel(
name='Group',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=80, verbose_name='name')),
('tags', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='passbook_core.Group')),
],
options={
'unique_together': {('name', 'parent')},
},
options={"unique_together": {("name", "parent")},},
),
migrations.AddField(
model_name='user',
name='groups',
field=models.ManyToManyField(to='passbook_core.Group'),
model_name="user",
name="groups",
field=models.ManyToManyField(to="passbook_core.Group"),
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
model_name="user",
name="user_permissions",
field=models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.Permission",
verbose_name="user permissions",
),
),
migrations.CreateModel(
name='UserSourceConnection',
name="UserSourceConnection",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Source')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("last_updated", models.DateTimeField(auto_now=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
(
"source",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="passbook_core.Source",
),
),
],
options={
'unique_together': {('user', 'source')},
},
options={"unique_together": {("user", "source")},},
),
migrations.CreateModel(
name='Application',
name="Application",
fields=[
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField()),
('launch_url', models.URLField(blank=True, null=True)),
('icon_url', models.TextField(blank=True, null=True)),
('skip_authorization', models.BooleanField(default=False)),
('provider', models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='passbook_core.Provider')),
(
"policymodel_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="passbook_core.PolicyModel",
),
),
("name", models.TextField()),
("slug", models.SlugField()),
("launch_url", models.URLField(blank=True, null=True)),
("icon_url", models.TextField(blank=True, null=True)),
("skip_authorization", models.BooleanField(default=False)),
(
"provider",
models.OneToOneField(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="passbook_core.Provider",
),
),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
options={"abstract": False,},
bases=("passbook_core.policymodel",),
),
migrations.AddField(
model_name='user',
name='sources',
field=models.ManyToManyField(through='passbook_core.UserSourceConnection', to='passbook_core.Source'),
model_name="user",
name="sources",
field=models.ManyToManyField(
through="passbook_core.UserSourceConnection", to="passbook_core.Source"
),
),
]

View File

@ -6,12 +6,12 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0001_initial'),
("passbook_core", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name='user',
options={'permissions': (('reset_user_password', 'Reset Password'),)},
name="user",
options={"permissions": (("reset_user_password", "Reset Password"),)},
),
]

View File

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0001_initial'),
("passbook_core", "0001_initial"),
]
operations = [
migrations.AddField(
model_name='nonce',
name='description',
field=models.TextField(blank=True, default=''),
model_name="nonce",
name="description",
field=models.TextField(blank=True, default=""),
),
]

View File

@ -7,23 +7,25 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0002_nonce_description'),
("passbook_core", "0002_nonce_description"),
]
operations = [
migrations.RenameField(
model_name='group',
old_name='tags',
new_name='attributes',
model_name="group", old_name="tags", new_name="attributes",
),
migrations.AddField(
model_name='source',
name='property_mappings',
field=models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping'),
model_name="source",
name="property_mappings",
field=models.ManyToManyField(
blank=True, default=None, to="passbook_core.PropertyMapping"
),
),
migrations.AddField(
model_name='user',
name='attributes',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
model_name="user",
name="attributes",
field=django.contrib.postgres.fields.jsonb.JSONField(
blank=True, default=dict
),
),
]

View File

@ -6,9 +6,8 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0002_auto_20191010_1058'),
('passbook_core', '0002_nonce_description'),
("passbook_core", "0002_auto_20191010_1058"),
("passbook_core", "0002_nonce_description"),
]
operations = [
]
operations = []

View File

@ -6,12 +6,9 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0003_auto_20191011_0914'),
("passbook_core", "0003_auto_20191011_0914"),
]
operations = [
migrations.RemoveField(
model_name='policy',
name='action',
),
migrations.RemoveField(model_name="policy", name="action",),
]

View File

@ -6,9 +6,8 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0004_remove_policy_action'),
('passbook_core', '0003_merge_20191010_1541'),
("passbook_core", "0004_remove_policy_action"),
("passbook_core", "0003_merge_20191010_1541"),
]
operations = [
]
operations = []

View File

@ -27,12 +27,18 @@ def default_nonce_duration():
"""Default duration a Nonce is valid"""
return now() + timedelta(hours=4)
class Group(UUIDModel):
"""Custom Group model which supports a basic hierarchy"""
name = models.CharField(_('name'), max_length=80)
parent = models.ForeignKey('Group', blank=True, null=True,
on_delete=models.SET_NULL, related_name='children')
name = models.CharField(_("name"), max_length=80)
parent = models.ForeignKey(
"Group",
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name="children",
)
attributes = JSONField(default=dict, blank=True)
def __str__(self):
@ -40,7 +46,8 @@ class Group(UUIDModel):
class Meta:
unique_together = (('name', 'parent',),)
unique_together = (("name", "parent",),)
class User(GuardianUserMixin, AbstractUser):
"""Custom User model to allow easier adding o f user-based settings"""
@ -48,8 +55,8 @@ class User(GuardianUserMixin, AbstractUser):
uuid = models.UUIDField(default=uuid4, editable=False)
name = models.TextField()
sources = models.ManyToManyField('Source', through='UserSourceConnection')
groups = models.ManyToManyField('Group')
sources = models.ManyToManyField("Source", through="UserSourceConnection")
groups = models.ManyToManyField("Group")
password_change_date = models.DateTimeField(auto_now_add=True)
attributes = JSONField(default=dict, blank=True)
@ -62,28 +69,29 @@ class User(GuardianUserMixin, AbstractUser):
class Meta:
permissions = (
('reset_user_password', 'Reset Password'),
)
permissions = (("reset_user_password", "Reset Password"),)
class Provider(models.Model):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
property_mappings = models.ManyToManyField('PropertyMapping', default=None, blank=True)
property_mappings = models.ManyToManyField(
"PropertyMapping", default=None, blank=True
)
objects = InheritanceManager()
# This class defines no field for easier inheritance
def __str__(self):
if hasattr(self, 'name'):
return getattr(self, 'name')
if hasattr(self, "name"):
return getattr(self, "name")
return super().__str__()
class PolicyModel(UUIDModel, CreatedUpdatedModel):
"""Base model which can have policies applied to it"""
policies = models.ManyToManyField('Policy', blank=True)
policies = models.ManyToManyField("Policy", blank=True)
class UserSettings:
@ -108,8 +116,8 @@ class Factor(PolicyModel):
enabled = models.BooleanField(default=True)
objects = InheritanceManager()
type = ''
form = ''
type = ""
form = ""
def user_settings(self) -> Optional[UserSettings]:
"""Entrypoint to integrate with User settings. Can either return None if no
@ -129,8 +137,9 @@ class Application(PolicyModel):
slug = models.SlugField()
launch_url = models.URLField(null=True, blank=True)
icon_url = models.TextField(null=True, blank=True)
provider = models.OneToOneField('Provider', null=True, blank=True,
default=None, on_delete=models.SET_DEFAULT)
provider = models.OneToOneField(
"Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT
)
skip_authorization = models.BooleanField(default=False)
objects = InheritanceManager()
@ -151,9 +160,11 @@ class Source(PolicyModel):
name = models.TextField()
slug = models.SlugField()
enabled = models.BooleanField(default=True)
property_mappings = models.ManyToManyField('PropertyMapping', default=None, blank=True)
property_mappings = models.ManyToManyField(
"PropertyMapping", default=None, blank=True
)
form = '' # ModelForm-based class ued to create/edit instance
form = "" # ModelForm-based class ued to create/edit instance
objects = InheritanceManager()
@ -185,7 +196,7 @@ class UserSourceConnection(CreatedUpdatedModel):
class Meta:
unique_together = (('user', 'source'),)
unique_together = (("user", "source"),)
class Policy(UUIDModel, CreatedUpdatedModel):
@ -215,25 +226,25 @@ class DebugPolicy(Policy):
wait_min = models.IntegerField(default=5)
wait_max = models.IntegerField(default=30)
form = 'passbook.core.forms.policies.DebugPolicyForm'
form = "passbook.core.forms.policies.DebugPolicyForm"
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Wait random time then return result"""
wait = SystemRandom().randrange(self.wait_min, self.wait_max)
LOGGER.debug("Policy waiting", policy=self, delay=wait)
sleep(wait)
return PolicyResult(self.result, 'Debugging')
return PolicyResult(self.result, "Debugging")
class Meta:
verbose_name = _('Debug Policy')
verbose_name_plural = _('Debug Policies')
verbose_name = _("Debug Policy")
verbose_name_plural = _("Debug Policies")
class Invitation(UUIDModel):
"""Single-use invitation link"""
created_by = models.ForeignKey('User', on_delete=models.CASCADE)
created_by = models.ForeignKey("User", on_delete=models.CASCADE)
expires = models.DateTimeField(default=None, blank=True, null=True)
fixed_username = models.TextField(blank=True, default=None)
fixed_email = models.TextField(blank=True, default=None)
@ -242,24 +253,26 @@ class Invitation(UUIDModel):
@property
def link(self):
"""Get link to use invitation"""
return reverse_lazy('passbook_core:auth-sign-up') + f'?invitation={self.uuid.hex}'
return (
reverse_lazy("passbook_core:auth-sign-up") + f"?invitation={self.uuid.hex}"
)
def __str__(self):
return f"Invitation {self.uuid.hex} created by {self.created_by}"
class Meta:
verbose_name = _('Invitation')
verbose_name_plural = _('Invitations')
verbose_name = _("Invitation")
verbose_name_plural = _("Invitations")
class Nonce(UUIDModel):
"""One-time link for password resets/sign-up-confirmations"""
expires = models.DateTimeField(default=default_nonce_duration)
user = models.ForeignKey('User', on_delete=models.CASCADE)
user = models.ForeignKey("User", on_delete=models.CASCADE)
expiring = models.BooleanField(default=True)
description = models.TextField(default='', blank=True)
description = models.TextField(default="", blank=True)
@property
def is_expired(self) -> bool:
@ -271,8 +284,8 @@ class Nonce(UUIDModel):
class Meta:
verbose_name = _('Nonce')
verbose_name_plural = _('Nonces')
verbose_name = _("Nonce")
verbose_name_plural = _("Nonces")
class PropertyMapping(UUIDModel):
@ -280,7 +293,7 @@ class PropertyMapping(UUIDModel):
name = models.TextField()
form = ''
form = ""
objects = InheritanceManager()
def __str__(self):
@ -288,5 +301,5 @@ class PropertyMapping(UUIDModel):
class Meta:
verbose_name = _('Property Mapping')
verbose_name_plural = _('Property Mappings')
verbose_name = _("Property Mapping")
verbose_name_plural = _("Property Mappings")

View File

@ -7,16 +7,18 @@ from structlog import get_logger
LOGGER = get_logger()
user_signed_up = Signal(providing_args=['request', 'user'])
invitation_created = Signal(providing_args=['request', 'invitation'])
invitation_used = Signal(providing_args=['request', 'invitation', 'user'])
password_changed = Signal(providing_args=['user', 'password'])
user_signed_up = Signal(providing_args=["request", "user"])
invitation_created = Signal(providing_args=["request", "invitation"])
invitation_used = Signal(providing_args=["request", "invitation", "user"])
password_changed = Signal(providing_args=["user", "password"])
@receiver(post_save)
# pylint: disable=unused-argument
def invalidate_policy_cache(sender, instance, **_):
"""Invalidate Policy cache when policy is updated"""
from passbook.core.models import Policy
if isinstance(instance, Policy):
LOGGER.debug("Invalidating policy cache", policy=instance)
keys = cache.keys("%s#*" % instance.pk)

View File

@ -7,8 +7,9 @@ from passbook.root.celery import CELERY_APP
LOGGER = get_logger()
@CELERY_APP.task()
def clean_nonces():
"""Remove expired nonces"""
amount, _ = Nonce.objects.filter(expires__lt=now(), expiring=True).delete()
LOGGER.debug('Deleted expired nonces', amount=amount)
LOGGER.debug("Deleted expired nonces", amount=amount)

View File

@ -9,29 +9,37 @@ from passbook.policies.engine import PolicyEngine
register = template.Library()
@register.simple_tag(takes_context=True)
def user_factors(context: RequestContext) -> List[UserSettings]:
"""Return list of all factors which apply to user"""
user = context.get('request').user
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
user = context.get("request").user
_all_factors = (
Factor.objects.filter(enabled=True).order_by("order").select_subclasses()
)
matching_factors: List[UserSettings] = []
for factor in _all_factors:
user_settings = factor.user_settings()
policy_engine = PolicyEngine(factor.policies.all(), user, context.get('request'))
policy_engine = PolicyEngine(
factor.policies.all(), user, context.get("request")
)
policy_engine.build()
if policy_engine.passing and user_settings:
matching_factors.append(user_settings)
return matching_factors
@register.simple_tag(takes_context=True)
def user_sources(context: RequestContext) -> List[UserSettings]:
"""Return a list of all sources which are enabled for the user"""
user = context.get('request').user
user = context.get("request").user
_all_sources = Source.objects.filter(enabled=True).select_subclasses()
matching_sources: List[UserSettings] = []
for factor in _all_sources:
user_settings = factor.user_settings()
policy_engine = PolicyEngine(factor.policies.all(), user, context.get('request'))
policy_engine = PolicyEngine(
factor.policies.all(), user, context.get("request")
)
policy_engine.build()
if policy_engine.passing and user_settings:
matching_sources.append(user_settings)

View File

@ -15,70 +15,78 @@ class TestAuthenticationViews(TestCase):
def setUp(self):
super().setUp()
self.sign_up_data = {
'name': 'Test',
'username': 'beryjuorg',
'email': 'unittest@passbook.beryju.org',
'password': 'B3ryju0rg!',
'password_repeat': 'B3ryju0rg!',
"name": "Test",
"username": "beryjuorg",
"email": "unittest@passbook.beryju.org",
"password": "B3ryju0rg!",
"password_repeat": "B3ryju0rg!",
}
self.login_data = {
'uid_field': 'unittest@example.com',
"uid_field": "unittest@example.com",
}
self.user = User.objects.create_superuser(
username='unittest user',
email='unittest@example.com',
password=''.join(SystemRandom().choice(
string.ascii_uppercase + string.digits) for _ in range(8)))
username="unittest user",
email="unittest@example.com",
password="".join(
SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
),
)
def test_sign_up_view(self):
"""Test account.sign_up view (Anonymous)"""
self.client.logout()
response = self.client.get(reverse('passbook_core:auth-sign-up'))
response = self.client.get(reverse("passbook_core:auth-sign-up"))
self.assertEqual(response.status_code, 200)
def test_login_view(self):
"""Test account.login view (Anonymous)"""
self.client.logout()
response = self.client.get(reverse('passbook_core:auth-login'))
response = self.client.get(reverse("passbook_core:auth-login"))
self.assertEqual(response.status_code, 200)
# test login with post
form = LoginForm(self.login_data)
self.assertTrue(form.is_valid())
response = self.client.post(reverse('passbook_core:auth-login'), data=form.cleaned_data)
response = self.client.post(
reverse("passbook_core:auth-login"), data=form.cleaned_data
)
self.assertEqual(response.status_code, 302)
def test_logout_view(self):
"""Test account.logout view"""
self.client.force_login(self.user)
response = self.client.get(reverse('passbook_core:auth-logout'))
response = self.client.get(reverse("passbook_core:auth-logout"))
self.assertEqual(response.status_code, 302)
def test_sign_up_view_auth(self):
"""Test account.sign_up view (Authenticated)"""
self.client.force_login(self.user)
response = self.client.get(reverse('passbook_core:auth-logout'))
response = self.client.get(reverse("passbook_core:auth-logout"))
self.assertEqual(response.status_code, 302)
def test_login_view_auth(self):
"""Test account.login view (Authenticated)"""
self.client.force_login(self.user)
response = self.client.get(reverse('passbook_core:auth-login'))
response = self.client.get(reverse("passbook_core:auth-login"))
self.assertEqual(response.status_code, 302)
def test_login_view_post(self):
"""Test account.login view POST (Anonymous)"""
login_response = self.client.post(reverse('passbook_core:auth-login'), data=self.login_data)
login_response = self.client.post(
reverse("passbook_core:auth-login"), data=self.login_data
)
self.assertEqual(login_response.status_code, 302)
self.assertEqual(login_response.url, reverse('passbook_core:auth-process'))
self.assertEqual(login_response.url, reverse("passbook_core:auth-process"))
def test_sign_up_view_post(self):
"""Test account.sign_up view POST (Anonymous)"""
form = SignUpForm(self.sign_up_data)
self.assertTrue(form.is_valid())
response = self.client.post(reverse('passbook_core:auth-sign-up'), data=form.cleaned_data)
response = self.client.post(
reverse("passbook_core:auth-sign-up"), data=form.cleaned_data
)
self.assertEqual(response.status_code, 302)
# def test_reset_password_init_view(self):

View File

@ -14,12 +14,17 @@ class TestOverviewViews(TestCase):
def setUp(self):
super().setUp()
self.user = User.objects.create_superuser(
username='unittest user',
email='unittest@example.com',
password=''.join(SystemRandom().choice(
string.ascii_uppercase + string.digits) for _ in range(8)))
username="unittest user",
email="unittest@example.com",
password="".join(
SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
),
)
self.client.force_login(self.user)
def test_overview(self):
"""Test UserSettingsView"""
self.assertEqual(self.client.get(reverse('passbook_core:overview')).status_code, 200)
self.assertEqual(
self.client.get(reverse("passbook_core:overview")).status_code, 200
)

View File

@ -15,33 +15,43 @@ class TestUserViews(TestCase):
def setUp(self):
super().setUp()
self.user = User.objects.create_superuser(
username='unittest user',
email='unittest@example.com',
password=''.join(SystemRandom().choice(
string.ascii_uppercase + string.digits) for _ in range(8)))
username="unittest user",
email="unittest@example.com",
password="".join(
SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
),
)
self.client.force_login(self.user)
def test_user_settings(self):
"""Test UserSettingsView"""
self.assertEqual(self.client.get(reverse('passbook_core:user-settings')).status_code, 200)
self.assertEqual(
self.client.get(reverse("passbook_core:user-settings")).status_code, 200
)
def test_user_delete(self):
"""Test UserDeleteView"""
self.assertEqual(self.client.post(reverse('passbook_core:user-delete')).status_code, 302)
self.assertEqual(User.objects.filter(username='unittest user').exists(), False)
self.assertEqual(
self.client.post(reverse("passbook_core:user-delete")).status_code, 302
)
self.assertEqual(User.objects.filter(username="unittest user").exists(), False)
self.setUp()
def test_user_change_password(self):
"""Test UserChangePasswordView"""
form_data = {
'password': 'test2',
'password_repeat': 'test2'
}
form_data = {"password": "test2", "password_repeat": "test2"}
form = PasswordChangeForm(data=form_data)
self.assertTrue(form.is_valid())
self.assertEqual(self.client.get(
reverse('passbook_core:user-change-password')).status_code, 200)
self.assertEqual(self.client.post(
reverse('passbook_core:user-change-password'), data=form_data).status_code, 302)
self.assertEqual(
self.client.get(reverse("passbook_core:user-change-password")).status_code,
200,
)
self.assertEqual(
self.client.post(
reverse("passbook_core:user-change-password"), data=form_data
).status_code,
302,
)
self.user.refresh_from_db()
self.assertTrue(self.user.check_password('test2'))
self.assertTrue(self.user.check_password("test2"))

View File

@ -13,22 +13,25 @@ class TestUtilViews(TestCase):
def setUp(self):
self.user = User.objects.create_superuser(
username='unittest user',
email='unittest@example.com',
password=''.join(SystemRandom().choice(
string.ascii_uppercase + string.digits) for _ in range(8)))
username="unittest user",
email="unittest@example.com",
password="".join(
SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
),
)
self.factory = RequestFactory()
def test_loading_view(self):
"""Test loading view"""
request = self.factory.get('something')
response = LoadingView.as_view(target_url='somestring')(request)
request = self.factory.get("something")
response = LoadingView.as_view(target_url="somestring")(request)
response.render()
self.assertIn('somestring', response.content.decode('utf-8'))
self.assertIn("somestring", response.content.decode("utf-8"))
def test_permission_denied_view(self):
"""Test PermissionDeniedView"""
request = self.factory.get('something')
request = self.factory.get("something")
request.user = self.user
response = PermissionDeniedView.as_view()(request)
self.assertEqual(response.status_code, 200)

View File

@ -9,21 +9,38 @@ LOGGER = get_logger()
urlpatterns = [
# Authentication views
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'),
path('auth/sign_up/', authentication.SignUpView.as_view(), name='auth-sign-up'),
path('auth/sign_up/<uuid:nonce>/confirm/', authentication.SignUpConfirmView.as_view(),
name='auth-sign-up-confirm'),
path('auth/process/denied/', view.FactorPermissionDeniedView.as_view(), name='auth-denied'),
path('auth/password/reset/<uuid:nonce>/', authentication.PasswordResetView.as_view(),
name='auth-password-reset'),
path('auth/process/', view.AuthenticationView.as_view(), name='auth-process'),
path('auth/process/<slug:factor>/', view.AuthenticationView.as_view(), name='auth-process'),
path("auth/login/", authentication.LoginView.as_view(), name="auth-login"),
path("auth/logout/", authentication.LogoutView.as_view(), name="auth-logout"),
path("auth/sign_up/", authentication.SignUpView.as_view(), name="auth-sign-up"),
path(
"auth/sign_up/<uuid:nonce>/confirm/",
authentication.SignUpConfirmView.as_view(),
name="auth-sign-up-confirm",
),
path(
"auth/process/denied/",
view.FactorPermissionDeniedView.as_view(),
name="auth-denied",
),
path(
"auth/password/reset/<uuid:nonce>/",
authentication.PasswordResetView.as_view(),
name="auth-password-reset",
),
path("auth/process/", view.AuthenticationView.as_view(), name="auth-process"),
path(
"auth/process/<slug:factor>/",
view.AuthenticationView.as_view(),
name="auth-process",
),
# User views
path('_/user/', user.UserSettingsView.as_view(), name='user-settings'),
path('_/user/delete/', user.UserDeleteView.as_view(), name='user-delete'),
path('_/user/change_password/', user.UserChangePasswordView.as_view(),
name='user-change-password'),
path("_/user/", user.UserSettingsView.as_view(), name="user-settings"),
path("_/user/delete/", user.UserDeleteView.as_view(), name="user-delete"),
path(
"_/user/change_password/",
user.UserChangePasswordView.as_view(),
name="user-change-password",
),
# Overview
path('', overview.OverviewView.as_view(), name='overview'),
path("", overview.OverviewView.as_view(), name="overview"),
]

View File

@ -11,6 +11,7 @@ from passbook.policies.engine import PolicyEngine
LOGGER = get_logger()
class AccessMixin:
"""Mixin class for usage in Authorization views.
Provider functions to check application access, etc"""
@ -23,12 +24,18 @@ class AccessMixin:
try:
return provider.application
except Application.DoesNotExist as exc:
messages.error(self.request, _('Provider "%(name)s" has no application assigned' % {
'name': provider
}))
messages.error(
self.request,
_(
'Provider "%(name)s" has no application assigned'
% {"name": provider}
),
)
raise exc
def user_has_access(self, application: Application, user: User) -> Tuple[bool, List[str]]:
def user_has_access(
self, application: Application, user: User
) -> Tuple[bool, List[str]]:
"""Check if user has access to application."""
LOGGER.debug("Checking permissions", user=user, application=application)
policy_engine = PolicyEngine(application.policies.all(), user, self.request)

View File

@ -25,41 +25,41 @@ LOGGER = get_logger()
class LoginView(UserPassesTestMixin, FormView):
"""Allow users to sign in"""
template_name = 'login/form.html'
template_name = "login/form.html"
form_class = LoginForm
success_url = '.'
success_url = "."
# Allow only not authenticated users to login
def test_func(self):
return self.request.user.is_authenticated is False
def handle_no_permission(self):
if 'next' in self.request.GET:
return redirect(self.request.GET.get('next'))
return redirect(reverse('passbook_core:overview'))
if "next" in self.request.GET:
return redirect(self.request.GET.get("next"))
return redirect(reverse("passbook_core:overview"))
def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True
kwargs['title'] = _('Log in to your account')
kwargs['primary_action'] = _('Log in')
kwargs['show_sign_up_notice'] = CONFIG.y('passbook.sign_up.enabled')
kwargs['sources'] = []
kwargs["config"] = CONFIG.y("passbook")
kwargs["is_login"] = True
kwargs["title"] = _("Log in to your account")
kwargs["primary_action"] = _("Log in")
kwargs["show_sign_up_notice"] = CONFIG.y("passbook.sign_up.enabled")
kwargs["sources"] = []
sources = Source.objects.filter(enabled=True).select_subclasses()
for source in sources:
login_button = source.login_button
if login_button:
kwargs['sources'].append(login_button)
if kwargs['sources']:
self.template_name = 'login/with_sources.html'
kwargs["sources"].append(login_button)
if kwargs["sources"]:
self.template_name = "login/with_sources.html"
return super().get_context_data(**kwargs)
def get_user(self, uid_value) -> Optional[User]:
"""Find user instance. Returns None if no user was found."""
for search_field in CONFIG.y('passbook.uid_fields'):
for search_field in CONFIG.y("passbook.uid_fields"):
# Workaround for E-Mail -> email
if search_field == 'e-mail':
search_field = 'email'
if search_field == "e-mail":
search_field = "email"
users = User.objects.filter(**{search_field: uid_value})
if users.exists():
LOGGER.debug("Found user", user=users.first(), uid_field=search_field)
@ -68,17 +68,20 @@ class LoginView(UserPassesTestMixin, FormView):
def form_valid(self, form: LoginForm) -> HttpResponse:
"""Form data is valid"""
pre_user = self.get_user(form.cleaned_data.get('uid_field'))
pre_user = self.get_user(form.cleaned_data.get("uid_field"))
if not pre_user:
# No user found
return self.invalid_login(self.request)
# self.request.session.flush()
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
return _redirect_with_qs("passbook_core:auth-process", self.request.GET)
def invalid_login(self, request: HttpRequest, disabled_user: User = None) -> HttpResponse:
def invalid_login(
self, request: HttpRequest, disabled_user: User = None
) -> HttpResponse:
"""Handle login for disabled users/invalid login attempts"""
messages.error(request, _('Failed to authenticate.'))
LOGGER.debug("invalid_login", user=disabled_user)
messages.error(request, _("Failed to authenticate."))
return self.render_to_response(self.get_context_data())
@ -89,15 +92,15 @@ class LogoutView(LoginRequiredMixin, View):
"""Log current user out"""
logout(request)
messages.success(request, _("You've successfully been logged out."))
return redirect(reverse('passbook_core:auth-login'))
return redirect(reverse("passbook_core:auth-login"))
class SignUpView(UserPassesTestMixin, FormView):
"""Sign up new user, optionally consume one-use invitation link."""
template_name = 'login/form.html'
template_name = "login/form.html"
form_class = SignUpForm
success_url = '.'
success_url = "."
# Invitation instance, if invitation link was used
_invitation = None
# Instance of newly created user
@ -108,38 +111,38 @@ class SignUpView(UserPassesTestMixin, FormView):
return self.request.user.is_authenticated is False
def handle_no_permission(self):
return redirect(reverse('passbook_core:overview'))
return redirect(reverse("passbook_core:overview"))
def dispatch(self, request, *args, **kwargs):
"""Check if sign-up is enabled or invitation link given"""
allowed = False
if 'invitation' in request.GET:
invitations = Invitation.objects.filter(uuid=request.GET.get('invitation'))
if "invitation" in request.GET:
invitations = Invitation.objects.filter(uuid=request.GET.get("invitation"))
allowed = invitations.exists()
if allowed:
self._invitation = invitations.first()
if CONFIG.y('passbook.sign_up.enabled'):
if CONFIG.y("passbook.sign_up.enabled"):
allowed = True
if not allowed:
messages.error(request, _('Sign-ups are currently disabled.'))
return redirect(reverse('passbook_core:auth-login'))
messages.error(request, _("Sign-ups are currently disabled."))
return redirect(reverse("passbook_core:auth-login"))
return super().dispatch(request, *args, **kwargs)
def get_initial(self):
if self._invitation:
initial = {}
if self._invitation.fixed_username:
initial['username'] = self._invitation.fixed_username
initial["username"] = self._invitation.fixed_username
if self._invitation.fixed_email:
initial['email'] = self._invitation.fixed_email
initial["email"] = self._invitation.fixed_email
return initial
return super().get_initial()
def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True
kwargs['title'] = _('Sign Up')
kwargs['primary_action'] = _('Sign up')
kwargs["config"] = CONFIG.y("passbook")
kwargs["is_login"] = True
kwargs["title"] = _("Sign Up")
kwargs["primary_action"] = _("Sign up")
return super().get_context_data(**kwargs)
def form_valid(self, form: SignUpForm) -> HttpResponse:
@ -172,9 +175,8 @@ class SignUpView(UserPassesTestMixin, FormView):
# self._user.save()
self.consume_invitation()
messages.success(self.request, _("Successfully signed up!"))
LOGGER.debug("Successfully signed up %s",
form.cleaned_data.get('email'))
return redirect(reverse('passbook_core:auth-login'))
LOGGER.debug("Successfully signed up %s", form.cleaned_data.get("email"))
return redirect(reverse("passbook_core:auth-login"))
def consume_invitation(self):
"""Consume invitation if an invitation was used"""
@ -183,7 +185,8 @@ class SignUpView(UserPassesTestMixin, FormView):
sender=self,
request=self.request,
invitation=self._invitation,
user=self._user)
user=self._user,
)
self._invitation.delete()
@staticmethod
@ -203,20 +206,17 @@ class SignUpView(UserPassesTestMixin, FormView):
"""
# Create user
new_user = User.objects.create(
username=data.get('username'),
email=data.get('email'),
name=data.get('name'),
username=data.get("username"),
email=data.get("email"),
name=data.get("name"),
)
new_user.is_active = True
try:
new_user.set_password(data.get('password'))
new_user.set_password(data.get("password"))
new_user.save()
request.user = new_user
# Send signal for other auth sources
user_signed_up.send(
sender=SignUpView,
user=new_user,
request=request)
user_signed_up.send(sender=SignUpView, user=new_user, request=request)
return new_user
except PasswordPolicyInvalid as exc:
new_user.delete()
@ -232,11 +232,11 @@ class SignUpConfirmView(View):
nonce.user.is_active = True
nonce.user.save()
# Workaround: hardcoded reference to ModelBackend, needs testing
nonce.user.backend = 'django.contrib.auth.backends.ModelBackend'
nonce.user.backend = "django.contrib.auth.backends.ModelBackend"
login(request, nonce.user)
nonce.delete()
messages.success(request, _('Successfully confirmed registration.'))
return redirect('passbook_core:overview')
messages.success(request, _("Successfully confirmed registration."))
return redirect("passbook_core:overview")
class PasswordResetView(View):
@ -247,9 +247,11 @@ class PasswordResetView(View):
# 3. (Optional) Trap user in password change view
nonce = get_object_or_404(Nonce, uuid=nonce)
# Workaround: hardcoded reference to ModelBackend, needs testing
nonce.user.backend = 'django.contrib.auth.backends.ModelBackend'
nonce.user.backend = "django.contrib.auth.backends.ModelBackend"
login(request, nonce.user)
nonce.delete()
messages.success(request, _(('Temporarily authenticated with Nonce, '
'please change your password')))
return redirect('passbook_core:user-change-password')
messages.success(
request,
_(("Temporarily authenticated with Nonce, " "please change your password")),
)
return redirect("passbook_core:user-change-password")

View File

@ -1,8 +1,11 @@
"""passbook core error views"""
from django.http.response import (HttpResponseBadRequest,
HttpResponseForbidden, HttpResponseNotFound,
HttpResponseServerError)
from django.http.response import (
HttpResponseBadRequest,
HttpResponseForbidden,
HttpResponseNotFound,
HttpResponseServerError,
)
from django.template.response import TemplateResponse
from django.views.generic import TemplateView
@ -10,54 +13,53 @@ from django.views.generic import TemplateView
class BadRequestTemplateResponse(TemplateResponse, HttpResponseBadRequest):
"""Combine Template response with Http Code 400"""
class ForbiddenTemplateResponse(TemplateResponse, HttpResponseForbidden):
"""Combine Template response with Http Code 403"""
class NotFoundTemplateResponse(TemplateResponse, HttpResponseNotFound):
"""Combine Template response with Http Code 404"""
class ServerErrorTemplateResponse(TemplateResponse, HttpResponseServerError):
"""Combine Template response with Http Code 500"""
class BadRequestView(TemplateView):
"""Show Bad Request message"""
response_class = BadRequestTemplateResponse
template_name = 'error/400.html'
template_name = "error/400.html"
extra_context = {"is_login": True}
extra_context = {
'is_login': True
}
class ForbiddenView(TemplateView):
"""Show Forbidden message"""
response_class = ForbiddenTemplateResponse
template_name = 'error/403.html'
template_name = "error/403.html"
extra_context = {"is_login": True}
extra_context = {
'is_login': True
}
class NotFoundView(TemplateView):
"""Show Not Found message"""
response_class = NotFoundTemplateResponse
template_name = 'error/404.html'
template_name = "error/404.html"
extra_context = {"is_login": True}
extra_context = {
'is_login': True
}
class ServerErrorView(TemplateView):
"""Show Server Error message"""
response_class = ServerErrorTemplateResponse
template_name = 'error/500.html'
template_name = "error/500.html"
extra_context = {
'is_login': True
}
extra_context = {"is_login": True}
# pylint: disable=useless-super-delegation
def dispatch(self, *args, **kwargs):

View File

@ -11,13 +11,15 @@ class OverviewView(LoginRequiredMixin, TemplateView):
"""Overview for logged in user, incase user opens passbook directly
and is not being forwarded"""
template_name = 'overview/index.html'
template_name = "overview/index.html"
def get_context_data(self, **kwargs):
kwargs['applications'] = []
kwargs["applications"] = []
for application in Application.objects.all():
engine = PolicyEngine(application.policies.all(), self.request.user, self.request)
engine = PolicyEngine(
application.policies.all(), self.request.user, self.request
)
engine.build()
if engine.passing:
kwargs['applications'].append(application)
kwargs["applications"].append(application)
return super().get_context_data(**kwargs)

View File

@ -17,11 +17,11 @@ from passbook.lib.config import CONFIG
class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
"""Update User settings"""
template_name = 'user/settings.html'
template_name = "user/settings.html"
form_class = UserDetailForm
success_message = _('Successfully updated user.')
success_url = reverse_lazy('passbook_core:user-settings')
success_message = _("Successfully updated user.")
success_url = reverse_lazy("passbook_core:user-settings")
def get_object(self):
return self.request.user
@ -30,44 +30,44 @@ class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
class UserDeleteView(LoginRequiredMixin, DeleteView):
"""Delete user account"""
template_name = 'generic/delete.html'
template_name = "generic/delete.html"
def get_object(self):
return self.request.user
def get_success_url(self):
messages.success(self.request, _('Successfully deleted user.'))
messages.success(self.request, _("Successfully deleted user."))
logout(self.request)
return reverse('passbook_core:auth-login')
return reverse("passbook_core:auth-login")
class UserChangePasswordView(LoginRequiredMixin, FormView):
"""View for users to update their password"""
form_class = PasswordChangeForm
template_name = 'login/form_with_user.html'
template_name = "login/form_with_user.html"
def form_valid(self, form: PasswordChangeForm):
try:
# user.set_password checks against Policies so we don't need to manually do it here
self.request.user.set_password(form.cleaned_data.get('password'))
self.request.user.set_password(form.cleaned_data.get("password"))
self.request.user.save()
update_session_auth_hash(self.request, self.request.user)
messages.success(self.request, _('Successfully changed password'))
messages.success(self.request, _("Successfully changed password"))
except PasswordPolicyInvalid as exc:
# Manually inject error into form
# pylint: disable=protected-access
errors = form._errors.setdefault("password_repeat", ErrorList(''))
errors = form._errors.setdefault("password_repeat", ErrorList(""))
# pylint: disable=protected-access
errors = form._errors.setdefault("password", ErrorList())
for error in exc.messages:
errors.append(error)
return self.form_invalid(form)
return redirect('passbook_core:overview')
return redirect("passbook_core:overview")
def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True
kwargs['title'] = _('Change Password')
kwargs['primary_action'] = _('Change')
kwargs["config"] = CONFIG.y("passbook")
kwargs["is_login"] = True
kwargs["title"] = _("Change Password")
kwargs["primary_action"] = _("Change")
return super().get_context_data(**kwargs)

View File

@ -6,8 +6,8 @@ from django.views.generic import TemplateView
class LoadingView(TemplateView):
"""View showing a loading template, and forwarding to real view using html forwarding."""
template_name = 'login/loading.html'
title = _('Loading')
template_name = "login/loading.html"
title = _("Loading")
target_url = None
def get_url(self):
@ -15,18 +15,19 @@ class LoadingView(TemplateView):
return self.target_url
def get_context_data(self, **kwargs):
kwargs['is_login'] = True
kwargs['title'] = self.title
kwargs['target_url'] = self.get_url()
kwargs["is_login"] = True
kwargs["title"] = self.title
kwargs["target_url"] = self.get_url()
return super().get_context_data(**kwargs)
class PermissionDeniedView(TemplateView):
"""Generic Permission denied view"""
template_name = 'login/denied.html'
title = _('Permission denied.')
template_name = "login/denied.html"
title = _("Permission denied.")
def get_context_data(self, **kwargs):
kwargs['is_login'] = True
kwargs['title'] = self.title
kwargs["is_login"] = True
kwargs["title"] = self.title
return super().get_context_data(**kwargs)

View File

@ -17,16 +17,16 @@ class AuthenticationFactor(TemplateView):
authenticator: AuthenticationView
pending_user: User
request: HttpRequest = None
template_name = 'login/form_with_user.html'
template_name = "login/form_with_user.html"
def __init__(self, authenticator: AuthenticationView):
self.authenticator = authenticator
self.pending_user = None
def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True
kwargs['title'] = _('Log in to your account')
kwargs['primary_action'] = _('Log in')
kwargs['user'] = self.pending_user
kwargs["config"] = CONFIG.y("passbook")
kwargs["is_login"] = True
kwargs["title"] = _("Log in to your account")
kwargs["primary_action"] = _("Log in")
kwargs["user"] = self.pending_user
return super().get_context_data(**kwargs)

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