Compare commits

...

23 Commits

Author SHA1 Message Date
4d51295db2 new release: 0.10.1-stable 2020-09-14 23:08:57 +02:00
3bbded3555 docs: remove default password for docker-compose, improve instructions 2020-09-14 23:08:04 +02:00
b3262e2a82 docs: add docs for passbook_user_debug 2020-09-14 22:51:50 +02:00
40614a65fc flows: move complete denied view and template to flows 2020-09-14 21:52:43 +02:00
3cf558d594 providers/*: pass policy result objects when access denied 2020-09-14 21:52:25 +02:00
812cc0d2f1 policies: add references for source_policy and source_results 2020-09-14 21:51:59 +02:00
e21ed92848 providers/oauth2: ensure flow is cleaned up on error 2020-09-14 18:40:44 +02:00
5184c4b7ef flows: fix FlowNonApplicableException and EmptyFlowException leading to infinite spinners 2020-09-14 18:40:26 +02:00
2c07859b68 core: add automatic launch_url detection based on provider 2020-09-14 18:12:42 +02:00
ae6304c05e providers/proxy: fix provider requiring a certificate to be selected 2020-09-14 17:37:06 +02:00
501683e3cb outposts: add tests for permissions 2020-09-14 17:34:07 +02:00
cc8afa8706 admin: don't show policy as unbound when used as validation policy 2020-09-14 15:44:33 +02:00
17a9e02bc0 docs: update kubernetes deployment example 2020-09-14 15:41:24 +02:00
6a669992a8 outposts: fix permissions not being updated when providers are modified 2020-09-14 15:41:02 +02:00
7ea5c22b6c root: fix channels not loading redis connection details 2020-09-14 14:21:43 +02:00
b11d6a5891 build(deps): bump django-storages from 1.10 to 1.10.1 (#212)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Jens L <jens@beryju.org>
2020-09-14 10:29:49 +02:00
49830367a7 build(deps-dev): bump coverage from 5.2.1 to 5.3 (#213)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Jens L <jens@beryju.org>
2020-09-14 10:26:25 +02:00
e69ca5a229 ci: fix coverage combine for unittest and e2e 2020-09-14 09:52:43 +02:00
a57d21f5e8 build(deps): bump boto3 from 1.14.59 to 1.14.60 (#210)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Jens L <jens@beryju.org>
2020-09-14 09:09:34 +02:00
c7026407c6 policies: fix type error 2020-09-14 00:28:23 +02:00
69eecd6b60 helm: add soft-affinity rules for worker and web 2020-09-14 00:12:40 +02:00
810f10edfe providers/oauth2: fix several small implicit flow errors 2020-09-14 00:11:11 +02:00
1c57128f11 providers/oauth2: fix token to code_token 2020-09-13 23:42:45 +02:00
51 changed files with 511 additions and 280 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.10.0-stable current_version = 0.10.1-stable
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)

View File

@ -1,5 +1,6 @@
[run] [run]
source = passbook source = passbook
relative_files = true
omit = omit =
*/asgi.py */asgi.py
manage.py manage.py

View File

@ -18,11 +18,11 @@ jobs:
- name: Building Docker Image - name: Building Docker Image
run: docker build run: docker build
--no-cache --no-cache
-t beryju/passbook:0.10.0-stable -t beryju/passbook:0.10.1-stable
-t beryju/passbook:latest -t beryju/passbook:latest
-f Dockerfile . -f Dockerfile .
- name: Push Docker Container to Registry (versioned) - 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) - name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook:latest run: docker push beryju/passbook:latest
build-proxy: build-proxy:
@ -48,11 +48,11 @@ jobs:
cd proxy cd proxy
docker build \ docker build \
--no-cache \ --no-cache \
-t beryju/passbook-proxy:0.10.0-stable \ -t beryju/passbook-proxy:0.10.1-stable \
-t beryju/passbook-proxy:latest \ -t beryju/passbook-proxy:latest \
-f Dockerfile . -f Dockerfile .
- name: Push Docker Container to Registry (versioned) - 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) - name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-proxy:latest run: docker push beryju/passbook-proxy:latest
build-static: build-static:
@ -77,11 +77,11 @@ jobs:
run: docker build run: docker build
--no-cache --no-cache
--network=$(docker network ls | grep github | awk '{print $1}') --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 -t beryju/passbook-static:latest
-f static.Dockerfile . -f static.Dockerfile .
- name: Push Docker Container to Registry (versioned) - 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) - name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-static:latest run: docker push beryju/passbook-static:latest
test-release: test-release:
@ -93,6 +93,9 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Run test suite in final docker images - name: Run test suite in final docker images
run: | 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 pull -q
docker-compose up --no-start docker-compose up --no-start
docker-compose start postgresql redis docker-compose start postgresql redis
@ -111,5 +114,5 @@ jobs:
SENTRY_PROJECT: passbook SENTRY_PROJECT: passbook
SENTRY_URL: https://sentry.beryju.org SENTRY_URL: https://sentry.beryju.org
with: with:
tagName: 0.10.0-stable tagName: 0.10.1-stable
environment: beryjuorg-prod environment: beryjuorg-prod

