Compare commits
117 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
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-pre5
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
|
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##*/}
|
12
.github/workflows/release.yml
vendored
12
.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-pre5
|
||||
-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-pre5
|
||||
- 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-pre5 \
|
||||
-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-pre5
|
||||
- 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-pre5
|
||||
-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-pre5
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -196,3 +196,6 @@ local.env.yml
|
||||
### Helm ###
|
||||
# Chart dependencies
|
||||
**/charts/*.tgz
|
||||
|
||||
# Selenium Screenshots
|
||||
selenium_screenshots/**
|
||||
|
74
Pipfile.lock
generated
74
Pipfile.lock
generated
@ -46,18 +46,18 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:16f83ca3aa98d3faeb4f0738b878525770323e5fb9952435ddf58ca09aacec7c",
|
||||
"sha256:dc87ef82c81d2938f91c7ebfa85dfd032fff1bd3b67c9f66d74b21f8ec1e353d"
|
||||
"sha256:c2a223f4b48782e8b160b2130265e2a66081df111f630a5a384d6909e29a5aa9",
|
||||
"sha256:ce5a4ab6af9e993d1864209cbbb6f4812f65fbc57ad6b95e5967d8bf38b1dcfb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.14.10"
|
||||
"version": "==1.14.16"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:b22db58da273b77529edef71425f9c281bc627b1b889f81960750507238abbb8",
|
||||
"sha256:cb0d7511a68439bf6f16683489130e06c5bbf9f5a9d647e0cbf63d79f3d3bdaa"
|
||||
"sha256:99d995ef99cf77458a661f3fc64e0c3a4ce77ca30facfdf0472f44b2953dd856",
|
||||
"sha256:fe0c4f7cd6b67eff3b7cb8dff6709a65d6fca10b7b7449a493b2036915e98b4c"
|
||||
],
|
||||
"version": "==1.17.10"
|
||||
"version": "==1.17.16"
|
||||
},
|
||||
"celery": {
|
||||
"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:054924b6aedd41f3f76940578127e7fac852a696805f956da39b7941c78bba91",
|
||||
"sha256:208e55e3a11ac9edd53a3b342b033e140ffe27914af1202f0b064edf99e83fc3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.1.0.dev40"
|
||||
"version": "==2.1.0.dev46"
|
||||
},
|
||||
"django-recaptcha": {
|
||||
"hashes": [
|
||||
@ -322,10 +322,10 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"version": "==2.9"
|
||||
"version": "==2.10"
|
||||
},
|
||||
"inflection": {
|
||||
"hashes": [
|
||||
@ -522,6 +522,7 @@
|
||||
"hashes": [
|
||||
"sha256:02e51e1d5828d58f154896ddfd003e2e7584869c275e5acbe290443575370fba",
|
||||
"sha256:03d5cca8618620f45fd40f827423f82b86b3a202c8d44108601b0f5f56b04299",
|
||||
"sha256:0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4",
|
||||
"sha256:132a56abba24e2e06a479d8e5db7a48271a73a215f605017bbd476d31f8e71c1",
|
||||
"sha256:1e655746f539421d923fd48df8f6f40b3443d80b75532501c0085b64afed9df5",
|
||||
"sha256:2b998dc45ef5f4e5cf5248a6edfcd8d8e9fb5e35df8e4259b13a1b10eda7b16b",
|
||||
@ -563,6 +564,7 @@
|
||||
"sha256:2710fc8d83b3352b370db932b3710033b9d630b970ff5aaa3e7458b5336e3b32",
|
||||
"sha256:35b9c9177a9fe7288b19dd41554c9c8ca1063deb426dd5a02e7e2a7416b6bd11",
|
||||
"sha256:3caa32cf807422adf33c10c88c22e9e2e08b9d9d042f12e1e25fe23113dd618f",
|
||||
"sha256:48cc2cfc251f04a6142badeb666d1ff49ca6fdfc303fd72579f62b768aaa52b9",
|
||||
"sha256:4ae6379350a09339109e9b6f419bb2c3f03d3e441f4b0f5b8ca699d47cc9ff7e",
|
||||
"sha256:4e0b27697fa1621c6d3d3b4edeec723c2e841285de6a8d378c1962da77b349be",
|
||||
"sha256:58e19560814dabf5d788b95a13f6b98279cf41a49b1e49ee6cf6c79a57adb4c9",
|
||||
@ -602,10 +604,10 @@
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492",
|
||||
"sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a"
|
||||
"sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2",
|
||||
"sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce"
|
||||
],
|
||||
"version": "==3.0.0a1"
|
||||
"version": "==3.0.0a2"
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
@ -733,11 +735,11 @@
|
||||
},
|
||||
"sentry-sdk": {
|
||||
"hashes": [
|
||||
"sha256:06825c15a78934e78941ea25910db71314c891608a46492fc32c15902c6b2119",
|
||||
"sha256:3ac0c430761b3cb7682ce612151d829f8644bb3830d4e530c75b02ceb745ff49"
|
||||
"sha256:da06bc3641e81ec2c942f87a0676cd9180044fa3d1697524a0005345997542e2",
|
||||
"sha256:e80d61af85d99a1222c1a3e2a24023618374cd50a99673aa7fa3cf920e7d813b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.15.1"
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"service-identity": {
|
||||
"hashes": [
|
||||
@ -779,11 +781,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": [
|
||||
@ -881,10 +883,10 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1",
|
||||
"sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"
|
||||
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||
],
|
||||
"version": "==2020.4.5.2"
|
||||
"version": "==2020.6.20"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
@ -1004,11 +1006,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 +1022,11 @@
|
||||
},
|
||||
"docker": {
|
||||
"hashes": [
|
||||
"sha256:380a20d38fbfaa872e96ee4d0d23ad9beb0f9ed57ff1c30653cbeb0c9c0964f2",
|
||||
"sha256:672f51aead26d90d1cfce84a87e6f71fca401bbc2a6287be18603583620a28ba"
|
||||
"sha256:03a46400c4080cb6f7aa997f881ddd84fef855499ece219d75fbdb53289c17ab",
|
||||
"sha256:26eebadce7e298f55b76a88c4f8802476c5eaddbdbe38dbc6cce8781c47c9b54"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.2.1"
|
||||
"version": "==4.2.2"
|
||||
},
|
||||
"gitdb": {
|
||||
"hashes": [
|
||||
@ -1042,10 +1044,10 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"version": "==2.9"
|
||||
"version": "==2.10"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
|
29
README.md
29
README.md
@ -1,6 +1,7 @@
|
||||
<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">
|
||||
|
||||

|
||||
=======
|
||||

|
||||

|
||||

|
||||

|
||||
@ -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:
|
||||
|
||||
|
230
azure-pipelines.yml
Normal file
230
azure-pipelines.yml
Normal file
@ -0,0 +1,230 @@
|
||||
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/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
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
dockerComposeFile: 'scripts/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/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 --failfast
|
||||
- task: PublishBuildArtifacts@1
|
||||
condition: failed()
|
||||
displayName: Upload screenshots if selenium tests fail
|
||||
inputs:
|
||||
PathtoPublish: 'selenium_screenshots/'
|
||||
ArtifactName: 'drop'
|
||||
publishLocation: 'Container'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
pipenv run coverage xml
|
||||
pipenv run coverage html
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: Cobertura
|
||||
summaryFileLocation: 'coverage.xml'
|
||||
- task: PublishTestResults@2
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testRunTitle: 'Publish test results for Python $(python.version)'
|
||||
testResultsFiles: 'unittest.xml'
|
||||
- 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: CmdLine@2
|
||||
inputs:
|
||||
script: cd gatekeeper
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'dockerhub'
|
||||
repository: 'beryju/passbook-gatekeeper'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'Dockerfile'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- job: build_static
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
dockerComposeFile: 'scripts/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)'
|
@ -286,6 +286,204 @@
|
||||
],
|
||||
"value": "foo@bar.baz"
|
||||
}]
|
||||
}, {
|
||||
"id": "1a3172e0-ac23-4781-9367-19afccee4f4a",
|
||||
"name": "flows stage setup password",
|
||||
"commands": [{
|
||||
"id": "77784f77-d840-4b3d-a42f-7928f02fb7e1",
|
||||
"comment": "",
|
||||
"command": "open",
|
||||
"target": "/flows/default-authentication-flow/?next=%2F",
|
||||
"targets": [],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "783aa9a6-81e5-49c6-8789-2f360a5750b1",
|
||||
"comment": "",
|
||||
"command": "setWindowSize",
|
||||
"target": "1699x1417",
|
||||
"targets": [],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "cb0cd63e-30e9-4443-af59-5345fe26dc88",
|
||||
"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": "8466ded1-c5f6-451c-b63f-0889da38503a",
|
||||
"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": "pbadmin"
|
||||
}, {
|
||||
"id": "27383093-d01a-4416-8fc6-9caad4926cd3",
|
||||
"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": "4602745a-0ebb-4425-a841-a1ed4899659d",
|
||||
"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": "d1ff4f81-d8f9-45dc-ad5d-f99b54c0cd18",
|
||||
"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": "014c8f57-7ef2-469c-b700-efa94ba81b66",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "css=.pf-c-page__header",
|
||||
"targets": [
|
||||
["css=.pf-c-page__header", "css:finder"],
|
||||
["xpath=//div[@id='page-default-nav-example']/header", "xpath:idRelative"],
|
||||
["xpath=//header", "xpath:position"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "14e86b6f-6add-4bcc-913a-42b1e7322c79",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "linkText=pbadmin",
|
||||
"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": ""
|
||||
}, {
|
||||
"id": "8280da13-632e-4cba-9e18-ecae0d57d052",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"target": "linkText=Change password",
|
||||
"targets": [
|
||||
["linkText=Change password", "linkText"],
|
||||
["css=.pf-c-nav__section:nth-child(2) .pf-c-nav__link", "css:finder"],
|
||||
["xpath=//a[contains(text(),'Change password')]", "xpath:link"],
|
||||
["xpath=//nav[@id='page-default-nav-example-primary-nav']/section[2]/ul/li/a", "xpath:idRelative"],
|
||||
["xpath=//a[contains(@href, '/-/user/stage/password/b929b529-e384-4409-8d40-ac4a195fcab2/change/?next=%2F-%2Fuser%2F')]", "xpath:href"],
|
||||
["xpath=//section[2]/ul/li/a", "xpath:position"],
|
||||
["xpath=//a[contains(.,'Change password')]", "xpath:innerText"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "716d7e0c-79dc-469b-a31f-dceaa0765e9c",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"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/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "77005d70-adf0-4add-8329-b092d43f829a",
|
||||
"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/input", "xpath:idRelative"],
|
||||
["xpath=//div/input", "xpath:position"]
|
||||
],
|
||||
"value": "test"
|
||||
}, {
|
||||
"id": "965ca365-99f4-45d1-97c3-c944269341b9",
|
||||
"comment": "",
|
||||
"command": "click",
|
||||
"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[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": ""
|
||||
}, {
|
||||
"id": "9b421468-c65e-4943-b6b1-1e80410a6b87",
|
||||
"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[2]/input", "xpath:idRelative"],
|
||||
["xpath=//div[2]/input", "xpath:position"]
|
||||
],
|
||||
"value": "test"
|
||||
}, {
|
||||
"id": "572c1400-a0f2-499f-808a-18c1f56bf13f",
|
||||
"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": ""
|
||||
}]
|
||||
}],
|
||||
"suites": [{
|
||||
"id": "495657fb-3f5e-4431-877c-4d0b248c0841",
|
||||
|
@ -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):
|
||||
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()
|
||||
|
||||
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):
|
41
e2e/test_flows_stage_setup.py
Normal file
41
e2e/test_flows_stage_setup.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""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
|
||||
|
||||
|
||||
class TestFlowsStageSetup(SeleniumTestCase):
|
||||
"""test stage setup flows"""
|
||||
|
||||
def test_password_change(self):
|
||||
"""test password change flow"""
|
||||
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.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
|
||||
|
||||
|
||||
@ -93,6 +95,8 @@ 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,
|
||||
@ -163,6 +167,7 @@ class TestProviderOAuth(SeleniumTestCase):
|
||||
)
|
||||
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,
|
||||
@ -189,3 +194,42 @@ class TestProviderOAuth(SeleniumTestCase):
|
||||
).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, "#flow-body > 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
|
||||
|
||||
|
||||
@ -252,3 +254,50 @@ class TestProviderOIDC(SeleniumTestCase):
|
||||
).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, "#flow-body > header > h1").text,
|
||||
"Permission denied",
|
||||
)
|
||||
|
@ -12,6 +12,8 @@ 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,
|
||||
@ -88,6 +90,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}!",
|
||||
@ -127,7 +130,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}!",
|
||||
@ -166,7 +171,46 @@ 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",
|
||||
processor_path=class_to_path(GenericProcessor),
|
||||
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, "#flow-body > header > h1").text,
|
||||
"Permission denied",
|
||||
)
|
||||
|
23
e2e/utils.py
23
e2e/utils.py
@ -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(300)
|
||||
self.wait = WebDriverWait(self.driver, 500)
|
||||
self.apply_default_data()
|
||||
self.logger = get_logger()
|
||||
|
||||
def _get_driver(self) -> WebDriver:
|
||||
return webdriver.Remote(
|
||||
@ -56,10 +58,23 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.driver.save_screenshot(f"out/{self.__class__.__name__}_{time()}.png")
|
||||
self.driver.save_screenshot(
|
||||
f"selenium_screenshots/{self.__class__.__name__}_{time()}.png"
|
||||
)
|
||||
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,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
appVersion: "0.9.0-pre3"
|
||||
appVersion: "0.9.0-pre5"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.9.0-pre3"
|
||||
version: "0.9.0-pre5"
|
||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
||||
|
@ -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-pre5
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.9.0-pre3"
|
||||
__version__ = "0.9.0-pre5"
|
||||
|
@ -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 %}
|
||||
|
@ -120,7 +120,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ version }}
|
||||
{% if version >= version_latest %}
|
||||
<i class="pf-icon pf-icon-ok"></i>
|
||||
{% blocktrans with version=version %}
|
||||
{{ version }} (Up-to-date!)
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i>
|
||||
{% blocktrans with version=version latest=version_latest %}
|
||||
{{ version }} ({{ latest }} is available!)
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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>
|
||||
|
83
passbook/admin/templates/administration/token/list.html
Normal file
83
passbook/admin/templates/administration/token/list.html
Normal file
@ -0,0 +1,83 @@
|
||||
{% 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 Applications.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no applications exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% 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)
|
||||
|
@ -1,7 +1,11 @@
|
||||
"""passbook administration overview"""
|
||||
from functools import lru_cache
|
||||
|
||||
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
|
||||
@ -12,6 +16,19 @@ from passbook.root.celery import CELERY_APP
|
||||
from passbook.stages.invitation.models import Invitation
|
||||
|
||||
|
||||
@lru_cache
|
||||
def latest_version() -> Version:
|
||||
"""Get latest release from GitHub, cached"""
|
||||
try:
|
||||
data = get(
|
||||
"https://api.github.com/repos/beryju/passbook/releases/latest"
|
||||
).json()
|
||||
tag_name = data.get("tag_name")
|
||||
return parse(tag_name.split("/")[1])
|
||||
except RequestException:
|
||||
return parse("0.0.0")
|
||||
|
||||
|
||||
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
"""Overview View"""
|
||||
|
||||
@ -33,7 +50,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
|
||||
|
@ -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,9 +7,10 @@ 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.admin.views.utils import DeleteMessageView
|
||||
from passbook.core.signals import invitation_created
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.stages.invitation.forms import InvitationForm
|
||||
@ -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})
|
||||
|
73
passbook/admin/views/utils.py
Normal file
73
passbook/admin/views/utils.py
Normal file
@ -0,0 +1,73 @@
|
||||
"""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, path_to_class
|
||||
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 path_to_class(model.form)
|
||||
|
||||
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):
|
||||
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 (
|
||||
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,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"),
|
||||
]
|
||||
|
@ -9,7 +9,7 @@ class EventListView(PermissionListMixin, 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",
|
||||
),
|
||||
),
|
||||
]
|
@ -6,6 +6,7 @@ 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.http import HttpRequest
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -71,6 +72,8 @@ class User(GuardianUserMixin, AbstractUser):
|
||||
class Meta:
|
||||
|
||||
permissions = (("reset_user_password", "Reset Password"),)
|
||||
verbose_name = _("User")
|
||||
verbose_name_plural = _("Users")
|
||||
|
||||
|
||||
class Provider(models.Model):
|
||||
@ -112,8 +115,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,6 +124,11 @@ 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"""
|
||||
@ -190,15 +196,39 @@ class UserSourceConnection(CreatedUpdatedModel):
|
||||
unique_together = (("user", "source"),)
|
||||
|
||||
|
||||
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(models.Model):
|
||||
"""One-time link for password resets/sign-up-confirmations"""
|
||||
"""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
|
||||
)
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def filter_not_expired(**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 Token.objects.filter(
|
||||
query & (query_not_expired_yet | query_not_expiring)
|
||||
)
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if token is expired yet."""
|
||||
@ -206,7 +236,7 @@ class Token(models.Model):
|
||||
|
||||
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:
|
||||
|
@ -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 %}
|
||||
|
@ -22,8 +22,7 @@
|
||||
<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" />
|
||||
<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">
|
||||
@ -50,7 +49,7 @@
|
||||
<li>
|
||||
<a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
|
||||
</li>
|
||||
<!-- todo: load config.passbook.footer.links -->
|
||||
<!-- TODO:load config.passbook.footer.links -->
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
@ -1,21 +1,62 @@
|
||||
{% extends 'login/base.html' %}
|
||||
{% extends 'base/skeleton.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>
|
||||
{% 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 'Permission denied' %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% 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 %}
|
||||
</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>
|
||||
{% if 'back' in request.GET %}
|
||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% 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">
|
||||
|
@ -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>
|
||||
@ -42,8 +41,7 @@
|
||||
{% 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>
|
||||
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 user view 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_tokens
|
||||
|
||||
|
||||
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_tokens()
|
||||
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
|
||||
|
@ -27,7 +27,7 @@ class FlowStageBindingSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
model = FlowStageBinding
|
||||
fields = ["pk", "flow", "stage", "re_evaluate_policies", "order", "policies"]
|
||||
fields = ["pk", "target", "stage", "re_evaluate_policies", "order", "policies"]
|
||||
|
||||
|
||||
class FlowStageBindingViewSet(ModelViewSet):
|
||||
|
@ -13,5 +13,5 @@ class PassbookFlowsConfig(AppConfig):
|
||||
verbose_name = "passbook Flows"
|
||||
|
||||
def ready(self):
|
||||
"""Load policy cache clearing signals"""
|
||||
"""Flow signals that clear the cache"""
|
||||
import_module("passbook.flows.signals")
|
||||
|
@ -3,7 +3,8 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.flows.models import Flow, FlowStageBinding
|
||||
from passbook.flows.models import Flow, FlowStageBinding, Stage
|
||||
from passbook.lib.widgets import GroupedModelChoiceField
|
||||
|
||||
|
||||
class FlowForm(forms.ModelForm):
|
||||
@ -35,11 +36,13 @@ class FlowForm(forms.ModelForm):
|
||||
class FlowStageBindingForm(forms.ModelForm):
|
||||
"""FlowStageBinding Form"""
|
||||
|
||||
stage = GroupedModelChoiceField(queryset=Stage.objects.all().select_subclasses(),)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = FlowStageBinding
|
||||
fields = [
|
||||
"flow",
|
||||
"target",
|
||||
"stage",
|
||||
"re_evaluate_policies",
|
||||
"order",
|
||||
|
@ -1,104 +0,0 @@
|
||||
# Generated by Django 3.0.3 on 2020-05-08 14:30
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from passbook.flows.models import FlowDesignation
|
||||
from passbook.stages.identification.models import Templates, UserFields
|
||||
|
||||
|
||||
def create_default_authentication_flow(
|
||||
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
||||
):
|
||||
Flow = apps.get_model("passbook_flows", "Flow")
|
||||
FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
|
||||
PasswordStage = apps.get_model("passbook_stages_password", "PasswordStage")
|
||||
UserLoginStage = apps.get_model("passbook_stages_user_login", "UserLoginStage")
|
||||
IdentificationStage = apps.get_model(
|
||||
"passbook_stages_identification", "IdentificationStage"
|
||||
)
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
if (
|
||||
Flow.objects.using(db_alias)
|
||||
.filter(designation=FlowDesignation.AUTHENTICATION)
|
||||
.exists()
|
||||
):
|
||||
# Only create default flow when none exist
|
||||
return
|
||||
|
||||
if not IdentificationStage.objects.using(db_alias).exists():
|
||||
IdentificationStage.objects.using(db_alias).create(
|
||||
name="identification",
|
||||
user_fields=[UserFields.E_MAIL, UserFields.USERNAME],
|
||||
template=Templates.DEFAULT_LOGIN,
|
||||
)
|
||||
|
||||
if not PasswordStage.objects.using(db_alias).exists():
|
||||
PasswordStage.objects.using(db_alias).create(
|
||||
name="password", backends=["django.contrib.auth.backends.ModelBackend"],
|
||||
)
|
||||
|
||||
if not UserLoginStage.objects.using(db_alias).exists():
|
||||
UserLoginStage.objects.using(db_alias).create(name="authentication")
|
||||
|
||||
flow = Flow.objects.using(db_alias).create(
|
||||
name="Welcome to passbook!",
|
||||
slug="default-authentication-flow",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).create(
|
||||
flow=flow, stage=IdentificationStage.objects.using(db_alias).first(), order=0,
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).create(
|
||||
flow=flow, stage=PasswordStage.objects.using(db_alias).first(), order=1,
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).create(
|
||||
flow=flow, stage=UserLoginStage.objects.using(db_alias).first(), order=2,
|
||||
)
|
||||
|
||||
|
||||
def create_default_invalidation_flow(
|
||||
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
||||
):
|
||||
Flow = apps.get_model("passbook_flows", "Flow")
|
||||
FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
|
||||
UserLogoutStage = apps.get_model("passbook_stages_user_logout", "UserLogoutStage")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
if (
|
||||
Flow.objects.using(db_alias)
|
||||
.filter(designation=FlowDesignation.INVALIDATION)
|
||||
.exists()
|
||||
):
|
||||
# Only create default flow when none exist
|
||||
return
|
||||
|
||||
if not UserLogoutStage.objects.using(db_alias).exists():
|
||||
UserLogoutStage.objects.using(db_alias).create(name="logout")
|
||||
|
||||
flow = Flow.objects.using(db_alias).create(
|
||||
name="default-invalidation-flow",
|
||||
slug="default-invalidation-flow",
|
||||
designation=FlowDesignation.INVALIDATION,
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).create(
|
||||
flow=flow, stage=UserLogoutStage.objects.using(db_alias).first(), order=0,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0001_initial"),
|
||||
("passbook_stages_user_login", "0001_initial"),
|
||||
("passbook_stages_user_logout", "0001_initial"),
|
||||
("passbook_stages_password", "0001_initial"),
|
||||
("passbook_stages_identification", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_default_authentication_flow),
|
||||
migrations.RunPython(create_default_invalidation_flow),
|
||||
]
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0002_default_flows"),
|
||||
("passbook_flows", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
29
passbook/flows/migrations/0006_auto_20200629_0857.py
Normal file
29
passbook/flows/migrations/0006_auto_20200629_0857.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.0.7 on 2020-06-29 08:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0003_auto_20200523_1133"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="flow",
|
||||
name="designation",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("authentication", "Authentication"),
|
||||
("authorization", "Authorization"),
|
||||
("invalidation", "Invalidation"),
|
||||
("enrollment", "Enrollment"),
|
||||
("unenrollment", "Unrenollment"),
|
||||
("recovery", "Recovery"),
|
||||
("stage_setup", "Stage Setup"),
|
||||
],
|
||||
max_length=100,
|
||||
),
|
||||
),
|
||||
]
|
42
passbook/flows/migrations/0007_auto_20200703_2059.py
Normal file
42
passbook/flows/migrations/0007_auto_20200703_2059.py
Normal file
@ -0,0 +1,42 @@
|
||||
# Generated by Django 3.0.7 on 2020-07-03 20:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_policies", "0002_auto_20200528_1647"),
|
||||
("passbook_flows", "0006_auto_20200629_0857"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="flowstagebinding",
|
||||
options={
|
||||
"ordering": ["order", "target"],
|
||||
"verbose_name": "Flow Stage Binding",
|
||||
"verbose_name_plural": "Flow Stage Bindings",
|
||||
},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="flowstagebinding", old_name="flow", new_name="target",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="flow", old_name="pbm", new_name="policybindingmodel_ptr",
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="flowstagebinding", unique_together={("target", "stage", "order")},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="flow",
|
||||
name="policybindingmodel_ptr",
|
||||
field=models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
to="passbook_policies.PolicyBindingModel",
|
||||
),
|
||||
),
|
||||
]
|
95
passbook/flows/migrations/0008_default_flows.py
Normal file
95
passbook/flows/migrations/0008_default_flows.py
Normal file
@ -0,0 +1,95 @@
|
||||
# Generated by Django 3.0.3 on 2020-05-08 14:30
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from passbook.flows.models import FlowDesignation
|
||||
from passbook.stages.identification.models import Templates, UserFields
|
||||
|
||||
|
||||
def create_default_authentication_flow(
|
||||
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
||||
):
|
||||
Flow = apps.get_model("passbook_flows", "Flow")
|
||||
FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
|
||||
PasswordStage = apps.get_model("passbook_stages_password", "PasswordStage")
|
||||
UserLoginStage = apps.get_model("passbook_stages_user_login", "UserLoginStage")
|
||||
IdentificationStage = apps.get_model(
|
||||
"passbook_stages_identification", "IdentificationStage"
|
||||
)
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
identification_stage, _ = IdentificationStage.objects.using(
|
||||
db_alias
|
||||
).update_or_create(
|
||||
name="default-authentication-identification",
|
||||
defaults={
|
||||
"user_fields": [UserFields.E_MAIL, UserFields.USERNAME],
|
||||
"template": Templates.DEFAULT_LOGIN,
|
||||
},
|
||||
)
|
||||
|
||||
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
|
||||
name="default-authentication-password",
|
||||
defaults={"backends": ["django.contrib.auth.backends.ModelBackend"]},
|
||||
)
|
||||
|
||||
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
||||
name="default-authentication-login"
|
||||
)
|
||||
|
||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||
slug="default-authentication-flow",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
defaults={"name": "Welcome to passbook!",},
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=identification_stage, defaults={"order": 0,},
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=password_stage, defaults={"order": 1,},
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=login_stage, defaults={"order": 2,},
|
||||
)
|
||||
|
||||
|
||||
def create_default_invalidation_flow(
|
||||
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
||||
):
|
||||
Flow = apps.get_model("passbook_flows", "Flow")
|
||||
FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
|
||||
UserLogoutStage = apps.get_model("passbook_stages_user_logout", "UserLogoutStage")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
UserLogoutStage.objects.using(db_alias).update_or_create(
|
||||
name="default-invalidation-logout"
|
||||
)
|
||||
|
||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||
slug="default-invalidation-flow",
|
||||
designation=FlowDesignation.INVALIDATION,
|
||||
defaults={"name": "Logout",},
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow,
|
||||
stage=UserLogoutStage.objects.using(db_alias).first(),
|
||||
defaults={"order": 0,},
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0007_auto_20200703_2059"),
|
||||
("passbook_stages_user_login", "0001_initial"),
|
||||
("passbook_stages_user_logout", "0001_initial"),
|
||||
("passbook_stages_password", "0001_initial"),
|
||||
("passbook_stages_identification", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_default_authentication_flow),
|
||||
migrations.RunPython(create_default_invalidation_flow),
|
||||
]
|
@ -34,60 +34,63 @@ def create_default_source_enrollment_flow(
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Create a policy that only allows this flow when doing an SSO Request
|
||||
flow_policy = ExpressionPolicy.objects.using(db_alias).create(
|
||||
name="default-source-enrollment-if-sso", expression=FLOW_POLICY_EXPRESSION
|
||||
flow_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
|
||||
name="default-source-enrollment-if-sso",
|
||||
defaults={"expression": FLOW_POLICY_EXPRESSION},
|
||||
)
|
||||
|
||||
# This creates a Flow used by sources to enroll users
|
||||
# It makes sure that a username is set, and if not, prompts the user for a Username
|
||||
flow = Flow.objects.using(db_alias).create(
|
||||
name="default-source-enrollment",
|
||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||
slug="default-source-enrollment",
|
||||
designation=FlowDesignation.ENROLLMENT,
|
||||
defaults={"name": "Welcome to passbook!",},
|
||||
)
|
||||
PolicyBinding.objects.using(db_alias).create(
|
||||
policy=flow_policy, target=flow, order=0
|
||||
PolicyBinding.objects.using(db_alias).update_or_create(
|
||||
policy=flow_policy, target=flow, defaults={"order": 0}
|
||||
)
|
||||
|
||||
# PromptStage to ask user for their username
|
||||
prompt_stage = PromptStage.objects.using(db_alias).create(
|
||||
prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create(
|
||||
name="default-source-enrollment-username-prompt",
|
||||
)
|
||||
prompt_stage.fields.add(
|
||||
Prompt.objects.using(db_alias).create(
|
||||
field_key="username",
|
||||
label="Username",
|
||||
type=FieldTypes.TEXT,
|
||||
required=True,
|
||||
placeholder="Username",
|
||||
)
|
||||
prompt, _ = Prompt.objects.using(db_alias).update_or_create(
|
||||
field_key="username",
|
||||
defaults={
|
||||
"label": "Username",
|
||||
"type": FieldTypes.TEXT,
|
||||
"required": True,
|
||||
"placeholder": "Username",
|
||||
},
|
||||
)
|
||||
prompt_stage.fields.add(prompt)
|
||||
|
||||
# Policy to only trigger prompt when no username is given
|
||||
prompt_policy = ExpressionPolicy.objects.using(db_alias).create(
|
||||
prompt_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
|
||||
name="default-source-enrollment-if-username",
|
||||
expression=PROMPT_POLICY_EXPRESSION,
|
||||
defaults={"expression": PROMPT_POLICY_EXPRESSION},
|
||||
)
|
||||
|
||||
# UserWrite stage to create the user, and login stage to log user in
|
||||
user_write = UserWriteStage.objects.using(db_alias).create(
|
||||
user_write, _ = UserWriteStage.objects.using(db_alias).update_or_create(
|
||||
name="default-source-enrollment-write"
|
||||
)
|
||||
user_login = UserLoginStage.objects.using(db_alias).create(
|
||||
user_login, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
||||
name="default-source-enrollment-login"
|
||||
)
|
||||
|
||||
binding = FlowStageBinding.objects.using(db_alias).create(
|
||||
flow=flow, stage=prompt_stage, order=0
|
||||
binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=prompt_stage, defaults={"order": 0}
|
||||
)
|
||||
PolicyBinding.objects.using(db_alias).create(
|
||||
policy=prompt_policy, target=binding, order=0
|
||||
PolicyBinding.objects.using(db_alias).update_or_create(
|
||||
policy=prompt_policy, target=binding, defaults={"order": 0}
|
||||
)
|
||||
|
||||
FlowStageBinding.objects.using(db_alias).create(
|
||||
flow=flow, stage=user_write, order=1
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=user_write, defaults={"order": 1}
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).create(
|
||||
flow=flow, stage=user_login, order=2
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=user_login, defaults={"order": 2}
|
||||
)
|
||||
|
||||
|
||||
@ -107,32 +110,33 @@ def create_default_source_authentication_flow(
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Create a policy that only allows this flow when doing an SSO Request
|
||||
flow_policy = ExpressionPolicy.objects.using(db_alias).create(
|
||||
name="default-source-authentication-if-sso", expression=FLOW_POLICY_EXPRESSION
|
||||
flow_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
|
||||
name="default-source-authentication-if-sso",
|
||||
defaults={"expression": FLOW_POLICY_EXPRESSION,},
|
||||
)
|
||||
|
||||
# This creates a Flow used by sources to authenticate users
|
||||
flow = Flow.objects.using(db_alias).create(
|
||||
name="default-source-authentication",
|
||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||
slug="default-source-authentication",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
defaults={"name": "Welcome to passbook!",},
|
||||
)
|
||||
PolicyBinding.objects.using(db_alias).create(
|
||||
policy=flow_policy, target=flow, order=0
|
||||
PolicyBinding.objects.using(db_alias).update_or_create(
|
||||
policy=flow_policy, target=flow, defaults={"order": 0}
|
||||
)
|
||||
|
||||
user_login = UserLoginStage.objects.using(db_alias).create(
|
||||
user_login, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
||||
name="default-source-authentication-login"
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).create(
|
||||
flow=flow, stage=user_login, order=0
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=user_login, defaults={"order": 0}
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0003_auto_20200523_1133"),
|
||||
("passbook_flows", "0008_default_flows"),
|
||||
("passbook_policies", "0001_initial"),
|
||||
("passbook_policies_expression", "0001_initial"),
|
||||
("passbook_stages_prompt", "0001_initial"),
|
@ -7,7 +7,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from passbook.flows.models import FlowDesignation
|
||||
|
||||
|
||||
def create_default_provider_authz_flow(
|
||||
def create_default_provider_authorization_flow(
|
||||
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
||||
):
|
||||
Flow = apps.get_model("passbook_flows", "Flow")
|
||||
@ -18,29 +18,31 @@ def create_default_provider_authz_flow(
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Empty flow for providers where consent is implicitly given
|
||||
Flow.objects.using(db_alias).create(
|
||||
name="Authorize Application",
|
||||
Flow.objects.using(db_alias).update_or_create(
|
||||
slug="default-provider-authorization-implicit-consent",
|
||||
designation=FlowDesignation.AUTHORIZATION,
|
||||
defaults={"name": "Authorize Application"},
|
||||
)
|
||||
|
||||
# Flow with consent form to obtain explicit user consent
|
||||
flow = Flow.objects.using(db_alias).create(
|
||||
name="Authorize Application",
|
||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||
slug="default-provider-authorization-explicit-consent",
|
||||
designation=FlowDesignation.AUTHORIZATION,
|
||||
defaults={"name": "Authorize Application"},
|
||||
)
|
||||
stage = ConsentStage.objects.using(db_alias).create(
|
||||
stage, _ = ConsentStage.objects.using(db_alias).update_or_create(
|
||||
name="default-provider-authorization-consent"
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).create(flow=flow, stage=stage, order=0)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=stage, defaults={"order": 0}
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0004_source_flows"),
|
||||
("passbook_flows", "0009_source_flows"),
|
||||
("passbook_stages_consent", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(create_default_provider_authz_flow)]
|
||||
operations = [migrations.RunPython(create_default_provider_authorization_flow)]
|
@ -15,6 +15,13 @@ from passbook.policies.models import PolicyBindingModel
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class NotConfiguredAction(models.TextChoices):
|
||||
"""Decides how the FlowExecutor should proceed when a stage isn't configured"""
|
||||
|
||||
SKIP = "skip"
|
||||
# CONFIGURE = "configure"
|
||||
|
||||
|
||||
class FlowDesignation(models.TextChoices):
|
||||
"""Designation of what a Flow should be used for. At a later point, this
|
||||
should be replaced by a database entry."""
|
||||
@ -25,7 +32,7 @@ class FlowDesignation(models.TextChoices):
|
||||
ENROLLMENT = "enrollment"
|
||||
UNRENOLLMENT = "unenrollment"
|
||||
RECOVERY = "recovery"
|
||||
PASSWORD_CHANGE = "password_change" # nosec # noqa
|
||||
STAGE_SETUP = "stage_setup"
|
||||
|
||||
|
||||
class Stage(models.Model):
|
||||
@ -72,10 +79,6 @@ class Flow(PolicyBindingModel):
|
||||
|
||||
stages = models.ManyToManyField(Stage, through="FlowStageBinding", blank=True)
|
||||
|
||||
pbm = models.OneToOneField(
|
||||
PolicyBindingModel, parent_link=True, on_delete=models.CASCADE, related_name="+"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def with_policy(request: HttpRequest, **flow_filter) -> Optional["Flow"]:
|
||||
"""Get a Flow by `**flow_filter` and check if the request from `request` can access it."""
|
||||
@ -116,7 +119,7 @@ class FlowStageBinding(PolicyBindingModel):
|
||||
|
||||
fsb_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
|
||||
flow = models.ForeignKey("Flow", on_delete=models.CASCADE)
|
||||
target = models.ForeignKey("Flow", on_delete=models.CASCADE)
|
||||
stage = models.ForeignKey(Stage, on_delete=models.CASCADE)
|
||||
|
||||
re_evaluate_policies = models.BooleanField(
|
||||
@ -131,12 +134,12 @@ class FlowStageBinding(PolicyBindingModel):
|
||||
objects = InheritanceManager()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Flow Stage Binding #{self.order} {self.flow} -> {self.stage}"
|
||||
return f"Flow Binding {self.target} -> {self.stage}"
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = ["order", "flow"]
|
||||
ordering = ["order", "target"]
|
||||
|
||||
verbose_name = _("Flow Stage Binding")
|
||||
verbose_name_plural = _("Flow Stage Bindings")
|
||||
unique_together = (("flow", "stage", "order"),)
|
||||
unique_together = (("target", "stage", "order"),)
|
||||
|
@ -146,7 +146,7 @@ class FlowPlanner:
|
||||
.select_related()
|
||||
):
|
||||
binding: FlowStageBinding = stage.flowstagebinding_set.get(
|
||||
flow__pk=self.flow.pk
|
||||
target__pk=self.flow.pk
|
||||
)
|
||||
engine = PolicyEngine(binding, user, request)
|
||||
engine.request.context = plan.context
|
||||
|
@ -7,6 +7,13 @@ from structlog import get_logger
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def delete_cache_prefix(prefix: str) -> int:
|
||||
"""Delete keys prefixed with `prefix` and return count of deleted keys."""
|
||||
keys = cache.keys(prefix)
|
||||
cache.delete_many(keys)
|
||||
return len(keys)
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
# pylint: disable=unused-argument
|
||||
def invalidate_flow_cache(sender, instance, **_):
|
||||
@ -15,17 +22,16 @@ def invalidate_flow_cache(sender, instance, **_):
|
||||
from passbook.flows.planner import cache_key
|
||||
|
||||
if isinstance(instance, Flow):
|
||||
LOGGER.debug("Invalidating Flow cache", flow=instance)
|
||||
cache.delete(f"{cache_key(instance)}*")
|
||||
total = delete_cache_prefix(f"{cache_key(instance)}*")
|
||||
LOGGER.debug("Invalidating Flow cache", flow=instance, len=total)
|
||||
if isinstance(instance, FlowStageBinding):
|
||||
LOGGER.debug("Invalidating Flow cache from FlowStageBinding", binding=instance)
|
||||
cache.delete(f"{cache_key(instance.flow)}*")
|
||||
total = delete_cache_prefix(f"{cache_key(instance.target)}*")
|
||||
LOGGER.debug(
|
||||
"Invalidating Flow cache from FlowStageBinding", binding=instance, len=total
|
||||
)
|
||||
if isinstance(instance, Stage):
|
||||
LOGGER.debug("Invalidating Flow cache from Stage", stage=instance)
|
||||
total = 0
|
||||
for binding in FlowStageBinding.objects.filter(stage=instance):
|
||||
prefix = cache_key(binding.flow)
|
||||
keys = cache.keys(f"{prefix}*")
|
||||
total += len(keys)
|
||||
cache.delete_many(keys)
|
||||
LOGGER.debug("Deleted keys", len=total)
|
||||
prefix = cache_key(binding.target)
|
||||
total += delete_cache_prefix(f"{prefix}*")
|
||||
LOGGER.debug("Invalidating Flow cache from Stage", stage=instance, len=total)
|
||||
|
22
passbook/flows/templates/flows/error.html
Normal file
22
passbook/flows/templates/flows/error.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% load i18n %}
|
||||
|
||||
<style>
|
||||
.pb-exception {
|
||||
font-family: monospace;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
</style>
|
||||
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% trans 'Whoops!' %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<h3>
|
||||
{% trans 'Something went wrong! Please try again later.' %}
|
||||
</h3>
|
||||
{% if debug %}
|
||||
<pre class="pb-exception">{{ tb }}</pre>
|
||||
{% endif %}
|
||||
</div>
|
@ -59,7 +59,7 @@
|
||||
<li>
|
||||
<a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
|
||||
</li>
|
||||
<!-- todo: load config.passbook.footer.links -->
|
||||
<!-- TODO:load config.passbook.footer.links -->
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
@ -73,7 +73,7 @@ class TestFlowPlanner(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy"), order=0
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
|
||||
)
|
||||
request = self.request_factory.get(
|
||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
@ -97,7 +97,7 @@ class TestFlowPlanner(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy"), order=0
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
|
||||
)
|
||||
|
||||
user = User.objects.create(username="test-user")
|
||||
@ -119,7 +119,7 @@ class TestFlowPlanner(TestCase):
|
||||
)
|
||||
|
||||
FlowStageBinding.objects.create(
|
||||
flow=flow,
|
||||
target=flow,
|
||||
stage=DummyStage.objects.create(name="dummy1"),
|
||||
order=0,
|
||||
re_evaluate_policies=True,
|
||||
@ -145,10 +145,10 @@ class TestFlowPlanner(TestCase):
|
||||
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
|
||||
|
||||
binding = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
)
|
||||
binding2 = FlowStageBinding.objects.create(
|
||||
flow=flow,
|
||||
target=flow,
|
||||
stage=DummyStage.objects.create(name="dummy2"),
|
||||
order=1,
|
||||
re_evaluate_policies=True,
|
||||
|
@ -107,10 +107,10 @@ class TestFlowExecutor(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
)
|
||||
FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy2"), order=1
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy2"), order=1
|
||||
)
|
||||
|
||||
exec_url = reverse(
|
||||
@ -143,10 +143,10 @@ class TestFlowExecutor(TestCase):
|
||||
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
|
||||
|
||||
binding = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
)
|
||||
binding2 = FlowStageBinding.objects.create(
|
||||
flow=flow,
|
||||
target=flow,
|
||||
stage=DummyStage.objects.create(name="dummy2"),
|
||||
order=1,
|
||||
re_evaluate_policies=True,
|
||||
@ -194,16 +194,16 @@ class TestFlowExecutor(TestCase):
|
||||
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
|
||||
|
||||
binding = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
)
|
||||
binding2 = FlowStageBinding.objects.create(
|
||||
flow=flow,
|
||||
target=flow,
|
||||
stage=DummyStage.objects.create(name="dummy2"),
|
||||
order=1,
|
||||
re_evaluate_policies=True,
|
||||
)
|
||||
binding3 = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy3"), order=2
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy3"), order=2
|
||||
)
|
||||
|
||||
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
|
||||
@ -261,22 +261,22 @@ class TestFlowExecutor(TestCase):
|
||||
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
|
||||
|
||||
binding = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
)
|
||||
binding2 = FlowStageBinding.objects.create(
|
||||
flow=flow,
|
||||
target=flow,
|
||||
stage=DummyStage.objects.create(name="dummy2"),
|
||||
order=1,
|
||||
re_evaluate_policies=True,
|
||||
)
|
||||
binding3 = FlowStageBinding.objects.create(
|
||||
flow=flow,
|
||||
target=flow,
|
||||
stage=DummyStage.objects.create(name="dummy3"),
|
||||
order=2,
|
||||
re_evaluate_policies=True,
|
||||
)
|
||||
binding4 = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy4"), order=2
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy4"), order=2
|
||||
)
|
||||
|
||||
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
|
||||
|
@ -3,6 +3,7 @@ from django.urls import path
|
||||
|
||||
from passbook.flows.models import FlowDesignation
|
||||
from passbook.flows.views import (
|
||||
CancelView,
|
||||
FlowExecutorShellView,
|
||||
FlowExecutorView,
|
||||
FlowPermissionDeniedView,
|
||||
@ -36,11 +37,7 @@ urlpatterns = [
|
||||
ToDefaultFlow.as_view(designation=FlowDesignation.UNRENOLLMENT),
|
||||
name="default-unenrollment",
|
||||
),
|
||||
path(
|
||||
"-/default/password_change/",
|
||||
ToDefaultFlow.as_view(designation=FlowDesignation.PASSWORD_CHANGE),
|
||||
name="default-password-change",
|
||||
),
|
||||
path("-/cancel/", CancelView.as_view(), name="cancel"),
|
||||
path("b/<slug:flow_slug>/", FlowExecutorView.as_view(), name="flow-executor"),
|
||||
path(
|
||||
"<slug:flow_slug>/", FlowExecutorShellView.as_view(), name="flow-executor-shell"
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""passbook multi-stage authentication engine"""
|
||||
from traceback import format_tb
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from django.http import (
|
||||
@ -8,13 +9,14 @@ from django.http import (
|
||||
HttpResponseRedirect,
|
||||
JsonResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||
from django.shortcuts import get_object_or_404, redirect, render, reverse
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from django.views.generic import TemplateView, View
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.audit.models import cleanse_dict
|
||||
from passbook.core.views.utils import PermissionDeniedView
|
||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from passbook.flows.models import Flow, FlowDesignation, Stage
|
||||
@ -105,8 +107,19 @@ class FlowExecutorView(View):
|
||||
stage=self.current_stage,
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
||||
stage_response = self.current_stage_view.get(request, *args, **kwargs)
|
||||
return to_stage_response(request, stage_response)
|
||||
try:
|
||||
stage_response = self.current_stage_view.get(request, *args, **kwargs)
|
||||
return to_stage_response(request, stage_response)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
LOGGER.exception(exc)
|
||||
return to_stage_response(
|
||||
request,
|
||||
render(
|
||||
request,
|
||||
"flows/error.html",
|
||||
{"error": exc, "tb": "".join(format_tb(exc.__traceback__))},
|
||||
),
|
||||
)
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""pass post request to current stage"""
|
||||
@ -116,8 +129,19 @@ class FlowExecutorView(View):
|
||||
stage=self.current_stage,
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
||||
stage_response = self.current_stage_view.post(request, *args, **kwargs)
|
||||
return to_stage_response(request, stage_response)
|
||||
try:
|
||||
stage_response = self.current_stage_view.post(request, *args, **kwargs)
|
||||
return to_stage_response(request, stage_response)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
LOGGER.exception(exc)
|
||||
return to_stage_response(
|
||||
request,
|
||||
render(
|
||||
request,
|
||||
"flows/error.html",
|
||||
{"error": exc, "tb": "".join(format_tb(exc.__traceback__))},
|
||||
),
|
||||
)
|
||||
|
||||
def _initiate_plan(self) -> FlowPlan:
|
||||
planner = FlowPlanner(self.flow)
|
||||
@ -161,7 +185,7 @@ class FlowExecutorView(View):
|
||||
LOGGER.debug(
|
||||
"f(exec): User passed all stages",
|
||||
flow_slug=self.flow.slug,
|
||||
context=self.plan.context,
|
||||
context=cleanse_dict(self.plan.context),
|
||||
)
|
||||
return self._flow_done()
|
||||
|
||||
@ -182,6 +206,30 @@ class FlowPermissionDeniedView(PermissionDeniedView):
|
||||
"""User could not be authenticated"""
|
||||
|
||||
|
||||
class FlowExecutorShellView(TemplateView):
|
||||
"""Executor Shell view, loads a dummy card with a spinner
|
||||
that loads the next stage in the background."""
|
||||
|
||||
template_name = "flows/shell.html"
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
kwargs["exec_url"] = reverse("passbook_flows:flow-executor", kwargs=self.kwargs)
|
||||
kwargs["msg_url"] = reverse("passbook_api:messages-list")
|
||||
self.request.session[SESSION_KEY_GET] = self.request.GET
|
||||
return kwargs
|
||||
|
||||
|
||||
class CancelView(View):
|
||||
"""View which canels the currently active plan"""
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
"""View which canels the currently active plan"""
|
||||
if SESSION_KEY_PLAN in request.session:
|
||||
del request.session[SESSION_KEY_PLAN]
|
||||
LOGGER.debug("Canceled current plan")
|
||||
return redirect("passbook_core:overview")
|
||||
|
||||
|
||||
class ToDefaultFlow(View):
|
||||
"""Redirect to default flow matching by designation"""
|
||||
|
||||
@ -205,19 +253,6 @@ class ToDefaultFlow(View):
|
||||
)
|
||||
|
||||
|
||||
class FlowExecutorShellView(TemplateView):
|
||||
"""Executor Shell view, loads a dummy card with a spinner
|
||||
that loads the next stage in the background."""
|
||||
|
||||
template_name = "flows/shell.html"
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
kwargs["exec_url"] = reverse("passbook_flows:flow-executor", kwargs=self.kwargs)
|
||||
kwargs["msg_url"] = reverse("passbook_api:messages-list")
|
||||
self.request.session[SESSION_KEY_GET] = self.request.GET
|
||||
return kwargs
|
||||
|
||||
|
||||
def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse:
|
||||
"""Convert normal HttpResponse into JSON Response"""
|
||||
if isinstance(source, HttpResponseRedirect) or source.status_code == 302:
|
||||
|
@ -5,7 +5,7 @@ from urllib.parse import urlencode
|
||||
from django import template
|
||||
from django.db.models import Model
|
||||
from django.template import Context
|
||||
from django.utils.html import escape
|
||||
from django.utils.html import escape, mark_safe
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.config import CONFIG
|
||||
@ -105,4 +105,4 @@ def debug(obj) -> str:
|
||||
@register.filter
|
||||
def doc(obj) -> str:
|
||||
"""Return docstring of object"""
|
||||
return obj.__doc__
|
||||
return mark_safe(obj.__doc__.replace("\n", "<br>"))
|
||||
|
@ -1,12 +1,19 @@
|
||||
"""passbook lib reflection utilities"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def all_subclasses(cls, sort=True):
|
||||
"""Recursively return all subclassess of cls"""
|
||||
classes = set(cls.__subclasses__()).union(
|
||||
[s for c in cls.__subclasses__() for s in all_subclasses(c, sort=sort)]
|
||||
)
|
||||
# Check if we're in debug mode, if not exclude classes which have `__debug_only__`
|
||||
if not settings.DEBUG:
|
||||
# Filter class out when __debug_only__ is not False
|
||||
classes = [x for x in classes if not getattr(x, "__debug_only__", False)]
|
||||
# classes = filter(lambda x: not getattr(x, "__debug_only__", False), classes)
|
||||
if sort:
|
||||
return sorted(classes, key=lambda x: x.__name__)
|
||||
return classes
|
||||
@ -34,10 +41,3 @@ def get_apps():
|
||||
for _app in apps.get_app_configs():
|
||||
if _app.name.startswith("passbook"):
|
||||
yield _app
|
||||
|
||||
|
||||
def app(name):
|
||||
"""Return true if app with `name` is enabled"""
|
||||
from django.conf import settings
|
||||
|
||||
return name in settings.INSTALLED_APPS
|
||||
|
@ -1,36 +1,26 @@
|
||||
"""Dynamic array widget"""
|
||||
from django import forms
|
||||
"""Utility Widgets"""
|
||||
from itertools import groupby
|
||||
|
||||
from django.forms.models import ModelChoiceField, ModelChoiceIterator
|
||||
|
||||
|
||||
class DynamicArrayWidget(forms.TextInput):
|
||||
"""Dynamic array widget"""
|
||||
class GroupedModelChoiceIterator(ModelChoiceIterator):
|
||||
"""ModelChoiceField which groups objects by their verbose_name"""
|
||||
|
||||
template_name = "lib/arrayfield.html"
|
||||
def __iter__(self):
|
||||
if self.field.empty_label is not None:
|
||||
yield ("", self.field.empty_label)
|
||||
queryset = self.queryset
|
||||
# Can't use iterator() when queryset uses prefetch_related()
|
||||
if not queryset._prefetch_related_lookups:
|
||||
queryset = queryset.iterator()
|
||||
# We can't use DB-level sorting as we sort by subclass
|
||||
queryset = sorted(queryset, key=lambda x: x._meta.verbose_name)
|
||||
for group, objs in groupby(queryset, key=lambda x: x._meta.verbose_name):
|
||||
yield (group, [self.choice(obj) for obj in objs])
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
value = value or [""]
|
||||
context = super().get_context(name, value, attrs)
|
||||
final_attrs = context["widget"]["attrs"]
|
||||
id_ = context["widget"]["attrs"].get("id")
|
||||
|
||||
subwidgets = []
|
||||
for index, item in enumerate(context["widget"]["value"]):
|
||||
widget_attrs = final_attrs.copy()
|
||||
if id_:
|
||||
widget_attrs["id"] = "{id_}_{index}".format(id_=id_, index=index)
|
||||
widget = forms.TextInput()
|
||||
widget.is_required = self.is_required
|
||||
subwidgets.append(widget.get_context(name, item, widget_attrs)["widget"])
|
||||
class GroupedModelChoiceField(ModelChoiceField):
|
||||
"""ModelChoiceField which groups objects by their verbose_name"""
|
||||
|
||||
context["widget"]["subwidgets"] = subwidgets
|
||||
return context
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
try:
|
||||
getter = data.getlist
|
||||
return [value for value in getter(name) if value]
|
||||
except AttributeError:
|
||||
return data.get(name)
|
||||
|
||||
def format_value(self, value):
|
||||
return value or []
|
||||
iterator = GroupedModelChoiceIterator
|
||||
|
@ -16,6 +16,8 @@ class DummyPolicy(Policy):
|
||||
"""Policy used for debugging the PolicyEngine. Returns a fixed result,
|
||||
but takes a random time to process."""
|
||||
|
||||
__debug_only__ = True
|
||||
|
||||
result = models.BooleanField(default=False)
|
||||
wait_min = models.IntegerField(default=5)
|
||||
wait_max = models.IntegerField(default=30)
|
||||
|
@ -8,7 +8,7 @@ from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
|
||||
class ExpressionPolicy(Policy):
|
||||
"""Implement custom logic using python."""
|
||||
"""Execute arbitrary Python code to implement custom checks and validation."""
|
||||
|
||||
expression = models.TextField()
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
"""General fields"""
|
||||
|
||||
from django import forms
|
||||
|
||||
from passbook.policies.models import PolicyBinding, PolicyBindingModel
|
||||
from passbook.lib.widgets import GroupedModelChoiceField
|
||||
from passbook.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
||||
|
||||
GENERAL_FIELDS = ["name"]
|
||||
GENERAL_SERIALIZER_FIELDS = ["pk", "name"]
|
||||
@ -10,10 +12,11 @@ GENERAL_SERIALIZER_FIELDS = ["pk", "name"]
|
||||
class PolicyBindingForm(forms.ModelForm):
|
||||
"""Form to edit Policy to PolicyBindingModel Binding"""
|
||||
|
||||
target = forms.ModelChoiceField(
|
||||
target = GroupedModelChoiceField(
|
||||
queryset=PolicyBindingModel.objects.all().select_subclasses(),
|
||||
to_field_name="pbm_uuid",
|
||||
)
|
||||
policy = GroupedModelChoiceField(queryset=Policy.objects.all().select_subclasses(),)
|
||||
|
||||
class Meta:
|
||||
|
||||
|
23
passbook/policies/group_membership/api.py
Normal file
23
passbook/policies/group_membership/api.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""Group Membership Policy API"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS
|
||||
from passbook.policies.group_membership.models import GroupMembershipPolicy
|
||||
|
||||
|
||||
class GroupMembershipPolicySerializer(ModelSerializer):
|
||||
"""Group Membership Policy Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = GroupMembershipPolicy
|
||||
fields = GENERAL_SERIALIZER_FIELDS + [
|
||||
"group",
|
||||
]
|
||||
|
||||
|
||||
class GroupMembershipPolicyViewSet(ModelViewSet):
|
||||
"""Group Membership Policy Viewset"""
|
||||
|
||||
queryset = GroupMembershipPolicy.objects.all()
|
||||
serializer_class = GroupMembershipPolicySerializer
|
11
passbook/policies/group_membership/apps.py
Normal file
11
passbook/policies/group_membership/apps.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""passbook Group Membership policy app config"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PassbookPoliciesGroupMembershipConfig(AppConfig):
|
||||
"""passbook Group Membership policy app config"""
|
||||
|
||||
name = "passbook.policies.group_membership"
|
||||
label = "passbook_policies_group_membership"
|
||||
verbose_name = "passbook Policies.Group Membership"
|
20
passbook/policies/group_membership/forms.py
Normal file
20
passbook/policies/group_membership/forms.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""passbook Group Membership Policy forms"""
|
||||
|
||||
from django import forms
|
||||
|
||||
from passbook.policies.forms import GENERAL_FIELDS
|
||||
from passbook.policies.group_membership.models import GroupMembershipPolicy
|
||||
|
||||
|
||||
class GroupMembershipPolicyForm(forms.ModelForm):
|
||||
"""GroupMembershipPolicy Form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = GroupMembershipPolicy
|
||||
fields = GENERAL_FIELDS + [
|
||||
"group",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user