Compare commits
94 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
| c7f078ffcc | |||
| 571cb3d65f | |||
| 8c500c38b1 | |||
| 5644e57e6a | |||
| cfc181eed1 | |||
| 91bea38b8e | |||
| d95c5aa739 | |||
| 0b250b897e | |||
| c6880a0f16 | |||
| beb5ffcbdd | |||
| 0715cac39b | |||
| 41117d873d | |||
| 231e448b1a | |||
| b3b8cd807d | |||
| 9021bbd5de | |||
| 169475ab39 | |||
| c00e01626e | |||
| 05d4a9ef62 | |||
| 17a2ac73e7 | |||
| 6bc6f947dd | |||
| b048a1fb4f | |||
| 363940ee8d | |||
| a64e53479c | |||
| 14fdbe7720 | |||
| f56332c954 | |||
| 21c53c748f | |||
| b12182c1d1 | |||
| d8f27f595a | |||
| b25dc2aaa3 | |||
| 3ec3849e72 | |||
| 2dc1b65718 | |||
| af22f507f4 | |||
| 9958019bf3 | |||
| 02d65972cb | |||
| 24ad893350 | |||
| 9c5792b1e1 | |||
| 094d191bff | |||
| 49fb9f688b | |||
| 7d161e5aa1 | |||
| 78e5d471e3 | |||
| 2e2c9f5287 | |||
| d5a3e09a98 | |||
| 2402cfe29d | |||
| 26613b6ea9 | |||
| e5165abf04 | |||
| b26882a450 | |||
| 94281bee88 | |||
| 16b966c16e | |||
| d3b0992456 | |||
| dd74b73b4f | |||
| 0bdfccc1f3 | |||
| ceb0793bc9 | |||
| abea85b635 | |||
| 01c83f6f4a | |||
| 9167c9c3ba | |||
| 04add2e52d | |||
| 1e9241d45b | |||
| 22ee198a31 | |||
| 1d9c92d548 | |||
| b30b58924f | |||
| bead19c64c | |||
| 76e2ba4764 | |||
| 8d095d7436 | |||
| d3a7fd5818 | |||
| 247a8dbc8f | |||
| 9241adfc68 | |||
| ae83ee6d31 | |||
| 4701374021 | |||
| bd40585247 | |||
| cc0b8164b0 | |||
| 310b31a8b7 | |||
| 13900bc603 | |||
| 6634cc2edf | |||
| 3478a2cf6d | |||
| 3b70d12a5f | |||
| 219acf76d5 | |||
| ec6f467fa2 | |||
| 0e6561987e | |||
| 62c20b6e67 | |||
| 13084562c5 | |||
| 02c1c434a2 | |||
| 9882342ed1 | |||
| 1c906b12be | |||
| 4d835b18cc | |||
| e02ff7ec30 | |||
| 2e67b0194b | |||
| 02f0712934 | |||
| 7e7ea47f39 | |||
| 7e52711e3a | |||
| 40fd1c9c1f | |||
| 4037a444eb | |||
| 1ed7e900f2 | |||
| cfc8d0a0f7 | |||
| df33616544 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.9.0-pre4
|
||||
current_version = 0.9.0-pre7
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
|
||||
230
.github/workflows/ci.yml
vendored
230
.github/workflows/ci.yml
vendored
@ -1,230 +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
|
||||
snyk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/python@master
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
pyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
- 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##*/}
|
||||
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
@ -16,11 +16,11 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.9.0-pre4
|
||||
-t beryju/passbook:0.9.0-pre7
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.9.0-pre4
|
||||
run: docker push beryju/passbook:0.9.0-pre7
|
||||
- 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-pre4 \
|
||||
-t beryju/passbook-gatekeeper:0.9.0-pre7 \
|
||||
-t beryju/passbook-gatekeeper:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-gatekeeper:0.9.0-pre4
|
||||
run: docker push beryju/passbook-gatekeeper:0.9.0-pre7
|
||||
- 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-pre4
|
||||
-t beryju/passbook-static:0.9.0-pre7
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.9.0-pre4
|
||||
run: docker push beryju/passbook-static:0.9.0-pre7
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
@ -86,3 +86,19 @@ jobs:
|
||||
docker-compose up --no-start
|
||||
docker-compose start postgresql redis
|
||||
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
|
||||
sentry-release:
|
||||
needs:
|
||||
- test-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Create a Sentry.io release
|
||||
uses: tclindner/sentry-releases-action@v1.2.0
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: beryjuorg
|
||||
SENTRY_PROJECT: passbook
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 0.9.0-pre7
|
||||
environment: production
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -196,3 +196,6 @@ local.env.yml
|
||||
### Helm ###
|
||||
# Chart dependencies
|
||||
**/charts/*.tgz
|
||||
|
||||
# Selenium Screenshots
|
||||
selenium_screenshots/**
|
||||
|
||||
1
Pipfile
1
Pipfile
@ -41,6 +41,7 @@ structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
facebook-sdk = "*"
|
||||
elastic-apm = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
|
||||
125
Pipfile.lock
generated
125
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "fd0192b73c01aaffb90716ce7b6d4e5be9adb8788d3ebd58e54ccd6f85d9b71b"
|
||||
"sha256": "f90d79a67bbd689ca4e7ccbfd528e4ed45078e848c36d84e53ff9c6b2a1e92ed"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -46,18 +46,18 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:4c2f5f9f28930e236845e2cddbe01cb093ca96dc1f5c6e2b2b254722018a2268",
|
||||
"sha256:87beffba2360b8077413f2d473cb828d0a5bda513bd1d6fb7b137c57b686aeb6"
|
||||
"sha256:ae57df1fbad7e29954a160d77cbf650d6562eb0d304c1206afa71d914e771a66",
|
||||
"sha256:cbe618d61cb8f75cd9495ea36e69bad7c8984eb11f02ad247be4c9a2eb7eb647"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.14.14"
|
||||
"version": "==1.14.17"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:6a2e9768dad8ae9771302d5922b977dca6bb9693f9b6a5f6ed0e7ac375e2ca40",
|
||||
"sha256:96d668ae5246d236ea83e4586349552d6584e8b1551ae2fccc0bd4ed528a746f"
|
||||
"sha256:5528c04c360019c24f2706ce82872c9ab767a8c581beffdfdaf006cce7499cac",
|
||||
"sha256:d65b5574dad8c221344496352245828d9ffecaa0868199eb04ccd2eb2ff09133"
|
||||
],
|
||||
"version": "==1.17.14"
|
||||
"version": "==1.17.17"
|
||||
},
|
||||
"celery": {
|
||||
"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": [
|
||||
@ -306,6 +306,38 @@
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"elastic-apm": {
|
||||
"hashes": [
|
||||
"sha256:0ffd86d8449d7b63c6053c5032e09abf398c753214a55de8a4e15cb9b56108d1",
|
||||
"sha256:18006ade25a91a8030b5fcfa825d5c364b13bc5e902b818725341f8c9a00895a",
|
||||
"sha256:1d8335f94660c246d5475ec3b15452cd0f5b51affb2e1d16eb2fbc36380308a8",
|
||||
"sha256:25fea9cb6c99efc229b1449d7fbdda76260404cc74abefcc0cc86b3a5102d99d",
|
||||
"sha256:2841bee5650b736d5ebb199d728a18b415dfed22cb367cd913619a691dfe39e8",
|
||||
"sha256:301d159933f19115b21f92bc1ff7f0073bfea13ca24c6ff34023c23077a08e44",
|
||||
"sha256:4eaaebd088315d7ba2726b21fea06279598cad128be073b28c0462049f093d5a",
|
||||
"sha256:515d027d380df818ec304d4d28121c39069a0d919cb2eb7f8e29019a14d62c2a",
|
||||
"sha256:5ad2b431298567f642d44826be2c557d9aca5761c0240be23f9a52b66833cc93",
|
||||
"sha256:5dbf19570bdf97e169b5901913e9a3e271ff5e10d298a608e214802dca8c9065",
|
||||
"sha256:724ded78cc24d2c7d8bc81642a938d9bfc2dcb8b5bdad1b1da242300f9f4ec73",
|
||||
"sha256:7e859162f4c187defe26fb00c974a128eee2bd8988cd30ccffdcaaeb56cb2248",
|
||||
"sha256:9180ed12b9c12cc794f3b57069d1e7b2a04352af02f8d0bc89c9251231f8660e",
|
||||
"sha256:92f885cc67a9d78e72b174feaa979ddb5188d7cef2b5a7739be740955e07c5ed",
|
||||
"sha256:976eaaf3825df760946f31b5426544fecc4c32fd66e124565ede7151f8152689",
|
||||
"sha256:abafeff08ff285cc03c33e822633c6e25a9434174413f72a5032393e9f95a1e0",
|
||||
"sha256:ad21169ebee7ae35d6c42cd6ac9e7658d6e07bc6a3f34dcc4f0a32e03d736fdc",
|
||||
"sha256:b2b4ff079a20d620d7f87a345d37cf9b7f2bc1c8cc8c9317fb0c3979371f0d41",
|
||||
"sha256:b3b72d26104de89124cca965b234b6b67be4604518e168aedcd52c7229c923e9",
|
||||
"sha256:c4a144ecb0b1570c1f6a285cd6f28f2eda89c0696ed494892e3250bb6fed7909",
|
||||
"sha256:dc368bbac6401fa0c9d7a35429257190759f4f33099783d9e0557ce12d64ca6c",
|
||||
"sha256:e28a81802784ea80d21c294a4ab4e47f658a4031caa5c320147925ab62c6a0d4",
|
||||
"sha256:e7832a5ad503d6cd4a7eaa4cee782ccdf113afa99708e3d005fe9aef539a8222",
|
||||
"sha256:f2674a3aee0c38df82dedade353c944a2f55b215c7d5b0776e1bb89ce87de57c",
|
||||
"sha256:f7b37f65c0ca971038f6b69c7581ea762fbc89d9631107babc04c646898686d2",
|
||||
"sha256:fbd1a68b4cf32298e09652958ec3cf13462a5269408522211cfd3e02b451c3b2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.8.0"
|
||||
},
|
||||
"facebook-sdk": {
|
||||
"hashes": [
|
||||
"sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e",
|
||||
@ -735,11 +767,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": [
|
||||
@ -945,40 +977,43 @@
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
|
||||
"sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
|
||||
"sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
|
||||
"sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
|
||||
"sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
|
||||
"sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
|
||||
"sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
|
||||
"sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
|
||||
"sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
|
||||
"sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
|
||||
"sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
|
||||
"sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
|
||||
"sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
|
||||
"sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
|
||||
"sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
|
||||
"sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
|
||||
"sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
|
||||
"sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
|
||||
"sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
|
||||
"sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
|
||||
"sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
|
||||
"sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
|
||||
"sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
|
||||
"sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
|
||||
"sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
|
||||
"sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
|
||||
"sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
|
||||
"sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
|
||||
"sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
|
||||
"sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
|
||||
"sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
|
||||
"sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d",
|
||||
"sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2",
|
||||
"sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703",
|
||||
"sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404",
|
||||
"sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7",
|
||||
"sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405",
|
||||
"sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d",
|
||||
"sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c",
|
||||
"sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6",
|
||||
"sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70",
|
||||
"sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40",
|
||||
"sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4",
|
||||
"sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613",
|
||||
"sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10",
|
||||
"sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b",
|
||||
"sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0",
|
||||
"sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec",
|
||||
"sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1",
|
||||
"sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d",
|
||||
"sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913",
|
||||
"sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e",
|
||||
"sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62",
|
||||
"sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e",
|
||||
"sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a",
|
||||
"sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d",
|
||||
"sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f",
|
||||
"sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e",
|
||||
"sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b",
|
||||
"sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c",
|
||||
"sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032",
|
||||
"sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a",
|
||||
"sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee",
|
||||
"sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c",
|
||||
"sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.1"
|
||||
"version": "==5.2"
|
||||
},
|
||||
"cryptography": {
|
||||
"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:
|
||||
|
||||
|
||||
228
azure-pipelines.yml
Normal file
228
azure-pipelines.yml
Normal file
@ -0,0 +1,228 @@
|
||||
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: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'dockerhub'
|
||||
repository: 'beryju/passbook-gatekeeper'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'gatekeeper/Dockerfile'
|
||||
buildContext: 'gatekeeper/'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- job: build_static
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
dockerComposeFile: 'scripts/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)'
|
||||
@ -22,6 +22,12 @@ config:
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
log_level: warning
|
||||
# Optionally enable Elastic APM Support
|
||||
apm:
|
||||
enabled: false
|
||||
server_url: ""
|
||||
secret_token: ""
|
||||
verify_server_cert: true
|
||||
|
||||
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
|
||||
# This requires the CoreOS Prometheus Operator.
|
||||
|
||||
@ -6,13 +6,13 @@ To export data from your old instance, run this command:
|
||||
|
||||
- docker-compose
|
||||
```
|
||||
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
|
||||
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event otp_totp.totpdevice otp_static.staticdevice otp_static.statictoken
|
||||
docker cp passbook_server_1:/tmp/passbook_dump.json passbook_dump.json
|
||||
```
|
||||
|
||||
- kubernetes
|
||||
```
|
||||
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
|
||||
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event otp_totp.totpdevice otp_static.staticdevice otp_static.statictoken
|
||||
kubectl cp passbook-web-...:/tmp/passbook_dump.json passbook_dump.json
|
||||
```
|
||||
|
||||
|
||||
@ -105,10 +105,10 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
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=user_login, order=3)
|
||||
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(
|
||||
@ -206,11 +206,11 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
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)
|
||||
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()
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -192,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,
|
||||
@ -174,3 +176,41 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
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",
|
||||
)
|
||||
|
||||
@ -43,7 +43,7 @@ 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(300)
|
||||
@ -58,7 +58,9 @@ 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"]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
appVersion: "0.9.0-pre4"
|
||||
appVersion: "0.9.0-pre7"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.9.0-pre4"
|
||||
version: "0.9.0-pre7"
|
||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
||||
|
||||
@ -21,3 +21,8 @@ data:
|
||||
message_queue_db: 1
|
||||
error_reporting: {{ .Values.config.error_reporting }}
|
||||
log_level: "{{ .Values.config.log_level }}"
|
||||
apm:
|
||||
enabled: {{ .Values.config.apm.enabled }}
|
||||
server_url: "{{ .Values.config.apm.server_url }}"
|
||||
secret_token: "{{ .Values.config.apm.server_token }}"
|
||||
verify_server_cert: {{ .Values.config.apm.verify_server_cert }}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
image:
|
||||
tag: 0.9.0-pre4
|
||||
tag: 0.9.0-pre7
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
@ -14,6 +14,12 @@ config:
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
log_level: warning
|
||||
# Optionally enable Elastic APM Support
|
||||
apm:
|
||||
enabled: false
|
||||
server_url: ""
|
||||
secret_token: ""
|
||||
verify_server_cert: true
|
||||
|
||||
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
|
||||
# This requires the CoreOS Prometheus Operator.
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.9.0-pre4"
|
||||
__version__ = "0.9.0-pre7"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -10,29 +10,33 @@
|
||||
</section>
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-l-gallery pf-m-gutter">
|
||||
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ application_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ application_count }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-middleware"></i> {% trans 'Sources' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ source_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ source_count }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
|
||||
@ -40,15 +44,19 @@
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if providers_without_application.exists %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i> {{ provider_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ provider_count }}
|
||||
</p>
|
||||
<p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p>
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ provider_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ provider_count }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Stages' %}
|
||||
@ -56,26 +64,32 @@
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if stage_count < 1 %}
|
||||
<i class="pficon-error-circle-o"></i> {{ stage_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="pficon-error-circle-o"></i> {{ stage_count }}
|
||||
</p>
|
||||
<p>{% trans 'No Stages configured. No Users will be able to login.' %}"></p>
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ stage_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ stage_count }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-topology"></i> {% trans 'Flows' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ flow_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ flow_count }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
|
||||
@ -83,58 +97,71 @@
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if policies_without_binding %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i> {{ policy_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ policy_count }}
|
||||
</p>
|
||||
<p>{% trans 'Policies without binding exist.' %}</p>
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ policy_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ policy_count }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-migration"></i> {% trans 'Invitation' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ invitation_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ invitation_count }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ user_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ user_count }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<p class="aggregate-status">
|
||||
{% if version >= version_latest %}
|
||||
<i class="fa fa-check-circle"></i> {{ version }}
|
||||
{% else %}
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ version }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if version >= version_latest %}
|
||||
<i class="pf-icon pf-icon-ok"></i>
|
||||
{% blocktrans with version=version %}
|
||||
{{ version }} (Up-to-date!)
|
||||
{% blocktrans %}
|
||||
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!)
|
||||
{% blocktrans with latest=version_latest %}
|
||||
{{ latest }} is available!
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
|
||||
@ -142,15 +169,19 @@
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if worker_count < 1 %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i> {{ worker_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ worker_count }}
|
||||
</p>
|
||||
<p>{% trans 'No workers connected.' %}</p>
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ worker_count }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ worker_count }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="pf-c-card pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
|
||||
<a class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
|
||||
@ -158,13 +189,37 @@
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if cached_policies < 1 %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i> {{ cached_policies }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-exclamation-triangle"></i> {{ cached_policies }}
|
||||
</p>
|
||||
<p>{% trans 'No policies cached. Users may experience slow response times.' %}</p>
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-ok"></i> {{ cached_policies }}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ cached_policies }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% if cached_flows < 1 %}
|
||||
<p class="aggregate-status">
|
||||
<span class="fa fa-exclamation-triangle"></span> {{ cached_flows }}
|
||||
</p>
|
||||
<p>{% trans 'No flows cached.' %}</p>
|
||||
{% else %}
|
||||
<p class="aggregate-status">
|
||||
<i class="fa fa-check-circle"></i> {{ cached_flows }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="pf-c-backdrop" id="clearCacheModalRoot" hidden>
|
||||
@ -173,7 +228,9 @@
|
||||
<button data-modal-close class="pf-c-button pf-m-plain" type="button" aria-label="Close dialog">
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1>
|
||||
<div class="pf-c-modal-box__header">
|
||||
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1>
|
||||
</div>
|
||||
<div class="pf-c-modal-box__body" id="modal-description">
|
||||
<form method="post" id="clearForm">
|
||||
{% csrf_token %}
|
||||
|
||||
@ -28,29 +28,50 @@
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Enabled' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Policy' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Target' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Enabled' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Order' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Timeout' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup" class="pf-m-expanded">
|
||||
{% for binding in object_list %}
|
||||
<tr role="row pf-c-table__expandable-row pf-m-expanded">
|
||||
<th role="cell">
|
||||
<div>{{ binding.enabled }}</div>
|
||||
</th>
|
||||
<th role="cell">
|
||||
<div>{{ binding.policy }}</div>
|
||||
</th>
|
||||
<th role="cell">
|
||||
<div>{{ binding.target|verbose_name }}</div>
|
||||
</th>
|
||||
<td>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:policy-binding-update' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:policy-binding-delete' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tbody role="rowgroup">
|
||||
{% for pbm in object_list %}
|
||||
<tr role="role">
|
||||
<td>
|
||||
{{ pbm }}
|
||||
<small>
|
||||
{{ pbm|fieldtype }}
|
||||
</small>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% for binding in pbm.bindings %}
|
||||
<tr class="row pf-c-table__expandable-row pf-m-expanded">
|
||||
<th role="cell">
|
||||
<div>{{ binding.policy }}</div>
|
||||
<small>
|
||||
{{ binding.policy|fieldtype }}
|
||||
</small>
|
||||
</th>
|
||||
<th role="cell">
|
||||
<div>{{ binding.enabled }}</div>
|
||||
</th>
|
||||
<th role="cell">
|
||||
<div>{{ binding.order }}</div>
|
||||
</th>
|
||||
<th role="cell">
|
||||
<div>{{ binding.timeout }}</div>
|
||||
</th>
|
||||
<td class="pb-table-action" role="cell">
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:policy-binding-update' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:policy-binding-delete' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -35,7 +35,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% regroup object_list by flow as grouped_bindings %}
|
||||
{% regroup object_list by target as grouped_bindings %}
|
||||
{% for flow in grouped_bindings %}
|
||||
<tr role="role">
|
||||
<td>
|
||||
@ -56,9 +56,9 @@
|
||||
</td>
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ binding.flow.slug }}</div>
|
||||
<div>{{ binding.target.slug }}</div>
|
||||
<small>
|
||||
{{ binding.flow.name }}
|
||||
{{ binding.target.name }}
|
||||
</small>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
82
passbook/admin/templates/administration/token/list.html
Normal file
82
passbook/admin/templates/administration/token/list.html
Normal file
@ -0,0 +1,82 @@
|
||||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="fas fa-key"></i>
|
||||
{% trans 'Tokens' %}
|
||||
</h1>
|
||||
<p>{% trans "Tokens are used throughout passbook for Email validation stages, Recovery keys and API access." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Token' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'User' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Expires?' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for token in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ token.pk }}</div>
|
||||
</div>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ token.user }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ token.expiring|yesno:"Yes,No" }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ token.expires }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:token-delete' pk=token.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Tokens.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no tokens exist.' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@ -12,7 +12,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block action %}
|
||||
{% blocktrans with type=form|form_verbose_name|title %}
|
||||
{% blocktrans with type=form|form_verbose_name %}
|
||||
Update {{ type }}
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
"""admin tests"""
|
||||
from importlib import import_module
|
||||
from typing import Callable
|
||||
|
||||
from django.forms import ModelForm
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
|
||||
from passbook.admin.urls import urlpatterns
|
||||
from passbook.core.models import User
|
||||
from passbook.lib.utils.reflection import get_apps
|
||||
|
||||
|
||||
class TestAdmin(TestCase):
|
||||
@ -34,4 +37,28 @@ def generic_view_tester(view_name: str) -> Callable:
|
||||
|
||||
for url in urlpatterns:
|
||||
method_name = url.name.replace("-", "_")
|
||||
setattr(TestAdmin, f"test_{method_name}", generic_view_tester(url.name))
|
||||
setattr(TestAdmin, f"test_view_{method_name}", generic_view_tester(url.name))
|
||||
|
||||
|
||||
def generic_form_tester(form: ModelForm) -> Callable:
|
||||
"""Test a form"""
|
||||
|
||||
def tester(self: TestAdmin):
|
||||
form_inst = form()
|
||||
self.assertFalse(form_inst.is_valid())
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
# Load the forms module from every app, so we have all forms loaded
|
||||
for app in get_apps():
|
||||
module = app.__module__.replace(".apps", ".forms")
|
||||
try:
|
||||
import_module(module)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
for form_class in ModelForm.__subclasses__():
|
||||
setattr(
|
||||
TestAdmin, f"test_form_{form_class.__name__}", generic_form_tester(form_class)
|
||||
)
|
||||
|
||||
@ -17,6 +17,7 @@ from passbook.admin.views import (
|
||||
stages_bindings,
|
||||
stages_invitations,
|
||||
stages_prompts,
|
||||
tokens,
|
||||
users,
|
||||
)
|
||||
|
||||
@ -41,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"),
|
||||
|
||||
@ -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,5 +1,4 @@
|
||||
"""passbook Flow 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 HttpRequest, HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
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
|
||||
|
||||
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
|
||||
@ -45,9 +45,30 @@ 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
|
||||
):
|
||||
"""Update flow"""
|
||||
|
||||
model = Flow
|
||||
form_class = FlowForm
|
||||
permission_required = "passbook_flows.change_flow"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:flows")
|
||||
success_message = _("Successfully updated Flow")
|
||||
|
||||
|
||||
class FlowDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete flow"""
|
||||
|
||||
model = Flow
|
||||
permission_required = "passbook_flows.delete_flow"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:flows")
|
||||
success_message = _("Successfully deleted Flow")
|
||||
|
||||
|
||||
class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
@ -67,34 +88,3 @@ class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
|
||||
return redirect_with_qs(
|
||||
"passbook_flows:flow-executor-shell", self.request.GET, flow_slug=flow.slug,
|
||||
)
|
||||
|
||||
|
||||
class FlowUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update flow"""
|
||||
|
||||
model = Flow
|
||||
form_class = FlowForm
|
||||
permission_required = "passbook_flows.change_flow"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:flows")
|
||||
success_message = _("Successfully updated Flow")
|
||||
|
||||
|
||||
class FlowDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
"""Delete flow"""
|
||||
|
||||
model = Flow
|
||||
permission_required = "passbook_flows.delete_flow"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
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)
|
||||
|
||||
@ -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,6 +1,4 @@
|
||||
"""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
|
||||
@ -15,18 +13,21 @@ from passbook.policies.models import Policy
|
||||
from passbook.root.celery import CELERY_APP
|
||||
from passbook.stages.invitation.models import Invitation
|
||||
|
||||
VERSION_CACHE_KEY = "passbook_latest_version"
|
||||
|
||||
|
||||
@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")
|
||||
if not cache.get(VERSION_CACHE_KEY):
|
||||
try:
|
||||
data = get(
|
||||
"https://api.github.com/repos/beryju/passbook/releases/latest"
|
||||
).json()
|
||||
tag_name = data.get("tag_name")
|
||||
cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], 30)
|
||||
except (RequestException, IndexError):
|
||||
cache.set(VERSION_CACHE_KEY, "0.0.0", 30)
|
||||
return parse(cache.get(VERSION_CACHE_KEY))
|
||||
|
||||
|
||||
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
@ -60,4 +61,5 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
Policy.objects.filter(bindings__isnull=True)
|
||||
)
|
||||
kwargs["cached_policies"] = len(cache.keys("policy_*"))
|
||||
kwargs["cached_flows"] = len(cache.keys("flow_*"))
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
@ -8,22 +8,25 @@ from django.contrib.auth.mixins import (
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db.models import QuerySet
|
||||
from django.forms import Form
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, FormView, ListView, UpdateView
|
||||
from django.views.generic import FormView
|
||||
from django.views.generic.detail import DetailView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.forms.policies import PolicyTestForm
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.admin.views.utils import (
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
)
|
||||
from passbook.policies.models import Policy, PolicyBinding
|
||||
from passbook.policies.process import PolicyProcess, PolicyRequest
|
||||
|
||||
|
||||
class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
class PolicyListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
||||
"""Show list of all policies"""
|
||||
|
||||
model = Policy
|
||||
@ -32,19 +35,12 @@ class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
ordering = "name"
|
||||
template_name = "administration/policy/list.html"
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs["types"] = {x.__name__: x for x in all_subclasses(Policy)}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class PolicyCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
InheritanceCreateView,
|
||||
):
|
||||
"""Create new Policy"""
|
||||
|
||||
@ -55,24 +51,12 @@ class PolicyCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:policies")
|
||||
success_message = _("Successfully created Policy")
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self) -> Form:
|
||||
policy_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(x for x in all_subclasses(Policy) if x.__name__ == policy_type)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class PolicyUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
PermissionRequiredMixin,
|
||||
InheritanceUpdateView,
|
||||
):
|
||||
"""Update policy"""
|
||||
|
||||
@ -83,27 +67,8 @@ class PolicyUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:policies")
|
||||
success_message = _("Successfully updated Policy")
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self) -> Form:
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None) -> Policy:
|
||||
return (
|
||||
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
|
||||
class PolicyDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
class PolicyDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete policy"""
|
||||
|
||||
model = Policy
|
||||
@ -113,15 +78,6 @@ class PolicyDeleteView(
|
||||
success_url = reverse_lazy("passbook_admin:policies")
|
||||
success_message = _("Successfully deleted Policy")
|
||||
|
||||
def get_object(self, queryset=None) -> Policy:
|
||||
return (
|
||||
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
def delete(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, FormView):
|
||||
"""View to test policy(s)"""
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
"""passbook PolicyBinding administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db.models import QuerySet
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.policies.forms import PolicyBindingForm
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.policies.models import PolicyBinding, PolicyBindingModel
|
||||
|
||||
|
||||
class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
@ -22,7 +23,20 @@ class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
permission_required = "passbook_policies.view_policybinding"
|
||||
paginate_by = 10
|
||||
ordering = ["order", "target"]
|
||||
template_name = "administration/policybinding/list.html"
|
||||
template_name = "administration/policy_binding/list.html"
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
# Since `select_subclasses` does not work with a foreign key, we have to do two queries here
|
||||
# First, get all pbm objects that have bindings attached
|
||||
objects = (
|
||||
PolicyBindingModel.objects.filter(policies__isnull=False)
|
||||
.select_subclasses()
|
||||
.select_related()
|
||||
.order_by("pk")
|
||||
)
|
||||
for pbm in objects:
|
||||
pbm.bindings = PolicyBinding.objects.filter(target__pk=pbm.pbm_uuid)
|
||||
return objects
|
||||
|
||||
|
||||
class PolicyBindingCreateView(
|
||||
@ -55,16 +69,9 @@ class PolicyBindingUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:policies-bindings")
|
||||
success_message = _("Successfully updated PolicyBinding")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
|
||||
class PolicyBindingDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete policybinding"""
|
||||
|
||||
@ -74,7 +81,3 @@ class PolicyBindingDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:policies-bindings")
|
||||
success_message = _("Successfully deleted PolicyBinding")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,22 +1,25 @@
|
||||
"""passbook PropertyMapping administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import (
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
)
|
||||
from passbook.core.models import PropertyMapping
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
class PropertyMappingListView(
|
||||
LoginRequiredMixin, PermissionListMixin, InheritanceListView
|
||||
):
|
||||
"""Show list of all property_mappings"""
|
||||
|
||||
model = PropertyMapping
|
||||
@ -25,19 +28,12 @@ class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView)
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["types"] = {x.__name__: x for x in all_subclasses(PropertyMapping)}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class PropertyMappingCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
InheritanceCreateView,
|
||||
):
|
||||
"""Create new PropertyMapping"""
|
||||
|
||||
@ -48,38 +44,12 @@ class PropertyMappingCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:property-mappings")
|
||||
success_message = _("Successfully created Property Mapping")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
property_mapping_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(
|
||||
x
|
||||
for x in all_subclasses(PropertyMapping)
|
||||
if x.__name__ == property_mapping_type
|
||||
)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
kwargs["type"] = model._meta.verbose_name
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self):
|
||||
property_mapping_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(
|
||||
x
|
||||
for x in all_subclasses(PropertyMapping)
|
||||
if x.__name__ == property_mapping_type
|
||||
)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class PropertyMappingUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
PermissionRequiredMixin,
|
||||
InheritanceUpdateView,
|
||||
):
|
||||
"""Update property_mapping"""
|
||||
|
||||
@ -90,28 +60,9 @@ class PropertyMappingUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:property-mappings")
|
||||
success_message = _("Successfully updated Property Mapping")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
class PropertyMappingDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete property_mapping"""
|
||||
|
||||
@ -121,14 +72,3 @@ class PropertyMappingDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:property-mappings")
|
||||
success_message = _("Successfully deleted Property Mapping")
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
"""passbook Provider administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import (
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
)
|
||||
from passbook.core.models import Provider
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
class ProviderListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
||||
"""Show list of all providers"""
|
||||
|
||||
model = Provider
|
||||
@ -25,19 +26,12 @@ class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
paginate_by = 10
|
||||
ordering = "id"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["types"] = {x.__name__: x for x in all_subclasses(Provider)}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class ProviderCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
InheritanceCreateView,
|
||||
):
|
||||
"""Create new Provider"""
|
||||
|
||||
@ -48,19 +42,12 @@ class ProviderCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:providers")
|
||||
success_message = _("Successfully created Provider")
|
||||
|
||||
def get_form_class(self):
|
||||
provider_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(
|
||||
x for x in all_subclasses(Provider) if x.__name__ == provider_type
|
||||
)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class ProviderUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
PermissionRequiredMixin,
|
||||
InheritanceUpdateView,
|
||||
):
|
||||
"""Update provider"""
|
||||
|
||||
@ -71,21 +58,9 @@ class ProviderUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:providers")
|
||||
success_message = _("Successfully updated Provider")
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Provider.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
class ProviderDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete provider"""
|
||||
|
||||
@ -95,14 +70,3 @@ class ProviderDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:providers")
|
||||
success_message = _("Successfully deleted Provider")
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Provider.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
"""passbook Source administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import (
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
)
|
||||
from passbook.core.models import Source
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
class SourceListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
||||
"""Show list of all sources"""
|
||||
|
||||
model = Source
|
||||
@ -25,19 +26,12 @@ class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
paginate_by = 40
|
||||
template_name = "administration/source/list.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["types"] = {x.__name__: x for x in all_subclasses(Source)}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class SourceCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
InheritanceCreateView,
|
||||
):
|
||||
"""Create new Source"""
|
||||
|
||||
@ -48,17 +42,12 @@ class SourceCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:sources")
|
||||
success_message = _("Successfully created Source")
|
||||
|
||||
def get_form_class(self):
|
||||
source_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(x for x in all_subclasses(Source) if x.__name__ == source_type)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class SourceUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
PermissionRequiredMixin,
|
||||
InheritanceUpdateView,
|
||||
):
|
||||
"""Update source"""
|
||||
|
||||
@ -69,20 +58,8 @@ class SourceUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:sources")
|
||||
success_message = _("Successfully updated Source")
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
|
||||
class SourceDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
class SourceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete source"""
|
||||
|
||||
model = Source
|
||||
@ -91,12 +68,3 @@ class SourceDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:sources")
|
||||
success_message = _("Successfully deleted Source")
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
"""passbook Stage administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import (
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
)
|
||||
from passbook.flows.models import Stage
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class StageListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
class StageListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
||||
"""Show list of all stages"""
|
||||
|
||||
model = Stage
|
||||
@ -25,19 +26,12 @@ class StageListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["types"] = {x.__name__: x for x in all_subclasses(Stage)}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class StageCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
InheritanceCreateView,
|
||||
):
|
||||
"""Create new Stage"""
|
||||
|
||||
@ -48,24 +42,12 @@ class StageCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:stages")
|
||||
success_message = _("Successfully created Stage")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
stage_type = self.request.GET.get("type")
|
||||
model = next(x for x in all_subclasses(Stage) if x.__name__ == stage_type)
|
||||
kwargs["type"] = model._meta.verbose_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self):
|
||||
stage_type = self.request.GET.get("type")
|
||||
try:
|
||||
model = next(x for x in all_subclasses(Stage) if x.__name__ == stage_type)
|
||||
except StopIteration as exc:
|
||||
raise Http404 from exc
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class StageUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
PermissionRequiredMixin,
|
||||
InheritanceUpdateView,
|
||||
):
|
||||
"""Update stage"""
|
||||
|
||||
@ -75,20 +57,8 @@ class StageUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:stages")
|
||||
success_message = _("Successfully updated Stage")
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Stage.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
|
||||
class StageDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
class StageDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete stage"""
|
||||
|
||||
model = Stage
|
||||
@ -96,12 +66,3 @@ class StageDeleteView(
|
||||
permission_required = "passbook_flows.delete_stage"
|
||||
success_url = reverse_lazy("passbook_admin:stages")
|
||||
success_message = _("Successfully deleted Stage")
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Stage.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
"""passbook StageBinding administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.flows.forms import FlowStageBindingForm
|
||||
from passbook.flows.models import FlowStageBinding
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
@ -21,7 +21,7 @@ class StageBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
model = FlowStageBinding
|
||||
permission_required = "passbook_flows.view_flowstagebinding"
|
||||
paginate_by = 10
|
||||
ordering = ["flow", "order"]
|
||||
ordering = ["target", "order"]
|
||||
template_name = "administration/stage_binding/list.html"
|
||||
|
||||
|
||||
@ -41,13 +41,6 @@ class StageBindingCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:stage-bindings")
|
||||
success_message = _("Successfully created StageBinding")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
|
||||
class StageBindingUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
@ -62,16 +55,9 @@ class StageBindingUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:stage-bindings")
|
||||
success_message = _("Successfully updated StageBinding")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
|
||||
class StageBindingDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete FlowStageBinding"""
|
||||
|
||||
@ -81,7 +67,3 @@ class StageBindingDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:stage-bindings")
|
||||
success_message = _("Successfully deleted FlowStageBinding")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
"""passbook Invitation administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
@ -8,13 +7,14 @@ from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView
|
||||
from django.views.generic import ListView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.core.signals import invitation_created
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.stages.invitation.forms import InvitationForm
|
||||
from passbook.stages.invitation.models import Invitation
|
||||
from passbook.stages.invitation.signals import invitation_created
|
||||
|
||||
|
||||
class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
@ -43,10 +43,6 @@ class InvitationCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:stage-invitations")
|
||||
success_message = _("Successfully created Invitation")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["type"] = "Invitation"
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
obj = form.save(commit=False)
|
||||
obj.created_by = self.request.user
|
||||
@ -56,7 +52,7 @@ class InvitationCreateView(
|
||||
|
||||
|
||||
class InvitationDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
|
||||
):
|
||||
"""Delete invitation"""
|
||||
|
||||
@ -66,7 +62,3 @@ class InvitationDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:stage-invitations")
|
||||
success_message = _("Successfully deleted Invitation")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
"""passbook Prompt administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
@ -7,9 +6,10 @@ from django.contrib.auth.mixins import (
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.stages.prompt.forms import PromptAdminForm
|
||||
from passbook.stages.prompt.models import Prompt
|
||||
@ -41,10 +41,6 @@ class PromptCreateView(
|
||||
success_url = reverse_lazy("passbook_admin:stage-prompts")
|
||||
success_message = _("Successfully created Prompt")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["type"] = "Prompt"
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class PromptUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
@ -60,9 +56,7 @@ class PromptUpdateView(
|
||||
success_message = _("Successfully updated Prompt")
|
||||
|
||||
|
||||
class PromptDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
class PromptDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete prompt"""
|
||||
|
||||
model = Prompt
|
||||
@ -71,7 +65,3 @@ class PromptDeleteView(
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:stage-prompts")
|
||||
success_message = _("Successfully deleted Prompt")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
30
passbook/admin/views/tokens.py
Normal file
30
passbook/admin/views/tokens.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""passbook Token administration"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import ListView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.core.models import Token
|
||||
|
||||
|
||||
class TokenListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all tokens"""
|
||||
|
||||
model = Token
|
||||
permission_required = "passbook_core.view_token"
|
||||
ordering = "expires"
|
||||
paginate_by = 40
|
||||
template_name = "administration/token/list.html"
|
||||
|
||||
|
||||
class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete token"""
|
||||
|
||||
model = Token
|
||||
permission_required = "passbook_core.delete_token"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:tokens")
|
||||
success_message = _("Successfully deleted Token")
|
||||
@ -5,10 +5,12 @@ from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DeleteView, DetailView, ListView, UpdateView
|
||||
from django.views.generic import DetailView, ListView, UpdateView
|
||||
from guardian.mixins import (
|
||||
PermissionListMixin,
|
||||
PermissionRequiredMixin,
|
||||
@ -16,6 +18,7 @@ from guardian.mixins import (
|
||||
)
|
||||
|
||||
from passbook.admin.forms.users import UserForm
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.core.models import Token, User
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
@ -66,9 +69,7 @@ class UserUpdateView(
|
||||
success_message = _("Successfully updated User")
|
||||
|
||||
|
||||
class UserDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
class UserDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete user"""
|
||||
|
||||
model = User
|
||||
@ -80,10 +81,6 @@ class UserDeleteView(
|
||||
success_url = reverse_lazy("passbook_admin:users")
|
||||
success_message = _("Successfully deleted User")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
"""Get Password reset link for user"""
|
||||
@ -91,13 +88,13 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
|
||||
model = User
|
||||
permission_required = "passbook_core.reset_user_password"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Create token for user and return link"""
|
||||
super().get(request, *args, **kwargs)
|
||||
# TODO: create plan for user, get token
|
||||
token = Token.objects.create(user=self.object)
|
||||
querystring = urlencode({"token": token.token_uuid})
|
||||
link = request.build_absolute_uri(
|
||||
reverse("passbook_flows:default-recovery", kwargs={"token": token.uuid})
|
||||
reverse("passbook_flows:default-recovery") + f"?{querystring}"
|
||||
)
|
||||
messages.success(
|
||||
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})
|
||||
|
||||
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
|
||||
@ -47,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)
|
||||
@ -71,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)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"""passbook audit signal listener"""
|
||||
from typing import Dict
|
||||
from threading import Thread
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from django.contrib.auth.signals import (
|
||||
user_logged_in,
|
||||
@ -11,21 +12,54 @@ from django.http import HttpRequest
|
||||
|
||||
from passbook.audit.models import Event, EventAction
|
||||
from passbook.core.models import User
|
||||
from passbook.core.signals import invitation_created, invitation_used, user_signed_up
|
||||
from passbook.stages.invitation.models import Invitation
|
||||
from passbook.stages.invitation.signals import invitation_created, invitation_used
|
||||
from passbook.stages.user_write.signals import user_write
|
||||
|
||||
|
||||
class EventNewThread(Thread):
|
||||
"""Create Event in background thread"""
|
||||
|
||||
action: EventAction
|
||||
request: HttpRequest
|
||||
kwargs: Dict[str, Any]
|
||||
user: Optional[User] = None
|
||||
|
||||
def __init__(self, action: EventAction, request: HttpRequest, **kwargs):
|
||||
super().__init__()
|
||||
self.action = action
|
||||
self.request = request
|
||||
self.kwargs = kwargs
|
||||
|
||||
def run(self):
|
||||
Event.new(self.action, **self.kwargs).from_http(self.request, user=self.user)
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
# pylint: disable=unused-argument
|
||||
def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
|
||||
"""Log successful login"""
|
||||
Event.new(EventAction.LOGIN).from_http(request)
|
||||
thread = EventNewThread(EventAction.LOGIN, request)
|
||||
thread.user = user
|
||||
thread.run()
|
||||
|
||||
|
||||
@receiver(user_logged_out)
|
||||
# pylint: disable=unused-argument
|
||||
def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
|
||||
"""Log successfully logout"""
|
||||
Event.new(EventAction.LOGOUT).from_http(request)
|
||||
thread = EventNewThread(EventAction.LOGOUT, request)
|
||||
thread.user = user
|
||||
thread.run()
|
||||
|
||||
|
||||
@receiver(user_write)
|
||||
# pylint: disable=unused-argument
|
||||
def on_user_write(sender, request: HttpRequest, user: User, data: Dict[str, Any], **_):
|
||||
"""Log User write"""
|
||||
thread = EventNewThread(EventAction.CUSTOM, request, **data)
|
||||
thread.user = user
|
||||
thread.run()
|
||||
|
||||
|
||||
@receiver(user_login_failed)
|
||||
@ -34,29 +68,25 @@ def on_user_login_failed(
|
||||
sender, credentials: Dict[str, str], request: HttpRequest, **_
|
||||
):
|
||||
"""Failed Login"""
|
||||
Event.new(EventAction.LOGIN_FAILED, **credentials).from_http(request)
|
||||
|
||||
|
||||
@receiver(user_signed_up)
|
||||
# pylint: disable=unused-argument
|
||||
def on_user_signed_up(sender, request: HttpRequest, user: User, **_):
|
||||
"""Log successfully signed up"""
|
||||
Event.new(EventAction.SIGN_UP).from_http(request)
|
||||
thread = EventNewThread(EventAction.LOGIN_FAILED, request, **credentials)
|
||||
thread.run()
|
||||
|
||||
|
||||
@receiver(invitation_created)
|
||||
# pylint: disable=unused-argument
|
||||
def on_invitation_created(sender, request: HttpRequest, invitation, **_):
|
||||
def on_invitation_created(sender, request: HttpRequest, invitation: Invitation, **_):
|
||||
"""Log Invitation creation"""
|
||||
Event.new(
|
||||
EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex
|
||||
).from_http(request)
|
||||
thread = EventNewThread(
|
||||
EventAction.INVITE_CREATED, request, invitation_uuid=invitation.invite_uuid.hex
|
||||
)
|
||||
thread.run()
|
||||
|
||||
|
||||
@receiver(invitation_used)
|
||||
# pylint: disable=unused-argument
|
||||
def on_invitation_used(sender, request: HttpRequest, invitation, **_):
|
||||
def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_):
|
||||
"""Log Invitation usage"""
|
||||
Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http(
|
||||
request
|
||||
thread = EventNewThread(
|
||||
EventAction.INVITE_USED, request, invitation_uuid=invitation.invite_uuid.hex
|
||||
)
|
||||
thread.run()
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
"""passbook core signals"""
|
||||
from django.core.signals import Signal
|
||||
|
||||
user_signed_up = Signal(providing_args=["request", "user"])
|
||||
invitation_created = Signal(providing_args=["request", "invitation"])
|
||||
invitation_used = Signal(providing_args=["request", "invitation", "user"])
|
||||
password_changed = Signal(providing_args=["user", "password"])
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
<ul class="pf-c-nav__list">
|
||||
{% for source in user_sources_loc %}
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{{ source.view_name }}"
|
||||
<a href="{{ source.url }}"
|
||||
class="pf-c-nav__link {% if source.url == request.get_full_path %} pf-m-current {% endif %}">
|
||||
{{ source.name }}
|
||||
</a>
|
||||
|
||||
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)
|
||||
@ -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
|
||||
|
||||
@ -56,7 +56,9 @@ class CertificateKeyPair(CreatedUpdatedModel):
|
||||
@property
|
||||
def fingerprint(self) -> str:
|
||||
"""Get SHA256 Fingerprint of certificate_data"""
|
||||
return hexlify(self.certificate.fingerprint(hashes.SHA256())).decode("utf-8")
|
||||
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Certificate-Key Pair {self.name} {self.fingerprint}"
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0005_provider_flows"),
|
||||
("passbook_flows", "0003_auto_20200523_1133"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -45,13 +45,13 @@ def create_default_authentication_flow(
|
||||
defaults={"name": "Welcome to passbook!",},
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
flow=flow, stage=identification_stage, defaults={"order": 0,},
|
||||
target=flow, stage=identification_stage, defaults={"order": 0,},
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
flow=flow, stage=password_stage, defaults={"order": 1,},
|
||||
target=flow, stage=password_stage, defaults={"order": 1,},
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
flow=flow, stage=login_stage, defaults={"order": 2,},
|
||||
target=flow, stage=login_stage, defaults={"order": 2,},
|
||||
)
|
||||
|
||||
|
||||
@ -73,7 +73,7 @@ def create_default_invalidation_flow(
|
||||
defaults={"name": "Logout",},
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
flow=flow,
|
||||
target=flow,
|
||||
stage=UserLogoutStage.objects.using(db_alias).first(),
|
||||
defaults={"order": 0,},
|
||||
)
|
||||
@ -82,7 +82,7 @@ def create_default_invalidation_flow(
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0001_initial"),
|
||||
("passbook_flows", "0007_auto_20200703_2059"),
|
||||
("passbook_stages_user_login", "0001_initial"),
|
||||
("passbook_stages_user_logout", "0001_initial"),
|
||||
("passbook_stages_password", "0001_initial"),
|
||||
@ -80,17 +80,17 @@ def create_default_source_enrollment_flow(
|
||||
)
|
||||
|
||||
binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
flow=flow, stage=prompt_stage, defaults={"order": 0}
|
||||
target=flow, stage=prompt_stage, defaults={"order": 0}
|
||||
)
|
||||
PolicyBinding.objects.using(db_alias).update_or_create(
|
||||
policy=prompt_policy, target=binding, defaults={"order": 0}
|
||||
)
|
||||
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
flow=flow, stage=user_write, defaults={"order": 1}
|
||||
target=flow, stage=user_write, defaults={"order": 1}
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
flow=flow, stage=user_login, defaults={"order": 2}
|
||||
target=flow, stage=user_login, defaults={"order": 2}
|
||||
)
|
||||
|
||||
|
||||
@ -129,14 +129,14 @@ def create_default_source_authentication_flow(
|
||||
name="default-source-authentication-login"
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
flow=flow, stage=user_login, defaults={"order": 0}
|
||||
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"),
|
||||
@ -34,14 +34,14 @@ def create_default_provider_authorization_flow(
|
||||
name="default-provider-authorization-consent"
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
flow=flow, stage=stage, defaults={"order": 0}
|
||||
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"),
|
||||
]
|
||||
|
||||
@ -79,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."""
|
||||
@ -123,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(
|
||||
@ -138,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"),)
|
||||
|
||||
@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpRequest
|
||||
from elasticapm import capture_span
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
@ -88,6 +89,7 @@ class FlowPlanner:
|
||||
self.allow_empty_flows = False
|
||||
self.flow = flow
|
||||
|
||||
@capture_span(name="FlowPlanner", span_type="flow.planner.plan")
|
||||
def plan(
|
||||
self, request: HttpRequest, default_context: Optional[Dict[str, Any]] = None
|
||||
) -> FlowPlan:
|
||||
@ -127,6 +129,7 @@ class FlowPlanner:
|
||||
raise EmptyFlowException()
|
||||
return plan
|
||||
|
||||
@capture_span(name="FlowPlanner", span_type="flow.planner.build_plan")
|
||||
def _build_plan(
|
||||
self,
|
||||
user: User,
|
||||
@ -146,7 +149,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
|
||||
|
||||
@ -25,13 +25,13 @@ def invalidate_flow_cache(sender, instance, **_):
|
||||
total = delete_cache_prefix(f"{cache_key(instance)}*")
|
||||
LOGGER.debug("Invalidating Flow cache", flow=instance, len=total)
|
||||
if isinstance(instance, FlowStageBinding):
|
||||
total = delete_cache_prefix(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):
|
||||
total = 0
|
||||
for binding in FlowStageBinding.objects.filter(stage=instance):
|
||||
prefix = cache_key(binding.flow)
|
||||
prefix = cache_key(binding.target)
|
||||
total += delete_cache_prefix(f"{prefix}*")
|
||||
LOGGER.debug("Invalidating Flow cache from Stage", stage=instance, len=total)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -4,6 +4,7 @@ from textwrap import indent
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from elasticapm import capture_span
|
||||
from requests import Session
|
||||
from structlog import get_logger
|
||||
|
||||
@ -68,6 +69,7 @@ class BaseEvaluator:
|
||||
full_expression += f"\nresult = handler({handler_signature})"
|
||||
return full_expression
|
||||
|
||||
@capture_span(name="BaseEvaluator", span_type="lib.evaluator.evaluate")
|
||||
def evaluate(self, expression_source: str) -> Any:
|
||||
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
|
||||
If any exception is raised during execution, it is raised.
|
||||
|
||||
@ -4,6 +4,7 @@ from botocore.client import ClientError
|
||||
from django.core.exceptions import DisallowedHost, ValidationError
|
||||
from django.db import InternalError, OperationalError, ProgrammingError
|
||||
from django_redis.exceptions import ConnectionInterrupted
|
||||
from elasticapm.transport.http import TransportException
|
||||
from redis.exceptions import RedisError
|
||||
from rest_framework.exceptions import APIException
|
||||
from structlog import get_logger
|
||||
@ -33,9 +34,10 @@ def before_send(event, hint):
|
||||
OSError,
|
||||
RedisError,
|
||||
SentryIgnoredException,
|
||||
TransportException,
|
||||
)
|
||||
if "exc_info" in hint:
|
||||
_exc_type, exc_value, _ = hint["exc_info"]
|
||||
_, exc_value, _ = hint["exc_info"]
|
||||
if isinstance(exc_value, ignored_classes):
|
||||
LOGGER.info("Supressing error %r", exc_value)
|
||||
return None
|
||||
|
||||
@ -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>"))
|
||||
|
||||
@ -23,4 +23,4 @@ def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]:
|
||||
Returns none if no IP Could be found"""
|
||||
if request:
|
||||
return _get_client_ip_from_meta(request.META)
|
||||
return ""
|
||||
return None
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -5,6 +5,7 @@ from typing import List, Optional
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpRequest
|
||||
from elasticapm import capture_span
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
@ -69,6 +70,7 @@ class PolicyEngine:
|
||||
if policy.__class__ == Policy:
|
||||
raise TypeError(f"Policy '{policy}' is root type")
|
||||
|
||||
@capture_span(name="PolicyEngine", span_type="policy.engine.build")
|
||||
def build(self) -> "PolicyEngine":
|
||||
"""Build task group"""
|
||||
for binding in self._iter_bindings():
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
# Generated by Django 3.0.7 on 2020-07-01 19:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("passbook_policies", "0002_auto_20200528_1647"),
|
||||
("passbook_core", "0003_default_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="GroupMembershipPolicy",
|
||||
fields=[
|
||||
(
|
||||
"policy_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="passbook_policies.Policy",
|
||||
),
|
||||
),
|
||||
(
|
||||
"group",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="passbook_core.Group",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Group Membership Policy",
|
||||
"verbose_name_plural": "Group Membership Policies",
|
||||
},
|
||||
bases=("passbook_policies.policy",),
|
||||
),
|
||||
]
|
||||
23
passbook/policies/group_membership/models.py
Normal file
23
passbook/policies/group_membership/models.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""user field matcher models"""
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Group
|
||||
from passbook.policies.models import Policy
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
|
||||
class GroupMembershipPolicy(Policy):
|
||||
"""Check that the user is member of the selected group."""
|
||||
|
||||
group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
|
||||
form = "passbook.policies.group_membership.forms.GroupMembershipPolicyForm"
|
||||
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists())
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Group Membership Policy")
|
||||
verbose_name_plural = _("Group Membership Policies")
|
||||
32
passbook/policies/group_membership/tests.py
Normal file
32
passbook/policies/group_membership/tests.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""evaluator tests"""
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from passbook.core.models import Group
|
||||
from passbook.policies.group_membership.models import GroupMembershipPolicy
|
||||
from passbook.policies.types import PolicyRequest
|
||||
|
||||
|
||||
class TestGroupMembershipPolicy(TestCase):
|
||||
"""GroupMembershipPolicy tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.request = PolicyRequest(user=get_anonymous_user())
|
||||
|
||||
def test_invalid(self):
|
||||
"""user not in group"""
|
||||
group = Group.objects.create(name="test")
|
||||
policy: GroupMembershipPolicy = GroupMembershipPolicy.objects.create(
|
||||
group=group
|
||||
)
|
||||
self.assertFalse(policy.passes(self.request).passing)
|
||||
|
||||
def test_valid(self):
|
||||
"""user in group"""
|
||||
group = Group.objects.create(name="test")
|
||||
group.user_set.add(get_anonymous_user())
|
||||
group.save()
|
||||
policy: GroupMembershipPolicy = GroupMembershipPolicy.objects.create(
|
||||
group=group
|
||||
)
|
||||
self.assertTrue(policy.passes(self.request).passing)
|
||||
@ -13,7 +13,7 @@ LOGGER = get_logger()
|
||||
|
||||
|
||||
class HaveIBeenPwendPolicy(Policy):
|
||||
"""Check if password is on HaveIBeenPwned's list by upload the first
|
||||
"""Check if password is on HaveIBeenPwned's list by uploading the first
|
||||
5 characters of the SHA1 Hash."""
|
||||
|
||||
allowed_count = models.IntegerField(default=0)
|
||||
|
||||
62
passbook/policies/mixins.py
Normal file
62
passbook/policies/mixins.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""passbook access helper classes"""
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
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 BaseMixin:
|
||||
"""Base Mixin class, used to annotate View Member variables"""
|
||||
|
||||
request: HttpRequest
|
||||
|
||||
|
||||
class PolicyAccessMixin(BaseMixin, AccessMixin):
|
||||
"""Mixin class for usage in Authorization views.
|
||||
Provider functions to check application access, etc"""
|
||||
|
||||
def handle_no_permission_authorized(self) -> HttpResponse:
|
||||
"""Function called when user has no permissions but is authorized"""
|
||||
return redirect("passbook_flows:denied")
|
||||
|
||||
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: Optional[User] = None
|
||||
) -> PolicyResult:
|
||||
"""Check if user has access to application."""
|
||||
user = user or self.request.user
|
||||
policy_engine = PolicyEngine(
|
||||
application, user or self.request.user, self.request
|
||||
)
|
||||
policy_engine.build()
|
||||
result = policy_engine.result
|
||||
LOGGER.debug(
|
||||
"AccessMixin user_has_access", user=user, app=application, result=result,
|
||||
)
|
||||
if not result.passing:
|
||||
for message in result.messages:
|
||||
messages.error(self.request, _(message))
|
||||
return result
|
||||
@ -1,4 +1,4 @@
|
||||
"""Source API Views"""
|
||||
"""Password Policy API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
@ -22,7 +22,7 @@ class PasswordPolicySerializer(ModelSerializer):
|
||||
|
||||
|
||||
class PasswordPolicyViewSet(ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
"""Password Policy Viewset"""
|
||||
|
||||
queryset = PasswordPolicy.objects.all()
|
||||
serializer_class = PasswordPolicySerializer
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user