View File

@ -13,7 +13,10 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Pre-release test - name: Pre-release test
run: | 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-compose pull -q
docker build \ docker build \
--no-cache \ --no-cache \

View File

@ -1,7 +1,7 @@
all: lint-fix lint coverage gen all: lint-fix lint coverage gen
coverage: coverage:
coverage run --concurrency=multiprocessing manage.py test --failfast coverage run --concurrency=multiprocessing manage.py test --failfast -v 3
coverage combine coverage combine
coverage html coverage html
coverage report coverage report

86
Pipfile.lock generated
View File

@ -74,11 +74,11 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:20edd03ae4c4e141b0d8a9a9afc773af4345d54b68202b6aa502956b57b18b3f", "sha256:79e95f428c485ea817969a78e77a311d2ec4d82e0955639d6126189c990ddad3",
"sha256:b596a80181fecd775ccc009286400f4d785136f250967895cb34beeeef65eb1f" "sha256:d8ca27ee13deeb1a9e79f2fe5f923effa60947ed49bbdfbc2a9f5790aef64217"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.14.59" "version": "==1.14.60"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
@ -327,11 +327,11 @@
}, },
"django-storages": { "django-storages": {
"hashes": [ "hashes": [
"sha256:1e37da57678e6cf1e9914f84099a305323e4e1f261afe54fdb703cae7aa6fbc3", "sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c",
"sha256:36ed8dab33d761954498189592ce005920095fcbc02dab4184eb51393c370991" "sha256:652275ab7844538c462b62810276c0244866f345878256a9e0e86f5b1283ae18"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.10" "version": "==1.10.1"
}, },
"djangorestframework": { "djangorestframework": {
"hashes": [ "hashes": [
@ -835,9 +835,9 @@
}, },
"pyrsistent": { "pyrsistent": {
"hashes": [ "hashes": [
"sha256:27515d2d5db0629c7dadf6fbe76973eb56f098c1b01d36de42eb69220d2c19e4" "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
], ],
"version": "==0.17.2" "version": "==0.17.3"
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
@ -1269,43 +1269,43 @@
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
"sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
"sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
"sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
"sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
"sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
"sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
"sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
"sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
"sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
"sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
"sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
"sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
"sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
"sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
"sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
"sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
"sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
"sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
"sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
"sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
"sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
"sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
"sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
"sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
"sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
"sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
"sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
"sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
"sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
"sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
"sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
"sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
"sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.2.1" "version": "==5.3"
}, },
"django": { "django": {
"hashes": [ "hashes": [

View File

@ -20,7 +20,7 @@ wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
# Optionally enable Error-reporting # Optionally enable Error-reporting
# export PASSBOOK_ERROR_REPORTING=true # export PASSBOOK_ERROR_REPORTING=true
# Optionally deploy a different version # 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 # If this is a productive installation, set a different PostgreSQL Password
# export PG_PASS=$(pwgen 40 1) # export PG_PASS=$(pwgen 40 1)
docker-compose pull docker-compose pull

View File

@ -139,7 +139,7 @@ stages:
displayName: Run full test suite displayName: Run full test suite
inputs: inputs:
script: | script: |
pipenv run coverage run ./manage.py test passbook pipenv run coverage run ./manage.py test passbook -v 3
mkdir output-unittest mkdir output-unittest
mv unittest.xml output-unittest/unittest.xml mv unittest.xml output-unittest/unittest.xml
mv .coverage output-unittest/coverage mv .coverage output-unittest/coverage
@ -181,7 +181,7 @@ stages:
- task: CmdLine@2 - task: CmdLine@2
displayName: Run full test suite displayName: Run full test suite
inputs: inputs:
script: pipenv run coverage run ./manage.py test e2e script: pipenv run coverage run ./manage.py test e2e -v 3
- task: CmdLine@2 - task: CmdLine@2
displayName: Prepare unittests and coverage for upload displayName: Prepare unittests and coverage for upload
inputs: inputs:
@ -225,11 +225,9 @@ stages:
script: | script: |
sudo pip install -U wheel pipenv sudo pip install -U wheel pipenv
pipenv install --dev pipenv install --dev
find .
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
pipenv run coverage xml pipenv run coverage xml
pipenv run coverage html pipenv run coverage html
find .
- task: PublishCodeCoverageResults@1 - task: PublishCodeCoverageResults@1
inputs: inputs:
codeCoverageTool: 'Cobertura' codeCoverageTool: 'Cobertura'

View File

@ -14,6 +14,8 @@ services:
- POSTGRES_DB=passbook - POSTGRES_DB=passbook
labels: labels:
- traefik.enable=false - traefik.enable=false
env_file:
- .env
redis: redis:
image: redis image: redis
networks: networks:
@ -21,13 +23,12 @@ services:
labels: labels:
- traefik.enable=false - traefik.enable=false
server: server:
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-stable} image: beryju/passbook:${PASSBOOK_TAG:-0.10.1-stable}
command: server command: server
environment: environment:
PASSBOOK_REDIS__HOST: redis PASSBOOK_REDIS__HOST: redis
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
PASSBOOK_POSTGRESQL__HOST: postgresql PASSBOOK_POSTGRESQL__HOST: postgresql
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword} PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
PASSBOOK_LOG_LEVEL: debug PASSBOOK_LOG_LEVEL: debug
ports: ports:
- 8000 - 8000
@ -37,8 +38,10 @@ services:
- traefik.port=8000 - traefik.port=8000
- traefik.docker.network=internal - traefik.docker.network=internal
- traefik.frontend.rule=PathPrefix:/ - traefik.frontend.rule=PathPrefix:/
env_file:
- .env
worker: worker:
image: beryju/passbook:${PASSBOOK_TAG:-0.10.0-stable} image: beryju/passbook:${PASSBOOK_TAG:-0.10.1-stable}
command: worker command: worker
networks: networks:
- internal - internal
@ -46,12 +49,13 @@ services:
- traefik.enable=false - traefik.enable=false
environment: environment:
PASSBOOK_REDIS__HOST: redis PASSBOOK_REDIS__HOST: redis
PASSBOOK_ERROR_REPORTING: ${PASSBOOK_ERROR_REPORTING:-false}
PASSBOOK_POSTGRESQL__HOST: postgresql PASSBOOK_POSTGRESQL__HOST: postgresql
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS:-thisisnotagoodpassword} PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
PASSBOOK_LOG_LEVEL: debug PASSBOOK_LOG_LEVEL: debug
env_file:
- .env
static: static:
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.0-stable} image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.1-stable}
networks: networks:
- internal - internal
labels: labels:

View File

@ -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. 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 pull
docker-compose up -d docker-compose up -d
docker-compose run --rm server migrate docker-compose run --rm server migrate

View File

@ -11,7 +11,7 @@ This installation automatically applies database migrations on startup. After th
image: image:
name: beryju/passbook name: beryju/passbook
name_static: beryju/passbook-static name_static: beryju/passbook-static
tag: 0.10.0-stable tag: 0.10.1-stable
nameOverride: "" nameOverride: ""

View File

@ -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. 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 ```yaml
api_version: v1 apiVersion: v1
kind: secret kind: Secret
metadata: metadata:
labels: labels:
app.kubernetes.io/instance: test app.kubernetes.io/instance: test
@ -14,65 +14,14 @@ metadata:
app.kubernetes.io/name: passbook-proxy app.kubernetes.io/name: passbook-proxy
app.kubernetes.io/version: 0.10.0 app.kubernetes.io/version: 0.10.0
name: passbook-outpost-api name: passbook-outpost-api
string_data: stringData:
passbook_host: '__PASSBOOK_URL__' passbook_host: '__PASSBOOK_URL__'
passbook_host_insecure: 'true' passbook_host_insecure: 'true'
token: '__PASSBOOK_TOKEN__' token: '__PASSBOOK_TOKEN__'
type: Opaque type: Opaque
--- ---
api_version: apps/v1 apiVersion: v1
kind: deployment kind: Service
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
metadata: metadata:
labels: labels:
app.kubernetes.io/instance: test app.kubernetes.io/instance: test
@ -96,4 +45,55 @@ spec:
app.kubernetes.io/name: passbook-proxy app.kubernetes.io/name: passbook-proxy
app.kubernetes.io/version: 0.10.0 app.kubernetes.io/version: 0.10.0
type: ClusterIP 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
``` ```

View 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:
![](./passbook_user_debug.png)
Afterwards, try to access the application again. You will now see a message explaining which policy denied you access:
![](./access_denied_message.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,8 +1,8 @@
apiVersion: v2 apiVersion: v2
appVersion: "0.10.0-stable" appVersion: "0.10.1-stable"
description: A Helm chart for passbook. description: A Helm chart for passbook.
name: 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 icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
dependencies: dependencies:
- name: postgresql - name: postgresql

View File

@ -22,10 +22,27 @@ spec:
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
k8s.passbook.beryju.org/component: web k8s.passbook.beryju.org/component: web
spec: 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: initContainers:
- name: passbook-database-migrations - name: passbook-database-migrations
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
imagePullPolicy: Always
args: [migrate] args: [migrate]
envFrom: envFrom:
- configMapRef: - configMapRef:
@ -50,7 +67,6 @@ spec:
containers: containers:
- name: {{ .Chart.Name }} - name: {{ .Chart.Name }}
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
imagePullPolicy: Always
args: [server] args: [server]
envFrom: envFrom:
- configMapRef: - configMapRef:

View File

@ -22,6 +22,24 @@ spec:
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
k8s.passbook.beryju.org/component: worker k8s.passbook.beryju.org/component: worker
spec: 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: containers:
- name: {{ .Chart.Name }} - name: {{ .Chart.Name }}
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}" image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"

View File

@ -4,7 +4,7 @@
image: image:
name: beryju/passbook name: beryju/passbook
name_static: beryju/passbook-static name_static: beryju/passbook-static
tag: 0.10.0-stable tag: 0.10.1-stable
nameOverride: "" nameOverride: ""

View File

@ -55,6 +55,8 @@ nav:
- Upgrading: - Upgrading:
- to 0.9: upgrading/to-0.9.md - to 0.9: upgrading/to-0.9.md
- to 0.10: upgrading/to-0.10.md - to 0.10: upgrading/to-0.10.md
- Troubleshooting:
- Access problems: troubleshooting/access.md
repo_name: "BeryJu/passbook" repo_name: "BeryJu/passbook"
repo_url: https://github.com/BeryJu/passbook repo_url: https://github.com/BeryJu/passbook

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = "0.10.0-stable" __version__ = "0.10.1-stable"

View File

@ -55,7 +55,7 @@
<th role="columnheader"> <th role="columnheader">
<div> <div>
<div>{{ policy.name }}</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> <i class="pf-icon pf-icon-warning-triangle"></i>
<small>{% trans 'Warning: Policy is not assigned.' %}</small> <small>{% trans 'Warning: Policy is not assigned.' %}</small>
{% else %} {% else %}

View File

@ -58,7 +58,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
application=None application=None
) )
kwargs["policies_without_binding"] = len( 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_policies"] = len(cache.keys("policy_*"))
kwargs["cached_flows"] = len(cache.keys("flow_*")) kwargs["cached_flows"] = len(cache.keys("flow_*"))

View File

@ -29,7 +29,16 @@ class ApplicationForm(forms.ModelForm):
] ]
widgets = { widgets = {
"name": forms.TextInput(), "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_icon_url": forms.TextInput(),
"meta_publisher": forms.TextInput(), "meta_publisher": forms.TextInput(),
} }

View File

@ -22,6 +22,7 @@ from passbook.lib.models import CreatedUpdatedModel
from passbook.policies.models import PolicyBindingModel from passbook.policies.models import PolicyBindingModel
LOGGER = get_logger() LOGGER = get_logger()
PASSBOOK_USER_DEBUG = "passbook_user_debug"
def default_token_duration(): def default_token_duration():
@ -92,6 +93,12 @@ class Provider(models.Model):
objects = InheritanceManager() 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]: def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object""" """Return Form class used to edit this object"""
raise NotImplementedError raise NotImplementedError
@ -119,6 +126,14 @@ class Application(PolicyBindingModel):
meta_description = models.TextField(default="", blank=True) meta_description = models.TextField(default="", blank=True)
meta_publisher = 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]: def get_provider(self) -> Optional[Provider]:
"""Get casted provider instance""" """Get casted provider instance"""
if not self.provider: if not self.provider:

View File

@ -6,8 +6,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff2' %}" as="font" type="font/woff2"> <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"> <link rel="preload" href="{% static 'passbook/fonts/DINEngschriftStd.woff' %}" as="font" type="font/woff" crossorigin>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <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> <title>{% block title %}{% trans title|default:config.passbook.branding.title %}{% endblock %}</title>

View File

@ -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 %}

View File

@ -24,7 +24,7 @@
{% if applications %} {% if applications %}
<div class="pf-l-gallery pf-m-gutter"> <div class="pf-l-gallery pf-m-gutter">
{% for app in applications %} {% 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"> <div class="pf-c-card__header">
{% if not app.meta_icon_url %} {% if not app.meta_icon_url %}
<i class="pf-icon pf-icon-arrow"></i> <i class="pf-icon pf-icon-arrow"></i>

View File

@ -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)

View File

@ -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)

View File

@ -2,8 +2,8 @@
class FlowNonApplicableException(BaseException): 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): class EmptyFlowException(BaseException):
"""Exception raised when a Flow Plan is empty""" """Flow has no stages."""

View 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 %}

View File

@ -5,7 +5,6 @@ from django.shortcuts import reverse
from django.test import Client, TestCase from django.test import Client, TestCase
from django.utils.encoding import force_str from django.utils.encoding import force_str
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from passbook.flows.markers import ReevaluateMarker, StageMarker from passbook.flows.markers import ReevaluateMarker, StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import FlowPlan from passbook.flows.planner import FlowPlan
@ -48,8 +47,8 @@ class TestFlowExecutor(TestCase):
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug} "passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
), ),
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 200)
self.assertEqual(cancel_mock.call_count, 1) self.assertEqual(cancel_mock.call_count, 2)
@patch( @patch(
"passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE, "passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE,
@ -66,8 +65,11 @@ class TestFlowExecutor(TestCase):
response = self.client.get( response = self.client.get(
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 200)
self.assertInHTML(FlowNonApplicableException.__doc__, response.rendered_content) self.assertJSONEqual(
force_str(response.content),
{"type": "redirect", "to": reverse("passbook_flows:denied")},
)
def test_invalid_empty_flow(self): def test_invalid_empty_flow(self):
"""Tests that an empty flow returns the correct error message""" """Tests that an empty flow returns the correct error message"""
@ -81,8 +83,11 @@ class TestFlowExecutor(TestCase):
response = self.client.get( response = self.client.get(
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 200)
self.assertInHTML(EmptyFlowException.__doc__, response.rendered_content) self.assertJSONEqual(
force_str(response.content),
{"type": "redirect", "to": reverse("passbook_flows:denied")},
)
def test_invalid_flow_redirect(self): def test_invalid_flow_redirect(self):
"""Tests that an invalid flow still redirects""" """Tests that an invalid flow still redirects"""

View File

@ -12,18 +12,18 @@ from django.http import (
from django.shortcuts import get_object_or_404, redirect, render, reverse from django.shortcuts import get_object_or_404, redirect, render, reverse
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator 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.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from structlog import get_logger from structlog import get_logger
from passbook.audit.models import cleanse_dict 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.exceptions import EmptyFlowException, FlowNonApplicableException
from passbook.flows.models import Flow, FlowDesignation, Stage from passbook.flows.models import Flow, FlowDesignation, Stage
from passbook.flows.planner import FlowPlan, FlowPlanner from passbook.flows.planner import FlowPlan, FlowPlanner
from passbook.lib.utils.reflection import class_to_path from passbook.lib.utils.reflection import class_to_path
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
from passbook.lib.views import bad_request_message
LOGGER = get_logger() LOGGER = get_logger()
# Argument used to redirect user after login # Argument used to redirect user after login
@ -31,6 +31,8 @@ NEXT_ARG_NAME = "next"
SESSION_KEY_PLAN = "passbook_flows_plan" SESSION_KEY_PLAN = "passbook_flows_plan"
SESSION_KEY_APPLICATION_PRE = "passbook_flows_application_pre" SESSION_KEY_APPLICATION_PRE = "passbook_flows_application_pre"
SESSION_KEY_GET = "passbook_flows_get" 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") @method_decorator(xframe_options_sameorigin, name="dispatch")
@ -54,7 +56,9 @@ class FlowExecutorView(View):
LOGGER.debug("f(exec): Redirecting to next on fail") LOGGER.debug("f(exec): Redirecting to next on fail")
return redirect(self.request.GET.get(NEXT_ARG_NAME)) return redirect(self.request.GET.get(NEXT_ARG_NAME))
message = exc.__doc__ if exc.__doc__ else str(exc) 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: def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
# Early check if theres an active Plan for the current session # Early check if theres an active Plan for the current session
@ -193,11 +197,19 @@ class FlowExecutorView(View):
) )
return self._flow_done() 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 """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) LOGGER.debug("f(exec): Stage invalid", flow_slug=self.flow.slug)
self.cancel() 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) return redirect_with_qs("passbook_flows:denied", self.request.GET)
def cancel(self): def cancel(self):
@ -212,9 +224,22 @@ class FlowExecutorView(View):
del self.request.session[key] del self.request.session[key]
class FlowPermissionDeniedView(PermissionDeniedView): class FlowPermissionDeniedView(TemplateView):
"""User could not be authenticated""" """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): class FlowExecutorShellView(TemplateView):
"""Executor Shell view, loads a dummy card with a spinner """Executor Shell view, loads a dummy card with a spinner

View File

@ -10,6 +10,7 @@ redis:
password: '' password: ''
cache_db: 0 cache_db: 0
message_queue_db: 1 message_queue_db: 1
ws_db: 2
debug: false debug: false
log_level: info log_level: info

View File

@ -27,12 +27,12 @@ class CreateAssignPermView(CreateView):
def bad_request_message( def bad_request_message(
request: HttpRequest, message: str, title="Bad Request" request: HttpRequest,
message: str,
title="Bad Request",
template="error/generic.html",
) -> TemplateResponse: ) -> TemplateResponse:
"""Return generic error page with message, with status code set to 400""" """Return generic error page with message, with status code set to 400"""
return TemplateResponse( return TemplateResponse(
request, request, template, {"message": message, "card_title": _(title)}, status=400,
"error/generic.html",
{"message": message, "card_title": _(title)},
status=400,
) )

View File

@ -7,9 +7,10 @@ from uuid import uuid4
from dacite import from_dict from dacite import from_dict
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.core.cache import cache 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.db.models.base import Model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from guardian.models import UserObjectPermission
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from passbook.core.models import Provider, Token, TokenIntents, User from passbook.core.models import Provider, Token, TokenIntents, User
@ -104,24 +105,24 @@ class Outpost(models.Model):
return datetime.fromtimestamp(value) return datetime.fromtimestamp(value)
return None 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 @property
def user(self) -> User: def user(self) -> User:
"""Get/create user with access to all required objects""" """Get/create user with access to all required objects"""
user = User.objects.filter(username=f"pb-outpost-{self.uuid.hex}") users = User.objects.filter(username=f"pb-outpost-{self.uuid.hex}")
if user.exists(): if not users.exists():
return user.first() user: User = User.objects.create(username=f"pb-outpost-{self.uuid.hex}")
return self._create_user() 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 @property
def token(self) -> Token: def token(self) -> Token:

View File

@ -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 """Send outpost update to all registered outposts, irregardless to which passbook
instance they are connected""" instance they are connected"""
model = path_to_class(model_class) 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(): for outpost in outpost_model.outpost_set.all():
channel_layer = get_channel_layer() channel_layer = get_channel_layer()
for channel in outpost.channels: for channel in outpost.channels:

View 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))

View File

@ -68,7 +68,7 @@ class PolicyEngine:
def _check_policy_type(self, policy: Policy): def _check_policy_type(self, policy: Policy):
"""Check policy type, make sure it's not the root class as that has no logic implemented""" """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: if policy.__class__ == Policy:
raise TypeError(f"Policy '{policy}' is root type") raise TypeError(f"Policy '{policy}' is root type")
@ -109,19 +109,25 @@ class PolicyEngine:
@property @property
def result(self) -> PolicyResult: def result(self) -> PolicyResult:
"""Get policy-checking result""" """Get policy-checking result"""
messages: List[str] = []
process_results: List[PolicyResult] = [ process_results: List[PolicyResult] = [
x.result for x in self.__processes if x.result 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: for result in process_results + self.__cached_policies:
LOGGER.debug( LOGGER.debug(
"P_ENG: result", passing=result.passing, messages=result.messages "P_ENG: result", passing=result.passing, messages=result.messages
) )
if result.messages: if result.messages:
messages += result.messages final_result.messages.extend(result.messages)
if not result.passing: if not result.passing:
return PolicyResult(False, *messages) final_result.messages = tuple(final_result.messages)
return PolicyResult(True, *messages) final_result.passing = False
return final_result
final_result.messages = tuple(final_result.messages)
final_result.passing = True
return final_result
@property @property
def passing(self) -> bool: def passing(self) -> bool:

View File

@ -7,7 +7,7 @@
<label for="" class="pf-c-form__label"></label> <label for="" class="pf-c-form__label"></label>
<div class="c-form__horizontal-group"> <div class="c-form__horizontal-group">
<p> <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> </p>
</div> </div>
</div> </div>

View File

@ -10,7 +10,10 @@ from django.utils.translation import gettext as _
from structlog import get_logger from structlog import get_logger
from passbook.core.models import Application, Provider, User 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.engine import PolicyEngine
from passbook.policies.types import PolicyResult from passbook.policies.types import PolicyResult
@ -36,8 +39,12 @@ class PolicyAccessMixin(BaseMixin, AccessMixin):
self.get_redirect_field_name(), self.get_redirect_field_name(),
) )
def handle_no_permission_authorized(self) -> HttpResponse: def handle_no_permission_authenticated(
"""Function called when user has no permissions but is authorized""" 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 # TODO: Remove this URL and render the view instead
return redirect("passbook_flows:denied") return redirect("passbook_flows:denied")

View File

@ -63,6 +63,7 @@ class PolicyProcess(Process):
except PolicyException as exc: except PolicyException as exc:
LOGGER.debug("P_ENG(proc): error", exc=exc) LOGGER.debug("P_ENG(proc): error", exc=exc)
policy_result = PolicyResult(False, str(exc)) policy_result = PolicyResult(False, str(exc))
policy_result.source_policy = self.binding.policy
# Invert result if policy.negate is set # Invert result if policy.negate is set
if self.binding.negate: if self.binding.negate:
policy_result.passing = not policy_result.passing policy_result.passing = not policy_result.passing

View File

@ -1,13 +1,14 @@
"""policy structures""" """policy structures"""
from __future__ import annotations 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.db.models import Model
from django.http import HttpRequest from django.http import HttpRequest
if TYPE_CHECKING: if TYPE_CHECKING:
from passbook.core.models import User from passbook.core.models import User
from passbook.policies.models import Policy
class PolicyRequest: class PolicyRequest:
@ -34,9 +35,14 @@ class PolicyResult:
passing: bool passing: bool
messages: Tuple[str, ...] messages: Tuple[str, ...]
source_policy: Optional[Policy]
source_results: Optional[List["PolicyResult"]]
def __init__(self, passing: bool, *messages: str): def __init__(self, passing: bool, *messages: str):
self.passing = passing self.passing = passing
self.messages = messages self.messages = messages
self.source_policy = None
self.source_results = []
def __repr__(self): def __repr__(self):
return self.__str__() return self.__str__()

View File

@ -6,6 +6,7 @@ import time
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from hashlib import sha256 from hashlib import sha256
from typing import Any, Dict, List, Optional, Type from typing import Any, Dict, List, Optional, Type
from urllib.parse import urlparse
from uuid import uuid4 from uuid import uuid4
from django.conf import settings from django.conf import settings
@ -230,6 +231,16 @@ class OAuth2Provider(Provider):
except Provider.application.RelatedObjectDoesNotExist: except Provider.application.RelatedObjectDoesNotExist:
return None 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]: def form(self) -> Type[ModelForm]:
from passbook.providers.oauth2.forms import OAuth2ProviderForm from passbook.providers.oauth2.forms import OAuth2ProviderForm

View File

@ -10,7 +10,7 @@ from django.utils import timezone
from django.views import View from django.views import View
from structlog import get_logger 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.models import in_memory_stage
from passbook.flows.planner import ( from passbook.flows.planner import (
PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_APPLICATION,
@ -95,7 +95,7 @@ class OAuthAuthorizationParams:
elif response_type in [ elif response_type in [
ResponseTypes.ID_TOKEN, ResponseTypes.ID_TOKEN,
ResponseTypes.ID_TOKEN_TOKEN, ResponseTypes.ID_TOKEN_TOKEN,
ResponseTypes.TOKEN, ResponseTypes.CODE_TOKEN,
]: ]:
grant_type = GrantTypes.IMPLICIT grant_type = GrantTypes.IMPLICIT
elif response_type in [ elif response_type in [
@ -220,9 +220,11 @@ class OAuthFulfillmentStage(StageView):
) )
return redirect(self.create_response_uri()) return redirect(self.create_response_uri())
except (ClientIdError, RedirectUriError) as error: except (ClientIdError, RedirectUriError) as error:
self.executor.stage_invalid()
# pylint: disable=no-member # pylint: disable=no-member
return bad_request_message(request, error.description, title=error.error) return bad_request_message(request, error.description, title=error.error)
except AuthorizeError as error: except AuthorizeError as error:
self.executor.stage_invalid()
uri = error.create_uri(self.params.redirect_uri, self.params.state) uri = error.create_uri(self.params.redirect_uri, self.params.state)
return redirect(uri) return redirect(uri)
@ -248,28 +250,26 @@ class OAuthFulfillmentStage(StageView):
str(self.params.state) if self.params.state else "" str(self.params.state) if self.params.state else ""
] ]
elif self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]: 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, user=self.request.user, scope=self.params.scope,
) )
# Check if response_type must include access_token in the response. # Check if response_type must include access_token in the response.
if self.params.response_type in [ if self.params.response_type in [
ResponseTypes.id_token_token, ResponseTypes.ID_TOKEN_TOKEN,
ResponseTypes.code_id_token_token, ResponseTypes.CODE_ID_TOKEN_TOKEN,
ResponseTypes.token, ResponseTypes.ID_TOKEN,
ResponseTypes.code_token, ResponseTypes.CODE_TOKEN,
]: ]:
query_fragment["access_token"] = token.access_token query_fragment["access_token"] = token.access_token
# We don't need id_token if it's an OAuth2 request. # We don't need id_token if it's an OAuth2 request.
if SCOPE_OPENID in self.params.scope: if SCOPE_OPENID in self.params.scope:
id_token = token.create_id_token( id_token = token.create_id_token(
user=self.request.user, user=self.request.user, request=self.request,
request=self.request,
scope=self.params.scope,
) )
id_token.nonce = self.params.nonce id_token.nonce = self.params.nonce
id_token.scope = self.params.scope
# Include at_hash when access_token is being returned. # Include at_hash when access_token is being returned.
if "access_token" in query_fragment: if "access_token" in query_fragment:
id_token.at_hash = token.at_hash id_token.at_hash = token.at_hash
@ -283,8 +283,6 @@ class OAuthFulfillmentStage(StageView):
]: ]:
query_fragment["id_token"] = id_token.encode(self.provider) query_fragment["id_token"] = id_token.encode(self.provider)
token.id_token = id_token token.id_token = id_token
else:
token.id_token = {}
# Store the token. # Store the token.
token.save() token.save()
@ -325,7 +323,7 @@ class AuthorizationFlowInitView(PolicyAccessMixin, View):
try: try:
application = self.provider_to_application(provider) application = self.provider_to_application(provider)
except Application.DoesNotExist: 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 # Check if user is unauthenticated, so we pass the application
# for the identification stage # for the identification stage
if not request.user.is_authenticated: if not request.user.is_authenticated:
@ -333,7 +331,7 @@ class AuthorizationFlowInitView(PolicyAccessMixin, View):
# Check permissions # Check permissions
result = self.user_has_access(application) result = self.user_has_access(application)
if not result.passing: if not result.passing:
return self.handle_no_permission_authorized() return self.handle_no_permission_authenticated(result)
# TODO: End block # TODO: End block
# Extract params so we can save them in the plan context # Extract params so we can save them in the plan context
try: try:

View File

@ -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",
),
),
]

View File

@ -50,7 +50,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
cookie_secret = models.TextField(default=get_cookie_secret) cookie_secret = models.TextField(default=get_cookie_secret)
certificate = models.ForeignKey( 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]: def form(self) -> Type[ModelForm]:

View File

@ -1,5 +1,6 @@
"""passbook saml_idp Models""" """passbook saml_idp Models"""
from typing import Optional, Type from typing import Optional, Type
from urllib.parse import urlparse
from django.db import models from django.db import models
from django.forms import ModelForm 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]: def form(self) -> Type[ModelForm]:
from passbook.providers.saml.forms import SAMLProviderForm from passbook.providers.saml.forms import SAMLProviderForm

View File

@ -62,8 +62,9 @@ class SAMLSSOView(PolicyAccessMixin, View):
) )
if not request.user.is_authenticated: if not request.user.is_authenticated:
return self.handle_no_permission(self.application) return self.handle_no_permission(self.application)
if not self.user_has_access(self.application).passing: has_access = self.user_has_access(self.application)
return self.handle_no_permission_authorized() if not has_access.passing:
return self.handle_no_permission_authenticated(has_access)
# Call the method handler, which checks the SAML Request # Call the method handler, which checks the SAML Request
method_response = super().dispatch(request, *args, application_slug, **kwargs) method_response = super().dispatch(request, *args, application_slug, **kwargs)
if method_response: if method_response:

View File

@ -193,7 +193,12 @@ ASGI_APPLICATION = "passbook.root.routing.application"
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "channels_redis.core.RedisChannelLayer", "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')}"
],
},
}, },
} }