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