Compare commits
313 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
| 316ac78e49 | |||
| de2b67b111 | |||
| e1bbbe6671 | |||
| 8b3839343c | |||
| 7897ca4744 | |||
| 2fd00c6c9d | |||
| 80f7f82fa4 | |||
| 1a21012911 | |||
| d4a5269bf1 | |||
| fcf70a3cd4 | |||
| e9411d856c | |||
| 1a6dd00681 | |||
| 330bd0932b | |||
| 250e77f40f | |||
| ef71aba544 | |||
| 567a8f53da | |||
| 88c87aa205 | |||
| 90ac3d56ca | |||
| a298e9e2ca | |||
| abdf86d9c9 | |||
| c58658d820 | |||
| a9b5e6ea13 | |||
| ddb0fdee98 | |||
| 83205f1b49 | |||
| 7221800a16 | |||
| 4515cb6bbe | |||
| 7f9da11eba | |||
| da69d2611d | |||
| 3b4be5695a | |||
| 9d68c9550b | |||
| 3b2d469780 | |||
| ae629d1159 | |||
| 72a6f9cbe0 | |||
| 9793b7461b | |||
| 9c1a824dc4 | |||
| 738ced3327 | |||
| ed1ee1fa55 | |||
| 95776bbc56 | |||
| 62a4beb3d6 | |||
| 466a825f5b | |||
| 3ffed279d7 | |||
| 4b6b36b2d2 | |||
| 2a8f63bf86 | |||
| 3c12cf96a9 | |||
| d787caf0e4 | |||
| 0fc2f32d3d | |||
| 894d5da1d8 | |||
| 985d20d025 | |||
| 94f3e6d0c5 | |||
| 0a196608c7 | |||
| d33f0fb2cf | |||
| ffff69ada0 | |||
| 37a432267d | |||
| 88029a4335 | |||
| 4040eb9619 | |||
| c9663a08da | |||
| a3d92ebc0a | |||
| 6fa825e372 | |||
| 6aefd072c8 | |||
| ac2dd3611f | |||
| 74e628ce9c | |||
| d4ee18ee32 | |||
| 9ff3ee7c0c | |||
| 418b94a45a | |||
| 1393078fe6 | |||
| 50612991fa | |||
| 37b2400cdb | |||
| 05c3393669 | |||
| c60d1e1f9a | |||
| 2be7d3191f | |||
| aa692fdacb | |||
| c163637bfd | |||
| 5552aca079 | |||
| ff2456dcfa | |||
| 539264c396 | |||
| 1acfaf1562 | |||
| a81e277cfa | |||
| b4cb78f33f | |||
| 35c0a9532f | |||
| aff074420b | |||
| edbea9ccff | |||
| 6b26e10ea2 | |||
| a737335fdd | |||
| e15f7d7f28 | |||
| fbf9554a9e | |||
| 5f34b08433 | |||
| f67a03ad66 | |||
| 6095301337 | |||
| 4a774b5885 | |||
| aa8fac3a06 | |||
| b8407f5bf6 | |||
| 989c426211 | |||
| 9a888cfcf1 | |||
| 72ec871729 | |||
| 8d58842c9b | |||
| a90aa5e069 | |||
| 639020a2e1 | |||
| 8e6f915ec6 | |||
| 6631471566 | |||
| b452e751ea | |||
| a3baa100d4 | |||
| f7b9de1261 | |||
| 47ca566d06 | |||
| a943d060d2 | |||
| 1675dab314 | |||
| 996aa367d3 | |||
| be6f342e58 | |||
| 464b558a02 | |||
| d1151091cd | |||
| f8e5383ba2 | |||
| 06f73512df | |||
| 0ff4545bab | |||
| ff6e270886 | |||
| 8aa0b72b67 | |||
| 91766a2162 | |||
| a393097504 | |||
| 2056b86ce7 | |||
| 1b0c013d8e | |||
| 92a09be8c0 | |||
| 1e31cd03ed | |||
| dc863a6e87 | |||
| d74366f413 | |||
| 5bcf2aef8c | |||
| 8de3c4fbd6 | |||
| c191b62245 | |||
| 0babbde00e | |||
| b8af312ab1 | |||
| 38cabfb325 | |||
| 0a3528b5f4 | |||
| 30a672758a | |||
| 723a825085 | |||
| 40e794099a | |||
| 111b037512 | |||
| 52f66717d3 | |||
| 7ac4242a38 | |||
| 4caa4be476 | |||
| c6d8bae147 | |||
| c70310730a | |||
| 2d2b2d08f4 | |||
| 8fe6a5b62d | |||
| 5e6221deb8 | |||
| c3b493f7d4 | |||
| dbcb5b4f63 | |||
| f0640fcea9 | |||
| 64c47a59f8 | |||
| 3450b8f1fe | |||
| 9518cefdd7 | |||
| 32d5c26577 | |||
| ef2cdf27b3 | |||
| e58ac7ae90 | |||
| d786fa4b7c | |||
| 0e3e73989d | |||
| d831599608 | |||
| 1e57926603 | |||
| 1524880eec | |||
| 0bfb623f97 | |||
| 429627494c | |||
| 9feea155fe | |||
| 2717e02d93 | |||
| 18bd803b0d | |||
| c7f078ffcc | |||
| 571cb3d65f | |||
| 8c500c38b1 | |||
| 5644e57e6a | |||
| cfc181eed1 | |||
| 91bea38b8e | |||
| d95c5aa739 | |||
| 0b250b897e | |||
| c6880a0f16 | |||
| beb5ffcbdd | |||
| 0715cac39b | |||
| 41117d873d | |||
| 231e448b1a | |||
| b3b8cd807d | |||
| 9021bbd5de | |||
| 169475ab39 | |||
| c00e01626e | |||
| 05d4a9ef62 | |||
| 17a2ac73e7 | |||
| 6bc6f947dd | |||
| b048a1fb4f | |||
| 363940ee8d | |||
| a64e53479c | |||
| 14fdbe7720 | |||
| f56332c954 | |||
| 21c53c748f | |||
| b12182c1d1 | |||
| d8f27f595a | |||
| b25dc2aaa3 | |||
| 3ec3849e72 | |||
| 2dc1b65718 | |||
| af22f507f4 | |||
| 9958019bf3 | |||
| 02d65972cb | |||
| 24ad893350 | |||
| 9c5792b1e1 | |||
| 094d191bff | |||
| 49fb9f688b | |||
| 7d161e5aa1 | |||
| 78e5d471e3 | |||
| 2e2c9f5287 | |||
| d5a3e09a98 | |||
| 2402cfe29d | |||
| 26613b6ea9 | |||
| e5165abf04 | |||
| b26882a450 | |||
| 94281bee88 | |||
| 16b966c16e | |||
| d3b0992456 | |||
| dd74b73b4f | |||
| 0bdfccc1f3 | |||
| ceb0793bc9 | |||
| abea85b635 | |||
| 01c83f6f4a | |||
| 9167c9c3ba | |||
| 04add2e52d | |||
| 1e9241d45b | |||
| 22ee198a31 | |||
| 1d9c92d548 | |||
| b30b58924f | |||
| bead19c64c | |||
| 76e2ba4764 | |||
| 8d095d7436 | |||
| d3a7fd5818 | |||
| 247a8dbc8f | |||
| 9241adfc68 | |||
| ae83ee6d31 | |||
| 4701374021 | |||
| bd40585247 | |||
| cc0b8164b0 | |||
| 310b31a8b7 | |||
| 13900bc603 | |||
| 6634cc2edf | |||
| 3478a2cf6d | |||
| 3b70d12a5f | |||
| 219acf76d5 | |||
| ec6f467fa2 | |||
| 0e6561987e | |||
| 62c20b6e67 | |||
| 13084562c5 | |||
| 02c1c434a2 | |||
| 5f04a75878 | |||
| 3556c76674 | |||
| c7d638de2f | |||
| 143733499f | |||
| 0d6a0ffe14 | |||
| 6d4c7312d8 | |||
| 2cb6a179e8 | |||
| 7de2ad77b5 | |||
| 89c33060d4 | |||
| b61f595562 | |||
| ce2230f774 | |||
| d18a78d04d | |||
| c59c6aa728 | |||
| 729910c383 | |||
| 37fe637422 | |||
| 3114d064ed | |||
| 2ca5e1eedb | |||
| d2bf579ff6 | |||
| 3716bda76e | |||
| a76eb4d30f | |||
| 7c191b0984 | |||
| 9613fcde89 | |||
| 885a2ed057 | |||
| b270fb0742 | |||
| 285a69d91f | |||
| de3b753a26 | |||
| 34be1dd9f4 | |||
| a4c0fb9e75 | |||
| f040223646 | |||
| bf297b8593 | |||
| 43eea9e99c | |||
| 8e38bc87bc | |||
| 50a57fb3dd | |||
| 38b8bc182f | |||
| 9743ad33d6 | |||
| b746ce97ba | |||
| dbee714dac | |||
| d33f632203 | |||
| 812aa4ced5 | |||
| 63466e3384 | |||
| 920858ff72 | |||
| 56f599e4aa | |||
| 05183ed937 | |||
| 8d31eef47d | |||
| 96a6ac85df | |||
| 5a60341a6e | |||
| 21ba969072 | |||
| d6a8d8292d | |||
| 693a92ada5 | |||
| ec823aebed | |||
| b8654c06bf | |||
| 9d03c4c7d2 | |||
| 8c36ab89e8 | |||
| e75e71a5ce | |||
| bf008e368e | |||
| 3c1d02bfc4 | |||
| c1b2093cf7 | |||
| cc7e4ad0e2 | |||
| c07bd6e733 | |||
| 9882342ed1 | |||
| 1c906b12be | |||
| 4d835b18cc | |||
| e02ff7ec30 | |||
| 2e67b0194b | |||
| 02f0712934 | |||
| 7e7ea47f39 | |||
| 7e52711e3a | |||
| 40fd1c9c1f | |||
| 4037a444eb | |||
| 1ed7e900f2 | |||
| cfc8d0a0f7 | |||
| df33616544 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.9.0-pre3
|
||||
current_version = 0.9.0-stable
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
@ -15,6 +15,10 @@ values =
|
||||
beta
|
||||
stable
|
||||
|
||||
[bumpversion:file:README.md]
|
||||
|
||||
[bumpversion:file:docs/installation/docker-compose.md]
|
||||
|
||||
[bumpversion:file:helm/values.yaml]
|
||||
|
||||
[bumpversion:file:helm/Chart.yaml]
|
||||
|
||||
@ -5,8 +5,6 @@ omit =
|
||||
manage.py
|
||||
*/migrations/*
|
||||
*/apps.py
|
||||
passbook/management/commands/web.py
|
||||
passbook/management/commands/worker.py
|
||||
docs/
|
||||
|
||||
[report]
|
||||
|
||||
220
.github/workflows/ci.yml
vendored
220
.github/workflows/ci.yml
vendored
@ -1,220 +0,0 @@
|
||||
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.8'
|
||||
- name: Install dependencies
|
||||
run: sudo pip install -U wheel 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.8'
|
||||
- name: Install dependencies
|
||||
run: sudo pip install -U wheel 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.8'
|
||||
- name: Install dependencies
|
||||
run: sudo pip install -U wheel pipenv && pipenv install --dev && pipenv install --dev prospector --skip-lock
|
||||
- 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.8'
|
||||
- name: Install dependencies
|
||||
run: sudo pip install -U wheel pipenv && pipenv install --dev
|
||||
- name: Lint with bandit
|
||||
run: pipenv run bandit -r passbook
|
||||
pyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.8'
|
||||
- name: Install pyright
|
||||
run: npm install -g pyright
|
||||
- name: Show pyright version
|
||||
run: pyright --version
|
||||
- name: Install dependencies
|
||||
run: sudo pip install -U wheel pipenv && pipenv install --dev
|
||||
- name: Lint with pyright
|
||||
run: pipenv run pyright
|
||||
# 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.8'
|
||||
- name: Install dependencies
|
||||
run: sudo pip install -U wheel 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.8'
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- name: Prepare Chrome node
|
||||
run: |
|
||||
cd e2e
|
||||
docker-compose pull -q chrome
|
||||
docker-compose up -d chrome
|
||||
- name: Build static files for e2e test
|
||||
run: |
|
||||
cd passbook/static/static
|
||||
yarn
|
||||
- name: Run coverage
|
||||
run: pipenv run coverage run ./manage.py test --failfast
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
path: out/
|
||||
- name: Create XML Report
|
||||
run: pipenv run coverage xml
|
||||
- uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# Build
|
||||
build-server:
|
||||
needs:
|
||||
- migrations
|
||||
- coverage
|
||||
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:gh-${GITHUB_REF##*/}
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry
|
||||
run: docker push beryju/passbook:gh-${GITHUB_REF##*/}
|
||||
build-gatekeeper:
|
||||
needs:
|
||||
- migrations
|
||||
- coverage
|
||||
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:gh-${GITHUB_REF##*/} \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry
|
||||
run: docker push beryju/passbook-gatekeeper:gh-${GITHUB_REF##*/}
|
||||
build-static:
|
||||
needs:
|
||||
- migrations
|
||||
- coverage
|
||||
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:gh-${GITHUB_REF##*/}
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry
|
||||
run: docker push beryju/passbook-static:gh-${GITHUB_REF##*/}
|
||||
54
.github/workflows/codeql-analysis.yml
vendored
Normal file
54
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, admin-more-info, ci-deploy-dev, gh-pages, provider-saml-v2]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 20 * * 2'
|
||||
|
||||
jobs:
|
||||
analyse:
|
||||
name: Analyse
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
@ -16,11 +16,11 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.9.0-pre3
|
||||
-t beryju/passbook:0.9.0-stable
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.9.0-pre3
|
||||
run: docker push beryju/passbook:0.9.0-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook:latest
|
||||
build-gatekeeper:
|
||||
@ -37,11 +37,11 @@ jobs:
|
||||
cd gatekeeper
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook-gatekeeper:0.9.0-pre3 \
|
||||
-t beryju/passbook-gatekeeper:0.9.0-stable \
|
||||
-t beryju/passbook-gatekeeper:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-gatekeeper:0.9.0-pre3
|
||||
run: docker push beryju/passbook-gatekeeper:0.9.0-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-gatekeeper:latest
|
||||
build-static:
|
||||
@ -66,11 +66,11 @@ jobs:
|
||||
run: docker build
|
||||
--no-cache
|
||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||
-t beryju/passbook-static:0.9.0-pre3
|
||||
-t beryju/passbook-static:0.9.0-stable
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.9.0-pre3
|
||||
run: docker push beryju/passbook-static:0.9.0-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
@ -86,3 +86,19 @@ jobs:
|
||||
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"
|
||||
sentry-release:
|
||||
needs:
|
||||
- test-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Create a Sentry.io release
|
||||
uses: tclindner/sentry-releases-action@v1.2.0
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: beryjuorg
|
||||
SENTRY_PROJECT: passbook
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 0.9.0-stable
|
||||
environment: production
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -196,3 +196,6 @@ local.env.yml
|
||||
### Helm ###
|
||||
# Chart dependencies
|
||||
**/charts/*.tgz
|
||||
|
||||
# Selenium Screenshots
|
||||
selenium_screenshots/**
|
||||
|
||||
1
Pipfile
1
Pipfile
@ -41,6 +41,7 @@ structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
facebook-sdk = "*"
|
||||
elastic-apm = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
|
||||
432
Pipfile.lock
generated
432
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "fd0192b73c01aaffb90716ce7b6d4e5be9adb8788d3ebd58e54ccd6f85d9b71b"
|
||||
"sha256": "5c22d3a514247b663a07c6492cea09ab140346894a528db06bd805a4a3a4a320"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -18,10 +18,10 @@
|
||||
"default": {
|
||||
"amqp": {
|
||||
"hashes": [
|
||||
"sha256:24dbaff8ce4f30566bb88976b398e8c4e77637171af3af6f1b9650f48890e60b",
|
||||
"sha256:bb68f8d2bced8f93ccfd07d96c689b716b3227720add971be980accfc2952139"
|
||||
"sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21",
|
||||
"sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"
|
||||
],
|
||||
"version": "==2.6.0"
|
||||
"version": "==2.6.1"
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
@ -46,18 +46,18 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:16f83ca3aa98d3faeb4f0738b878525770323e5fb9952435ddf58ca09aacec7c",
|
||||
"sha256:dc87ef82c81d2938f91c7ebfa85dfd032fff1bd3b67c9f66d74b21f8ec1e353d"
|
||||
"sha256:35553b05b47fb6b3494bc447428342ca840348ede485e586d002399a32cae0a3",
|
||||
"sha256:e47537d530d523855e52367c2ff278c732651934cd6b33daf9487649dab8e674"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.14.10"
|
||||
"version": "==1.14.33"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:b22db58da273b77529edef71425f9c281bc627b1b889f81960750507238abbb8",
|
||||
"sha256:cb0d7511a68439bf6f16683489130e06c5bbf9f5a9d647e0cbf63d79f3d3bdaa"
|
||||
"sha256:273dbd8e26d4faa568e4cd4ca3180890b59ff0e3e8df7fb352796796c6808527",
|
||||
"sha256:c12a0dc7021fca9d11c2bdbafdc44372e38180b56a1fab97c27b152f79455cd1"
|
||||
],
|
||||
"version": "==1.17.10"
|
||||
"version": "==1.17.33"
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
@ -76,36 +76,36 @@
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
|
||||
"sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
|
||||
"sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
|
||||
"sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
|
||||
"sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
|
||||
"sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
|
||||
"sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
|
||||
"sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
|
||||
"sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
|
||||
"sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
|
||||
"sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
|
||||
"sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
|
||||
"sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
|
||||
"sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
|
||||
"sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
|
||||
"sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
|
||||
"sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
|
||||
"sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
|
||||
"sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
|
||||
"sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
|
||||
"sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
|
||||
"sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
|
||||
"sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
|
||||
"sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
|
||||
"sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
|
||||
"sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
|
||||
"sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
|
||||
"sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
|
||||
"sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc",
|
||||
"sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9",
|
||||
"sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792",
|
||||
"sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2",
|
||||
"sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022",
|
||||
"sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8",
|
||||
"sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96",
|
||||
"sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2",
|
||||
"sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995",
|
||||
"sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1",
|
||||
"sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849",
|
||||
"sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c",
|
||||
"sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe",
|
||||
"sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3",
|
||||
"sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90",
|
||||
"sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f",
|
||||
"sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1",
|
||||
"sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf",
|
||||
"sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa",
|
||||
"sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc",
|
||||
"sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939",
|
||||
"sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e",
|
||||
"sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0",
|
||||
"sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9",
|
||||
"sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168",
|
||||
"sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33",
|
||||
"sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f",
|
||||
"sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
"version": "==1.14.1"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@ -162,11 +162,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2",
|
||||
"sha256:e1630333248c9b3d4e38f02093a26f1e07b271ca896d73097457996e0fae12e8"
|
||||
"sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582",
|
||||
"sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.7"
|
||||
"version": "==3.0.8"
|
||||
},
|
||||
"django-cors-middleware": {
|
||||
"hashes": [
|
||||
@ -232,11 +232,11 @@
|
||||
},
|
||||
"django-prometheus": {
|
||||
"hashes": [
|
||||
"sha256:7b44f45b18f5cc4322b206887646c1848aab42a842218875c5400333fa5d17ff",
|
||||
"sha256:7b7a2a09bde96ca8e66bcf9de040a239d28f52e55f51884da9380e2d4b1c7550"
|
||||
"sha256:441bd85531ecdeddacbe73c930f16de926c426869ce388fa1e8c8092f7ee5a1b",
|
||||
"sha256:6e824cd407b56c01810c69d2e296940d00afe609b58818794525f9760a9a5364"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.1.0.dev40"
|
||||
"version": "==2.1.0.dev52"
|
||||
},
|
||||
"django-recaptcha": {
|
||||
"hashes": [
|
||||
@ -306,6 +306,39 @@
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"elastic-apm": {
|
||||
"hashes": [
|
||||
"sha256:0c766621a4d15ed4ff7dd195499df1af6d7eb8c13790a727bf05773de2952de0",
|
||||
"sha256:2187a0fd080cac7ed65dabfd64d7693ff187ae9b5ad4a810772387dca6877160",
|
||||
"sha256:2a0bb663d3f9388db233784356f218807b9cfe1f4d4fa4569f41b567c068b50f",
|
||||
"sha256:317e2a897b2a81d79bce42688975cfe0ccf6a3dc8025540c47093ea8ac5f1771",
|
||||
"sha256:3a91d2df89af564dbf0abccb3d370940083205247903fe6d708fa771b16fca38",
|
||||
"sha256:44fe2ce3ea57f97fce5fb32e747f6a9c9b361f5055608d59747c39ae06d1c526",
|
||||
"sha256:4ca9f42d4b841ce598819f2f3a4d516c549cd5c02ab43c8283ca406c3b92a2db",
|
||||
"sha256:56b34b30420aebf9566eeee3ffd633131ce51d1e2a4da6061f143a2b547d1980",
|
||||
"sha256:5a56d20734771a4f7823ec12492fcd17a15dac761ecf1452d034a9b9b8b83388",
|
||||
"sha256:6279cc28bd2f2bc2da478cebd5ace711b52549f736d138f950ebe0fa8f706a6f",
|
||||
"sha256:69bcac2cee8f16a093f57000128caab7d1d3d8ac1474e24ce45190264ffc5ebe",
|
||||
"sha256:7021b931210140e02540f3e56fdc8be07542eed10de82c9e5464dbe449a4c9aa",
|
||||
"sha256:70237e1242ae461500ed455f47a5518abdbdc565e47265eddf3ca1dad530a541",
|
||||
"sha256:7545f27703151ce71d73271a95662735cffb537189c214f778195a6fdab58533",
|
||||
"sha256:8525ba800fbd955b65af667c43889df2358c22b1ef66ee92a846f5f4bc8d7286",
|
||||
"sha256:8ba4239862f0b043d191a19e021637a25c3490f677cb8b1dd752bc425bb382e0",
|
||||
"sha256:8c98625cb825c404954763ca5a6f82e06b833a6e6a9e2035065dc9894b4dc6dc",
|
||||
"sha256:b02394f4d55af4f39086aee7bacf8652fde703f7226c5a564cdae9f7e2bf3f71",
|
||||
"sha256:b3b1815765638ce01f9dbd136822d79e887d8d09cd10bc8770d4cc1d530bb853",
|
||||
"sha256:b7bce10060abd98198d8a96e7f3e2e0e169dbd860c76e2c09e6a8874384eebb7",
|
||||
"sha256:b8f849202dffe97512843dd366c4104d07d3b319e42916e3e031cff3db7475db",
|
||||
"sha256:bc677614c198486ca4ef1026bde0c4efd74b936598ff9d64ea109f978a6381bb",
|
||||
"sha256:d19fe00915c60ceabee42ae8c0aa76c6a48c2ffa67c5ba7f0d0fbb856ac36c09",
|
||||
"sha256:d5561eb57eaa43c721258797dfab67b13938fdc94b7daec7a6ccb56dc524fe02",
|
||||
"sha256:dc04aa32c7a3a17c688e3cc4c6293f2176be2482d67efccc651ff1fbb5c00ed6",
|
||||
"sha256:e0d2c3463061b0e50ca53530bd5317498517d208618d90cf6e9933e93f9c727e",
|
||||
"sha256:e9a416418cb2f6deb7a18b68bd75dad0552b4fd85d3e72e59ae4add0e8739b1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.8.1"
|
||||
},
|
||||
"facebook-sdk": {
|
||||
"hashes": [
|
||||
"sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e",
|
||||
@ -322,10 +355,10 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"version": "==2.9"
|
||||
"version": "==2.10"
|
||||
},
|
||||
"inflection": {
|
||||
"hashes": [
|
||||
@ -380,36 +413,40 @@
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:06748c7192eab0f48e3d35a7adae609a329c6257495d5e53878003660dc0fec6",
|
||||
"sha256:0790ddca3f825dd914978c94c2545dbea5f56f008b050e835403714babe62a5f",
|
||||
"sha256:1aa7a6197c1cdd65d974f3e4953764eee3d9c7b67e3966616b41fab7f8f516b7",
|
||||
"sha256:22c6d34fdb0e65d5f782a4d1a1edb52e0a8365858dafb1c08cb1d16546cf0786",
|
||||
"sha256:2754d4406438c83144f9ffd3628bbe2dcc6d62b20dbc5c1ec4bc4385e5d44b42",
|
||||
"sha256:27ee0faf8077c7c1a589573b1450743011117f1aa1a91d5ae776bbc5ca6070f2",
|
||||
"sha256:2b02c106709466a93ed424454ce4c970791c486d5fcdf52b0d822a7e29789626",
|
||||
"sha256:2d1ddce96cf15f1254a68dba6935e6e0f1fe39247de631c115e84dd404a6f031",
|
||||
"sha256:4f282737d187ae723b2633856085c31ae5d4d432968b7f3f478a48a54835f5c4",
|
||||
"sha256:51bb4edeb36d24ec97eb3e6a6007be128b720114f9a875d6b370317d62ac80b9",
|
||||
"sha256:7eee37c1b9815e6505847aa5e68f192e8a1b730c5c7ead39ff317fde9ce29448",
|
||||
"sha256:7fd88cb91a470b383aafad554c3fe1ccf6dfb2456ff0e84b95335d582a799804",
|
||||
"sha256:9144ce36ca0824b29ebc2e02ca186e54040ebb224292072250467190fb613b96",
|
||||
"sha256:925baf6ff1ef2c45169f548cc85204433e061360bfa7d01e1be7ae38bef73194",
|
||||
"sha256:a636346c6c0e1092ffc202d97ec1843a75937d8c98aaf6771348ad6422e44bb0",
|
||||
"sha256:a87dbee7ad9dce3aaefada2081843caf08a44a8f52e03e0a4cc5819f8398f2f4",
|
||||
"sha256:a9e3b8011388e7e373565daa5e92f6c9cb844790dc18e43073212bb3e76f7007",
|
||||
"sha256:afb53edf1046599991fb4a7d03e601ab5f5422a5435c47ee6ba91ec3b61416a6",
|
||||
"sha256:b26719890c79a1dae7d53acac5f089d66fd8cc68a81f4e4bd355e45470dc25e1",
|
||||
"sha256:b7462cdab6fffcda853338e1741ce99706cdf880d921b5a769202ea7b94e8528",
|
||||
"sha256:b77975465234ff49fdad871c08aa747aae06f5e5be62866595057c43f8d2f62c",
|
||||
"sha256:c47a8a5d00060122ca5908909478abce7bbf62d812e3fc35c6c802df8fb01fe7",
|
||||
"sha256:c79e5debbe092e3c93ca4aee44c9a7631bdd407b2871cb541b979fd350bbbc29",
|
||||
"sha256:d8d40e0121ca1606aa9e78c28a3a7d88a05c06b3ca61630242cded87d8ce55fa",
|
||||
"sha256:ee2be8b8f72a2772e72ab926a3bccebf47bb727bda41ae070dc91d1fb759b726",
|
||||
"sha256:f95d28193c3863132b1f55c1056036bf580b5a488d908f7d22a04ace8935a3a9",
|
||||
"sha256:fadd2a63a2bfd7fb604508e553d1cf68eca250b2fbdbd81213b5f6f2fbf23529"
|
||||
"sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f",
|
||||
"sha256:08fc93257dcfe9542c0a6883a25ba4971d78297f63d7a5a26ffa34861ca78730",
|
||||
"sha256:107781b213cf7201ec3806555657ccda67b1fccc4261fb889ef7fc56976db81f",
|
||||
"sha256:121b665b04083a1e85ff1f5243d4a93aa1aaba281bc12ea334d5a187278ceaf1",
|
||||
"sha256:1fa21263c3aba2b76fd7c45713d4428dbcc7644d73dcf0650e9d344e433741b3",
|
||||
"sha256:2b30aa2bcff8e958cd85d907d5109820b01ac511eae5b460803430a7404e34d7",
|
||||
"sha256:4b4a111bcf4b9c948e020fd207f915c24a6de3f1adc7682a2d92660eb4e84f1a",
|
||||
"sha256:5591c4164755778e29e69b86e425880f852464a21c7bb53c7ea453bbe2633bbe",
|
||||
"sha256:59daa84aef650b11bccd18f99f64bfe44b9f14a08a28259959d33676554065a1",
|
||||
"sha256:5a9c8d11aa2c8f8b6043d845927a51eb9102eb558e3f936df494e96393f5fd3e",
|
||||
"sha256:5dd20538a60c4cc9a077d3b715bb42307239fcd25ef1ca7286775f95e9e9a46d",
|
||||
"sha256:74f48ec98430e06c1fa8949b49ebdd8d27ceb9df8d3d1c92e1fdc2773f003f20",
|
||||
"sha256:786aad2aa20de3dbff21aab86b2fb6a7be68064cbbc0219bde414d3a30aa47ae",
|
||||
"sha256:7ad7906e098ccd30d8f7068030a0b16668ab8aa5cda6fcd5146d8d20cbaa71b5",
|
||||
"sha256:80a38b188d20c0524fe8959c8ce770a8fdf0e617c6912d23fc97c68301bb9aba",
|
||||
"sha256:8f0ec6b9b3832e0bd1d57af41f9238ea7709bbd7271f639024f2fc9d3bb01293",
|
||||
"sha256:92282c83547a9add85ad658143c76a64a8d339028926d7dc1998ca029c88ea6a",
|
||||
"sha256:94150231f1e90c9595ccc80d7d2006c61f90a5995db82bccbca7944fd457f0f6",
|
||||
"sha256:9dc9006dcc47e00a8a6a029eb035c8f696ad38e40a27d073a003d7d1443f5d88",
|
||||
"sha256:a76979f728dd845655026ab991df25d26379a1a8fc1e9e68e25c7eda43004bed",
|
||||
"sha256:aa8eba3db3d8761db161003e2d0586608092e217151d7458206e243be5a43843",
|
||||
"sha256:bea760a63ce9bba566c23f726d72b3c0250e2fa2569909e2d83cda1534c79443",
|
||||
"sha256:c3f511a3c58676147c277eff0224c061dd5a6a8e1373572ac817ac6324f1b1e0",
|
||||
"sha256:c9d317efde4bafbc1561509bfa8a23c5cab66c44d49ab5b63ff690f5159b2304",
|
||||
"sha256:cc411ad324a4486b142c41d9b2b6a722c534096963688d879ea6fa8a35028258",
|
||||
"sha256:cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6",
|
||||
"sha256:cfd7c5dd3c35c19cec59c63df9571c67c6d6e5c92e0fe63517920e97f61106d1",
|
||||
"sha256:e1cacf4796b20865789083252186ce9dc6cc59eca0c2e79cca332bdff24ac481",
|
||||
"sha256:e70d4e467e243455492f5de463b72151cc400710ac03a0678206a5f27e79ddef",
|
||||
"sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd",
|
||||
"sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.5.1"
|
||||
"version": "==4.5.2"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
@ -522,6 +559,7 @@
|
||||
"hashes": [
|
||||
"sha256:02e51e1d5828d58f154896ddfd003e2e7584869c275e5acbe290443575370fba",
|
||||
"sha256:03d5cca8618620f45fd40f827423f82b86b3a202c8d44108601b0f5f56b04299",
|
||||
"sha256:0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4",
|
||||
"sha256:132a56abba24e2e06a479d8e5db7a48271a73a215f605017bbd476d31f8e71c1",
|
||||
"sha256:1e655746f539421d923fd48df8f6f40b3443d80b75532501c0085b64afed9df5",
|
||||
"sha256:2b998dc45ef5f4e5cf5248a6edfcd8d8e9fb5e35df8e4259b13a1b10eda7b16b",
|
||||
@ -563,6 +601,7 @@
|
||||
"sha256:2710fc8d83b3352b370db932b3710033b9d630b970ff5aaa3e7458b5336e3b32",
|
||||
"sha256:35b9c9177a9fe7288b19dd41554c9c8ca1063deb426dd5a02e7e2a7416b6bd11",
|
||||
"sha256:3caa32cf807422adf33c10c88c22e9e2e08b9d9d042f12e1e25fe23113dd618f",
|
||||
"sha256:48cc2cfc251f04a6142badeb666d1ff49ca6fdfc303fd72579f62b768aaa52b9",
|
||||
"sha256:4ae6379350a09339109e9b6f419bb2c3f03d3e441f4b0f5b8ca699d47cc9ff7e",
|
||||
"sha256:4e0b27697fa1621c6d3d3b4edeec723c2e841285de6a8d378c1962da77b349be",
|
||||
"sha256:58e19560814dabf5d788b95a13f6b98279cf41a49b1e49ee6cf6c79a57adb4c9",
|
||||
@ -602,10 +641,10 @@
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492",
|
||||
"sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a"
|
||||
"sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2",
|
||||
"sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce"
|
||||
],
|
||||
"version": "==3.0.0a1"
|
||||
"version": "==3.0.0a2"
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
@ -733,11 +772,11 @@
|
||||
},
|
||||
"sentry-sdk": {
|
||||
"hashes": [
|
||||
"sha256:06825c15a78934e78941ea25910db71314c891608a46492fc32c15902c6b2119",
|
||||
"sha256:3ac0c430761b3cb7682ce612151d829f8644bb3830d4e530c75b02ceb745ff49"
|
||||
"sha256:2de15b13836fa3522815a933bd9c887c77f4868071043349f94f1b896c1bcfb8",
|
||||
"sha256:38bb09d0277117f76507c8728d9a5156f09a47ac5175bb8072513859d19a593b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.15.1"
|
||||
"version": "==0.16.2"
|
||||
},
|
||||
"service-identity": {
|
||||
"hashes": [
|
||||
@ -779,11 +818,11 @@
|
||||
},
|
||||
"swagger-spec-validator": {
|
||||
"hashes": [
|
||||
"sha256:b651f881d718b0e3e867f19151bb47f7a50da611f285262f4d4aea092998347c",
|
||||
"sha256:cb8a140c9c5d7d061d465416f156f432a92aa1a812b9c04f44e66c1568f13811"
|
||||
"sha256:d1514ec7e3c058c701f27cc74f85ceb876d6418c9db57786b9c54085ed5e29eb",
|
||||
"sha256:f4f23ee4dbd52bfcde90b1144dde22304add6260e9f29252e9fd7814c9b8fd16"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.7.2"
|
||||
"version": "==2.7.3"
|
||||
},
|
||||
"uritemplate": {
|
||||
"hashes": [
|
||||
@ -797,12 +836,12 @@
|
||||
"secure"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
|
||||
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
|
||||
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==1.25.9"
|
||||
"version": "==1.25.10"
|
||||
},
|
||||
"vine": {
|
||||
"hashes": [
|
||||
@ -881,43 +920,43 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1",
|
||||
"sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"
|
||||
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||
],
|
||||
"version": "==2020.4.5.2"
|
||||
"version": "==2020.6.20"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
|
||||
"sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
|
||||
"sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
|
||||
"sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
|
||||
"sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
|
||||
"sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
|
||||
"sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
|
||||
"sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
|
||||
"sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
|
||||
"sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
|
||||
"sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
|
||||
"sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
|
||||
"sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
|
||||
"sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
|
||||
"sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
|
||||
"sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
|
||||
"sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
|
||||
"sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
|
||||
"sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
|
||||
"sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
|
||||
"sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
|
||||
"sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
|
||||
"sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
|
||||
"sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
|
||||
"sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
|
||||
"sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
|
||||
"sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
|
||||
"sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
|
||||
"sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc",
|
||||
"sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9",
|
||||
"sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792",
|
||||
"sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2",
|
||||
"sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022",
|
||||
"sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8",
|
||||
"sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96",
|
||||
"sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2",
|
||||
"sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995",
|
||||
"sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1",
|
||||
"sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849",
|
||||
"sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c",
|
||||
"sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe",
|
||||
"sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3",
|
||||
"sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90",
|
||||
"sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f",
|
||||
"sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1",
|
||||
"sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf",
|
||||
"sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa",
|
||||
"sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc",
|
||||
"sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939",
|
||||
"sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e",
|
||||
"sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0",
|
||||
"sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9",
|
||||
"sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168",
|
||||
"sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33",
|
||||
"sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f",
|
||||
"sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
"version": "==1.14.1"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@ -943,40 +982,43 @@
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
|
||||
"sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
|
||||
"sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
|
||||
"sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
|
||||
"sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
|
||||
"sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
|
||||
"sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
|
||||
"sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
|
||||
"sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
|
||||
"sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
|
||||
"sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
|
||||
"sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
|
||||
"sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
|
||||
"sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
|
||||
"sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
|
||||
"sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
|
||||
"sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
|
||||
"sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
|
||||
"sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
|
||||
"sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
|
||||
"sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
|
||||
"sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
|
||||
"sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
|
||||
"sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
|
||||
"sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
|
||||
"sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
|
||||
"sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
|
||||
"sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
|
||||
"sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
|
||||
"sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
|
||||
"sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
|
||||
"sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb",
|
||||
"sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3",
|
||||
"sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716",
|
||||
"sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034",
|
||||
"sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3",
|
||||
"sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8",
|
||||
"sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0",
|
||||
"sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f",
|
||||
"sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4",
|
||||
"sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962",
|
||||
"sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d",
|
||||
"sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b",
|
||||
"sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4",
|
||||
"sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3",
|
||||
"sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258",
|
||||
"sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59",
|
||||
"sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01",
|
||||
"sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd",
|
||||
"sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b",
|
||||
"sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d",
|
||||
"sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89",
|
||||
"sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd",
|
||||
"sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b",
|
||||
"sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d",
|
||||
"sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46",
|
||||
"sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546",
|
||||
"sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082",
|
||||
"sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b",
|
||||
"sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4",
|
||||
"sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8",
|
||||
"sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811",
|
||||
"sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd",
|
||||
"sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651",
|
||||
"sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.1"
|
||||
"version": "==5.2.1"
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
@ -1004,11 +1046,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2",
|
||||
"sha256:e1630333248c9b3d4e38f02093a26f1e07b271ca896d73097457996e0fae12e8"
|
||||
"sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582",
|
||||
"sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.7"
|
||||
"version": "==3.0.8"
|
||||
},
|
||||
"django-debug-toolbar": {
|
||||
"hashes": [
|
||||
@ -1020,11 +1062,11 @@
|
||||
},
|
||||
"docker": {
|
||||
"hashes": [
|
||||
"sha256:380a20d38fbfaa872e96ee4d0d23ad9beb0f9ed57ff1c30653cbeb0c9c0964f2",
|
||||
"sha256:672f51aead26d90d1cfce84a87e6f71fca401bbc2a6287be18603583620a28ba"
|
||||
"sha256:03a46400c4080cb6f7aa997f881ddd84fef855499ece219d75fbdb53289c17ab",
|
||||
"sha256:26eebadce7e298f55b76a88c4f8802476c5eaddbdbe38dbc6cce8781c47c9b54"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2.1"
|
||||
"version": "==4.2.2"
|
||||
},
|
||||
"gitdb": {
|
||||
"hashes": [
|
||||
@ -1035,17 +1077,17 @@
|
||||
},
|
||||
"gitpython": {
|
||||
"hashes": [
|
||||
"sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a",
|
||||
"sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"
|
||||
"sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858",
|
||||
"sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"
|
||||
],
|
||||
"version": "==3.1.3"
|
||||
"version": "==3.1.7"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"version": "==2.9"
|
||||
"version": "==2.10"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
@ -1125,11 +1167,11 @@
|
||||
},
|
||||
"pylint-django": {
|
||||
"hashes": [
|
||||
"sha256:06a64331c498a3f049ba669dc0c174b92209e164198d43e589b1096ee616d5f8",
|
||||
"sha256:3d3436ba8d0fae576ae2db160e33a8f2746a101fda4463f2b3ff3a8b6fccec38"
|
||||
"sha256:20e4d5f3987e96d29ce51ef24f13187f0d23f37a0558b6eed9b5571487ba3f4c",
|
||||
"sha256:d47f278f2ef9244decc006a7412d0ea6bebe1594e6b5402703febbac036ba401"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.15"
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"pylint-plugin-utils": {
|
||||
"hashes": [
|
||||
@ -1171,29 +1213,29 @@
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a",
|
||||
"sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938",
|
||||
"sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29",
|
||||
"sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae",
|
||||
"sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387",
|
||||
"sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a",
|
||||
"sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf",
|
||||
"sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610",
|
||||
"sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9",
|
||||
"sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5",
|
||||
"sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3",
|
||||
"sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89",
|
||||
"sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded",
|
||||
"sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754",
|
||||
"sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f",
|
||||
"sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868",
|
||||
"sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd",
|
||||
"sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910",
|
||||
"sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3",
|
||||
"sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac",
|
||||
"sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"
|
||||
"sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204",
|
||||
"sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162",
|
||||
"sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f",
|
||||
"sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb",
|
||||
"sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6",
|
||||
"sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7",
|
||||
"sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88",
|
||||
"sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99",
|
||||
"sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644",
|
||||
"sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a",
|
||||
"sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840",
|
||||
"sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067",
|
||||
"sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd",
|
||||
"sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4",
|
||||
"sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e",
|
||||
"sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89",
|
||||
"sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e",
|
||||
"sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc",
|
||||
"sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf",
|
||||
"sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341",
|
||||
"sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"
|
||||
],
|
||||
"version": "==2020.6.8"
|
||||
"version": "==2020.7.14"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@ -1233,10 +1275,10 @@
|
||||
},
|
||||
"stevedore": {
|
||||
"hashes": [
|
||||
"sha256:609912b87df5ad338ff8e44d13eaad4f4170a65b79ae9cb0aa5632598994a1b7",
|
||||
"sha256:c4724f8d7b8f6be42130663855d01a9c2414d6046055b5a65ab58a0e38637688"
|
||||
"sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5",
|
||||
"sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"
|
||||
],
|
||||
"version": "==2.0.1"
|
||||
"version": "==3.2.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
@ -1284,12 +1326,12 @@
|
||||
"secure"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
|
||||
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
|
||||
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==1.25.9"
|
||||
"version": "==1.25.10"
|
||||
},
|
||||
"websocket-client": {
|
||||
"hashes": [
|
||||
|
||||
33
README.md
33
README.md
@ -1,11 +1,12 @@
|
||||
<img src="passbook/static/static/passbook/logo.svg" height="50" alt="passbook logo"><img src="passbook/static/static/passbook/brand_inverted.svg" height="50" alt="passbook">
|
||||
|
||||

|
||||
[](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=1)
|
||||

|
||||
[](https://codecov.io/gh/BeryJu/passbook)
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## What is passbook?
|
||||
|
||||
@ -20,7 +21,7 @@ wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
||||
# Optionally enable Error-reporting
|
||||
# export PASSBOOK_ERROR_REPORTING=true
|
||||
# Optionally deploy a different version
|
||||
# export PASSBOOK_TAG=0.8.15-beta
|
||||
# export PASSBOOK_TAG=0.9.0-stable
|
||||
# If this is a productive installation, set a different PostgreSQL Password
|
||||
# export PG_PASS=$(pwgen 40 1)
|
||||
docker-compose pull
|
||||
@ -50,31 +51,7 @@ pipenv sync -d
|
||||
```
|
||||
|
||||
Since passbook uses PostgreSQL-specific fields, you also need a local PostgreSQL instance to develop. passbook also uses redis for caching and message queueing.
|
||||
For these databases you can use [Postgres.app](https://postgresapp.com/) and [Redis.app](https://jpadilla.github.io/redisapp/) on macOS or use it via docker-comppose:
|
||||
|
||||
```yaml
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
container_name: postgres
|
||||
image: postgres:11
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 127.0.0.1:5432:5432
|
||||
restart: always
|
||||
redis:
|
||||
container_name: redis
|
||||
image: redis
|
||||
ports:
|
||||
- 127.0.0.1:6379:6379
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
driver: local
|
||||
```
|
||||
For these databases you can use [Postgres.app](https://postgresapp.com/) and [Redis.app](https://jpadilla.github.io/redisapp/) on macOS or use it the docker-compose file in `scripts/docker-compose.yml`.
|
||||
|
||||
To tell passbook about these databases, create a file in the project root called `local.env.yml` with the following contents:
|
||||
|
||||
|
||||
299
azure-pipelines.yml
Normal file
299
azure-pipelines.yml
Normal file
@ -0,0 +1,299 @@
|
||||
trigger:
|
||||
- master
|
||||
|
||||
resources:
|
||||
- repo: self
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: passbook
|
||||
POSTGRES_USER: passbook
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
|
||||
stages:
|
||||
- stage: Lint
|
||||
jobs:
|
||||
- job: pylint
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run pylint passbook
|
||||
- job: black
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run black --check passbook
|
||||
- job: prospector
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
pipenv install --dev prospector --skip-lock
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run prospector passbook
|
||||
- job: bandit
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run bandit -r passbook
|
||||
- job: pyright
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- task: UseNode@1
|
||||
inputs:
|
||||
version: '12.x'
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: npm install -g pyright
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run pyright
|
||||
- stage: Test
|
||||
jobs:
|
||||
- job: migrations
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
dockerComposeFile: 'scripts/ci.docker-compose.yml'
|
||||
action: 'Run services'
|
||||
buildImages: false
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: pipenv run ./manage.py migrate
|
||||
- job: coverage_unittest
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
dockerComposeFile: 'scripts/ci.docker-compose.yml'
|
||||
action: 'Run services'
|
||||
buildImages: false
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
displayName: Run full test suite
|
||||
inputs:
|
||||
script: |
|
||||
pipenv run coverage run ./manage.py test passbook
|
||||
mkdir output-unittest
|
||||
mv unittest.xml output-unittest/unittest.xml
|
||||
mv .coverage output-unittest/coverage
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
targetPath: 'output-unittest/'
|
||||
artifact: 'coverage-unittest'
|
||||
publishLocation: 'pipeline'
|
||||
- job: coverage_e2e
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
dockerComposeFile: 'scripts/ci.docker-compose.yml'
|
||||
action: 'Run services'
|
||||
buildImages: false
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: DockerCompose@0
|
||||
displayName: Run ChromeDriver
|
||||
inputs:
|
||||
dockerComposeFile: 'e2e/ci.docker-compose.yml'
|
||||
action: 'Run a specific service'
|
||||
serviceName: 'chrome'
|
||||
- task: CmdLine@2
|
||||
displayName: Build static files for e2e
|
||||
inputs:
|
||||
script: |
|
||||
cd passbook/static/static
|
||||
yarn
|
||||
- task: CmdLine@2
|
||||
displayName: Run full test suite
|
||||
inputs:
|
||||
script: pipenv run coverage run ./manage.py test e2e
|
||||
- task: CmdLine@2
|
||||
displayName: Prepare unittests and coverage for upload
|
||||
inputs:
|
||||
script: |
|
||||
mkdir output-e2e
|
||||
mv unittest.xml output-e2e/unittest.xml
|
||||
mv .coverage output-e2e/coverage
|
||||
- task: PublishPipelineArtifact@1
|
||||
condition: failed()
|
||||
displayName: Upload screenshots if selenium tests fail
|
||||
inputs:
|
||||
targetPath: 'selenium_screenshots/'
|
||||
artifact: 'selenium screenshots'
|
||||
publishLocation: 'pipeline'
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
targetPath: 'output-e2e/'
|
||||
artifact: 'coverage-e2e'
|
||||
publishLocation: 'pipeline'
|
||||
- stage: test_combine
|
||||
jobs:
|
||||
- job: test_coverage_combine
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'coverage-e2e'
|
||||
path: "coverage-e2e/"
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'coverage-unittest'
|
||||
path: "coverage-unittest/"
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
find .
|
||||
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
|
||||
pipenv run coverage xml
|
||||
pipenv run coverage html
|
||||
find .
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: 'Cobertura'
|
||||
summaryFileLocation: 'coverage.xml'
|
||||
pathToSources: '$(System.DefaultWorkingDirectory)'
|
||||
- task: PublishTestResults@2
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFormat: 'JUnit'
|
||||
testResultsFiles: |
|
||||
coverage-e2e/unittest.xml
|
||||
coverage-unittest/unittest.xml
|
||||
mergeTestResults: true
|
||||
- task: CmdLine@2
|
||||
env:
|
||||
CODECOV_TOKEN: $(CODECOV_TOKEN)
|
||||
inputs:
|
||||
script: bash <(curl -s https://codecov.io/bash)
|
||||
- stage: Build
|
||||
jobs:
|
||||
- job: build_server
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'dockerhub'
|
||||
repository: 'beryju/passbook'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'Dockerfile'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- job: build_gatekeeper
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'dockerhub'
|
||||
repository: 'beryju/passbook-gatekeeper'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'gatekeeper/Dockerfile'
|
||||
buildContext: 'gatekeeper/'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- job: build_static
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
dockerComposeFile: 'scripts/ci.docker-compose.yml'
|
||||
action: 'Run services'
|
||||
buildImages: false
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'dockerhub'
|
||||
repository: 'beryju/passbook-static'
|
||||
command: 'build'
|
||||
Dockerfile: 'static.Dockerfile'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
arguments: "--network=beryjupassbook_default"
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'dockerhub'
|
||||
repository: 'beryju/passbook-static'
|
||||
command: 'push'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
@ -62,7 +62,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.frontend.rule=PathPrefix:/static, /robots.txt
|
||||
- traefik.frontend.rule=PathPrefix:/static, /robots.txt, /favicon.ico
|
||||
- traefik.port=80
|
||||
- traefik.docker.network=internal
|
||||
traefik:
|
||||
|
||||
@ -53,3 +53,14 @@ Example:
|
||||
```python
|
||||
other_user = pb_user_by(username="other_user")
|
||||
```
|
||||
|
||||
## Comparing IP Addresses
|
||||
|
||||
To compare IP Addresses or check if an IP Address is within a given subnet, you can use the functions `ip_address('192.0.2.1')` and `ip_network('192.0.2.0/24')`. With these objects you can do [arithmetic operations](https://docs.python.org/3/library/ipaddress.html#operators).
|
||||
|
||||
You can also check if an IP Address is within a subnet by writing the following:
|
||||
|
||||
```python
|
||||
ip_address('192.0.2.1') in ip_network('192.0.2.0/24')
|
||||
# evaluates to True
|
||||
```
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 253 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 338 KiB |
@ -16,7 +16,7 @@ wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
||||
# Optionally enable Error-reporting
|
||||
# export PASSBOOK_ERROR_REPORTING=true
|
||||
# Optionally deploy a different version
|
||||
# export PASSBOOK_TAG=0.8.15-beta
|
||||
# export PASSBOOK_TAG=0.9.0-stable
|
||||
# If this is a productive installation, set a different PostgreSQL Password
|
||||
# export PG_PASS=$(pwgen 40 1)
|
||||
docker-compose pull
|
||||
|
||||
@ -22,6 +22,12 @@ config:
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
log_level: warning
|
||||
# Optionally enable Elastic APM Support
|
||||
apm:
|
||||
enabled: false
|
||||
server_url: ""
|
||||
secret_token: ""
|
||||
verify_server_cert: true
|
||||
|
||||
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
|
||||
# This requires the CoreOS Prometheus Operator.
|
||||
|
||||
@ -16,6 +16,7 @@ Create an application in passbook and note the slug, as this will be used later.
|
||||
- ACS URL: `https://signin.aws.amazon.com/saml`
|
||||
- Audience: `urn:amazon:webservices`
|
||||
- Issuer: `passbook`
|
||||
- Binding: `Post`
|
||||
|
||||
You can of course use a custom signing certificate, and adjust durations.
|
||||
|
||||
|
||||
@ -26,5 +26,5 @@ return False
|
||||
- `request.obj`: A Django Model instance. This is only set if the policy is ran against an object.
|
||||
- `request.context`: A dictionary with dynamic data. This depends on the origin of the execution.
|
||||
- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external provider.
|
||||
- `pb_client_ip`: Client's IP Address or '255.255.255.255' if no IP Address could be extracted.
|
||||
- `pb_client_ip`: Client's IP Address or '255.255.255.255' if no IP Address could be extracted. Can be [compared](../expressions/index.md#comparing-ip-addresses)
|
||||
- `pb_flow_plan`: Current Plan if Policy is called from the Flow Planner.
|
||||
|
||||
@ -6,13 +6,13 @@ To export data from your old instance, run this command:
|
||||
|
||||
- docker-compose
|
||||
```
|
||||
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
|
||||
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event otp_totp.totpdevice otp_static.staticdevice otp_static.statictoken
|
||||
docker cp passbook_server_1:/tmp/passbook_dump.json passbook_dump.json
|
||||
```
|
||||
|
||||
- kubernetes
|
||||
```
|
||||
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
|
||||
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event otp_totp.totpdevice otp_static.staticdevice otp_static.statictoken
|
||||
kubectl cp passbook-web-...:/tmp/passbook_dump.json passbook_dump.json
|
||||
```
|
||||
|
||||
|
||||
8
e2e/ci.docker-compose.yml
Normal file
8
e2e/ci.docker-compose.yml
Normal file
@ -0,0 +1,8 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
chrome:
|
||||
image: selenium/standalone-chrome
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
@ -6,15 +6,3 @@ services:
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
|
||||
postgresql:
|
||||
image: postgres:11
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
POSTGRES_DB: passbook
|
||||
network_mode: host
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
network_mode: host
|
||||
|
||||
@ -1,300 +0,0 @@
|
||||
{
|
||||
"id": "7d9b2407-1520-4c04-b040-68e8ada9aecc",
|
||||
"version": "2.0",
|
||||
"name": "passbook",
|
||||
"url": "http://localhost:8000",
|
||||
"tests": [{
|
||||
"id": "94b39863-74ec-4b7d-98c5-2b380b6d2c55",
|
||||
"name": "passbook login simple",
|
||||
"commands": [{
|
||||
"id": "e60e4382-4f96-44c3-ba06-5e18609c9c2b",
|
||||
"comment": "",
|
||||
"command": "open",
|
||||
"target": "/flows/default-authentication-flow/?next=%2F",
|
||||
"targets": [],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "b2652f24-931e-45b0-b01d-2f0ac0f74db8",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "id=id_uid_field",
|
||||
"targets": [
|
||||
["id=id_uid_field", "id"],
|
||||
["name=uid_field", "name"],
|
||||
["css=#id_uid_field", "css:finder"],
|
||||
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "f1930f8a-984a-4076-a925-20937bb2f8d3",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_uid_field",
|
||||
"targets": [
|
||||
["id=id_uid_field", "id"],
|
||||
["name=uid_field", "name"],
|
||||
["css=#id_uid_field", "css:finder"],
|
||||
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "admin@example.tld"
|
||||
}, {
|
||||
"id": "0b568ee3-1bed-4821-a3bc-f6b960dbed9d",
|
||||
"comment": "",
|
||||
"command": "sendKeys",
|
||||
"target": "id=id_uid_field",
|
||||
"targets": [
|
||||
["id=id_uid_field", "id"],
|
||||
["name=uid_field", "name"],
|
||||
["css=#id_uid_field", "css:finder"],
|
||||
["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "${KEY_ENTER}"
|
||||
}, {
|
||||
"id": "6d98e479-2825-484d-996a-ccf350d2761f",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_password",
|
||||
"targets": [
|
||||
["id=id_password", "id"],
|
||||
["name=password", "name"],
|
||||
["css=#id_password", "css:finder"],
|
||||
["xpath=//input[@id='id_password']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "pbadmin"
|
||||
}, {
|
||||
"id": "6f7abec6-ff44-4eb5-ae23-520c1c29a706",
|
||||
"comment": "",
|
||||
"command": "sendKeys",
|
||||
"target": "id=id_password",
|
||||
"targets": [
|
||||
["id=id_password", "id"],
|
||||
["name=password", "name"],
|
||||
["css=#id_password", "css:finder"],
|
||||
["xpath=//input[@id='id_password']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "${KEY_ENTER}"
|
||||
}, {
|
||||
"id": "04c5876f-1405-4077-a98b-e911f09113d7",
|
||||
"comment": "",
|
||||
"command": "assertText",
|
||||
"target": "xpath=//a[contains(@href, '/-/user/')]",
|
||||
"targets": [
|
||||
["linkText=pbadmin", "linkText"],
|
||||
["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
|
||||
["xpath=//a[contains(text(),'pbadmin')]", "xpath:link"],
|
||||
["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
|
||||
["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
|
||||
["xpath=//div[2]/a", "xpath:position"],
|
||||
["xpath=//a[contains(.,'pbadmin')]", "xpath:innerText"]
|
||||
],
|
||||
"value": "pbadmin"
|
||||
}]
|
||||
}, {
|
||||
"id": "61948b3c-3012-4f97-aa52-bc8f34fec333",
|
||||
"name": "passbook enroll simple",
|
||||
"commands": [{
|
||||
"id": "0f4884b3-4891-41bc-956d-1fa433e892e9",
|
||||
"comment": "",
|
||||
"command": "open",
|
||||
"target": "/flows/default-authentication-flow/?next=%2F",
|
||||
"targets": [],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "84d3861f-a60c-4650-8689-535f82b39577",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "linkText=Sign up.",
|
||||
"targets": [
|
||||
["linkText=Sign up.", "linkText"],
|
||||
["css=.pf-c-login__main-footer-band-item > a", "css:finder"],
|
||||
["xpath=//a[contains(text(),'Sign up.')]", "xpath:link"],
|
||||
["xpath=//main[@id='flow-body']/footer/div/p/a", "xpath:idRelative"],
|
||||
["xpath=//a[contains(@href, '/flows/default-enrollment-flow/')]", "xpath:href"],
|
||||
["xpath=//a", "xpath:position"],
|
||||
["xpath=//a[contains(.,'Sign up.')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "a32435ca-d84a-41e7-a915-fcbbc5f88341",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_username",
|
||||
"targets": [
|
||||
["id=id_username", "id"],
|
||||
["name=username", "name"],
|
||||
["css=#id_username", "css:finder"],
|
||||
["xpath=//input[@id='id_username']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "foo"
|
||||
}, {
|
||||
"id": "3b5dcf53-8297-46c5-88b7-11c2eb25f34f",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_password",
|
||||
"targets": [
|
||||
["id=id_password", "id"],
|
||||
["name=password", "name"],
|
||||
["css=#id_password", "css:finder"],
|
||||
["xpath=//input[@id='id_password']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "pbadmin"
|
||||
}, {
|
||||
"id": "e948d61c-dae6-4994-b56f-ff130892b342",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_password_repeat",
|
||||
"targets": [
|
||||
["id=id_password_repeat", "id"],
|
||||
["name=password_repeat", "name"],
|
||||
["css=#id_password_repeat", "css:finder"],
|
||||
["xpath=//input[@id='id_password_repeat']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[3]/input", "xpath:idRelative"],
|
||||
["xpath=//div[3]/input", "xpath:position"]
|
||||
],
|
||||
"value": "pbadmin"
|
||||
}, {
|
||||
"id": "e7527bfc-ec74-4d96-86f0-5a3a55a59025",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "css=.pf-c-button",
|
||||
"targets": [
|
||||
["css=.pf-c-button", "css:finder"],
|
||||
["xpath=//button[@type='submit']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[4]/button", "xpath:idRelative"],
|
||||
["xpath=//button", "xpath:position"],
|
||||
["xpath=//button[contains(.,'Continue')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "434b842c-a659-4ff5-aca8-06a6a3489597",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_name",
|
||||
"targets": [
|
||||
["id=id_name", "id"],
|
||||
["name=name", "name"],
|
||||
["css=#id_name", "css:finder"],
|
||||
["xpath=//input[@id='id_name']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "some name"
|
||||
}, {
|
||||
"id": "cbc43a1b-2cfe-46e2-85bc-476fb32c6cb1",
|
||||
"comment": "",
|
||||
"command": "type",
|
||||
"target": "id=id_email",
|
||||
"targets": [
|
||||
["id=id_email", "id"],
|
||||
["name=email", "name"],
|
||||
["css=#id_email", "css:finder"],
|
||||
["xpath=//input[@id='id_email']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "foo@bar.baz"
|
||||
}, {
|
||||
"id": "e74389a0-228b-4312-9677-e9add6358de3",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "css=.pf-c-button",
|
||||
"targets": [
|
||||
["css=.pf-c-button", "css:finder"],
|
||||
["xpath=//button[@type='submit']", "xpath:attributes"],
|
||||
["xpath=//main[@id='flow-body']/div/form/div[3]/button", "xpath:idRelative"],
|
||||
["xpath=//button", "xpath:position"],
|
||||
["xpath=//button[contains(.,'Continue')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "3e22f9c2-5ebd-49c2-81b1-340fa0435bbc",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "linkText=foo",
|
||||
"targets": [
|
||||
["linkText=foo", "linkText"],
|
||||
["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
|
||||
["xpath=//a[contains(text(),'foo')]", "xpath:link"],
|
||||
["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
|
||||
["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
|
||||
["xpath=//div[2]/a", "xpath:position"],
|
||||
["xpath=//a[contains(.,'foo')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "60124cfd-f11c-4d7f-8b01-bef54c8cbd73",
|
||||
"comment": "",
|
||||
"command": "assertText",
|
||||
"target": "xpath=//a[contains(@href, '/-/user/')]",
|
||||
"targets": [
|
||||
["linkText=foo", "linkText"],
|
||||
["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
|
||||
["xpath=//a[contains(text(),'foo')]", "xpath:link"],
|
||||
["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
|
||||
["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
|
||||
["xpath=//div[2]/a", "xpath:position"],
|
||||
["xpath=//a[contains(.,'foo')]", "xpath:innerText"]
|
||||
],
|
||||
"value": "foo"
|
||||
}, {
|
||||
"id": "429ee61b-9991-4919-8131-55f8e1bd9a0d",
|
||||
"comment": "",
|
||||
"command": "assertValue",
|
||||
"target": "id=id_username",
|
||||
"targets": [],
|
||||
"value": "foo"
|
||||
}, {
|
||||
"id": "f6c50760-52ed-4c1d-b232-30f8afe144eb",
|
||||
"comment": "",
|
||||
"command": "assertText",
|
||||
"target": "id=id_name",
|
||||
"targets": [
|
||||
["id=id_name", "id"],
|
||||
["name=name", "name"],
|
||||
["css=#id_name", "css:finder"],
|
||||
["xpath=//input[@id='id_name']", "xpath:attributes"],
|
||||
["xpath=//main[@id='main-content']/section/div/div/div/div[2]/form/div[2]/div/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/div/input", "xpath:position"]
|
||||
],
|
||||
"value": "some name"
|
||||
}, {
|
||||
"id": "b26905b5-89b5-4b41-abf5-a9f848f08622",
|
||||
"comment": "",
|
||||
"command": "assertText",
|
||||
"target": "id=id_email",
|
||||
"targets": [
|
||||
["id=id_email", "id"],
|
||||
["name=email", "name"],
|
||||
["css=#id_email", "css:finder"],
|
||||
["xpath=//input[@id='id_email']", "xpath:attributes"],
|
||||
["xpath=//main[@id='main-content']/section/div/div/div/div[2]/form/div[3]/div/input", "xpath:idRelative"],
|
||||
["xpath=//div[3]/div/input", "xpath:position"]
|
||||
],
|
||||
"value": "foo@bar.baz"
|
||||
}]
|
||||
}],
|
||||
"suites": [{
|
||||
"id": "495657fb-3f5e-4431-877c-4d0b248c0841",
|
||||
"name": "Default Suite",
|
||||
"persistSession": false,
|
||||
"parallel": false,
|
||||
"timeout": 300,
|
||||
"tests": ["94b39863-74ec-4b7d-98c5-2b380b6d2c55"]
|
||||
}],
|
||||
"urls": ["http://localhost:8000/"],
|
||||
"plugins": []
|
||||
}
|
||||
@ -1,476 +0,0 @@
|
||||
"""Test Enroll flow"""
|
||||
from time import sleep
|
||||
|
||||
from django.test import override_settings
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.stages.email.models import EmailStage, EmailTemplates
|
||||
from passbook.stages.identification.models import IdentificationStage
|
||||
from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||
from passbook.stages.user_login.models import UserLoginStage
|
||||
from passbook.stages.user_write.models import UserWriteStage
|
||||
|
||||
|
||||
class TestEnroll(SeleniumTestCase):
|
||||
"""Test Enroll flow"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.container = self.setup_client()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test IdP container"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="mailhog/mailhog",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "wget", "-s", "http://localhost:8025"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
# pylint: disable=too-many-statements
|
||||
def setup_test_enroll_2_step(self):
|
||||
"""Setup all required objects"""
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.LINK_TEXT, "Administrate").click()
|
||||
self.driver.find_element(By.LINK_TEXT, "Prompts").click()
|
||||
|
||||
# Create Password Prompt
|
||||
self.driver.find_element(By.LINK_TEXT, "Create").click()
|
||||
self.driver.find_element(By.ID, "id_field_key").send_keys("password")
|
||||
self.driver.find_element(By.ID, "id_label").send_keys("Password")
|
||||
dropdown = self.driver.find_element(By.ID, "id_type")
|
||||
dropdown.find_element(By.XPATH, "//option[. = 'Password']").click()
|
||||
self.driver.find_element(By.ID, "id_placeholder").send_keys("Password")
|
||||
self.driver.find_element(By.ID, "id_order").send_keys("1")
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
# Create Password Repeat Prompt
|
||||
self.driver.find_element(By.LINK_TEXT, "Create").click()
|
||||
self.driver.find_element(By.ID, "id_field_key").send_keys("password_repeat")
|
||||
self.driver.find_element(By.ID, "id_label").send_keys("Password (repeat)")
|
||||
dropdown = self.driver.find_element(By.ID, "id_type")
|
||||
dropdown.find_element(By.XPATH, "//option[. = 'Password']").click()
|
||||
self.driver.find_element(By.ID, "id_placeholder").send_keys("Password (repeat)")
|
||||
self.driver.find_element(By.ID, "id_order").send_keys("2")
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
# Create Name Prompt
|
||||
self.driver.find_element(By.LINK_TEXT, "Create").click()
|
||||
self.driver.find_element(By.ID, "id_field_key").send_keys("name")
|
||||
self.driver.find_element(By.ID, "id_label").send_keys("Name")
|
||||
dropdown = self.driver.find_element(By.ID, "id_type")
|
||||
dropdown.find_element(By.XPATH, "//option[. = 'Text']").click()
|
||||
self.driver.find_element(By.ID, "id_placeholder").send_keys("Name")
|
||||
self.driver.find_element(By.ID, "id_order").send_keys("0")
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
# Create Email Prompt
|
||||
self.driver.find_element(By.LINK_TEXT, "Create").click()
|
||||
self.driver.find_element(By.ID, "id_field_key").send_keys("email")
|
||||
self.driver.find_element(By.ID, "id_label").send_keys("Email")
|
||||
dropdown = self.driver.find_element(By.ID, "id_type")
|
||||
dropdown.find_element(By.XPATH, "//option[. = 'Email']").click()
|
||||
self.driver.find_element(By.ID, "id_placeholder").send_keys("Email")
|
||||
self.driver.find_element(By.ID, "id_order").send_keys("1")
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
self.driver.find_element(By.LINK_TEXT, "Stages").click()
|
||||
|
||||
# Create first enroll prompt stage
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click()
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "li:nth-child(9) > .pf-c-dropdown__menu-item > small"
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys(
|
||||
"enroll-prompt-stage-first"
|
||||
)
|
||||
dropdown = self.driver.find_element(By.ID, "id_fields")
|
||||
dropdown.find_element(
|
||||
By.XPATH, "//option[. = \"Prompt 'username' type=text\"]"
|
||||
).click()
|
||||
dropdown.find_element(
|
||||
By.XPATH, "//option[. = \"Prompt 'password' type=password\"]"
|
||||
).click()
|
||||
dropdown.find_element(
|
||||
By.XPATH, "//option[. = \"Prompt 'password_repeat' type=password\"]"
|
||||
).click()
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
# Create second enroll prompt stage
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click()
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "li:nth-child(9) > .pf-c-dropdown__menu-item"
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys(
|
||||
"enroll-prompt-stage-second"
|
||||
)
|
||||
dropdown = self.driver.find_element(By.ID, "id_fields")
|
||||
dropdown.find_element(
|
||||
By.XPATH, "//option[. = \"Prompt 'name' type=text\"]"
|
||||
).click()
|
||||
dropdown.find_element(
|
||||
By.XPATH, "//option[. = \"Prompt 'email' type=email\"]"
|
||||
).click()
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
# Create user write stage
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click()
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "li:nth-child(13) > .pf-c-dropdown__menu-item"
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys("enroll-user-write")
|
||||
self.driver.find_element(By.ID, "id_name").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click()
|
||||
|
||||
# Create user login stage
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "li:nth-child(11) > .pf-c-dropdown__menu-item"
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys("enroll-user-login")
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
".pf-c-nav__item:nth-child(7) .pf-c-nav__item:nth-child(1) > .pf-c-nav__link",
|
||||
).click()
|
||||
|
||||
# Create password policy
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click()
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "li:nth-child(2) > .pf-c-dropdown__menu-item > small"
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys(
|
||||
"policy-enrollment-password-equals"
|
||||
)
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, ".CodeMirror-scroll"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".CodeMirror-scroll").click()
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".CodeMirror textarea").send_keys(
|
||||
"return request.context['password'] == request.context['password_repeat']"
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
# Create password policy binding
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
".pf-c-nav__item:nth-child(7) .pf-c-nav__item:nth-child(2) > .pf-c-nav__link",
|
||||
).click()
|
||||
self.driver.find_element(By.LINK_TEXT, "Create").click()
|
||||
dropdown = self.driver.find_element(By.ID, "id_policy")
|
||||
dropdown.find_element(
|
||||
By.XPATH, '//option[. = "Policy policy-enrollment-password-equals"]'
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_target").click()
|
||||
dropdown = self.driver.find_element(By.ID, "id_target")
|
||||
dropdown.find_element(
|
||||
By.XPATH, '//option[. = "Prompt Stage enroll-prompt-stage-first"]'
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_order").send_keys("0")
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
# Create Flow
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
".pf-c-nav__item:nth-child(6) .pf-c-nav__item:nth-child(1) > .pf-c-nav__link",
|
||||
).click()
|
||||
self.driver.find_element(By.LINK_TEXT, "Create").click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys("Welcome")
|
||||
self.driver.find_element(By.ID, "id_slug").clear()
|
||||
self.driver.find_element(By.ID, "id_slug").send_keys("default-enrollment-flow")
|
||||
dropdown = self.driver.find_element(By.ID, "id_designation")
|
||||
dropdown.find_element(By.XPATH, '//option[. = "Enrollment"]').click()
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
self.driver.find_element(By.LINK_TEXT, "Stages").click()
|
||||
|
||||
# Edit identification stage
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, "tr:nth-child(11) .pf-m-secondary"
|
||||
).click()
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
".pf-c-form__group:nth-child(5) .pf-c-form__horizontal-group",
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_enrollment_flow").click()
|
||||
dropdown = self.driver.find_element(By.ID, "id_enrollment_flow")
|
||||
dropdown.find_element(
|
||||
By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]'
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_user_fields_add_all_link").click()
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
self.driver.find_element(By.LINK_TEXT, "Bindings").click()
|
||||
|
||||
# Create Stage binding for first prompt stage
|
||||
self.driver.find_element(By.LINK_TEXT, "Create").click()
|
||||
self.driver.find_element(By.ID, "id_flow").click()
|
||||
dropdown = self.driver.find_element(By.ID, "id_flow")
|
||||
dropdown.find_element(
|
||||
By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]'
|
||||
).click()
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-form").click()
|
||||
self.driver.find_element(By.ID, "id_stage").click()
|
||||
dropdown = self.driver.find_element(By.ID, "id_stage")
|
||||
dropdown.find_element(
|
||||
By.XPATH, '//option[. = "Stage enroll-prompt-stage-first"]'
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_order").click()
|
||||
self.driver.find_element(By.ID, "id_order").send_keys("0")
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
# Create Stage binding for second prompt stage
|
||||
self.driver.find_element(By.LINK_TEXT, "Create").click()
|
||||
self.driver.find_element(By.ID, "id_flow").click()
|
||||
dropdown = self.driver.find_element(By.ID, "id_flow")
|
||||
dropdown.find_element(
|
||||
By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]'
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_stage").click()
|
||||
dropdown = self.driver.find_element(By.ID, "id_stage")
|
||||
dropdown.find_element(
|
||||
By.XPATH, '//option[. = "Stage enroll-prompt-stage-second"]'
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_order").click()
|
||||
self.driver.find_element(By.ID, "id_order").send_keys("1")
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
# Create Stage binding for user write stage
|
||||
self.driver.find_element(By.LINK_TEXT, "Create").click()
|
||||
self.driver.find_element(By.ID, "id_flow").click()
|
||||
dropdown = self.driver.find_element(By.ID, "id_flow")
|
||||
dropdown.find_element(
|
||||
By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]'
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_stage").click()
|
||||
dropdown = self.driver.find_element(By.ID, "id_stage")
|
||||
dropdown.find_element(
|
||||
By.XPATH, '//option[. = "Stage enroll-user-write"]'
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_order").click()
|
||||
self.driver.find_element(By.ID, "id_order").send_keys("2")
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
# Create Stage binding for user login stage
|
||||
self.driver.find_element(By.LINK_TEXT, "Create").click()
|
||||
dropdown = self.driver.find_element(By.ID, "id_flow")
|
||||
dropdown.find_element(
|
||||
By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]'
|
||||
).click()
|
||||
dropdown = self.driver.find_element(By.ID, "id_stage")
|
||||
dropdown.find_element(
|
||||
By.XPATH, '//option[. = "Stage enroll-user-login"]'
|
||||
).click()
|
||||
self.driver.find_element(By.ID, "id_order").send_keys("3")
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary"
|
||||
).click()
|
||||
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[aria-label=logout]").click()
|
||||
|
||||
def test_enroll_2_step(self):
|
||||
"""Test 2-step enroll flow"""
|
||||
self.driver.get(self.live_server_url)
|
||||
self.setup_test_enroll_2_step()
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[role=enroll]"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||
self.driver.find_element(By.ID, "id_username").send_keys("foo")
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys("some name")
|
||||
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
|
||||
self.driver.find_element(By.LINK_TEXT, "foo").click()
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
||||
"some name",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"foo@bar.baz",
|
||||
)
|
||||
|
||||
@override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend")
|
||||
def test_enroll_email(self):
|
||||
"""Test enroll with Email verification"""
|
||||
# First stage fields
|
||||
username_prompt = Prompt.objects.create(
|
||||
field_key="username", label="Username", order=0, type=FieldTypes.TEXT
|
||||
)
|
||||
password = Prompt.objects.create(
|
||||
field_key="password", label="Password", order=1, type=FieldTypes.PASSWORD
|
||||
)
|
||||
password_repeat = Prompt.objects.create(
|
||||
field_key="password_repeat",
|
||||
label="Password (repeat)",
|
||||
order=2,
|
||||
type=FieldTypes.PASSWORD,
|
||||
)
|
||||
|
||||
# Second stage fields
|
||||
name_field = Prompt.objects.create(
|
||||
field_key="name", label="Name", order=0, type=FieldTypes.TEXT
|
||||
)
|
||||
email = Prompt.objects.create(
|
||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.save()
|
||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||
second_stage.fields.set([name_field, email])
|
||||
second_stage.save()
|
||||
email_stage = EmailStage.objects.create(
|
||||
name="enroll-email",
|
||||
host="localhost",
|
||||
port=1025,
|
||||
template=EmailTemplates.ACCOUNT_CONFIRM,
|
||||
)
|
||||
user_write = UserWriteStage.objects.create(name="enroll-user-write")
|
||||
user_login = UserLoginStage.objects.create(name="enroll-user-login")
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
PolicyBinding.objects.create(
|
||||
target=first_stage, policy=password_policy, order=0
|
||||
)
|
||||
|
||||
flow = Flow.objects.create(
|
||||
name="default-enrollment-flow",
|
||||
slug="default-enrollment-flow",
|
||||
designation=FlowDesignation.ENROLLMENT,
|
||||
)
|
||||
|
||||
# Attach enrollment flow to identification stage
|
||||
ident_stage: IdentificationStage = IdentificationStage.objects.first()
|
||||
ident_stage.enrollment_flow = flow
|
||||
ident_stage.save()
|
||||
|
||||
FlowStageBinding.objects.create(flow=flow, stage=first_stage, order=0)
|
||||
FlowStageBinding.objects.create(flow=flow, stage=second_stage, order=1)
|
||||
FlowStageBinding.objects.create(flow=flow, stage=user_write, order=2)
|
||||
FlowStageBinding.objects.create(flow=flow, stage=email_stage, order=3)
|
||||
FlowStageBinding.objects.create(flow=flow, stage=user_login, order=4)
|
||||
|
||||
self.driver.get(self.live_server_url)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click()
|
||||
self.driver.find_element(By.ID, "id_username").send_keys("foo")
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys("some name")
|
||||
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
sleep(3)
|
||||
|
||||
# Open Mailhog
|
||||
self.driver.get("http://localhost:8025")
|
||||
|
||||
# Click on first message
|
||||
self.driver.find_element(By.CLASS_NAME, "msglist-message").click()
|
||||
sleep(3)
|
||||
self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane"))
|
||||
self.driver.find_element(By.ID, "confirm").click()
|
||||
self.driver.close()
|
||||
self.driver.switch_to.window(self.driver.window_handles[0])
|
||||
|
||||
# We're now logged in
|
||||
sleep(3)
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
||||
"some name",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"foo@bar.baz",
|
||||
)
|
||||
260
e2e/test_flows_enroll.py
Normal file
260
e2e/test_flows_enroll.py
Normal file
@ -0,0 +1,260 @@
|
||||
"""Test Enroll flow"""
|
||||
from time import sleep
|
||||
|
||||
from django.test import override_settings
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.stages.email.models import EmailStage, EmailTemplates
|
||||
from passbook.stages.identification.models import IdentificationStage
|
||||
from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||
from passbook.stages.user_login.models import UserLoginStage
|
||||
from passbook.stages.user_write.models import UserWriteStage
|
||||
|
||||
|
||||
class TestFlowsEnroll(SeleniumTestCase):
|
||||
"""Test Enroll flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test IdP container"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="mailhog/mailhog",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "wget", "-s", "http://localhost:8025"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
def test_enroll_2_step(self):
|
||||
"""Test 2-step enroll flow"""
|
||||
# First stage fields
|
||||
username_prompt = Prompt.objects.create(
|
||||
field_key="username", label="Username", order=0, type=FieldTypes.TEXT
|
||||
)
|
||||
password = Prompt.objects.create(
|
||||
field_key="password", label="Password", order=1, type=FieldTypes.PASSWORD
|
||||
)
|
||||
password_repeat = Prompt.objects.create(
|
||||
field_key="password_repeat",
|
||||
label="Password (repeat)",
|
||||
order=2,
|
||||
type=FieldTypes.PASSWORD,
|
||||
)
|
||||
|
||||
# Second stage fields
|
||||
name_field = Prompt.objects.create(
|
||||
field_key="name", label="Name", order=0, type=FieldTypes.TEXT
|
||||
)
|
||||
email = Prompt.objects.create(
|
||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.save()
|
||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||
second_stage.fields.set([name_field, email])
|
||||
second_stage.save()
|
||||
user_write = UserWriteStage.objects.create(name="enroll-user-write")
|
||||
user_login = UserLoginStage.objects.create(name="enroll-user-login")
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
PolicyBinding.objects.create(
|
||||
target=first_stage, policy=password_policy, order=0
|
||||
)
|
||||
|
||||
flow = Flow.objects.create(
|
||||
name="default-enrollment-flow",
|
||||
slug="default-enrollment-flow",
|
||||
designation=FlowDesignation.ENROLLMENT,
|
||||
)
|
||||
|
||||
# Attach enrollment flow to identification stage
|
||||
ident_stage: IdentificationStage = IdentificationStage.objects.first()
|
||||
ident_stage.enrollment_flow = flow
|
||||
ident_stage.save()
|
||||
|
||||
FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0)
|
||||
FlowStageBinding.objects.create(target=flow, stage=second_stage, order=1)
|
||||
FlowStageBinding.objects.create(target=flow, stage=user_write, order=2)
|
||||
FlowStageBinding.objects.create(target=flow, stage=user_login, order=3)
|
||||
|
||||
self.driver.get(self.live_server_url)
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[role=enroll]"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||
self.driver.find_element(By.ID, "id_username").send_keys("foo")
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys("some name")
|
||||
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
|
||||
self.driver.find_element(By.LINK_TEXT, "foo").click()
|
||||
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
||||
"some name",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"foo@bar.baz",
|
||||
)
|
||||
|
||||
@override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend")
|
||||
def test_enroll_email(self):
|
||||
"""Test enroll with Email verification"""
|
||||
# First stage fields
|
||||
username_prompt = Prompt.objects.create(
|
||||
field_key="username", label="Username", order=0, type=FieldTypes.TEXT
|
||||
)
|
||||
password = Prompt.objects.create(
|
||||
field_key="password", label="Password", order=1, type=FieldTypes.PASSWORD
|
||||
)
|
||||
password_repeat = Prompt.objects.create(
|
||||
field_key="password_repeat",
|
||||
label="Password (repeat)",
|
||||
order=2,
|
||||
type=FieldTypes.PASSWORD,
|
||||
)
|
||||
|
||||
# Second stage fields
|
||||
name_field = Prompt.objects.create(
|
||||
field_key="name", label="Name", order=0, type=FieldTypes.TEXT
|
||||
)
|
||||
email = Prompt.objects.create(
|
||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.save()
|
||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||
second_stage.fields.set([name_field, email])
|
||||
second_stage.save()
|
||||
email_stage = EmailStage.objects.create(
|
||||
name="enroll-email",
|
||||
host="localhost",
|
||||
port=1025,
|
||||
template=EmailTemplates.ACCOUNT_CONFIRM,
|
||||
)
|
||||
user_write = UserWriteStage.objects.create(name="enroll-user-write")
|
||||
user_login = UserLoginStage.objects.create(name="enroll-user-login")
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
PolicyBinding.objects.create(
|
||||
target=first_stage, policy=password_policy, order=0
|
||||
)
|
||||
|
||||
flow = Flow.objects.create(
|
||||
name="default-enrollment-flow",
|
||||
slug="default-enrollment-flow",
|
||||
designation=FlowDesignation.ENROLLMENT,
|
||||
)
|
||||
|
||||
# Attach enrollment flow to identification stage
|
||||
ident_stage: IdentificationStage = IdentificationStage.objects.first()
|
||||
ident_stage.enrollment_flow = flow
|
||||
ident_stage.save()
|
||||
|
||||
FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0)
|
||||
FlowStageBinding.objects.create(target=flow, stage=second_stage, order=1)
|
||||
FlowStageBinding.objects.create(target=flow, stage=user_write, order=2)
|
||||
FlowStageBinding.objects.create(target=flow, stage=email_stage, order=3)
|
||||
FlowStageBinding.objects.create(target=flow, stage=user_login, order=4)
|
||||
|
||||
self.driver.get(self.live_server_url)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click()
|
||||
self.driver.find_element(By.ID, "id_username").send_keys("foo")
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
self.driver.find_element(By.ID, "id_name").send_keys("some name")
|
||||
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
sleep(3)
|
||||
|
||||
# Open Mailhog
|
||||
self.driver.get("http://localhost:8025")
|
||||
|
||||
# Click on first message
|
||||
self.driver.find_element(By.CLASS_NAME, "msglist-message").click()
|
||||
sleep(3)
|
||||
self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane"))
|
||||
self.driver.find_element(By.ID, "confirm").click()
|
||||
self.driver.close()
|
||||
self.driver.switch_to.window(self.driver.window_handles[0])
|
||||
|
||||
# We're now logged in
|
||||
sleep(3)
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
||||
"some name",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"foo@bar.baz",
|
||||
)
|
||||
@ -5,7 +5,7 @@ from selenium.webdriver.common.keys import Keys
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
|
||||
|
||||
class TestLogin(SeleniumTestCase):
|
||||
class TestFlowsLogin(SeleniumTestCase):
|
||||
"""test default login flow"""
|
||||
|
||||
def test_login(self):
|
||||
54
e2e/test_flows_stage_setup.py
Normal file
54
e2e/test_flows_stage_setup.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""test stage setup flows (password change)"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
from time import sleep
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.models import Flow, FlowDesignation
|
||||
from passbook.stages.password.models import PasswordStage
|
||||
|
||||
|
||||
class TestFlowsStageSetup(SeleniumTestCase):
|
||||
"""test stage setup flows"""
|
||||
|
||||
def test_password_change(self):
|
||||
"""test password change flow"""
|
||||
# Ensure that password stage has change_flow set
|
||||
flow = Flow.objects.get(
|
||||
slug="default-password-change", designation=FlowDesignation.STAGE_SETUP,
|
||||
)
|
||||
|
||||
stages = PasswordStage.objects.filter(name="default-authentication-password")
|
||||
stage = stages.first()
|
||||
stage.change_flow = flow
|
||||
stage.save()
|
||||
|
||||
new_password = "".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
||||
for _ in range(8)
|
||||
)
|
||||
|
||||
self.driver.get(
|
||||
f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F"
|
||||
)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
self.driver.find_element(By.LINK_TEXT, "Change password").click()
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(new_password)
|
||||
self.driver.find_element(By.ID, "id_password_repeat").click()
|
||||
self.driver.find_element(By.ID, "id_password_repeat").send_keys(new_password)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
|
||||
sleep(2)
|
||||
# Because USER() is cached, we need to get the user manually here
|
||||
user = User.objects.get(username=USER().username)
|
||||
self.assertTrue(user.check_password(new_password))
|
||||
@ -11,6 +11,8 @@ from docker.types import Healthcheck
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.core.models import Application
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.providers.oauth.models import OAuth2Provider
|
||||
|
||||
|
||||
@ -18,16 +20,16 @@ class TestProviderOAuth(SeleniumTestCase):
|
||||
"""test OAuth Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client_id = generate_client_id()
|
||||
self.client_secret = generate_client_secret()
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup client grafana container which we test OAuth against"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="grafana/grafana:latest",
|
||||
image="grafana/grafana:7.1.0",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
@ -93,29 +95,28 @@ class TestProviderOAuth(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
|
||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().username,
|
||||
)
|
||||
@ -161,31 +162,69 @@ class TestProviderOAuth(SeleniumTestCase):
|
||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]"
|
||||
).text,
|
||||
)
|
||||
sleep(1)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
||||
|
||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().username,
|
||||
)
|
||||
|
||||
def test_denied(self):
|
||||
"""test OAuth Provider flow (default authorization flow, denied)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type=OAuth2Provider.CLIENT_CONFIDENTIAL,
|
||||
authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
redirect_uris="http://localhost:3000/login/github",
|
||||
skip_authorization=True,
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
||||
|
||||
negative_policy = ExpressionPolicy.objects.create(
|
||||
name="negative-static", expression="return False"
|
||||
)
|
||||
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
||||
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--github").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
"Permission denied",
|
||||
)
|
||||
|
||||
@ -14,6 +14,8 @@ from docker.types import Healthcheck
|
||||
from e2e.utils import USER, SeleniumTestCase, ensure_rsa_key
|
||||
from passbook.core.models import Application
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.providers.oidc.models import OpenIDProvider
|
||||
|
||||
|
||||
@ -21,16 +23,16 @@ class TestProviderOIDC(SeleniumTestCase):
|
||||
"""test OpenID Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client_id = generate_client_id()
|
||||
self.client_secret = generate_client_secret()
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup client grafana container which we test OIDC against"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="grafana/grafana:latest",
|
||||
image="grafana/grafana:7.1.0",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
@ -151,23 +153,20 @@ class TestProviderOIDC(SeleniumTestCase):
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
@ -219,6 +218,7 @@ class TestProviderOIDC(SeleniumTestCase):
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
|
||||
)
|
||||
sleep(1)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
||||
|
||||
self.wait.until(
|
||||
@ -232,23 +232,67 @@ class TestProviderOIDC(SeleniumTestCase):
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
|
||||
def test_authorization_denied(self):
|
||||
"""test OpenID Provider flow (default authorization with access deny)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
client = Client.objects.create(
|
||||
name="grafana",
|
||||
client_type="confidential",
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
_redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
_scope="openid profile email",
|
||||
reuse_consent=False,
|
||||
require_consent=False,
|
||||
)
|
||||
# At least one of these objects must exist
|
||||
ensure_rsa_key()
|
||||
# This response_code object might exist or not, depending on the order the tests are run
|
||||
rp_type, _ = ResponseType.objects.get_or_create(value="code")
|
||||
client.response_types.set([rp_type])
|
||||
client.save()
|
||||
provider = OpenIDProvider.objects.create(
|
||||
oidc_client=client, authorization_flow=authorization_flow,
|
||||
)
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
||||
|
||||
negative_policy = ExpressionPolicy.objects.create(
|
||||
name="negative-static", expression="return False"
|
||||
)
|
||||
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
||||
self.driver.get("http://localhost:3000")
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
"Permission denied",
|
||||
)
|
||||
|
||||
@ -11,13 +11,13 @@ from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.core.models import Application
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.lib.utils.reflection import class_to_path
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.providers.saml.models import (
|
||||
SAMLBindings,
|
||||
SAMLPropertyMapping,
|
||||
SAMLProvider,
|
||||
)
|
||||
from passbook.providers.saml.processors.generic import GenericProcessor
|
||||
|
||||
|
||||
class TestProviderSAML(SeleniumTestCase):
|
||||
@ -68,7 +68,6 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
)
|
||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
name="saml-test",
|
||||
processor_path=class_to_path(GenericProcessor),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="passbook-e2e",
|
||||
issuer="passbook-e2e",
|
||||
@ -88,6 +87,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "/html/body/pre").text,
|
||||
f"Hello, {USER().name}!",
|
||||
@ -101,7 +101,6 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
)
|
||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
name="saml-test",
|
||||
processor_path=class_to_path(GenericProcessor),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="passbook-e2e",
|
||||
issuer="passbook-e2e",
|
||||
@ -127,7 +126,9 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
|
||||
).text,
|
||||
)
|
||||
sleep(1)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "/html/body/pre").text,
|
||||
f"Hello, {USER().name}!",
|
||||
@ -141,7 +142,6 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
)
|
||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
name="saml-test",
|
||||
processor_path=class_to_path(GenericProcessor),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="passbook-e2e",
|
||||
issuer="passbook-e2e",
|
||||
@ -166,7 +166,45 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "/html/body/pre").text,
|
||||
f"Hello, {USER().name}!",
|
||||
)
|
||||
|
||||
def test_sp_initiated_denied(self):
|
||||
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
negative_policy = ExpressionPolicy.objects.create(
|
||||
name="negative-static", expression="return False"
|
||||
)
|
||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
name="saml-test",
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="passbook-e2e",
|
||||
issuer="passbook-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="SAML", slug="passbook-saml", provider=provider,
|
||||
)
|
||||
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
||||
self.container = self.setup_client(provider)
|
||||
self.driver.get("http://localhost:9009/")
|
||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.wait_for_url(self.url("passbook_flows:denied"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
"Permission denied",
|
||||
)
|
||||
|
||||
@ -35,13 +35,42 @@ NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG
|
||||
aQ==
|
||||
-----END CERTIFICATE-----"""
|
||||
|
||||
IDP_KEY = """-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNQIWjOA1vWHUz
|
||||
SPM1FIKOE4GdH65VtWlpZ9dghH4CFYN0R7mvJj4KBq86Dxt8vJvLMV16GVh0NGCR
|
||||
50QH8aMbxonDTqXSoXiMM4DDSQTKBYK7aZwftc7FG35gAfdNUdr8e7VbdaPOShuq
|
||||
qotDyCQpZYzbt86ABnoaJ5okE3pUFIwxw97LcdYsGZz5Ngma/V1to7aMeEqHyl8r
|
||||
DRbXZUzw/U8g7yC/g+G7+64liJ4FYqLEETLLSUePKLFgUJHXbF2HgIDjur3nxlEa
|
||||
ecNQYVUTVCGBFpwkI5n1t3m32avwotpUFhMImjkRETyPKZpvl0+p7mop8mwJmKpa
|
||||
CVuNSj23AgMBAAECggEABn4I/B20xxXcNzASiVZJvua9DdRHtmxTlkLznBj0x2oY
|
||||
y1/Nbs3d3oFRn5uEuhBZOTcphsgwdRSHDXZsP3gUObew+d2N/zieUIj8hLDVlvJP
|
||||
rU/s4U/l53Q0LiNByE9ThvL+zJLPCKJtd5uHZjB5fFm69+Q7gu8xg4xHIub+0pP5
|
||||
PHanmHCDrbgNN/oqlar4FZ2MXTgekW6Amyc/koE9hIn4Baa2Ke/B/AUGY4pMRLqp
|
||||
TArt+GTVeWeoFY9QACUpaHpJhGb/Piou6tlU57e42cLoki1f0+SARsBBKyXA7BB1
|
||||
1fMH10KQYFA68dTYWlKzQau/K4xaqg4FKmtwF66GQQKBgQD9OpNUS7oRxMHVJaBR
|
||||
TNWW+V1FXycqojekFpDijPb2X5CWV16oeWgaXp0nOHFdy9EWs3GtGpfZasaRVHsX
|
||||
SHtPh4Nb8JqHdGE0/CD6t0+4Dns8Bn9cSqtdQB7R3Jn7IMXi9X/U8LDKo+A18/Jq
|
||||
V8VgUngMny9YjMkQIbK8TRWkYQKBgQDPf4nxO6ju+tOHHORQty3bYDD0+OV3I0+L
|
||||
0yz0uPreryBVi9nY43KakH52D7UZEwwsBjjGXD+WH8xEsmBWsGNXJu025PvzIJoz
|
||||
lAEiXvMp/NmYp+tY4rDmO8RhyVocBqWHzh38m0IFOd4ByFD5nLEDrA3pDVo0aNgY
|
||||
n0GwRysZFwKBgQDkCj3m6ZMUsUWEty+aR0EJhmKyODBDOnY09IVhH2S/FexVFzUN
|
||||
LtfK9206hp/Awez3Ln2uT4Zzqq5K7fMzUniJdBWdVB004l8voeXpIe9OZuwfcBJ9
|
||||
gFi1zypx/uFDv421BzQpBN+QfOdKbvbdQVFjnqCxbSDr80yVlGMrI5fbwQKBgG09
|
||||
oRrepO7EIO8GN/GCruLK/ptKGkyhy3Q6xnVEmdb47hX7ncJA5IoZPmrblCVSUNsw
|
||||
n11XHabksL8OBgg9rt8oQEThQv/aDzTOW9aDlJNragejiBTwq99aYeZ1gjo1CZq4
|
||||
2jKubpCfyZC4rGDtrIfZYi1q+S2UcQhtd8DdhwQbAoGAAM4EpDA4yHB5yiek1p/o
|
||||
CbqRCta/Dx6Eyo0KlNAyPuFPAshupG4NBx7mT2ASfL+2VBHoi6mHSri+BDX5ryYF
|
||||
fMYvp7URYoq7w7qivRlvvEg5yoYrK13F2+Gj6xJ4jEN9m0KdM/g3mJGq0HBTIQrp
|
||||
Sm75WXsflOxuTn08LbgGc4s=
|
||||
-----END PRIVATE KEY-----"""
|
||||
|
||||
|
||||
class TestSourceSAML(SeleniumTestCase):
|
||||
"""test SAML Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test IdP container"""
|
||||
@ -81,7 +110,7 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
keypair = CertificateKeyPair.objects.create(
|
||||
name="test-idp-cert", certificate_data=IDP_CERT
|
||||
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
|
||||
)
|
||||
|
||||
SAMLSource.objects.create(
|
||||
@ -125,3 +154,109 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.assertNotEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
||||
)
|
||||
|
||||
def test_idp_post(self):
|
||||
"""test SAML Source With post binding"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
keypair = CertificateKeyPair.objects.create(
|
||||
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
|
||||
)
|
||||
|
||||
SAMLSource.objects.create(
|
||||
name="saml-idp-test",
|
||||
slug="saml-idp-test",
|
||||
authentication_flow=authentication_flow,
|
||||
enrollment_flow=enrollment_flow,
|
||||
issuer="entity-id",
|
||||
sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
|
||||
binding_type=SAMLBindingTypes.POST,
|
||||
signing_kp=keypair,
|
||||
)
|
||||
|
||||
self.driver.get(self.live_server_url)
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
sleep(1)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
|
||||
# Now we should be at the IDP, wait for the username field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
|
||||
self.driver.find_element(By.ID, "username").send_keys("user1")
|
||||
self.driver.find_element(By.ID, "password").send_keys("user1pass")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||
self.assertNotEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
||||
)
|
||||
|
||||
def test_idp_post_auto(self):
|
||||
"""test SAML Source With post binding (auto redirect)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
keypair = CertificateKeyPair.objects.create(
|
||||
name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
|
||||
)
|
||||
|
||||
SAMLSource.objects.create(
|
||||
name="saml-idp-test",
|
||||
slug="saml-idp-test",
|
||||
authentication_flow=authentication_flow,
|
||||
enrollment_flow=enrollment_flow,
|
||||
issuer="entity-id",
|
||||
sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
|
||||
binding_type=SAMLBindingTypes.POST_AUTO,
|
||||
signing_kp=keypair,
|
||||
)
|
||||
|
||||
self.driver.get(self.live_server_url)
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the username field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
|
||||
self.driver.find_element(By.ID, "username").send_keys("user1")
|
||||
self.driver.find_element(By.ID, "password").send_keys("user1pass")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||
self.assertNotEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
||||
)
|
||||
|
||||
212
e2e/test_sources_oauth.py
Normal file
212
e2e/test_sources_oauth.py
Normal file
@ -0,0 +1,212 @@
|
||||
"""test OAuth Source"""
|
||||
from os.path import abspath
|
||||
from time import sleep
|
||||
|
||||
from oauth2_provider.generators import generate_client_secret
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from yaml import safe_dump
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import SeleniumTestCase
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.sources.oauth.models import OAuthSource
|
||||
|
||||
TOKEN_URL = "http://127.0.0.1:5556/dex/token"
|
||||
CONFIG_PATH = "/tmp/dex.yml"
|
||||
|
||||
|
||||
class TestSourceOAuth(SeleniumTestCase):
|
||||
"""test OAuth Source flow"""
|
||||
|
||||
container: Container
|
||||
|
||||
def setUp(self):
|
||||
self.client_secret = generate_client_secret()
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def prepare_dex_config(self):
|
||||
"""Since Dex does not document which environment
|
||||
variables can be used to configure clients"""
|
||||
config = {
|
||||
"enablePasswordDB": True,
|
||||
"issuer": "http://127.0.0.1:5556/dex",
|
||||
"logger": {"level": "debug"},
|
||||
"staticClients": [
|
||||
{
|
||||
"id": "example-app",
|
||||
"name": "Example App",
|
||||
"redirectURIs": [
|
||||
self.url(
|
||||
"passbook_sources_oauth:oauth-client-callback",
|
||||
source_slug="dex",
|
||||
)
|
||||
],
|
||||
"secret": self.client_secret,
|
||||
}
|
||||
],
|
||||
"staticPasswords": [
|
||||
{
|
||||
"email": "admin@example.com",
|
||||
# hash for password
|
||||
"hash": "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W",
|
||||
"userID": "08a8684b-db88-4b73-90a9-3cd1661f5466",
|
||||
"username": "admin",
|
||||
}
|
||||
],
|
||||
"storage": {"config": {"file": "/tmp/dex.db"}, "type": "sqlite3"},
|
||||
"web": {"http": "0.0.0.0:5556"},
|
||||
}
|
||||
with open(CONFIG_PATH, "w+") as _file:
|
||||
safe_dump(config, _file)
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test Dex container"""
|
||||
self.prepare_dex_config()
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="quay.io/dexidp/dex:v2.24.0",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
command="serve /config.yml",
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
volumes={abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro",}},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def create_objects(self):
|
||||
"""Create required objects"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
|
||||
OAuthSource.objects.create(
|
||||
name="dex",
|
||||
slug="dex",
|
||||
authentication_flow=authentication_flow,
|
||||
enrollment_flow=enrollment_flow,
|
||||
provider_type="openid-connect",
|
||||
authorization_url="http://127.0.0.1:5556/dex/auth",
|
||||
access_token_url=TOKEN_URL,
|
||||
profile_url="http://127.0.0.1:5556/dex/userinfo",
|
||||
consumer_key="example-app",
|
||||
consumer_secret=self.client_secret,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
def test_oauth_enroll(self):
|
||||
"""test OAuth Source With With OIDC"""
|
||||
self.create_objects()
|
||||
self.driver.get(self.live_server_url)
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
# At this point we've been redirected back
|
||||
# and we're asked for the username
|
||||
self.driver.find_element(By.NAME, "username").click()
|
||||
self.driver.find_element(By.NAME, "username").send_keys("foo")
|
||||
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
|
||||
self.driver.find_element(By.LINK_TEXT, "foo").click()
|
||||
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"admin@example.com",
|
||||
)
|
||||
|
||||
def test_oauth_enroll_auth(self):
|
||||
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
|
||||
self.test_oauth_enroll()
|
||||
# We're logged in at the end of this, log out and re-login
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[aria-label=logout]").click()
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
|
||||
self.driver.find_element(By.LINK_TEXT, "foo").click()
|
||||
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
||||
"foo",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||
"admin@example.com",
|
||||
)
|
||||
28
e2e/utils.py
28
e2e/utils.py
@ -3,7 +3,7 @@ from functools import lru_cache
|
||||
from glob import glob
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from inspect import getmembers, isfunction
|
||||
from os import makedirs
|
||||
from os import environ, makedirs
|
||||
from time import time
|
||||
|
||||
from Cryptodome.PublicKey import RSA
|
||||
@ -16,6 +16,7 @@ from selenium import webdriver
|
||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
|
||||
@ -42,12 +43,13 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
makedirs("out", exist_ok=True)
|
||||
makedirs("selenium_screenshots/", exist_ok=True)
|
||||
self.driver = self._get_driver()
|
||||
self.driver.maximize_window()
|
||||
self.driver.implicitly_wait(5)
|
||||
self.wait = WebDriverWait(self.driver, 60)
|
||||
self.driver.implicitly_wait(30)
|
||||
self.wait = WebDriverWait(self.driver, 50)
|
||||
self.apply_default_data()
|
||||
self.logger = get_logger()
|
||||
|
||||
def _get_driver(self) -> WebDriver:
|
||||
return webdriver.Remote(
|
||||
@ -56,10 +58,26 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.driver.save_screenshot(f"out/{self.__class__.__name__}_{time()}.png")
|
||||
if "TF_BUILD" in environ:
|
||||
screenshot_file = (
|
||||
f"selenium_screenshots/{self.__class__.__name__}_{time()}.png"
|
||||
)
|
||||
self.driver.save_screenshot(screenshot_file)
|
||||
self.logger.warning("Saved screenshot", file=screenshot_file)
|
||||
for line in self.driver.get_log("browser"):
|
||||
self.logger.warning(
|
||||
line["message"], source=line["source"], level=line["level"]
|
||||
)
|
||||
self.driver.quit()
|
||||
super().tearDown()
|
||||
|
||||
def wait_for_url(self, desired_url):
|
||||
"""Wait until URL is `desired_url`."""
|
||||
self.wait.until(
|
||||
lambda driver: driver.current_url == desired_url,
|
||||
f"URL {self.driver.current_url} doesn't match expected URL {desired_url}",
|
||||
)
|
||||
|
||||
def url(self, view, **kwargs) -> str:
|
||||
"""reverse `view` with `**kwargs` into full URL using live_server_url"""
|
||||
return self.live_server_url + reverse(view, kwargs=kwargs)
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
FROM quay.io/oauth2-proxy/oauth2-proxy
|
||||
|
||||
COPY templates /templates
|
||||
|
||||
ENV OAUTH2_PROXY_EMAIL_DOMAINS=*
|
||||
ENV OAUTH2_PROXY_PROVIDER=oidc
|
||||
ENV OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR=/templates
|
||||
ENV OAUTH2_PROXY_HTTP_ADDRESS=:4180
|
||||
# TODO: If service is access over HTTPS, this needs to be set to true (default), otherwise needs to be false
|
||||
# ENV OAUTH2_PROXY_COOKIE_SECURE=true
|
||||
ENV OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
{{define "error.html"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
|
||||
<head>
|
||||
<title>{{.Title}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>{{.Title}}</h2>
|
||||
<p>{{.Message}}</p>
|
||||
<hr>
|
||||
<p><a href="{{.ProxyPrefix}}/sign_in">Sign In</a></p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
||||
@ -1,119 +0,0 @@
|
||||
{{define "sign_in.html"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
<head>
|
||||
<title>Sign In with passbook</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
color: #333;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.signin {
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
max-width: 400px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
color: #fff;
|
||||
background-color: #428bca;
|
||||
border: 1px solid #357ebd;
|
||||
-webkit-border-radius: 4;
|
||||
-moz-border-radius: 4;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 6px 12px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #3071a9;
|
||||
border-color: #285e8e;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
color: #555;
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
|
||||
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
footer a {
|
||||
display: inline-block;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
color: #aaa;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="signin center">
|
||||
<form method="GET" action="{{.ProxyPrefix}}/start">
|
||||
<input type="hidden" name="rd" value="{{.Redirect}}">
|
||||
<button type="submit" class="btn">Sign in with passbook</button><br />
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
if (window.location.hash) {
|
||||
(function () {
|
||||
var inputs = document.getElementsByName('rd');
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
inputs[i].value += window.location.hash;
|
||||
}
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
||||
@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
appVersion: "0.9.0-pre3"
|
||||
appVersion: "0.9.0-stable"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.9.0-pre3"
|
||||
version: "0.9.0-stable"
|
||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
||||
|
||||
@ -21,3 +21,8 @@ data:
|
||||
message_queue_db: 1
|
||||
error_reporting: {{ .Values.config.error_reporting }}
|
||||
log_level: "{{ .Values.config.log_level }}"
|
||||
apm:
|
||||
enabled: {{ .Values.config.apm.enabled }}
|
||||
server_url: "{{ .Values.config.apm.server_url }}"
|
||||
secret_token: "{{ .Values.config.apm.server_token }}"
|
||||
verify_server_cert: {{ .Values.config.apm.verify_server_cert }}
|
||||
|
||||
@ -41,5 +41,9 @@ spec:
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
- path: /favicon.ico
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
image:
|
||||
tag: 0.9.0-pre3
|
||||
tag: 0.9.0-stable
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
@ -14,6 +14,12 @@ config:
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
log_level: warning
|
||||
# Optionally enable Elastic APM Support
|
||||
apm:
|
||||
enabled: false
|
||||
server_url: ""
|
||||
secret_token: ""
|
||||
verify_server_cert: true
|
||||
|
||||
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
|
||||
# This requires the CoreOS Prometheus Operator.
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.9.0-pre3"
|
||||
__version__ = "0.9.0-stable"
|
||||
|
||||
@ -122,15 +122,21 @@
|
||||
{% trans 'Certificates' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:tokens' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:tokens' 'passbook_admin:token-delete' %}">
|
||||
{% trans 'Tokens' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:users' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-create' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
|
||||
{% trans 'Users' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:groups' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-create' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
|
||||
{% trans 'Groups' %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Identifier' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Designation' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Stages' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Policies' %}</th>
|
||||
@ -39,8 +39,8 @@
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ flow.name }}</div>
|
||||
<small>{{ flow.slug }}</small>
|
||||
<div>{{ flow.slug }}</div>
|
||||
<small>{{ flow.name }}</small>
|
||||
</div>
|
||||
</th>
|
||||
<td role="cell">
|
||||
@ -61,6 +61,7 @@
|
||||
<td>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-update' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:flow-delete' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-execute' pk=flow.pk %}?next={{ request.get_full_path }}">{% trans 'Execute' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@ -10,29 +10,33 @@
|
||||
</section>
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-l-gallery pf-m-gutter">
|
||||
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ application_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ application_count }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-middleware"></i> {% trans 'Sources' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ source_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ source_count }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
|
||||
@ -40,15 +44,19 @@
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if providers_without_application.exists %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i> {{ provider_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ provider_count }}
|
||||
</p>
|
||||
<p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p>
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ provider_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ provider_count }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Stages' %}
|
||||
@ -56,26 +64,32 @@
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if stage_count < 1 %}
|
||||
<i class="pficon-error-circle-o"></i> {{ stage_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="pficon-error-circle-o"></i> {{ stage_count }}
|
||||
</p>
|
||||
<p>{% trans 'No Stages configured. No Users will be able to login.' %}"></p>
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ stage_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ stage_count }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-topology"></i> {% trans 'Flows' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ flow_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ flow_count }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
|
||||
@ -83,48 +97,71 @@
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if policies_without_binding %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i> {{ policy_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ policy_count }}
|
||||
</p>
|
||||
<p>{% trans 'Policies without binding exist.' %}</p>
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ policy_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ policy_count }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-migration"></i> {% trans 'Invitation' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ invitation_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ invitation_count }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ user_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ user_count }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ version }}
|
||||
<p class="aggregate-status">
|
||||
{% if version >= version_latest %}
|
||||
<i class="fa fa-check-circle"></i> {{ version }}
|
||||
{% else %}
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ version }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if version >= version_latest %}
|
||||
{% blocktrans %}
|
||||
Up-to-date!
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with latest=version_latest %}
|
||||
{{ latest }} is available!
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
|
||||
@ -132,15 +169,19 @@
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if worker_count < 1 %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i> {{ worker_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ worker_count }}
|
||||
</p>
|
||||
<p>{% trans 'No workers connected.' %}</p>
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ worker_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ worker_count }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="pf-c-card pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
|
||||
<a class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
|
||||
@ -148,13 +189,37 @@
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if cached_policies < 1 %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i> {{ cached_policies }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ cached_policies }}
|
||||
</p>
|
||||
<p>{% trans 'No policies cached. Users may experience slow response times.' %}</p>
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ cached_policies }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ cached_policies }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if cached_flows < 1 %}
|
||||
<p class="aggregate-status">
|
||||
<span class="fa fa-exclamation-triangle"></span> {{ cached_flows }}
|
||||
</p>
|
||||
<p>{% trans 'No flows cached.' %}</p>
|
||||
{% else %}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ cached_flows }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="pf-c-backdrop" id="clearCacheModalRoot" hidden>
|
||||
@ -163,7 +228,9 @@
|
||||
<button data-modal-close class="pf-c-button pf-m-plain" type="button" aria-label="Close dialog">
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1>
|
||||
<div class="pf-c-modal-box__header">
|
||||
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1>
|
||||
</div>
|
||||
<div class="pf-c-modal-box__body" id="modal-description">
|
||||
<form method="post" id="clearForm">
|
||||
{% csrf_token %}
|
||||
|
||||
@ -28,29 +28,50 @@
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Enabled' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Policy' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Target' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Enabled' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Order' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Timeout' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup" class="pf-m-expanded">
|
||||
{% for binding in object_list %}
|
||||
<tr role="row pf-c-table__expandable-row pf-m-expanded">
|
||||
<th role="cell">
|
||||
<div>{{ binding.enabled }}</div>
|
||||
</th>
|
||||
<th role="cell">
|
||||
<div>{{ binding.policy }}</div>
|
||||
</th>
|
||||
<th role="cell">
|
||||
<div>{{ binding.target|verbose_name }}</div>
|
||||
</th>
|
||||
<td>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:policy-binding-update' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:policy-binding-delete' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tbody role="rowgroup">
|
||||
{% for pbm in object_list %}
|
||||
<tr role="role">
|
||||
<td>
|
||||
{{ pbm }}
|
||||
<small>
|
||||
{{ pbm|fieldtype }}
|
||||
</small>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% for binding in pbm.bindings %}
|
||||
<tr class="row pf-c-table__expandable-row pf-m-expanded">
|
||||
<th role="cell">
|
||||
<div>{{ binding.policy }}</div>
|
||||
<small>
|
||||
{{ binding.policy|fieldtype }}
|
||||
</small>
|
||||
</th>
|
||||
<th role="cell">
|
||||
<div>{{ binding.enabled }}</div>
|
||||
</th>
|
||||
<th role="cell">
|
||||
<div>{{ binding.order }}</div>
|
||||
</th>
|
||||
<th role="cell">
|
||||
<div>{{ binding.timeout }}</div>
|
||||
</th>
|
||||
<td class="pb-table-action" role="cell">
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:policy-binding-update' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:policy-binding-delete' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -35,7 +35,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% regroup object_list by flow as grouped_bindings %}
|
||||
{% regroup object_list by target as grouped_bindings %}
|
||||
{% for flow in grouped_bindings %}
|
||||
<tr role="role">
|
||||
<td>
|
||||
@ -56,9 +56,9 @@
|
||||
</td>
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ binding.flow.slug }}</div>
|
||||
<div>{{ binding.target.slug }}</div>
|
||||
<small>
|
||||
{{ binding.flow.name }}
|
||||
{{ binding.target.name }}
|
||||
</small>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
82
passbook/admin/templates/administration/token/list.html
Normal file
82
passbook/admin/templates/administration/token/list.html
Normal file
@ -0,0 +1,82 @@
|
||||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="fas fa-key"></i>
|
||||
{% trans 'Tokens' %}
|
||||
</h1>
|
||||
<p>{% trans "Tokens are used throughout passbook for Email validation stages, Recovery keys and API access." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Token' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'User' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Expires?' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for token in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ token.pk }}</div>
|
||||
</div>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ token.user }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ token.expiring|yesno:"Yes,No" }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ token.expires }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:token-delete' pk=token.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Tokens.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no tokens exist.' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@ -12,7 +12,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block action %}
|
||||
{% blocktrans with type=form|form_verbose_name|title %}
|
||||
{% blocktrans with type=form|form_verbose_name %}
|
||||
Update {{ type }}
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
"""admin tests"""
|
||||
from importlib import import_module
|
||||
from typing import Callable
|
||||
|
||||
from django.forms import ModelForm
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
|
||||
from passbook.admin.urls import urlpatterns
|
||||
from passbook.core.models import User
|
||||
from passbook.lib.utils.reflection import get_apps
|
||||
|
||||
|
||||
class TestAdmin(TestCase):
|
||||
@ -34,4 +37,28 @@ def generic_view_tester(view_name: str) -> Callable:
|
||||
|
||||
for url in urlpatterns:
|
||||
method_name = url.name.replace("-", "_")
|
||||
setattr(TestAdmin, f"test_{method_name}", generic_view_tester(url.name))
|
||||
setattr(TestAdmin, f"test_view_{method_name}", generic_view_tester(url.name))
|
||||
|
||||
|
||||
def generic_form_tester(form: ModelForm) -> Callable:
|
||||
"""Test a form"""
|
||||
|
||||
def tester(self: TestAdmin):
|
||||
form_inst = form()
|
||||
self.assertFalse(form_inst.is_valid())
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
# Load the forms module from every app, so we have all forms loaded
|
||||
for app in get_apps():
|
||||
module = app.__module__.replace(".apps", ".forms")
|
||||
try:
|
||||
import_module(module)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
for form_class in ModelForm.__subclasses__():
|
||||
setattr(
|
||||
TestAdmin, f"test_form_{form_class.__name__}", generic_form_tester(form_class)
|
||||
)
|
||||
|
||||
@ -3,7 +3,6 @@ from django.urls import path
|
||||
|
||||
from passbook.admin.views import (
|
||||
applications,
|
||||
audit,
|
||||
certificate_key_pair,
|
||||
debug,
|
||||
flows,
|
||||
@ -18,6 +17,7 @@ from passbook.admin.views import (
|
||||
stages_bindings,
|
||||
stages_invitations,
|
||||
stages_prompts,
|
||||
tokens,
|
||||
users,
|
||||
)
|
||||
|
||||
@ -42,6 +42,13 @@ urlpatterns = [
|
||||
applications.ApplicationDeleteView.as_view(),
|
||||
name="application-delete",
|
||||
),
|
||||
# Tokens
|
||||
path("tokens/", tokens.TokenListView.as_view(), name="tokens"),
|
||||
path(
|
||||
"tokens/<uuid:pk>/delete/",
|
||||
tokens.TokenDeleteView.as_view(),
|
||||
name="token-delete",
|
||||
),
|
||||
# Sources
|
||||
path("sources/", sources.SourceListView.as_view(), name="sources"),
|
||||
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
|
||||
@ -188,6 +195,11 @@ urlpatterns = [
|
||||
path(
|
||||
"flows/<uuid:pk>/update/", flows.FlowUpdateView.as_view(), name="flow-update",
|
||||
),
|
||||
path(
|
||||
"flows/<uuid:pk>/execute/",
|
||||
flows.FlowDebugExecuteView.as_view(),
|
||||
name="flow-execute",
|
||||
),
|
||||
path(
|
||||
"flows/<uuid:pk>/delete/", flows.FlowDeleteView.as_view(), name="flow-delete",
|
||||
),
|
||||
@ -252,8 +264,6 @@ urlpatterns = [
|
||||
certificate_key_pair.CertificateKeyPairDeleteView.as_view(),
|
||||
name="certificatekeypair-delete",
|
||||
),
|
||||
# Audit Log
|
||||
path("audit/", audit.EventListView.as_view(), name="audit-log"),
|
||||
# Groups
|
||||
path("groups/", groups.GroupListView.as_view(), name="groups"),
|
||||
# Debug
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
"""passbook Application administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.core.forms.applications import ApplicationForm
|
||||
from passbook.core.models import Application
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
@ -24,9 +24,6 @@ class ApplicationListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
paginate_by = 40
|
||||
template_name = "administration/application/list.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class ApplicationCreateView(
|
||||
SuccessMessageMixin,
|
||||
@ -44,10 +41,6 @@ class ApplicationCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:applications")
|
||||
success_message = _("Successfully created Application")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["type"] = "Application"
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class ApplicationUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
@ -64,7 +57,7 @@ class ApplicationUpdateView(
|
||||
|
||||
|
||||
class ApplicationDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete application"""
|
||||
|
||||
@ -74,7 +67,3 @@ class ApplicationDeleteView(
|
||||
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)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
"""passbook CertificateKeyPair administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.crypto.forms import CertificateKeyPairForm
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
@ -41,10 +41,6 @@ class CertificateKeyPairCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:certificate_key_pair")
|
||||
success_message = _("Successfully created CertificateKeyPair")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["type"] = "Certificate-Key Pair"
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class CertificateKeyPairUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
@ -61,7 +57,7 @@ class CertificateKeyPairUpdateView(
|
||||
|
||||
|
||||
class CertificateKeyPairDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete certificatekeypair"""
|
||||
|
||||
@ -71,7 +67,3 @@ class CertificateKeyPairDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:certificate_key_pair")
|
||||
success_message = _("Successfully deleted Certificate-Key Pair")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,17 +1,21 @@
|
||||
"""passbook Flow 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.messages.views import SuccessMessageMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from django.views.generic import DetailView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.flows.forms import FlowForm
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from passbook.flows.views import SESSION_KEY_PLAN, FlowPlanner
|
||||
from passbook.lib.utils.urls import redirect_with_qs
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
@ -41,10 +45,6 @@ class FlowCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:flows")
|
||||
success_message = _("Successfully created Flow")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["type"] = "Flow"
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class FlowUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
@ -60,9 +60,7 @@ class FlowUpdateView(
|
||||
success_message = _("Successfully updated Flow")
|
||||
|
||||
|
||||
class FlowDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
class FlowDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete flow"""
|
||||
|
||||
model = Flow
|
||||
@ -72,6 +70,21 @@ class FlowDeleteView(
|
||||
success_url = reverse_lazy("passbook_admin:flows")
|
||||
success_message = _("Successfully deleted Flow")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
"""Debug exectue flow, setting the current user as pending user"""
|
||||
|
||||
model = Flow
|
||||
permission_required = "passbook_flows.view_flow"
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request: HttpRequest, pk: str) -> HttpResponse:
|
||||
"""Debug exectue flow, setting the current user as pending user"""
|
||||
flow: Flow = self.get_object()
|
||||
planner = FlowPlanner(flow)
|
||||
planner.use_cache = False
|
||||
plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user})
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"passbook_flows:flow-executor-shell", self.request.GET, flow_slug=flow.slug,
|
||||
)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
"""passbook Group administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.core.forms.groups import GroupForm
|
||||
from passbook.core.models import Group
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
@ -41,10 +41,6 @@ class GroupCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:groups")
|
||||
success_message = _("Successfully created Group")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["type"] = "Group"
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class GroupUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
@ -60,15 +56,12 @@ class GroupUpdateView(
|
||||
success_message = _("Successfully updated Group")
|
||||
|
||||
|
||||
class GroupDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView):
|
||||
class GroupDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete group"""
|
||||
|
||||
model = Group
|
||||
permission_required = "passbook_flows.delete_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)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.views.generic import TemplateView
|
||||
from packaging.version import Version, parse
|
||||
from requests import RequestException, get
|
||||
|
||||
from passbook import __version__
|
||||
from passbook.admin.mixins import AdminRequiredMixin
|
||||
@ -11,6 +13,22 @@ from passbook.policies.models import Policy
|
||||
from passbook.root.celery import CELERY_APP
|
||||
from passbook.stages.invitation.models import Invitation
|
||||
|
||||
VERSION_CACHE_KEY = "passbook_latest_version"
|
||||
|
||||
|
||||
def latest_version() -> Version:
|
||||
"""Get latest release from GitHub, cached"""
|
||||
if not cache.get(VERSION_CACHE_KEY):
|
||||
try:
|
||||
data = get(
|
||||
"https://api.github.com/repos/beryju/passbook/releases/latest"
|
||||
).json()
|
||||
tag_name = data.get("tag_name")
|
||||
cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], 30)
|
||||
except (RequestException, IndexError):
|
||||
cache.set(VERSION_CACHE_KEY, "0.0.0", 30)
|
||||
return parse(cache.get(VERSION_CACHE_KEY))
|
||||
|
||||
|
||||
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
"""Overview View"""
|
||||
@ -33,7 +51,8 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
kwargs["stage_count"] = len(Stage.objects.all())
|
||||
kwargs["flow_count"] = len(Flow.objects.all())
|
||||
kwargs["invitation_count"] = len(Invitation.objects.all())
|
||||
kwargs["version"] = __version__
|
||||
kwargs["version"] = parse(__version__)
|
||||
kwargs["version_latest"] = latest_version()
|
||||
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
|
||||
kwargs["providers_without_application"] = Provider.objects.filter(
|
||||
application=None
|
||||
@ -42,4 +61,5 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
Policy.objects.filter(bindings__isnull=True)
|
||||
)
|
||||
kwargs["cached_policies"] = len(cache.keys("policy_*"))
|
||||
kwargs["cached_flows"] = len(cache.keys("flow_*"))
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
@ -8,22 +8,25 @@ from django.contrib.auth.mixins import (
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db.models import QuerySet
|
||||
from django.forms import Form
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, FormView, ListView, UpdateView
|
||||
from django.views.generic import FormView
|
||||
from django.views.generic.detail import DetailView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.forms.policies import PolicyTestForm
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.admin.views.utils import (
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
)
|
||||
from passbook.policies.models import Policy, PolicyBinding
|
||||
from passbook.policies.process import PolicyProcess, PolicyRequest
|
||||
|
||||
|
||||
class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
class PolicyListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
||||
"""Show list of all policies"""
|
||||
|
||||
model = Policy
|
||||
@ -32,19 +35,12 @@ class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
ordering = "name"
|
||||
template_name = "administration/policy/list.html"
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs["types"] = {x.__name__: x for x in all_subclasses(Policy)}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class PolicyCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
InheritanceCreateView,
|
||||
):
|
||||
"""Create new Policy"""
|
||||
|
||||
@ -55,24 +51,12 @@ class PolicyCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:policies")
|
||||
success_message = _("Successfully created Policy")
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self) -> Form:
|
||||
policy_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(x for x in all_subclasses(Policy) if x.__name__ == policy_type)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class PolicyUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
PermissionRequiredMixin,
|
||||
InheritanceUpdateView,
|
||||
):
|
||||
"""Update policy"""
|
||||
|
||||
@ -83,27 +67,8 @@ class PolicyUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:policies")
|
||||
success_message = _("Successfully updated Policy")
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self) -> Form:
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None) -> Policy:
|
||||
return (
|
||||
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
|
||||
class PolicyDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
class PolicyDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete policy"""
|
||||
|
||||
model = Policy
|
||||
@ -113,15 +78,6 @@ class PolicyDeleteView(
|
||||
success_url = reverse_lazy("passbook_admin:policies")
|
||||
success_message = _("Successfully deleted Policy")
|
||||
|
||||
def get_object(self, queryset=None) -> Policy:
|
||||
return (
|
||||
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
def delete(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, FormView):
|
||||
"""View to test policy(s)"""
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
"""passbook PolicyBinding 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.messages.views import SuccessMessageMixin
|
||||
from django.db.models import QuerySet
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.policies.forms import PolicyBindingForm
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.policies.models import PolicyBinding, PolicyBindingModel
|
||||
|
||||
|
||||
class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
@ -22,7 +23,20 @@ class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
permission_required = "passbook_policies.view_policybinding"
|
||||
paginate_by = 10
|
||||
ordering = ["order", "target"]
|
||||
template_name = "administration/policybinding/list.html"
|
||||
template_name = "administration/policy_binding/list.html"
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
# Since `select_subclasses` does not work with a foreign key, we have to do two queries here
|
||||
# First, get all pbm objects that have bindings attached
|
||||
objects = (
|
||||
PolicyBindingModel.objects.filter(policies__isnull=False)
|
||||
.select_subclasses()
|
||||
.select_related()
|
||||
.order_by("pk")
|
||||
)
|
||||
for pbm in objects:
|
||||
pbm.bindings = PolicyBinding.objects.filter(target__pk=pbm.pbm_uuid)
|
||||
return objects
|
||||
|
||||
|
||||
class PolicyBindingCreateView(
|
||||
@ -55,16 +69,9 @@ class PolicyBindingUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:policies-bindings")
|
||||
success_message = _("Successfully updated PolicyBinding")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
|
||||
class PolicyBindingDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete policybinding"""
|
||||
|
||||
@ -74,7 +81,3 @@ class PolicyBindingDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:policies-bindings")
|
||||
success_message = _("Successfully deleted PolicyBinding")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,22 +1,25 @@
|
||||
"""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.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import (
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
)
|
||||
from passbook.core.models import PropertyMapping
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
class PropertyMappingListView(
|
||||
LoginRequiredMixin, PermissionListMixin, InheritanceListView
|
||||
):
|
||||
"""Show list of all property_mappings"""
|
||||
|
||||
model = PropertyMapping
|
||||
@ -25,19 +28,12 @@ class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView)
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["types"] = {x.__name__: x 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,
|
||||
InheritanceCreateView,
|
||||
):
|
||||
"""Create new PropertyMapping"""
|
||||
|
||||
@ -48,38 +44,12 @@ class PropertyMappingCreateView(
|
||||
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")
|
||||
try:
|
||||
model = next(
|
||||
x
|
||||
for x in all_subclasses(PropertyMapping)
|
||||
if x.__name__ == property_mapping_type
|
||||
)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
kwargs["type"] = model._meta.verbose_name
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self):
|
||||
property_mapping_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(
|
||||
x
|
||||
for x in all_subclasses(PropertyMapping)
|
||||
if x.__name__ == property_mapping_type
|
||||
)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class PropertyMappingUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
PermissionRequiredMixin,
|
||||
InheritanceUpdateView,
|
||||
):
|
||||
"""Update property_mapping"""
|
||||
|
||||
@ -90,28 +60,9 @@ class PropertyMappingUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:property-mappings")
|
||||
success_message = _("Successfully updated Property Mapping")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
class PropertyMappingDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete property_mapping"""
|
||||
|
||||
@ -121,14 +72,3 @@ class PropertyMappingDeleteView(
|
||||
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()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
"""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.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import (
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
)
|
||||
from passbook.core.models import Provider
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
class ProviderListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
||||
"""Show list of all providers"""
|
||||
|
||||
model = Provider
|
||||
@ -25,19 +26,12 @@ class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
paginate_by = 10
|
||||
ordering = "id"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["types"] = {x.__name__: x for x in all_subclasses(Provider)}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class ProviderCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
InheritanceCreateView,
|
||||
):
|
||||
"""Create new Provider"""
|
||||
|
||||
@ -48,19 +42,12 @@ class ProviderCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:providers")
|
||||
success_message = _("Successfully created Provider")
|
||||
|
||||
def get_form_class(self):
|
||||
provider_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(
|
||||
x for x in all_subclasses(Provider) if x.__name__ == provider_type
|
||||
)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class ProviderUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
PermissionRequiredMixin,
|
||||
InheritanceUpdateView,
|
||||
):
|
||||
"""Update provider"""
|
||||
|
||||
@ -71,21 +58,9 @@ class ProviderUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:providers")
|
||||
success_message = _("Successfully updated Provider")
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Provider.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
class ProviderDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete provider"""
|
||||
|
||||
@ -95,14 +70,3 @@ class ProviderDeleteView(
|
||||
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()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
"""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.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import (
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
)
|
||||
from passbook.core.models import Source
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
class SourceListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
||||
"""Show list of all sources"""
|
||||
|
||||
model = Source
|
||||
@ -25,19 +26,12 @@ class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
paginate_by = 40
|
||||
template_name = "administration/source/list.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["types"] = {x.__name__: x 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,
|
||||
InheritanceCreateView,
|
||||
):
|
||||
"""Create new Source"""
|
||||
|
||||
@ -48,17 +42,12 @@ class SourceCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:sources")
|
||||
success_message = _("Successfully created Source")
|
||||
|
||||
def get_form_class(self):
|
||||
source_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(x for x in all_subclasses(Source) if x.__name__ == source_type)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class SourceUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
PermissionRequiredMixin,
|
||||
InheritanceUpdateView,
|
||||
):
|
||||
"""Update source"""
|
||||
|
||||
@ -69,20 +58,8 @@ class SourceUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:sources")
|
||||
success_message = _("Successfully updated Source")
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
|
||||
class SourceDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
class SourceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete source"""
|
||||
|
||||
model = Source
|
||||
@ -91,12 +68,3 @@ class SourceDeleteView(
|
||||
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()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
"""passbook Stage 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.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import (
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
)
|
||||
from passbook.flows.models import Stage
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class StageListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
class StageListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
||||
"""Show list of all stages"""
|
||||
|
||||
model = Stage
|
||||
@ -25,19 +26,12 @@ class StageListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["types"] = {x.__name__: x for x in all_subclasses(Stage)}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class StageCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
InheritanceCreateView,
|
||||
):
|
||||
"""Create new Stage"""
|
||||
|
||||
@ -48,24 +42,12 @@ class StageCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:stages")
|
||||
success_message = _("Successfully created Stage")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
stage_type = self.request.GET.get("type")
|
||||
model = next(x for x in all_subclasses(Stage) if x.__name__ == stage_type)
|
||||
kwargs["type"] = model._meta.verbose_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self):
|
||||
stage_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(x for x in all_subclasses(Stage) if x.__name__ == stage_type)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class StageUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
PermissionRequiredMixin,
|
||||
InheritanceUpdateView,
|
||||
):
|
||||
"""Update stage"""
|
||||
|
||||
@ -75,20 +57,8 @@ class StageUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:stages")
|
||||
success_message = _("Successfully updated Stage")
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Stage.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
|
||||
class StageDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
class StageDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete stage"""
|
||||
|
||||
model = Stage
|
||||
@ -96,12 +66,3 @@ class StageDeleteView(
|
||||
permission_required = "passbook_flows.delete_stage"
|
||||
success_url = reverse_lazy("passbook_admin:stages")
|
||||
success_message = _("Successfully deleted Stage")
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Stage.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
"""passbook StageBinding administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.flows.forms import FlowStageBindingForm
|
||||
from passbook.flows.models import FlowStageBinding
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
@ -21,7 +21,7 @@ class StageBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
model = FlowStageBinding
|
||||
permission_required = "passbook_flows.view_flowstagebinding"
|
||||
paginate_by = 10
|
||||
ordering = ["flow", "order"]
|
||||
ordering = ["target", "order"]
|
||||
template_name = "administration/stage_binding/list.html"
|
||||
|
||||
|
||||
@ -41,13 +41,6 @@ class StageBindingCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:stage-bindings")
|
||||
success_message = _("Successfully created StageBinding")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
|
||||
class StageBindingUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
@ -62,16 +55,9 @@ class StageBindingUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:stage-bindings")
|
||||
success_message = _("Successfully updated StageBinding")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
|
||||
class StageBindingDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete FlowStageBinding"""
|
||||
|
||||
@ -81,7 +67,3 @@ class StageBindingDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:stage-bindings")
|
||||
success_message = _("Successfully deleted FlowStageBinding")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
"""passbook Invitation administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
@ -8,13 +7,14 @@ from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView
|
||||
from django.views.generic import ListView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.core.signals import invitation_created
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.stages.invitation.forms import InvitationForm
|
||||
from passbook.stages.invitation.models import Invitation
|
||||
from passbook.stages.invitation.signals import invitation_created
|
||||
|
||||
|
||||
class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
@ -43,10 +43,6 @@ class InvitationCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:stage-invitations")
|
||||
success_message = _("Successfully created Invitation")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
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
|
||||
@ -56,7 +52,7 @@ class InvitationCreateView(
|
||||
|
||||
|
||||
class InvitationDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete invitation"""
|
||||
|
||||
@ -66,7 +62,3 @@ class InvitationDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:stage-invitations")
|
||||
success_message = _("Successfully deleted Invitation")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
"""passbook Prompt administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.stages.prompt.forms import PromptAdminForm
|
||||
from passbook.stages.prompt.models import Prompt
|
||||
@ -41,10 +41,6 @@ class PromptCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:stage-prompts")
|
||||
success_message = _("Successfully created Prompt")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["type"] = "Prompt"
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class PromptUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
@ -60,9 +56,7 @@ class PromptUpdateView(
|
||||
success_message = _("Successfully updated Prompt")
|
||||
|
||||
|
||||
class PromptDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
class PromptDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete prompt"""
|
||||
|
||||
model = Prompt
|
||||
@ -71,7 +65,3 @@ class PromptDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:stage-prompts")
|
||||
success_message = _("Successfully deleted Prompt")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
30
passbook/admin/views/tokens.py
Normal file
30
passbook/admin/views/tokens.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""passbook Token administration"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import ListView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.core.models import Token
|
||||
|
||||
|
||||
class TokenListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all tokens"""
|
||||
|
||||
model = Token
|
||||
permission_required = "passbook_core.view_token"
|
||||
ordering = "expires"
|
||||
paginate_by = 40
|
||||
template_name = "administration/token/list.html"
|
||||
|
||||
|
||||
class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete token"""
|
||||
|
||||
model = Token
|
||||
permission_required = "passbook_core.delete_token"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:tokens")
|
||||
success_message = _("Successfully deleted Token")
|
||||
@ -5,10 +5,12 @@ from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, DetailView, ListView, UpdateView
|
||||
from django.views.generic import DetailView, ListView, UpdateView
|
||||
from guardian.mixins import (
|
||||
PermissionListMixin,
|
||||
PermissionRequiredMixin,
|
||||
@ -16,6 +18,7 @@ from guardian.mixins import (
|
||||
)
|
||||
|
||||
from passbook.admin.forms.users import UserForm
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.core.models import Token, User
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
@ -66,9 +69,7 @@ class UserUpdateView(
|
||||
success_message = _("Successfully updated User")
|
||||
|
||||
|
||||
class UserDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
class UserDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete user"""
|
||||
|
||||
model = User
|
||||
@ -80,10 +81,6 @@ class UserDeleteView(
|
||||
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)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
"""Get Password reset link for user"""
|
||||
@ -91,13 +88,13 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
|
||||
model = User
|
||||
permission_required = "passbook_core.reset_user_password"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Create token for user and return link"""
|
||||
super().get(request, *args, **kwargs)
|
||||
# TODO: create plan for user, get token
|
||||
token = Token.objects.create(user=self.object)
|
||||
querystring = urlencode({"token": token.token_uuid})
|
||||
link = request.build_absolute_uri(
|
||||
reverse("passbook_flows:default-recovery", kwargs={"token": token.uuid})
|
||||
reverse("passbook_flows:default-recovery") + f"?{querystring}"
|
||||
)
|
||||
messages.success(
|
||||
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})
|
||||
|
||||
71
passbook/admin/views/utils.py
Normal file
71
passbook/admin/views/utils.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""passbook admin util views"""
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
|
||||
from passbook.lib.utils.reflection import all_subclasses
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class DeleteMessageView(SuccessMessageMixin, DeleteView):
|
||||
"""DeleteView which shows `self.success_message` on successful deletion"""
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
class InheritanceListView(ListView):
|
||||
"""ListView for objects using InheritanceManager"""
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["types"] = {x.__name__: x for x in all_subclasses(self.model)}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class InheritanceCreateView(CreateAssignPermView):
|
||||
"""CreateView for objects using InheritanceManager"""
|
||||
|
||||
def get_form_class(self):
|
||||
provider_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(
|
||||
x for x in all_subclasses(self.model) if x.__name__ == provider_type
|
||||
)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
return model.form(model)
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
|
||||
class InheritanceUpdateView(UpdateView):
|
||||
"""UpdateView for objects using InheritanceManager"""
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self):
|
||||
return self.get_object().form()
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
self.model.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
)
|
||||
43
passbook/api/auth.py
Normal file
43
passbook/api/auth.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""API Authentication"""
|
||||
from base64 import b64decode
|
||||
from typing import Any, Tuple, Union
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import HTTP_HEADER_ENCODING, exceptions
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
from rest_framework.request import Request
|
||||
|
||||
from passbook.core.models import Token, TokenIntents, User
|
||||
|
||||
|
||||
class PassbookTokenAuthentication(BaseAuthentication):
|
||||
"""Token-based authentication using HTTP Basic authentication"""
|
||||
|
||||
def authenticate(self, request: Request) -> Union[Tuple[User, Any], None]:
|
||||
"""Token-based authentication using HTTP Basic authentication"""
|
||||
auth = get_authorization_header(request).split()
|
||||
|
||||
if not auth or auth[0].lower() != b"basic":
|
||||
return None
|
||||
|
||||
if len(auth) == 1:
|
||||
msg = _("Invalid basic header. No credentials provided.")
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
if len(auth) > 2:
|
||||
msg = _(
|
||||
"Invalid basic header. Credentials string should not contain spaces."
|
||||
)
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
|
||||
header_data = b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(":")
|
||||
|
||||
tokens = Token.filter_not_expired(
|
||||
token_uuid=header_data[2], intent=TokenIntents.INTENT_API
|
||||
)
|
||||
if not tokens.exists():
|
||||
raise exceptions.AuthenticationFailed(_("Invalid token."))
|
||||
|
||||
return (tokens.first().user, None)
|
||||
|
||||
def authenticate_header(self, request: Request) -> str:
|
||||
return 'Basic realm="passbook"'
|
||||
@ -6,5 +6,5 @@ from passbook.api.v2.urls import urlpatterns as v2_urls
|
||||
|
||||
urlpatterns = [
|
||||
path("v1/", include(v1_urls)),
|
||||
path("v2/", include(v2_urls)),
|
||||
path("v2beta/", include(v2_urls)),
|
||||
]
|
||||
|
||||
@ -4,7 +4,6 @@ from django.urls import path
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
from rest_framework import routers
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.api.permissions import CustomObjectPermissions
|
||||
from passbook.audit.api import EventViewSet
|
||||
@ -16,11 +15,11 @@ from passbook.core.api.providers import ProviderViewSet
|
||||
from passbook.core.api.sources import SourceViewSet
|
||||
from passbook.core.api.users import UserViewSet
|
||||
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
|
||||
from passbook.lib.utils.reflection import get_apps
|
||||
from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet
|
||||
from passbook.policies.dummy.api import DummyPolicyViewSet
|
||||
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
|
||||
from passbook.policies.expression.api import ExpressionPolicyViewSet
|
||||
from passbook.policies.group_membership.api import GroupMembershipPolicyViewSet
|
||||
from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
|
||||
from passbook.policies.password.api import PasswordPolicyViewSet
|
||||
from passbook.policies.reputation.api import ReputationPolicyViewSet
|
||||
@ -32,11 +31,14 @@ from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceView
|
||||
from passbook.sources.oauth.api import OAuthSourceViewSet
|
||||
from passbook.sources.saml.api import SAMLSourceViewSet
|
||||
from passbook.stages.captcha.api import CaptchaStageViewSet
|
||||
from passbook.stages.consent.api import ConsentStageViewSet
|
||||
from passbook.stages.dummy.api import DummyStageViewSet
|
||||
from passbook.stages.email.api import EmailStageViewSet
|
||||
from passbook.stages.identification.api import IdentificationStageViewSet
|
||||
from passbook.stages.invitation.api import InvitationStageViewSet, InvitationViewSet
|
||||
from passbook.stages.otp.api import OTPStageViewSet
|
||||
from passbook.stages.otp_static.api import OTPStaticStageViewSet
|
||||
from passbook.stages.otp_time.api import OTPTimeStageViewSet
|
||||
from passbook.stages.otp_validate.api import OTPValidateStageViewSet
|
||||
from passbook.stages.password.api import PasswordStageViewSet
|
||||
from passbook.stages.prompt.api import PromptStageViewSet, PromptViewSet
|
||||
from passbook.stages.user_delete.api import UserDeleteStageViewSet
|
||||
@ -44,15 +46,8 @@ from passbook.stages.user_login.api import UserLoginStageViewSet
|
||||
from passbook.stages.user_logout.api import UserLogoutStageViewSet
|
||||
from passbook.stages.user_write.api import UserWriteStageViewSet
|
||||
|
||||
LOGGER = get_logger()
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
for _passbook_app in get_apps():
|
||||
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/groups", GroupViewSet)
|
||||
router.register("core/users", UserViewSet)
|
||||
@ -68,9 +63,10 @@ router.register("sources/oauth", OAuthSourceViewSet)
|
||||
router.register("policies/all", PolicyViewSet)
|
||||
router.register("policies/bindings", PolicyBindingViewSet)
|
||||
router.register("policies/expression", ExpressionPolicyViewSet)
|
||||
router.register("policies/group_membership", GroupMembershipPolicyViewSet)
|
||||
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
|
||||
router.register("policies/password_expiry", PasswordExpiryPolicyViewSet)
|
||||
router.register("policies/password", PasswordPolicyViewSet)
|
||||
router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
|
||||
router.register("policies/reputation", ReputationPolicyViewSet)
|
||||
|
||||
router.register("providers/all", ProviderViewSet)
|
||||
@ -85,14 +81,17 @@ router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
||||
|
||||
router.register("stages/all", StageViewSet)
|
||||
router.register("stages/captcha", CaptchaStageViewSet)
|
||||
router.register("stages/consent", ConsentStageViewSet)
|
||||
router.register("stages/email", EmailStageViewSet)
|
||||
router.register("stages/identification", IdentificationStageViewSet)
|
||||
router.register("stages/invitation", InvitationStageViewSet)
|
||||
router.register("stages/invitation/invitations", InvitationViewSet)
|
||||
router.register("stages/otp", OTPStageViewSet)
|
||||
router.register("stages/otp_static", OTPStaticStageViewSet)
|
||||
router.register("stages/otp_time", OTPTimeStageViewSet)
|
||||
router.register("stages/otp_validate", OTPValidateStageViewSet)
|
||||
router.register("stages/password", PasswordStageViewSet)
|
||||
router.register("stages/prompt/stages", PromptStageViewSet)
|
||||
router.register("stages/prompt/prompts", PromptViewSet)
|
||||
router.register("stages/prompt/stages", PromptStageViewSet)
|
||||
router.register("stages/user_delete", UserDeleteStageViewSet)
|
||||
router.register("stages/user_login", UserLoginStageViewSet)
|
||||
router.register("stages/user_logout", UserLogoutStageViewSet)
|
||||
|
||||
@ -12,6 +12,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.debug import CLEANSED_SUBSTITUTE, HIDDEN_SETTINGS
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from structlog import get_logger
|
||||
|
||||
@ -20,6 +21,22 @@ from passbook.lib.utils.http import get_client_ip
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
"""Cleanse a dictionary, recursively"""
|
||||
final_dict = {}
|
||||
for key, value in source.items():
|
||||
try:
|
||||
if HIDDEN_SETTINGS.search(key):
|
||||
final_dict[key] = CLEANSED_SUBSTITUTE
|
||||
else:
|
||||
final_dict[key] = value
|
||||
except TypeError:
|
||||
final_dict[key] = value
|
||||
if isinstance(value, dict):
|
||||
final_dict[key] = cleanse_dict(value)
|
||||
return final_dict
|
||||
|
||||
|
||||
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 {
|
||||
@ -27,15 +44,16 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
name: str,
|
||||
pk: Any
|
||||
}"""
|
||||
final_dict = {}
|
||||
for key, value in source.items():
|
||||
if isinstance(value, dict):
|
||||
source[key] = sanitize_dict(value)
|
||||
final_dict[key] = sanitize_dict(value)
|
||||
elif isinstance(value, models.Model):
|
||||
model_content_type = ContentType.objects.get_for_model(value)
|
||||
name = str(value)
|
||||
if hasattr(value, "name"):
|
||||
name = value.name
|
||||
source[key] = sanitize_dict(
|
||||
final_dict[key] = sanitize_dict(
|
||||
{
|
||||
"app": model_content_type.app_label,
|
||||
"model_name": model_content_type.model,
|
||||
@ -44,8 +62,10 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
}
|
||||
)
|
||||
elif isinstance(value, UUID):
|
||||
source[key] = value.hex
|
||||
return source
|
||||
final_dict[key] = value.hex
|
||||
else:
|
||||
final_dict[key] = value
|
||||
return final_dict
|
||||
|
||||
|
||||
class EventAction(Enum):
|
||||
@ -104,7 +124,7 @@ class Event(models.Model):
|
||||
)
|
||||
if not app:
|
||||
app = getmodule(stack()[_inspect_offset][0]).__name__
|
||||
cleaned_kwargs = sanitize_dict(kwargs)
|
||||
cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs))
|
||||
event = Event(action=action.value, app=app, context=cleaned_kwargs)
|
||||
return event
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"""passbook audit signal listener"""
|
||||
from typing import Dict
|
||||
from threading import Thread
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from django.contrib.auth.signals import (
|
||||
user_logged_in,
|
||||
@ -11,21 +12,54 @@ from django.http import HttpRequest
|
||||
|
||||
from passbook.audit.models import Event, EventAction
|
||||
from passbook.core.models import User
|
||||
from passbook.core.signals import invitation_created, invitation_used, user_signed_up
|
||||
from passbook.stages.invitation.models import Invitation
|
||||
from passbook.stages.invitation.signals import invitation_created, invitation_used
|
||||
from passbook.stages.user_write.signals import user_write
|
||||
|
||||
|
||||
class EventNewThread(Thread):
|
||||
"""Create Event in background thread"""
|
||||
|
||||
action: EventAction
|
||||
request: HttpRequest
|
||||
kwargs: Dict[str, Any]
|
||||
user: Optional[User] = None
|
||||
|
||||
def __init__(self, action: EventAction, request: HttpRequest, **kwargs):
|
||||
super().__init__()
|
||||
self.action = action
|
||||
self.request = request
|
||||
self.kwargs = kwargs
|
||||
|
||||
def run(self):
|
||||
Event.new(self.action, **self.kwargs).from_http(self.request, user=self.user)
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
# pylint: disable=unused-argument
|
||||
def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
|
||||
"""Log successful login"""
|
||||
Event.new(EventAction.LOGIN).from_http(request)
|
||||
thread = EventNewThread(EventAction.LOGIN, request)
|
||||
thread.user = user
|
||||
thread.run()
|
||||
|
||||
|
||||
@receiver(user_logged_out)
|
||||
# pylint: disable=unused-argument
|
||||
def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
|
||||
"""Log successfully logout"""
|
||||
Event.new(EventAction.LOGOUT).from_http(request)
|
||||
thread = EventNewThread(EventAction.LOGOUT, request)
|
||||
thread.user = user
|
||||
thread.run()
|
||||
|
||||
|
||||
@receiver(user_write)
|
||||
# pylint: disable=unused-argument
|
||||
def on_user_write(sender, request: HttpRequest, user: User, data: Dict[str, Any], **_):
|
||||
"""Log User write"""
|
||||
thread = EventNewThread(EventAction.CUSTOM, request, **data)
|
||||
thread.user = user
|
||||
thread.run()
|
||||
|
||||
|
||||
@receiver(user_login_failed)
|
||||
@ -34,29 +68,25 @@ def on_user_login_failed(
|
||||
sender, credentials: Dict[str, str], request: HttpRequest, **_
|
||||
):
|
||||
"""Failed Login"""
|
||||
Event.new(EventAction.LOGIN_FAILED, **credentials).from_http(request)
|
||||
|
||||
|
||||
@receiver(user_signed_up)
|
||||
# pylint: disable=unused-argument
|
||||
def on_user_signed_up(sender, request: HttpRequest, user: User, **_):
|
||||
"""Log successfully signed up"""
|
||||
Event.new(EventAction.SIGN_UP).from_http(request)
|
||||
thread = EventNewThread(EventAction.LOGIN_FAILED, request, **credentials)
|
||||
thread.run()
|
||||
|
||||
|
||||
@receiver(invitation_created)
|
||||
# pylint: disable=unused-argument
|
||||
def on_invitation_created(sender, request: HttpRequest, invitation, **_):
|
||||
def on_invitation_created(sender, request: HttpRequest, invitation: Invitation, **_):
|
||||
"""Log Invitation creation"""
|
||||
Event.new(
|
||||
EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex
|
||||
).from_http(request)
|
||||
thread = EventNewThread(
|
||||
EventAction.INVITE_CREATED, request, invitation_uuid=invitation.invite_uuid.hex
|
||||
)
|
||||
thread.run()
|
||||
|
||||
|
||||
@receiver(invitation_used)
|
||||
# pylint: disable=unused-argument
|
||||
def on_invitation_used(sender, request: HttpRequest, invitation, **_):
|
||||
def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_):
|
||||
"""Log Invitation usage"""
|
||||
Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http(
|
||||
request
|
||||
thread = EventNewThread(
|
||||
EventAction.INVITE_USED, request, invitation_uuid=invitation.invite_uuid.hex
|
||||
)
|
||||
thread.run()
|
||||
|
||||
@ -1,2 +1,9 @@
|
||||
"""passbook audit urls"""
|
||||
urlpatterns = []
|
||||
from django.urls import path
|
||||
|
||||
from passbook.audit.views import EventListView
|
||||
|
||||
urlpatterns = [
|
||||
# Audit Log
|
||||
path("audit/", EventListView.as_view(), name="log"),
|
||||
]
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
"""passbook Event administration"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import ListView
|
||||
from guardian.mixins import PermissionListMixin
|
||||
|
||||
from passbook.audit.models import Event
|
||||
|
||||
|
||||
class EventListView(PermissionListMixin, ListView):
|
||||
class EventListView(PermissionListMixin, LoginRequiredMixin, ListView):
|
||||
"""Show list of all invitations"""
|
||||
|
||||
model = Event
|
||||
template_name = "administration/audit/list.html"
|
||||
template_name = "audit/list.html"
|
||||
permission_required = "passbook_audit.view_event"
|
||||
ordering = "-created"
|
||||
paginate_by = 20
|
||||
@ -3,12 +3,13 @@ from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.core.models import Application, Provider
|
||||
from passbook.lib.widgets import GroupedModelChoiceField
|
||||
|
||||
|
||||
class ApplicationForm(forms.ModelForm):
|
||||
"""Application Form"""
|
||||
|
||||
provider = forms.ModelChoiceField(
|
||||
provider = GroupedModelChoiceField(
|
||||
queryset=Provider.objects.all().order_by("pk").select_subclasses(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
from django import forms
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
|
||||
from passbook.admin.fields import CodeMirrorWidget, YAMLField
|
||||
from passbook.core.models import Group, User
|
||||
|
||||
|
||||
@ -34,4 +35,8 @@ class GroupForm(forms.ModelForm):
|
||||
fields = ["name", "parent", "members", "attributes"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"attributes": CodeMirrorWidget,
|
||||
}
|
||||
field_classes = {
|
||||
"attributes": YAMLField,
|
||||
}
|
||||
|
||||
28
passbook/core/migrations/0004_auto_20200703_2213.py
Normal file
28
passbook/core/migrations/0004_auto_20200703_2213.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.0.7 on 2020-07-03 22:13
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0003_default_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="application",
|
||||
options={
|
||||
"verbose_name": "Application",
|
||||
"verbose_name_plural": "Applications",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="user",
|
||||
options={
|
||||
"permissions": (("reset_user_password", "Reset Password"),),
|
||||
"verbose_name": "User",
|
||||
"verbose_name_plural": "Users",
|
||||
},
|
||||
),
|
||||
]
|
||||
24
passbook/core/migrations/0005_token_intent.py
Normal file
24
passbook/core/migrations/0005_token_intent.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.0.7 on 2020-07-05 21:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0004_auto_20200703_2213"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="token",
|
||||
name="intent",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("verification", "Intent Verification"),
|
||||
("api", "Intent Api"),
|
||||
],
|
||||
default="verification",
|
||||
),
|
||||
),
|
||||
]
|
||||
20
passbook/core/migrations/0006_auto_20200709_1608.py
Normal file
20
passbook/core/migrations/0006_auto_20200709_1608.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.0.8 on 2020-07-09 16:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0005_token_intent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="source",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="Internal source name, used in URLs.", unique=True
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,11 +1,13 @@
|
||||
"""passbook core models"""
|
||||
from datetime import timedelta
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Type
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.db import models
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.forms import ModelForm
|
||||
from django.http import HttpRequest
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -71,6 +73,8 @@ class User(GuardianUserMixin, AbstractUser):
|
||||
class Meta:
|
||||
|
||||
permissions = (("reset_user_password", "Reset Password"),)
|
||||
verbose_name = _("User")
|
||||
verbose_name_plural = _("Users")
|
||||
|
||||
|
||||
class Provider(models.Model):
|
||||
@ -89,6 +93,10 @@ class Provider(models.Model):
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
def form(self) -> Type[ModelForm]:
|
||||
"""Return Form class used to edit this object"""
|
||||
raise NotImplementedError
|
||||
|
||||
# This class defines no field for easier inheritance
|
||||
def __str__(self):
|
||||
if hasattr(self, "name"):
|
||||
@ -112,8 +120,6 @@ class Application(PolicyBindingModel):
|
||||
meta_description = models.TextField(default="", blank=True)
|
||||
meta_publisher = models.TextField(default="", blank=True)
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
def get_provider(self) -> Optional[Provider]:
|
||||
"""Get casted provider instance"""
|
||||
if not self.provider:
|
||||
@ -123,12 +129,19 @@ class Application(PolicyBindingModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Application")
|
||||
verbose_name_plural = _("Applications")
|
||||
|
||||
|
||||
class Source(PolicyBindingModel):
|
||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||
|
||||
name = models.TextField(help_text=_("Source's display Name."))
|
||||
slug = models.SlugField(help_text=_("Internal source name, used in URLs."))
|
||||
slug = models.SlugField(
|
||||
help_text=_("Internal source name, used in URLs."), unique=True
|
||||
)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
property_mappings = models.ManyToManyField(
|
||||
@ -154,10 +167,12 @@ class Source(PolicyBindingModel):
|
||||
related_name="source_enrollment",
|
||||
)
|
||||
|
||||
form = "" # ModelForm-based class ued to create/edit instance
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
def form(self) -> Type[ModelForm]:
|
||||
"""Return Form class used to edit this object"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def ui_login_button(self) -> Optional[UILoginButton]:
|
||||
"""If source uses a http-based flow, return UI Information about the login
|
||||
@ -190,23 +205,54 @@ class UserSourceConnection(CreatedUpdatedModel):
|
||||
unique_together = (("user", "source"),)
|
||||
|
||||
|
||||
class Token(models.Model):
|
||||
"""One-time link for password resets/sign-up-confirmations"""
|
||||
class ExpiringModel(models.Model):
|
||||
"""Base Model which can expire, and is automatically cleaned up."""
|
||||
|
||||
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
expires = models.DateTimeField(default=default_token_duration)
|
||||
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+")
|
||||
expiring = models.BooleanField(default=True)
|
||||
description = models.TextField(default="", blank=True)
|
||||
|
||||
@classmethod
|
||||
def filter_not_expired(cls, **kwargs) -> QuerySet:
|
||||
"""Filer for tokens which are not expired yet or are not expiring,
|
||||
and match filters in `kwargs`"""
|
||||
query = Q(**kwargs)
|
||||
query_not_expired_yet = Q(expires__lt=now(), expiring=True)
|
||||
query_not_expiring = Q(expiring=False)
|
||||
return cls.objects.filter(query & (query_not_expired_yet | query_not_expiring))
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if token is expired yet."""
|
||||
return now() > self.expires
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
|
||||
|
||||
class TokenIntents(models.TextChoices):
|
||||
"""Intents a Token can be created for."""
|
||||
|
||||
# Single user token
|
||||
INTENT_VERIFICATION = "verification"
|
||||
|
||||
# Allow access to API
|
||||
INTENT_API = "api"
|
||||
|
||||
|
||||
class Token(ExpiringModel):
|
||||
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
|
||||
|
||||
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
intent = models.TextField(
|
||||
choices=TokenIntents.choices, default=TokenIntents.INTENT_VERIFICATION
|
||||
)
|
||||
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+")
|
||||
description = models.TextField(default="", blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"Token f{self.token_uuid.hex} {self.description} (expires={self.expires})"
|
||||
f"Token {self.token_uuid.hex} {self.description} (expires={self.expires})"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -222,9 +268,12 @@ class PropertyMapping(models.Model):
|
||||
name = models.TextField()
|
||||
expression = models.TextField()
|
||||
|
||||
form = ""
|
||||
objects = InheritanceManager()
|
||||
|
||||
def form(self) -> Type[ModelForm]:
|
||||
"""Return Form class used to edit this object"""
|
||||
raise NotImplementedError
|
||||
|
||||
def evaluate(
|
||||
self, user: Optional[User], request: Optional[HttpRequest], **kwargs
|
||||
) -> Any:
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
"""passbook core signals"""
|
||||
from django.core.signals import Signal
|
||||
|
||||
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"])
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
"""passbook core tasks"""
|
||||
from django.utils.timezone import now
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Token
|
||||
from passbook.core.models import ExpiringModel
|
||||
from passbook.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def clean_tokens():
|
||||
"""Remove expired tokens"""
|
||||
amount, _ = Token.objects.filter(expires__lt=now(), expiring=True).delete()
|
||||
LOGGER.debug("Deleted expired tokens", amount=amount)
|
||||
def clean_expired_models():
|
||||
"""Remove expired objects"""
|
||||
for cls in ExpiringModel.__subclasses__():
|
||||
cls: ExpiringModel
|
||||
amount, _ = cls.filter_not_expired().delete()
|
||||
LOGGER.debug("Deleted expired models", model=cls, amount=amount)
|
||||
|
||||
@ -32,8 +32,8 @@
|
||||
{% if user.is_superuser %}
|
||||
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_app 'passbook_admin' %}"
|
||||
href="{% url 'passbook_admin:overview' %}">{% trans 'Administrate' %}</a></li>
|
||||
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_admin:audit-log' %}"
|
||||
href="{% url 'passbook_admin:audit-log' %}">{% trans 'Monitor' %}</a></li>
|
||||
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_audit:log' %}"
|
||||
href="{% url 'passbook_audit:log' %}">{% trans 'Monitor' %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@ -20,11 +20,15 @@
|
||||
</head>
|
||||
<body>
|
||||
{% if 'impersonate_id' in request.session %}
|
||||
<div class="experimental-pf-bar">
|
||||
<span id="experimentalBar" class="experimental-pf-text">
|
||||
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
|
||||
<a href="?__unimpersonate=True" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
|
||||
</span>
|
||||
<div class="pf-c-banner pf-m-warning pf-c-alert pf-m-sticky">
|
||||
<div class="pf-l-flex pf-m-justify-content-center pf-m-justify-content-space-between-on-lg pf-m-nowrap" style="height: 100%;">
|
||||
<div class=""></div>
|
||||
<div class="pf-u-display-none pf-u-display-block-on-lg">
|
||||
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
|
||||
<a href="?__unimpersonate=True" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
|
||||
</div>
|
||||
<div class=""></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block body %}
|
||||
|
||||
@ -1,58 +1,20 @@
|
||||
{% extends 'base/skeleton.html' %}
|
||||
{% extends 'login/base_full.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block body %}
|
||||
<div class="pf-c-background-image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
|
||||
<filter id="image_overlay">
|
||||
<feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
|
||||
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
|
||||
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
|
||||
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
|
||||
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
|
||||
<feFuncA type="table" tableValues="0 1"></feFuncA>
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<header class="pf-c-login__header">
|
||||
<img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;" alt="passbook icon" />
|
||||
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;"
|
||||
alt="passbook branding" />
|
||||
</header>
|
||||
<main class="pf-c-login__main" id="flow-body">
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% trans 'Bad Request' %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% if message %}
|
||||
<h3>{% trans message %}</h3>
|
||||
{% endif %}
|
||||
{% if 'back' in request.GET %}
|
||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
<footer class="pf-c-login__footer">
|
||||
<p></p>
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
<li>
|
||||
<a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
|
||||
</li>
|
||||
<!-- todo: load config.passbook.footer.links -->
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
{% block title %}
|
||||
{% trans 'Bad Request' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% if message %}
|
||||
<h3>{% trans message %}</h3>
|
||||
{% endif %}
|
||||
{% if 'back' in request.GET %}
|
||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
31
passbook/core/templates/generic/autosubmit_form.html
Normal file
31
passbook/core/templates/generic/autosubmit_form.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "login/base.html" %}
|
||||
|
||||
{% load passbook_utils %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{{ title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" action="{{ url }}" autosubmit>
|
||||
{% csrf_token %}
|
||||
{% for key, value in attrs.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||
{% endfor %}
|
||||
<div class="pf-c-form__group pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-c-form__group-control">
|
||||
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__actions">
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans 'Continue' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
34
passbook/core/templates/generic/autosubmit_form_full.html
Normal file
34
passbook/core/templates/generic/autosubmit_form_full.html
Normal file
@ -0,0 +1,34 @@
|
||||
{% extends "login/base_full.html" %}
|
||||
|
||||
{% load passbook_utils %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{{ title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" action="{{ url }}" autosubmit>
|
||||
{% csrf_token %}
|
||||
{% for key, value in attrs.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||
{% endfor %}
|
||||
<div class="pf-c-form__group pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-c-form__group-control">
|
||||
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__actions">
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans 'Continue' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
document.querySelector("form").submit();
|
||||
</script>
|
||||
{% endblock %}
|
||||
54
passbook/core/templates/login/base_full.html
Normal file
54
passbook/core/templates/login/base_full.html
Normal file
@ -0,0 +1,54 @@
|
||||
{% extends 'base/skeleton.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block body %}
|
||||
<div class="pf-c-background-image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
|
||||
<filter id="image_overlay">
|
||||
<feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
|
||||
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
|
||||
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
|
||||
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
|
||||
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
|
||||
<feFuncA type="table" tableValues="0 1"></feFuncA>
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
</svg>
|
||||
</div>
|
||||
{% include 'partials/messages.html' %}
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<header class="pf-c-login__header">
|
||||
<img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;" alt="passbook icon" />
|
||||
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;" alt="passbook branding" />
|
||||
</header>
|
||||
{% block main_container %}
|
||||
<main class="pf-c-login__main">
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% block title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
<footer class="pf-c-login__footer">
|
||||
<p></p>
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
{% for link in config.passbook.footer_links %}
|
||||
<li>
|
||||
<a href="{{ link.href }}">{{ link.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,21 +1,25 @@
|
||||
{% extends 'login/base.html' %}
|
||||
{% extends 'login/base_full.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% csrf_token %}
|
||||
{% include 'partials/form.html' %}
|
||||
<div class="pf-c-form__group">
|
||||
<p>
|
||||
<i class="pf-icon pf-icon-error-circle-o"></i>
|
||||
{% trans 'Access denied' %}
|
||||
</p>
|
||||
</div>
|
||||
{% if 'back' in request.GET %}
|
||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% block title %}
|
||||
{% trans 'Permission denied' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% csrf_token %}
|
||||
{% include 'partials/form.html' %}
|
||||
<div class="pf-c-form__group">
|
||||
<p>
|
||||
<i class="pf-icon pf-icon-error-circle-o"></i>
|
||||
{% trans 'Access denied' %}
|
||||
</p>
|
||||
</div>
|
||||
{% if 'back' in request.GET %}
|
||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@ -5,16 +5,13 @@
|
||||
|
||||
{% block above_form %}
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{% trans "Username" %}</span>
|
||||
</label>
|
||||
<div class="form-control-static">
|
||||
<div class="left">
|
||||
<img class="pf-c-avatar" src="{% gravatar user.email %}" alt="">
|
||||
{{ user.username }}
|
||||
</div>
|
||||
<div class="right">
|
||||
<a href="{% url 'passbook_flows:default-authentication' %}">{% trans 'Not you?' %}</a>
|
||||
<a href="{% url 'passbook_flows:cancel' %}">{% trans 'Not you?' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -24,21 +24,23 @@
|
||||
{% if applications %}
|
||||
<div class="pf-l-gallery pf-m-gutter">
|
||||
{% for app in applications %}
|
||||
<a href="{{ app.meta_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact" id="card-1">
|
||||
<div class="pf-c-card__head">
|
||||
<a href="{{ app.meta_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
{% if not app.meta_icon_url %}
|
||||
<i class="pf-icon pf-icon-arrow"></i>
|
||||
{% else %}
|
||||
<img class="app-icon pf-c-avatar" src="{{ app.meta_icon_url }}" alt="{% trans 'Application Icon' %}">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pf-c-card__header pf-c-title pf-m-md">
|
||||
<div class="pf-c-card__title">
|
||||
<p id="card-1-check-label">{{ app.name }}</p>
|
||||
<div class="pf-c-content">
|
||||
<small>{{ app.meta_publisher }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">{% trans app.meta_description %}</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% trans app.meta_description|truncatewords:35 %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -50,7 +52,7 @@
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans "Either no applications are defined, or you don't have access to any." %}
|
||||
</div>
|
||||
{% if user.is_superuser %} {# todo: use guardian permissions instead #}
|
||||
{% if user.is_superuser %} {# TODO:use guardian permissions instead #}
|
||||
<a href="{% url 'passbook_admin:application-create' %}" class="pf-c-button pf-m-primary" type="button">
|
||||
{% trans 'Create Application' %}
|
||||
</a>
|
||||
|
||||
@ -15,16 +15,18 @@
|
||||
</div>
|
||||
<div class="pf-c-form__group-control">
|
||||
{% for c in field %}
|
||||
<div class="radio col-sm-10">
|
||||
<input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
|
||||
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}"
|
||||
{% if c.data.selected %} checked {% endif %}>
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
|
||||
<div class="pf-c-radio">
|
||||
<input class="pf-c-radio__input"
|
||||
type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
|
||||
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}"
|
||||
value="{{ c.data.value }}"
|
||||
{% if c.data.selected %} checked {% endif %}/>
|
||||
<label class="pf-c-radio__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text }}</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif field.field.widget|fieldtype == 'Select' %}
|
||||
<div class="pf-c-form__group-label">
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
{% if messages %}
|
||||
<ul class="pf-c-alert-group pf-m-toast">
|
||||
{% for msg in messages %}
|
||||
<li class="pf-c-alert-group__item">
|
||||
@ -21,4 +20,3 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
@ -25,8 +25,7 @@
|
||||
<ul class="pf-c-nav__list">
|
||||
{% for stage in user_stages_loc %}
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url stage.view_name %}" class="pf-c-nav__link {% is_active stage.view_name %}">
|
||||
<i class="{{ stage.icon }}"></i>
|
||||
<a href="{{ stage.url }}" class="pf-c-nav__link {% if stage.url == request.get_full_path %} pf-m-current {% endif %}">
|
||||
{{ stage.name }}
|
||||
</a>
|
||||
</li>
|
||||
@ -41,9 +40,8 @@
|
||||
<ul class="pf-c-nav__list">
|
||||
{% for source in user_sources_loc %}
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{{ source.view_name }}"
|
||||
class="pf-c-nav__link {% if user_settings.view_name == request.get_full_path %} pf-m-current {% endif %}">
|
||||
<i class="{{ source.icon }}"></i>
|
||||
<a href="{{ source.url }}"
|
||||
class="pf-c-nav__link {% if source.url == request.get_full_path %} pf-m-current {% endif %}">
|
||||
{{ source.name }}
|
||||
</a>
|
||||
</li>
|
||||
@ -56,9 +54,11 @@
|
||||
</div>
|
||||
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-l-split pf-m-gutter">
|
||||
{% block page %}
|
||||
{% endblock %}
|
||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-u-w-75">
|
||||
{% block page %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@ -3,28 +3,26 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block page %}
|
||||
<div class="pf-l-split__item">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__header pf-c-title pf-m-md">
|
||||
<h1>{% trans 'Update details' %}</h1>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<form action="" method="post" class="pf-c-form pf-m-horizontal">
|
||||
{% include 'partials/form_horizontal.html' with form=form %}
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-form__actions">
|
||||
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
|
||||
{% if unenrollment_enabled %}
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{% trans "Delete account" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__header pf-c-title pf-m-md">
|
||||
{% trans 'Update details' %}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<form action="" method="post" class="pf-c-form pf-m-horizontal">
|
||||
{% include 'partials/form_horizontal.html' with form=form %}
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-form__actions">
|
||||
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
|
||||
{% if unenrollment_enabled %}
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{% trans "Delete account" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -23,7 +23,7 @@ def user_stages(context: RequestContext) -> List[UIUserSettings]:
|
||||
if not user_settings:
|
||||
continue
|
||||
matching_stages.append(user_settings)
|
||||
return matching_stages
|
||||
return sorted(matching_stages, key=lambda x: x.name)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
@ -38,10 +38,8 @@ def user_sources(context: RequestContext) -> List[UIUserSettings]:
|
||||
user_settings = source.ui_user_settings
|
||||
if not user_settings:
|
||||
continue
|
||||
policy_engine = PolicyEngine(
|
||||
source.policies.all(), user, context.get("request")
|
||||
)
|
||||
policy_engine = PolicyEngine(source, user, context.get("request"))
|
||||
policy_engine.build()
|
||||
if policy_engine.passing:
|
||||
matching_sources.append(user_settings)
|
||||
return matching_sources
|
||||
return sorted(matching_sources, key=lambda x: x.name)
|
||||
|
||||
18
passbook/core/tests/tests_tasks.py
Normal file
18
passbook/core/tests/tests_tasks.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""passbook core task tests"""
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from passbook.core.models import Token
|
||||
from passbook.core.tasks import clean_expired_models
|
||||
|
||||
|
||||
class TestTasks(TestCase):
|
||||
"""Test Tasks"""
|
||||
|
||||
def test_token_cleanup(self):
|
||||
"""Test Token cleanup task"""
|
||||
Token.objects.create(expires=now(), user=get_anonymous_user())
|
||||
self.assertEqual(Token.objects.all().count(), 1)
|
||||
clean_expired_models()
|
||||
self.assertEqual(Token.objects.all().count(), 0)
|
||||
@ -8,8 +8,7 @@ class UIUserSettings:
|
||||
"""Dataclass for Stage and Source's user_settings"""
|
||||
|
||||
name: str
|
||||
icon: str
|
||||
view_name: str
|
||||
url: str
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
"""passbook access helper classes"""
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext as _
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Application, Provider, User
|
||||
from passbook.policies.engine import PolicyEngine
|
||||
from passbook.policies.types import PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class AccessMixin:
|
||||
"""Mixin class for usage in Authorization views.
|
||||
Provider functions to check application access, etc"""
|
||||
|
||||
# request is set by view but since this Mixin has no base class
|
||||
request: HttpRequest = None
|
||||
|
||||
def provider_to_application(self, provider: Provider) -> Application:
|
||||
"""Lookup application assigned to provider, throw error if no application assigned"""
|
||||
try:
|
||||
return provider.application
|
||||
except Application.DoesNotExist as exc:
|
||||
messages.error(
|
||||
self.request,
|
||||
_(
|
||||
'Provider "%(name)s" has no application assigned'
|
||||
% {"name": provider}
|
||||
),
|
||||
)
|
||||
raise exc
|
||||
|
||||
def user_has_access(self, application: Application, user: User) -> PolicyResult:
|
||||
"""Check if user has access to application."""
|
||||
LOGGER.debug("Checking permissions", user=user, application=application)
|
||||
policy_engine = PolicyEngine(application, user, self.request)
|
||||
policy_engine.build()
|
||||
return policy_engine.result
|
||||
@ -23,7 +23,7 @@ class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
unenrollment_flow = Flow.with_policy(
|
||||
self.request, designation=FlowDesignation.UNRENOLLMENT
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user