Compare commits
23 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d51295db2 | |||
| 3bbded3555 | |||
| b3262e2a82 | |||
| 40614a65fc | |||
| 3cf558d594 | |||
| 812cc0d2f1 | |||
| e21ed92848 | |||
| 5184c4b7ef | |||
| 2c07859b68 | |||
| ae6304c05e | |||
| 501683e3cb | |||
| cc8afa8706 | |||
| 17a9e02bc0 | |||
| 6a669992a8 | |||
| 7ea5c22b6c | |||
| b11d6a5891 | |||
| 49830367a7 | |||
| e69ca5a229 | |||
| a57d21f5e8 | |||
| c7026407c6 | |||
| 69eecd6b60 | |||
| 810f10edfe | |||
| 1c57128f11 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.10.0-stable
|
||||
current_version = 0.10.1-stable
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
[run]
|
||||
source = passbook
|
||||
relative_files = true
|
||||
omit =
|
||||
*/asgi.py
|
||||
manage.py
|
||||
|
||||
17
.github/workflows/release.yml
vendored
17
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.10.0-stable
|
||||
-t beryju/passbook:0.10.1-stable
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.10.0-stable
|
||||
run: docker push beryju/passbook:0.10.1-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook:latest
|
||||
build-proxy:
|
||||
@ -48,11 +48,11 @@ jobs:
|
||||
cd proxy
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook-proxy:0.10.0-stable \
|
||||
-t beryju/passbook-proxy:0.10.1-stable \
|
||||
-t beryju/passbook-proxy:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-proxy:0.10.0-stable
|
||||
run: docker push beryju/passbook-proxy:0.10.1-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-proxy:latest
|
||||
build-static:
|
||||
@ -77,11 +77,11 @@ jobs:
|
||||
run: docker build
|
||||
--no-cache
|
||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||
-t beryju/passbook-static:0.10.0-stable
|
||||
-t beryju/passbook-static:0.10.1-stable
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.10.0-stable
|
||||
run: docker push beryju/passbook-static:0.10.1-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
@ -93,6 +93,9 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
sudo apt-get install -y pwgen
|
||||
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
||||
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||
docker-compose pull -q
|
||||
docker-compose up --no-start
|
||||
docker-compose start postgresql redis
|
||||
@ -111,5 +114,5 @@ jobs:
|
||||
SENTRY_PROJECT: passbook
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 0.10.0-stable
|
||||
tagName: 0.10.1-stable
|
||||
environment: beryjuorg-prod
|
||||
|
||||
5
.github/workflows/tag.yml
vendored
5
.github/workflows/tag.yml
vendored
@ -13,7 +13,10 @@ jobs:
|
||||
- uses: actions/checkout@master
|
||||
- name: Pre-release test
|
||||
run: |
|
||||
export PASSBOOK_TAG=latest
|
||||
sudo apt-get install -y pwgen
|
||||
echo "PASSBOOK_TAG=latest" >> .env
|
||||
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
||||
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||
docker-compose pull -q
|
||||
docker build \
|
||||
--no-cache \
|
||||
|
||||
2
Makefile
2
Makefile
@ -1,7 +1,7 @@
|
||||
all: lint-fix lint coverage gen
|
||||
|
||||
coverage:
|
||||
coverage run --concurrency=multiprocessing manage.py test --failfast
|
||||
coverage run --concurrency=multiprocessing manage.py test --failfast -v 3
|
||||
coverage combine
|
||||
coverage html
|
||||
coverage report
|
||||
|
||||
86
Pipfile.lock
generated
86
Pipfile.lock
generated
@ -74,11 +74,11 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:20edd03ae4c4e141b0d8a9a9afc773af4345d54b68202b6aa502956b57b18b3f",
|
||||
"sha256:b596a80181fecd775ccc009286400f4d785136f250967895cb34beeeef65eb1f"
|
||||
"sha256:79e95f428c485ea817969a78e77a311d2ec4d82e0955639d6126189c990ddad3",
|
||||
"sha256:d8ca27ee13deeb1a9e79f2fe5f923effa60947ed49bbdfbc2a9f5790aef64217"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.14.59"
|
||||
"version": "==1.14.60"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
@ -327,11 +327,11 @@
|
||||
},
|
||||
"django-storages": {
|
||||
"hashes": [
|
||||
"sha256:1e37da57678e6cf1e9914f84099a305323e4e1f261afe54fdb703cae7aa6fbc3",
|
||||
"sha256:36ed8dab33d761954498189592ce005920095fcbc02dab4184eb51393c370991"
|
||||
"sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c",
|
||||
"sha256:652275ab7844538c462b62810276c0244866f345878256a9e0e86f5b1283ae18"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.10"
|
||||
"version": "==1.10.1"
|
||||
},
|
||||
"djangorestframework": {
|
||||
"hashes": [
|
||||
@ -835,9 +835,9 @@
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:27515d2d5db0629c7dadf6fbe76973eb56f098c1b01d36de42eb69220d2c19e4"
|
||||
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
|
||||
],
|
||||
"version": "==0.17.2"
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
@ -1269,43 +1269,43 @@
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb",
|
||||
"sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3",
|
||||
"sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716",
|
||||
"sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034",
|
||||
"sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3",
|
||||
"sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8",
|
||||
"sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0",
|
||||
"sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f",
|
||||
"sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4",
|
||||
"sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962",
|
||||
"sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d",
|
||||
"sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b",
|
||||
"sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4",
|
||||
"sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3",
|
||||
"sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258",
|
||||
"sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59",
|
||||
"sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01",
|
||||
"sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd",
|
||||
"sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b",
|
||||
"sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d",
|
||||
"sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89",
|
||||
"sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd",
|
||||
"sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b",
|
||||
"sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d",
|
||||
"sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46",
|
||||
"sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546",
|
||||
"sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082",
|
||||
"sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b",
|
||||
"sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4",
|
||||
"sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8",
|
||||
"sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811",
|
||||
"sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd",
|
||||
"sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651",
|
||||
"sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"
|
||||
"sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
|
||||
"sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
|
||||
"sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
|
||||
"sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
|
||||
"sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
|
||||
"sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
|
||||
"sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
|
||||
"sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
|
||||
"sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
|
||||
"sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
|
||||
"sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
|
||||
"sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
|
||||
"sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
|
||||
"sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
|
||||
"sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
|
||||
"sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
|
||||
"sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
|
||||
"sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
|
||||
"sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
|
||||
"sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
|
||||
"sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
|
||||
"sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
|
||||
"sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
|
||||
"sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
|
||||
"sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
|
||||
"sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
|
||||
"sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
|
||||
"sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
|
||||
"sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
|
||||
"sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
|
||||
"sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
|
||||
"sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
|
||||
"sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
|
||||
"sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.2.1"
|
||||
"version": "==5.3"
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
|
||||
@ -20,7 +20,7 @@ wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
||||
# Optionally enable Error-reporting
|
||||
# export PASSBOOK_ERROR_REPORTING=true
|
||||
# Optionally deploy a different version
|
||||
# export PASSBOOK_TAG=0.10.0-stable
|
||||
# export PASSBOOK_TAG=0.10.1-stable
|
||||
# If this is a productive installation, set a different PostgreSQL Password
|
||||
# export PG_PASS=$(pwgen 40 1)
|
||||
docker-compose pull
|
||||
|
||||
@ -139,7 +139,7 @@ stages:
|
||||
displayName: Run full test suite
|
||||
inputs:
|
||||
script: |
|
||||
pipenv run coverage run ./manage.py test passbook
|
||||
pipenv run coverage run ./manage.py test passbook -v 3
|
||||
mkdir output-unittest
|
||||
mv unittest.xml output-unittest/unittest.xml
|
||||
mv .coverage output-unittest/coverage
|
||||
@ -181,7 +181,7 @@ stages:
|
||||
- task: CmdLine@2
|
||||
displayName: Run full test suite
|
||||
inputs:
|
||||
script: pipenv run coverage run ./manage.py test e2e
|
||||
script: pipenv run coverage run ./manage.py test e2e -v 3
|
||||
- task: CmdLine@2
|
||||
displayName: Prepare unittests and coverage for upload
|
||||
inputs:
|
||||
@ -225,11 +225,9 @@ stages:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
find .
|
||||
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
|
||||
pipenv run coverage xml
|
||||
pipenv run coverage html
|
||||
find .
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: 'Cobertura'
|
||||
|
||||
@ -14,6 +14,8 @@ services:
|
||||
- POSTGRES_DB=passbook
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
env_file:
|
||||
- .env
|
||||
redis:
|
||||
image: redis
|
||||
networks:
|
||||
@ -21,13 +23,12 @@ services:
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
server:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-stable}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.1-stable}
|
||||
command: server
|
||||
environment:
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
|
||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
PASSBOOK_LOG_LEVEL: debug
|
||||
ports:
|
||||
- 8000
|
||||
@ -37,8 +38,10 @@ services:
|
||||
- traefik.port=8000
|
||||
- traefik.docker.network=internal
|
||||
- traefik.frontend.rule=PathPrefix:/
|
||||
env_file:
|
||||
- .env
|
||||
worker:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-stable}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.1-stable}
|
||||
command: worker
|
||||
networks:
|
||||
- internal
|
||||
@ -46,12 +49,13 @@ services:
|
||||
- traefik.enable=false
|
||||
environment:
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
|
||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
PASSBOOK_LOG_LEVEL: debug
|
||||
env_file:
|
||||
- .env
|
||||
static:
|
||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.0-stable}
|
||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.1-stable}
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
|
||||
@ -11,14 +11,21 @@ This installation method is for test-setups and small-scale productive setups.
|
||||
|
||||
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). Place it in a directory of your choice.
|
||||
|
||||
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING=true >> .env`
|
||||
|
||||
To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.1-stable >> .env`
|
||||
|
||||
If this is a fresh passbook install run the following commands to generate a password:
|
||||
|
||||
```
|
||||
sudo apt-get install -y pwgen
|
||||
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
||||
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||
```
|
||||
|
||||
Afterwards, run these commands to finish
|
||||
|
||||
```
|
||||
wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
||||
# Optionally enable Error-reporting
|
||||
# export PASSBOOK_ERROR_REPORTING=true
|
||||
# Optionally deploy a different version
|
||||
# export PASSBOOK_TAG=0.10.0-stable
|
||||
# If this is a productive installation, set a different PostgreSQL Password
|
||||
# export PG_PASS=$(pwgen 40 1)
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker-compose run --rm server migrate
|
||||
|
||||
@ -11,7 +11,7 @@ This installation automatically applies database migrations on startup. After th
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.10.0-stable
|
||||
tag: 0.10.1-stable
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@ Use the following manifest, replacing all values surrounded with `__`.
|
||||
Afterwards, configure the proxy provider to connect to `<service name>.<namespace>.svc.cluster.local`, and update your Ingress to connect to the `passbook-outpost` service.
|
||||
|
||||
```yaml
|
||||
api_version: v1
|
||||
kind: secret
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
@ -14,65 +14,14 @@ metadata:
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
name: passbook-outpost-api
|
||||
string_data:
|
||||
stringData:
|
||||
passbook_host: '__PASSBOOK_URL__'
|
||||
passbook_host_insecure: 'true'
|
||||
token: '__PASSBOOK_TOKEN__'
|
||||
type: Opaque
|
||||
---
|
||||
api_version: apps/v1
|
||||
kind: deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
name: passbook-outpost
|
||||
spec:
|
||||
selector:
|
||||
match_labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: PASSBOOK_HOST
|
||||
value_from:
|
||||
secret_key_ref:
|
||||
key: passbook_host
|
||||
name: passbook-outpost-api
|
||||
- name: PASSBOOK_TOKEN
|
||||
value_from:
|
||||
secret_key_ref:
|
||||
key: token
|
||||
name: passbook-outpost-api
|
||||
- name: PASSBOOK_INSECURE
|
||||
value_from:
|
||||
secret_key_ref:
|
||||
key: passbook_host_insecure
|
||||
name: passbook-outpost-api
|
||||
image: beryju/passbook-proxy:0.10.0
|
||||
name: proxy
|
||||
ports:
|
||||
- containerPort: 4180
|
||||
name: http
|
||||
protocol: TCP
|
||||
- containerPort: 4443
|
||||
name: http
|
||||
protocol: TCP
|
||||
---
|
||||
api_version: v1
|
||||
kind: service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
@ -96,4 +45,55 @@ spec:
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
name: passbook-outpost
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: PASSBOOK_HOST
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: passbook_host
|
||||
name: passbook-outpost-api
|
||||
- name: PASSBOOK_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: token
|
||||
name: passbook-outpost-api
|
||||
- name: PASSBOOK_INSECURE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: passbook_host_insecure
|
||||
name: passbook-outpost-api
|
||||
image: beryju/passbook-proxy:0.10.0-stable
|
||||
name: proxy
|
||||
ports:
|
||||
- containerPort: 4180
|
||||
name: http
|
||||
protocol: TCP
|
||||
- containerPort: 4443
|
||||
name: https
|
||||
protocol: TCP
|
||||
```
|
||||
|
||||
11
docs/troubleshooting/access.md
Normal file
11
docs/troubleshooting/access.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Troubleshooting access problems
|
||||
|
||||
## I get an access denied error when trying to access an application.
|
||||
|
||||
If your user is a superuser, or has the attribute `passbook_user_debug` set to true:
|
||||
|
||||

|
||||
|
||||
Afterwards, try to access the application again. You will now see a message explaining which policy denied you access:
|
||||
|
||||

|
||||
BIN
docs/troubleshooting/access_denied_message.png
Normal file
BIN
docs/troubleshooting/access_denied_message.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
BIN
docs/troubleshooting/passbook_user_debug.png
Normal file
BIN
docs/troubleshooting/passbook_user_debug.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@ -1,8 +1,8 @@
|
||||
apiVersion: v2
|
||||
appVersion: "0.10.0-stable"
|
||||
appVersion: "0.10.1-stable"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.10.0-stable"
|
||||
version: "0.10.1-stable"
|
||||
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
|
||||
@ -22,10 +22,27 @@ spec:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.beryju.org/component: web
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/name
|
||||
operator: In
|
||||
values:
|
||||
- {{ include "passbook.name" . }}
|
||||
- key: app.kubernetes.io/instance
|
||||
operator: In
|
||||
values:
|
||||
- {{ .Release.Name }}
|
||||
- key: k8s.passbook.beryju.org/component
|
||||
operator: In
|
||||
values:
|
||||
- web
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
initContainers:
|
||||
- name: passbook-database-migrations
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: Always
|
||||
args: [migrate]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
@ -50,7 +67,6 @@ spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: Always
|
||||
args: [server]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
|
||||
@ -22,6 +22,24 @@ spec:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.beryju.org/component: worker
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/name
|
||||
operator: In
|
||||
values:
|
||||
- {{ include "passbook.name" . }}
|
||||
- key: app.kubernetes.io/instance
|
||||
operator: In
|
||||
values:
|
||||
- {{ .Release.Name }}
|
||||
- key: k8s.passbook.beryju.org/component
|
||||
operator: In
|
||||
values:
|
||||
- worker
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.10.0-stable
|
||||
tag: 0.10.1-stable
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
|
||||
@ -55,6 +55,8 @@ nav:
|
||||
- Upgrading:
|
||||
- to 0.9: upgrading/to-0.9.md
|
||||
- to 0.10: upgrading/to-0.10.md
|
||||
- Troubleshooting:
|
||||
- Access problems: troubleshooting/access.md
|
||||
|
||||
repo_name: "BeryJu/passbook"
|
||||
repo_url: https://github.com/BeryJu/passbook
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.10.0-stable"
|
||||
__version__ = "0.10.1-stable"
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ policy.name }}</div>
|
||||
{% if not policy.bindings.exists %}
|
||||
{% if not policy.bindings.exists and not policy.promptstage_set.exists %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i>
|
||||
<small>{% trans 'Warning: Policy is not assigned.' %}</small>
|
||||
{% else %}
|
||||
|
||||
@ -58,7 +58,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
application=None
|
||||
)
|
||||
kwargs["policies_without_binding"] = len(
|
||||
Policy.objects.filter(bindings__isnull=True)
|
||||
Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True)
|
||||
)
|
||||
kwargs["cached_policies"] = len(cache.keys("policy_*"))
|
||||
kwargs["cached_flows"] = len(cache.keys("flow_*"))
|
||||
|
||||
@ -29,7 +29,16 @@ class ApplicationForm(forms.ModelForm):
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"meta_launch_url": forms.TextInput(),
|
||||
"meta_launch_url": forms.TextInput(
|
||||
attrs={
|
||||
"placeholder": _(
|
||||
(
|
||||
"If left empty, passbook will try to extract the launch URL "
|
||||
"based on the selected provider."
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
"meta_icon_url": forms.TextInput(),
|
||||
"meta_publisher": forms.TextInput(),
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ from passbook.lib.models import CreatedUpdatedModel
|
||||
from passbook.policies.models import PolicyBindingModel
|
||||
|
||||
LOGGER = get_logger()
|
||||
PASSBOOK_USER_DEBUG = "passbook_user_debug"
|
||||
|
||||
|
||||
def default_token_duration():
|
||||
@ -92,6 +93,12 @@ class Provider(models.Model):
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@property
|
||||
def launch_url(self) -> Optional[str]:
|
||||
"""URL to this provider and initiate authorization for the user.
|
||||
Can return None for providers that are not URL-based"""
|
||||
return None
|
||||
|
||||
def form(self) -> Type[ModelForm]:
|
||||
"""Return Form class used to edit this object"""
|
||||
raise NotImplementedError
|
||||
@ -119,6 +126,14 @@ class Application(PolicyBindingModel):
|
||||
meta_description = models.TextField(default="", blank=True)
|
||||
meta_publisher = models.TextField(default="", blank=True)
|
||||
|
||||
def get_launch_url(self) -> Optional[str]:
|
||||
"""Get launch URL if set, otherwise attempt to get launch URL based on provider."""
|
||||
if self.meta_launch_url:
|
||||
return self.meta_launch_url
|
||||
if self.provider:
|
||||
return self.provider.launch_url
|
||||
return None
|
||||
|
||||
def get_provider(self) -> Optional[Provider]:
|
||||
"""Get casted provider instance"""
|
||||
if not self.provider:
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff2' %}" as="font" type="font/woff2">
|
||||
<link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff' %}" as="font" type="font/woff">
|
||||
<link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff2' %}" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff' %}" as="font" type="font/woff" crossorigin>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>{% block title %}{% trans title|default:config.passbook.branding.title %}{% endblock %}</title>
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
{% extends 'login/base_full.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block card_title %}
|
||||
{% trans 'Permission denied' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'Permission denied' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% csrf_token %}
|
||||
{% include 'partials/form.html' %}
|
||||
<div class="pf-c-form__group">
|
||||
<p>
|
||||
<i class="pf-icon pf-icon-error-circle-o"></i>
|
||||
{% trans 'Access denied' %}
|
||||
</p>
|
||||
</div>
|
||||
{% if 'back' in request.GET %}
|
||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
@ -24,7 +24,7 @@
|
||||
{% 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">
|
||||
<a href="{{ app.get_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>
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
"""passbook util view tests"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.core.views.utils import PermissionDeniedView
|
||||
|
||||
|
||||
class TestUtilViews(TestCase):
|
||||
"""Test Utility Views"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_superuser(
|
||||
username="unittest user",
|
||||
email="unittest@example.com",
|
||||
password="".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
||||
for _ in range(8)
|
||||
),
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_permission_denied_view(self):
|
||||
"""Test PermissionDeniedView"""
|
||||
request = self.factory.get("something")
|
||||
request.user = self.user
|
||||
response = PermissionDeniedView.as_view()(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@ -1,14 +0,0 @@
|
||||
"""passbook core utils view"""
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class PermissionDeniedView(TemplateView):
|
||||
"""Generic Permission denied view"""
|
||||
|
||||
template_name = "login/denied.html"
|
||||
title = _("Permission denied.")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["title"] = self.title
|
||||
return super().get_context_data(**kwargs)
|
||||
@ -2,8 +2,8 @@
|
||||
|
||||
|
||||
class FlowNonApplicableException(BaseException):
|
||||
"""Exception raised when a Flow does not apply to a user."""
|
||||
"""Flow does not apply to current user (denied by policy)."""
|
||||
|
||||
|
||||
class EmptyFlowException(BaseException):
|
||||
"""Exception raised when a Flow Plan is empty"""
|
||||
"""Flow has no stages."""
|
||||
|
||||
57
passbook/flows/templates/flows/denied.html
Normal file
57
passbook/flows/templates/flows/denied.html
Normal file
@ -0,0 +1,57 @@
|
||||
{% extends 'login/base_full.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block card_title %}
|
||||
{% trans 'Permission denied' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'Permission denied' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% csrf_token %}
|
||||
{% include 'partials/form.html' %}
|
||||
<div class="pf-c-form__group">
|
||||
<p>
|
||||
<i class="pf-icon pf-icon-error-circle-o"></i>
|
||||
{% trans 'Access denied' %}
|
||||
</p>
|
||||
{% if error %}
|
||||
<hr>
|
||||
<p>
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if policy_result %}
|
||||
<hr>
|
||||
<em>
|
||||
{% trans 'Explanation:' %}
|
||||
</em>
|
||||
<ul class="pf-c-list">
|
||||
{% for source_result in policy_result.source_results %}
|
||||
<li>
|
||||
{% blocktrans with name=source_result.source_policy.name result=source_result.passing %}
|
||||
Policy '{{ name }}' returned result '{{ result }}'
|
||||
{% endblocktrans %}
|
||||
{% if source_result.messages %}
|
||||
<ul class="pf-c-list">
|
||||
{% for message in source_result.messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if 'back' in request.GET %}
|
||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
@ -5,7 +5,6 @@ from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from passbook.flows.markers import ReevaluateMarker, StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import FlowPlan
|
||||
@ -48,8 +47,8 @@ class TestFlowExecutor(TestCase):
|
||||
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
||||
),
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(cancel_mock.call_count, 1)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(cancel_mock.call_count, 2)
|
||||
|
||||
@patch(
|
||||
"passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE,
|
||||
@ -66,8 +65,11 @@ class TestFlowExecutor(TestCase):
|
||||
response = self.client.get(
|
||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertInHTML(FlowNonApplicableException.__doc__, response.rendered_content)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
{"type": "redirect", "to": reverse("passbook_flows:denied")},
|
||||
)
|
||||
|
||||
def test_invalid_empty_flow(self):
|
||||
"""Tests that an empty flow returns the correct error message"""
|
||||
@ -81,8 +83,11 @@ class TestFlowExecutor(TestCase):
|
||||
response = self.client.get(
|
||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertInHTML(EmptyFlowException.__doc__, response.rendered_content)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
{"type": "redirect", "to": reverse("passbook_flows:denied")},
|
||||
)
|
||||
|
||||
def test_invalid_flow_redirect(self):
|
||||
"""Tests that an invalid flow still redirects"""
|
||||
|
||||
@ -12,18 +12,18 @@ from django.http import (
|
||||
from django.shortcuts import get_object_or_404, redirect, render, reverse
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from django.views.generic import TemplateView, View
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.audit.models import cleanse_dict
|
||||
from passbook.core.views.utils import PermissionDeniedView
|
||||
from passbook.core.models import PASSBOOK_USER_DEBUG
|
||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from passbook.flows.models import Flow, FlowDesignation, Stage
|
||||
from passbook.flows.planner import FlowPlan, FlowPlanner
|
||||
from passbook.lib.utils.reflection import class_to_path
|
||||
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||
from passbook.lib.views import bad_request_message
|
||||
|
||||
LOGGER = get_logger()
|
||||
# Argument used to redirect user after login
|
||||
@ -31,6 +31,8 @@ NEXT_ARG_NAME = "next"
|
||||
SESSION_KEY_PLAN = "passbook_flows_plan"
|
||||
SESSION_KEY_APPLICATION_PRE = "passbook_flows_application_pre"
|
||||
SESSION_KEY_GET = "passbook_flows_get"
|
||||
SESSION_KEY_DENIED_ERROR = "passbook_flows_denied_error"
|
||||
SESSION_KEY_DENIED_POLICY_RESULT = "passbook_flows_denied_policy_result"
|
||||
|
||||
|
||||
@method_decorator(xframe_options_sameorigin, name="dispatch")
|
||||
@ -54,7 +56,9 @@ class FlowExecutorView(View):
|
||||
LOGGER.debug("f(exec): Redirecting to next on fail")
|
||||
return redirect(self.request.GET.get(NEXT_ARG_NAME))
|
||||
message = exc.__doc__ if exc.__doc__ else str(exc)
|
||||
return bad_request_message(self.request, message)
|
||||
return to_stage_response(
|
||||
self.request, self.stage_invalid(error_message=message)
|
||||
)
|
||||
|
||||
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
||||
# Early check if theres an active Plan for the current session
|
||||
@ -193,11 +197,19 @@ class FlowExecutorView(View):
|
||||
)
|
||||
return self._flow_done()
|
||||
|
||||
def stage_invalid(self) -> HttpResponse:
|
||||
def stage_invalid(self, error_message: Optional[str] = None) -> HttpResponse:
|
||||
"""Callback used stage when data is correct but a policy denies access
|
||||
or the user account is disabled."""
|
||||
or the user account is disabled.
|
||||
|
||||
Optionally, an exception can be passed, which will be shown if the current user
|
||||
is a superuser."""
|
||||
LOGGER.debug("f(exec): Stage invalid", flow_slug=self.flow.slug)
|
||||
self.cancel()
|
||||
if self.request.user and self.request.user.is_authenticated:
|
||||
if self.request.user.is_superuser or self.request.user.attributes.get(
|
||||
PASSBOOK_USER_DEBUG, False
|
||||
):
|
||||
self.request.session[SESSION_KEY_DENIED_ERROR] = error_message
|
||||
return redirect_with_qs("passbook_flows:denied", self.request.GET)
|
||||
|
||||
def cancel(self):
|
||||
@ -212,9 +224,22 @@ class FlowExecutorView(View):
|
||||
del self.request.session[key]
|
||||
|
||||
|
||||
class FlowPermissionDeniedView(PermissionDeniedView):
|
||||
class FlowPermissionDeniedView(TemplateView):
|
||||
"""User could not be authenticated"""
|
||||
|
||||
template_name = "flows/denied.html"
|
||||
title = _("Permission denied.")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["title"] = self.title
|
||||
if SESSION_KEY_DENIED_ERROR in self.request.session:
|
||||
kwargs["error"] = self.request.session[SESSION_KEY_DENIED_ERROR]
|
||||
if SESSION_KEY_DENIED_POLICY_RESULT in self.request.session:
|
||||
kwargs["policy_result"] = self.request.session[
|
||||
SESSION_KEY_DENIED_POLICY_RESULT
|
||||
]
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class FlowExecutorShellView(TemplateView):
|
||||
"""Executor Shell view, loads a dummy card with a spinner
|
||||
|
||||
@ -10,6 +10,7 @@ redis:
|
||||
password: ''
|
||||
cache_db: 0
|
||||
message_queue_db: 1
|
||||
ws_db: 2
|
||||
|
||||
debug: false
|
||||
log_level: info
|
||||
|
||||
@ -27,12 +27,12 @@ class CreateAssignPermView(CreateView):
|
||||
|
||||
|
||||
def bad_request_message(
|
||||
request: HttpRequest, message: str, title="Bad Request"
|
||||
request: HttpRequest,
|
||||
message: str,
|
||||
title="Bad Request",
|
||||
template="error/generic.html",
|
||||
) -> TemplateResponse:
|
||||
"""Return generic error page with message, with status code set to 400"""
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"error/generic.html",
|
||||
{"message": message, "card_title": _(title)},
|
||||
status=400,
|
||||
request, template, {"message": message, "card_title": _(title)}, status=400,
|
||||
)
|
||||
|
||||
@ -7,9 +7,10 @@ from uuid import uuid4
|
||||
from dacite import from_dict
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.db.models.base import Model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from guardian.models import UserObjectPermission
|
||||
from guardian.shortcuts import assign_perm
|
||||
|
||||
from passbook.core.models import Provider, Token, TokenIntents, User
|
||||
@ -104,24 +105,24 @@ class Outpost(models.Model):
|
||||
return datetime.fromtimestamp(value)
|
||||
return None
|
||||
|
||||
def _create_user(self) -> User:
|
||||
"""Create user and assign permissions for all required objects"""
|
||||
user: User = User.objects.create(username=f"pb-outpost-{self.uuid.hex}")
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
for model in self.get_required_objects():
|
||||
assign_perm(
|
||||
f"{model._meta.app_label}.view_{model._meta.model_name}", user, model
|
||||
)
|
||||
return user
|
||||
|
||||
@property
|
||||
def user(self) -> User:
|
||||
"""Get/create user with access to all required objects"""
|
||||
user = User.objects.filter(username=f"pb-outpost-{self.uuid.hex}")
|
||||
if user.exists():
|
||||
return user.first()
|
||||
return self._create_user()
|
||||
users = User.objects.filter(username=f"pb-outpost-{self.uuid.hex}")
|
||||
if not users.exists():
|
||||
user: User = User.objects.create(username=f"pb-outpost-{self.uuid.hex}")
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
else:
|
||||
user = users.first()
|
||||
# To ensure the user only has the correct permissions, we delete all of them and re-add
|
||||
# the ones the user needs
|
||||
with transaction.atomic():
|
||||
UserObjectPermission.objects.filter(user=user).delete()
|
||||
for model in self.get_required_objects():
|
||||
code_name = f"{model._meta.app_label}.view_{model._meta.model_name}"
|
||||
assign_perm(code_name, user, model)
|
||||
return user
|
||||
|
||||
@property
|
||||
def token(self) -> Token:
|
||||
|
||||
@ -41,7 +41,7 @@ def outpost_send_update(model_class: str, model_pk: Any):
|
||||
"""Send outpost update to all registered outposts, irregardless to which passbook
|
||||
instance they are connected"""
|
||||
model = path_to_class(model_class)
|
||||
outpost_model: OutpostModel = model.objects.get(model_pk)
|
||||
outpost_model: OutpostModel = model.objects.get(pk=model_pk)
|
||||
for outpost in outpost_model.outpost_set.all():
|
||||
channel_layer = get_channel_layer()
|
||||
for channel in outpost.channels:
|
||||
|
||||
60
passbook/outposts/tests.py
Normal file
60
passbook/outposts/tests.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""outpost tests"""
|
||||
from django.test import TestCase
|
||||
from guardian.models import UserObjectPermission
|
||||
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
|
||||
from passbook.providers.proxy.models import ProxyProvider
|
||||
|
||||
|
||||
class OutpostTests(TestCase):
|
||||
"""Outpost Tests"""
|
||||
|
||||
def test_service_account_permissions(self):
|
||||
"""Test that the service account has correct permissions"""
|
||||
provider: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="test",
|
||||
internal_host="http://localhost",
|
||||
external_host="http://localhost",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name="test",
|
||||
type=OutpostType.PROXY,
|
||||
deployment_type=OutpostDeploymentType.CUSTOM,
|
||||
)
|
||||
|
||||
# Before we add a provider, the user should only have access to the outpost
|
||||
permissions = UserObjectPermission.objects.filter(user=outpost.user)
|
||||
self.assertEqual(len(permissions), 1)
|
||||
self.assertEqual(permissions[0].object_pk, str(outpost.pk))
|
||||
|
||||
# We add a provider, user should only have access to outpost and provider
|
||||
outpost.providers.add(provider)
|
||||
outpost.save()
|
||||
permissions = UserObjectPermission.objects.filter(user=outpost.user).order_by(
|
||||
"content_type__model"
|
||||
)
|
||||
self.assertEqual(len(permissions), 2)
|
||||
self.assertEqual(permissions[0].object_pk, str(outpost.pk))
|
||||
self.assertEqual(permissions[1].object_pk, str(provider.pk))
|
||||
|
||||
# Provider requires a certificate-key-pair, user should have permissions for it
|
||||
keypair = CertificateKeyPair.objects.first()
|
||||
provider.certificate = keypair
|
||||
provider.save()
|
||||
permissions = UserObjectPermission.objects.filter(user=outpost.user).order_by(
|
||||
"content_type__model"
|
||||
)
|
||||
self.assertEqual(len(permissions), 3)
|
||||
self.assertEqual(permissions[0].object_pk, str(keypair.pk))
|
||||
self.assertEqual(permissions[1].object_pk, str(outpost.pk))
|
||||
self.assertEqual(permissions[2].object_pk, str(provider.pk))
|
||||
|
||||
# Remove provider from outpost, user should only have access to outpost
|
||||
outpost.providers.remove(provider)
|
||||
outpost.save()
|
||||
permissions = UserObjectPermission.objects.filter(user=outpost.user)
|
||||
self.assertEqual(len(permissions), 1)
|
||||
self.assertEqual(permissions[0].object_pk, str(outpost.pk))
|
||||
@ -68,7 +68,7 @@ class PolicyEngine:
|
||||
|
||||
def _check_policy_type(self, policy: Policy):
|
||||
"""Check policy type, make sure it's not the root class as that has no logic implemented"""
|
||||
# policy_type = type(policy)
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
if policy.__class__ == Policy:
|
||||
raise TypeError(f"Policy '{policy}' is root type")
|
||||
|
||||
@ -109,19 +109,25 @@ class PolicyEngine:
|
||||
@property
|
||||
def result(self) -> PolicyResult:
|
||||
"""Get policy-checking result"""
|
||||
messages: List[str] = []
|
||||
process_results: List[PolicyResult] = [
|
||||
x.result for x in self.__processes if x.result
|
||||
]
|
||||
final_result = PolicyResult(False)
|
||||
final_result.messages = []
|
||||
final_result.source_results = list(process_results + self.__cached_policies)
|
||||
for result in process_results + self.__cached_policies:
|
||||
LOGGER.debug(
|
||||
"P_ENG: result", passing=result.passing, messages=result.messages
|
||||
)
|
||||
if result.messages:
|
||||
messages += result.messages
|
||||
final_result.messages.extend(result.messages)
|
||||
if not result.passing:
|
||||
return PolicyResult(False, *messages)
|
||||
return PolicyResult(True, *messages)
|
||||
final_result.messages = tuple(final_result.messages)
|
||||
final_result.passing = False
|
||||
return final_result
|
||||
final_result.messages = tuple(final_result.messages)
|
||||
final_result.passing = True
|
||||
return final_result
|
||||
|
||||
@property
|
||||
def passing(self) -> bool:
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<label for="" class="pf-c-form__label"></label>
|
||||
<div class="c-form__horizontal-group">
|
||||
<p>
|
||||
Expression using Python. See <a href="https://passbook.beryju.org/policies/expression/">here</a> for a list of all variables.
|
||||
Expression using Python. See <a target="_blank" href="https://passbook.beryju.org/policies/expression/">here</a> for a list of all variables.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,10 @@ from django.utils.translation import gettext as _
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Application, Provider, User
|
||||
from passbook.flows.views import SESSION_KEY_APPLICATION_PRE
|
||||
from passbook.flows.views import (
|
||||
SESSION_KEY_APPLICATION_PRE,
|
||||
SESSION_KEY_DENIED_POLICY_RESULT,
|
||||
)
|
||||
from passbook.policies.engine import PolicyEngine
|
||||
from passbook.policies.types import PolicyResult
|
||||
|
||||
@ -36,8 +39,12 @@ class PolicyAccessMixin(BaseMixin, AccessMixin):
|
||||
self.get_redirect_field_name(),
|
||||
)
|
||||
|
||||
def handle_no_permission_authorized(self) -> HttpResponse:
|
||||
"""Function called when user has no permissions but is authorized"""
|
||||
def handle_no_permission_authenticated(
|
||||
self, result: Optional[PolicyResult] = None
|
||||
) -> HttpResponse:
|
||||
"""Function called when user has no permissions but is authenticated"""
|
||||
if result:
|
||||
self.request.session[SESSION_KEY_DENIED_POLICY_RESULT] = result
|
||||
# TODO: Remove this URL and render the view instead
|
||||
return redirect("passbook_flows:denied")
|
||||
|
||||
|
||||
@ -63,6 +63,7 @@ class PolicyProcess(Process):
|
||||
except PolicyException as exc:
|
||||
LOGGER.debug("P_ENG(proc): error", exc=exc)
|
||||
policy_result = PolicyResult(False, str(exc))
|
||||
policy_result.source_policy = self.binding.policy
|
||||
# Invert result if policy.negate is set
|
||||
if self.binding.negate:
|
||||
policy_result.passing = not policy_result.passing
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
"""policy structures"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||
|
||||
from django.db.models import Model
|
||||
from django.http import HttpRequest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from passbook.core.models import User
|
||||
from passbook.policies.models import Policy
|
||||
|
||||
|
||||
class PolicyRequest:
|
||||
@ -34,9 +35,14 @@ class PolicyResult:
|
||||
passing: bool
|
||||
messages: Tuple[str, ...]
|
||||
|
||||
source_policy: Optional[Policy]
|
||||
source_results: Optional[List["PolicyResult"]]
|
||||
|
||||
def __init__(self, passing: bool, *messages: str):
|
||||
self.passing = passing
|
||||
self.messages = messages
|
||||
self.source_policy = None
|
||||
self.source_results = []
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@ -6,6 +6,7 @@ import time
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from hashlib import sha256
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
@ -230,6 +231,16 @@ class OAuth2Provider(Provider):
|
||||
except Provider.application.RelatedObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
@property
|
||||
def launch_url(self) -> Optional[str]:
|
||||
"""Guess launch_url based on first redirect_uri"""
|
||||
if not self.redirect_uris:
|
||||
return None
|
||||
main_url = self.redirect_uris[0]
|
||||
launch_url = urlparse(main_url)
|
||||
launch_url.path = ""
|
||||
return launch_url.geturl()
|
||||
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.providers.oauth2.forms import OAuth2ProviderForm
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ from django.utils import timezone
|
||||
from django.views import View
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Application, Token
|
||||
from passbook.core.models import Application
|
||||
from passbook.flows.models import in_memory_stage
|
||||
from passbook.flows.planner import (
|
||||
PLAN_CONTEXT_APPLICATION,
|
||||
@ -95,7 +95,7 @@ class OAuthAuthorizationParams:
|
||||
elif response_type in [
|
||||
ResponseTypes.ID_TOKEN,
|
||||
ResponseTypes.ID_TOKEN_TOKEN,
|
||||
ResponseTypes.TOKEN,
|
||||
ResponseTypes.CODE_TOKEN,
|
||||
]:
|
||||
grant_type = GrantTypes.IMPLICIT
|
||||
elif response_type in [
|
||||
@ -220,9 +220,11 @@ class OAuthFulfillmentStage(StageView):
|
||||
)
|
||||
return redirect(self.create_response_uri())
|
||||
except (ClientIdError, RedirectUriError) as error:
|
||||
self.executor.stage_invalid()
|
||||
# pylint: disable=no-member
|
||||
return bad_request_message(request, error.description, title=error.error)
|
||||
except AuthorizeError as error:
|
||||
self.executor.stage_invalid()
|
||||
uri = error.create_uri(self.params.redirect_uri, self.params.state)
|
||||
return redirect(uri)
|
||||
|
||||
@ -248,28 +250,26 @@ class OAuthFulfillmentStage(StageView):
|
||||
str(self.params.state) if self.params.state else ""
|
||||
]
|
||||
elif self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]:
|
||||
token: Token = self.provider.create_token(
|
||||
token = self.provider.create_refresh_token(
|
||||
user=self.request.user, scope=self.params.scope,
|
||||
)
|
||||
|
||||
# Check if response_type must include access_token in the response.
|
||||
if self.params.response_type in [
|
||||
ResponseTypes.id_token_token,
|
||||
ResponseTypes.code_id_token_token,
|
||||
ResponseTypes.token,
|
||||
ResponseTypes.code_token,
|
||||
ResponseTypes.ID_TOKEN_TOKEN,
|
||||
ResponseTypes.CODE_ID_TOKEN_TOKEN,
|
||||
ResponseTypes.ID_TOKEN,
|
||||
ResponseTypes.CODE_TOKEN,
|
||||
]:
|
||||
query_fragment["access_token"] = token.access_token
|
||||
|
||||
# We don't need id_token if it's an OAuth2 request.
|
||||
if SCOPE_OPENID in self.params.scope:
|
||||
id_token = token.create_id_token(
|
||||
user=self.request.user,
|
||||
request=self.request,
|
||||
scope=self.params.scope,
|
||||
user=self.request.user, request=self.request,
|
||||
)
|
||||
id_token.nonce = self.params.nonce
|
||||
id_token.scope = self.params.scope
|
||||
|
||||
# Include at_hash when access_token is being returned.
|
||||
if "access_token" in query_fragment:
|
||||
id_token.at_hash = token.at_hash
|
||||
@ -283,8 +283,6 @@ class OAuthFulfillmentStage(StageView):
|
||||
]:
|
||||
query_fragment["id_token"] = id_token.encode(self.provider)
|
||||
token.id_token = id_token
|
||||
else:
|
||||
token.id_token = {}
|
||||
|
||||
# Store the token.
|
||||
token.save()
|
||||
@ -325,7 +323,7 @@ class AuthorizationFlowInitView(PolicyAccessMixin, View):
|
||||
try:
|
||||
application = self.provider_to_application(provider)
|
||||
except Application.DoesNotExist:
|
||||
return self.handle_no_permission_authorized()
|
||||
return self.handle_no_permission_authenticated()
|
||||
# Check if user is unauthenticated, so we pass the application
|
||||
# for the identification stage
|
||||
if not request.user.is_authenticated:
|
||||
@ -333,7 +331,7 @@ class AuthorizationFlowInitView(PolicyAccessMixin, View):
|
||||
# Check permissions
|
||||
result = self.user_has_access(application)
|
||||
if not result.passing:
|
||||
return self.handle_no_permission_authorized()
|
||||
return self.handle_no_permission_authenticated(result)
|
||||
# TODO: End block
|
||||
# Extract params so we can save them in the plan context
|
||||
try:
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-14 15:36
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_crypto", "0002_create_self_signed_kp"),
|
||||
("passbook_providers_proxy", "0004_auto_20200913_1947"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="proxyprovider",
|
||||
name="certificate",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="passbook_crypto.certificatekeypair",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -50,7 +50,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||
cookie_secret = models.TextField(default=get_cookie_secret)
|
||||
|
||||
certificate = models.ForeignKey(
|
||||
CertificateKeyPair, on_delete=models.SET_NULL, null=True
|
||||
CertificateKeyPair, on_delete=models.SET_NULL, null=True, blank=True,
|
||||
)
|
||||
|
||||
def form(self) -> Type[ModelForm]:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"""passbook saml_idp Models"""
|
||||
from typing import Optional, Type
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.db import models
|
||||
from django.forms import ModelForm
|
||||
@ -102,6 +103,13 @@ class SAMLProvider(Provider):
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def launch_url(self) -> Optional[str]:
|
||||
"""Guess launch_url based on acs URL"""
|
||||
launch_url = urlparse(self.acs_url)
|
||||
launch_url.path = ""
|
||||
return launch_url.geturl()
|
||||
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.providers.saml.forms import SAMLProviderForm
|
||||
|
||||
|
||||
@ -62,8 +62,9 @@ class SAMLSSOView(PolicyAccessMixin, View):
|
||||
)
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission(self.application)
|
||||
if not self.user_has_access(self.application).passing:
|
||||
return self.handle_no_permission_authorized()
|
||||
has_access = self.user_has_access(self.application)
|
||||
if not has_access.passing:
|
||||
return self.handle_no_permission_authenticated(has_access)
|
||||
# Call the method handler, which checks the SAML Request
|
||||
method_response = super().dispatch(request, *args, application_slug, **kwargs)
|
||||
if method_response:
|
||||
|
||||
@ -193,7 +193,12 @@ ASGI_APPLICATION = "passbook.root.routing.application"
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {"hosts": [(CONFIG.y("redis.host"), 6379)]},
|
||||
"CONFIG": {
|
||||
"hosts": [
|
||||
f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:6379"
|
||||
f"/{CONFIG.y('redis.ws_db')}"
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user