Compare commits
	
		
			265 Commits
		
	
	
		
			version/0.
			...
			version/0.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d6afdc575e | |||
| 287b38efee | |||
| e805fb62fb | |||
| c92dda77f1 | |||
| f12fd78822 | |||
| caba183c9b | |||
| 3aeaa121a3 | |||
| a9f3118a7d | |||
| 054b819262 | |||
| 6b3411f63b | |||
| 6a8000ea0d | |||
| 352d4db0d7 | |||
| 4b665cfb8f | |||
| 4e12003944 | |||
| 6bfd465855 | |||
| e8670aa693 | |||
| 5263e750b1 | |||
| a2a9d73296 | |||
| 6befc9d627 | |||
| 73497a27cc | |||
| f3098418f2 | |||
| a5197963b2 | |||
| e4634bcc78 | |||
| 74da44a6a9 | |||
| 3324473cd0 | |||
| 39d8038533 | |||
| bbcf58705f | |||
| 7b5a0964b2 | |||
| 8eca76e464 | |||
| fb9ab368f8 | |||
| 877279b2ee | |||
| 301be4b411 | |||
| 728f527ccb | |||
| 3f1c790b1d | |||
| b00573bde2 | |||
| aeee3ad7f9 | |||
| ef021495ef | |||
| 061eab4b36 | |||
| 870e01f836 | |||
| e2ca72adf0 | |||
| 395ef43eae | |||
| a4cc653757 | |||
| db4ff20906 | |||
| 1f0fbd33b6 | |||
| 5de8d2721e | |||
| 0d65da9a9e | |||
| 4316ee4330 | |||
| 2ed9a1dbe3 | |||
| 8e03824d20 | |||
| 754dbdd0e5 | |||
| e13d348315 | |||
| 169f3ebe5b | |||
| f8ad604e85 | |||
| 774b9c8a61 | |||
| d8c522233e | |||
| 82d50f7eaa | |||
| 1c426c5136 | |||
| d6e14cc551 | |||
| c3917ebc2e | |||
| 7203bd37a3 | |||
| 597188c7ee | |||
| ac4c314042 | |||
| 05866d3544 | |||
| 6596bc6034 | |||
| c6661ef4d2 | |||
| 386e23dfac | |||
| 5d7220ca70 | |||
| 5de0d03acf | |||
| b0cc91f343 | |||
| 029a78f108 | |||
| 3f4a8dc4f6 | |||
| 32f6ba6302 | |||
| 8da0b14f29 | |||
| 83eb4aff02 | |||
| 927d02f591 | |||
| d04afcd6d0 | |||
| 89c6db66fd | |||
| e6ffa65a7e | |||
| 8a2f982a77 | |||
| 16cf6315e3 | |||
| 1d85874f41 | |||
| ff64182ae8 | |||
| a9ee67bf2d | |||
| e87d52a76b | |||
| 8b09cf55a2 | |||
| 0203d20759 | |||
| 7861e2e0bd | |||
| ad29d54bbf | |||
| c698ba37d9 | |||
| 6a53069653 | |||
| 152b2d863d | |||
| ee670d5e19 | |||
| 36e095671c | |||
| 1088b947a8 | |||
| c4a30c50ac | |||
| 2831df45a0 | |||
| ee5bac099f | |||
| 69f7b41044 | |||
| f9cede7b31 | |||
| 903cdeaa7f | |||
| e909e7fa8a | |||
| bee38551f3 | |||
| c0ec6388df | |||
| 8f08836885 | |||
| dd0d7e7481 | |||
| 25d0ac6534 | |||
| 971713d1aa | |||
| 5135d828b4 | |||
| b2c571bf1b | |||
| 6b1d30d230 | |||
| 3454760731 | |||
| 96846220c3 | |||
| a4f5678144 | |||
| a18baa3cb3 | |||
| dfedd4a7f1 | |||
| 897f64600a | |||
| c6eb015d18 | |||
| 54088239ab | |||
| aa9c7a6567 | |||
| 6c0c12c90a | |||
| c49b57ad1d | |||
| 2339e855bb | |||
| bdc019c7cf | |||
| 5e2fb6d56e | |||
| 3b9524cdfc | |||
| 7154f19668 | |||
| 8fedd9ec07 | |||
| 4ac87d8739 | |||
| e4f45eba0a | |||
| 4b3e0f0f96 | |||
| 482da81522 | |||
| c5226fd0e8 | |||
| 7806cff96f | |||
| fa504e4bf9 | |||
| 86cfb10b9b | |||
| f6b8171624 | |||
| 91ce7f7363 | |||
| 17060238f0 | |||
| c392c2a74b | |||
| 8cbaec8ba8 | |||
| 4750f8c653 | |||
| 69d2a1cf3b | |||
| 635f6c1ef2 | |||
| 18da7565c2 | |||
| 45699a1a69 | |||
| 5556e9f8e7 | |||
| 327bb09dd4 | |||
| 8ca23451c6 | |||
| b99e2b10fe | |||
| e966dff1a7 | |||
| 481fbedef2 | |||
| d104012eee | |||
| b03a508475 | |||
| 8ede4b6a13 | |||
| 41323afccc | |||
| 4a10b4999b | |||
| 20ee634cda | |||
| 713025d218 | |||
| 58ae159835 | |||
| c95efe3cde | |||
| b6eb0bf53d | |||
| 610b6c7f70 | |||
| 1ea2d99ff2 | |||
| 67be43679c | |||
| fd42389bd5 | |||
| 71b1df2fec | |||
| 7a3122f25c | |||
| 63041d788b | |||
| bfc1bae0bb | |||
| 8ab7f7fcbb | |||
| c1eb8317f7 | |||
| 7a578e5e83 | |||
| b10912d8ba | |||
| ef24b1cde2 | |||
| 26cacc2a06 | |||
| ca0e89c799 | |||
| 17950119ad | |||
| 876618c1ec | |||
| 2293ab69b9 | |||
| 9df00e09a4 | |||
| cf6ce9c915 | |||
| 3b61191614 | |||
| 9954eeac86 | |||
| ac88bd5d44 | |||
| 2406a619df | |||
| 63087c9393 | |||
| da9aaf69df | |||
| ae125dd1f0 | |||
| f636595230 | |||
| d506e8f1a3 | |||
| d3a96ac7aa | |||
| 189b0ec324 | |||
| c5a6b4961f | |||
| b590589324 | |||
| 9fb1ac98ec | |||
| 195d8fe71f | |||
| b0602a3215 | |||
| 0150a5c58c | |||
| b35d27c83e | |||
| 801bb90806 | |||
| 55a83abb26 | |||
| c09b4e9713 | |||
| 247015e955 | |||
| fe3634be64 | |||
| ead20b03aa | |||
| 932a475af7 | |||
| e9a1a18ba3 | |||
| 6cd9edd38a | |||
| 9b5f9167cd | |||
| 1f30bcd335 | |||
| 94eaeb5a60 | |||
| a5420fe019 | |||
| 2e1849a732 | |||
| 4039e96803 | |||
| 8f585eca70 | |||
| 516455f482 | |||
| 719099a5af | |||
| 7f74d32253 | |||
| 525d271535 | |||
| 9ef39f1e04 | |||
| 9099dc5713 | |||
| c3c525a3f0 | |||
| e699dfe88c | |||
| c0b334eb02 | |||
| 815ad26b91 | |||
| 03647fa6af | |||
| 5aec581585 | |||
| 68e9b7e140 | |||
| b42bca4e3e | |||
| 42c9ac61b2 | |||
| 7cdc5f0568 | |||
| a063613f4c | |||
| 3af04bf1e4 | |||
| 74f8b68af8 | |||
| 59dbc15be7 | |||
| 9d5dd896f3 | |||
| 02f5f12089 | |||
| 90ea6dba90 | |||
| b0b2c0830b | |||
| acb2b825f3 | |||
| e956b86649 | |||
| 739c66da1c | |||
| e8c7cce68f | |||
| f741d382c2 | |||
| a13d4047b6 | |||
| e0d8189442 | |||
| 760352202e | |||
| 9724ded194 | |||
| 5da4ff4ff1 | |||
| e54b98a80e | |||
| 67b69cb5d3 | |||
| 863111ac57 | |||
| bd78087582 | |||
| 8f4e954160 | |||
| 553f184aad | |||
| b6d7847eae | |||
| ad0d339794 | |||
| 737cd22bb9 | |||
| 6ad1465f8f | |||
| d74fa4abbf | |||
| b24938fc6b | |||
| ea1564548c | |||
| 3663c3c8a1 | |||
| 07e20a2950 | |||
| 6366d50a0e | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 0.10.7-stable | current_version = 0.12.7-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>.*) | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -6,12 +6,16 @@ updates: | |||||||
|     interval: daily |     interval: daily | ||||||
|     time: "04:00" |     time: "04:00" | ||||||
|   open-pull-requests-limit: 10 |   open-pull-requests-limit: 10 | ||||||
|  |   assignees: | ||||||
|  |   - BeryJu | ||||||
| - package-ecosystem: npm | - package-ecosystem: npm | ||||||
|   directory: "/passbook/static/static" |   directory: "/passbook/static/static" | ||||||
|   schedule: |   schedule: | ||||||
|     interval: daily |     interval: daily | ||||||
|     time: "04:00" |     time: "04:00" | ||||||
|   open-pull-requests-limit: 10 |   open-pull-requests-limit: 10 | ||||||
|  |   assignees: | ||||||
|  |   - BeryJu | ||||||
| - package-ecosystem: pip | - package-ecosystem: pip | ||||||
|   directory: "/" |   directory: "/" | ||||||
|   schedule: |   schedule: | ||||||
| @ -20,3 +24,19 @@ updates: | |||||||
|   open-pull-requests-limit: 10 |   open-pull-requests-limit: 10 | ||||||
|   assignees: |   assignees: | ||||||
|   - BeryJu |   - BeryJu | ||||||
|  | - package-ecosystem: docker | ||||||
|  |   directory: "/" | ||||||
|  |   schedule: | ||||||
|  |     interval: daily | ||||||
|  |     time: "04:00" | ||||||
|  |   open-pull-requests-limit: 10 | ||||||
|  |   assignees: | ||||||
|  |   - BeryJu | ||||||
|  | - package-ecosystem: docker | ||||||
|  |   directory: "/proxy" | ||||||
|  |   schedule: | ||||||
|  |     interval: daily | ||||||
|  |     time: "04:00" | ||||||
|  |   open-pull-requests-limit: 10 | ||||||
|  |   assignees: | ||||||
|  |   - BeryJu | ||||||
|  | |||||||
							
								
								
									
										54
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -1,54 +0,0 @@ | |||||||
| name: "CodeQL" |  | ||||||
|  |  | ||||||
| on: |  | ||||||
|   push: |  | ||||||
|     branches: [master, admin-more-info, ci-deploy-dev, gh-pages, provider-saml-v2] |  | ||||||
|   pull_request: |  | ||||||
|     # The branches below must be a subset of the branches above |  | ||||||
|     branches: [master] |  | ||||||
|   schedule: |  | ||||||
|     - cron: '0 20 * * 2' |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   analyse: |  | ||||||
|     name: Analyse |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|  |  | ||||||
|     steps: |  | ||||||
|     - name: Checkout repository |  | ||||||
|       uses: actions/checkout@v2 |  | ||||||
|       with: |  | ||||||
|         # We must fetch at least the immediate parents so that if this is |  | ||||||
|         # a pull request then we can checkout the head. |  | ||||||
|         fetch-depth: 2 |  | ||||||
|  |  | ||||||
|     # If this run was triggered by a pull request event, then checkout |  | ||||||
|     # the head of the pull request instead of the merge commit. |  | ||||||
|     - run: git checkout HEAD^2 |  | ||||||
|       if: ${{ github.event_name == 'pull_request' }} |  | ||||||
|  |  | ||||||
|     # Initializes the CodeQL tools for scanning. |  | ||||||
|     - name: Initialize CodeQL |  | ||||||
|       uses: github/codeql-action/init@v1 |  | ||||||
|       # Override language selection by uncommenting this and choosing your languages |  | ||||||
|       # with: |  | ||||||
|       #   languages: go, javascript, csharp, python, cpp, java |  | ||||||
|  |  | ||||||
|     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). |  | ||||||
|     # If this step fails, then you should remove it and run the build manually (see below) |  | ||||||
|     - name: Autobuild |  | ||||||
|       uses: github/codeql-action/autobuild@v1 |  | ||||||
|  |  | ||||||
|     # ℹ️ Command-line programs to run using the OS shell. |  | ||||||
|     # 📚 https://git.io/JvXDl |  | ||||||
|  |  | ||||||
|     # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines |  | ||||||
|     #    and modify them (or add more) to build your code if your project |  | ||||||
|     #    uses a compiled language |  | ||||||
|  |  | ||||||
|     #- run: | |  | ||||||
|     #   make bootstrap |  | ||||||
|     #   make release |  | ||||||
|  |  | ||||||
|     - name: Perform CodeQL Analysis |  | ||||||
|       uses: github/codeql-action/analyze@v1 |  | ||||||
							
								
								
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -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.7-stable |           -t beryju/passbook:0.12.7-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.7-stable |         run: docker push beryju/passbook:0.12.7-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.7-stable \ |           -t beryju/passbook-proxy:0.12.7-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.7-stable |         run: docker push beryju/passbook-proxy:0.12.7-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.7-stable |           -t beryju/passbook-static:0.12.7-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.7-stable |         run: docker push beryju/passbook-static:0.12.7-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: | ||||||
| @ -114,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.7-stable |           tagName: 0.12.7-stable | ||||||
|           environment: beryjuorg-prod |           environment: beryjuorg-prod | ||||||
|  | |||||||
							
								
								
									
										21
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						| @ -16,12 +16,25 @@ COPY --from=locker /app/requirements.txt / | |||||||
| COPY --from=locker /app/requirements-dev.txt / | COPY --from=locker /app/requirements-dev.txt / | ||||||
|  |  | ||||||
| RUN apt-get update && \ | RUN apt-get update && \ | ||||||
|     apt-get install -y --no-install-recommends postgresql-client-11 build-essential && \ |     apt-get install -y --no-install-recommends curl ca-certificates gnupg && \ | ||||||
|     rm -rf /var/lib/apt/ && \ |     curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ | ||||||
|  |     echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ | ||||||
|  |     apt-get update && \ | ||||||
|  |     apt-get install -y --no-install-recommends postgresql-client-12 postgresql-client-11 build-essential && \ | ||||||
|  |     apt-get clean && \ | ||||||
|     pip install -r /requirements.txt --no-cache-dir && \ |     pip install -r /requirements.txt --no-cache-dir && \ | ||||||
|     apt-get remove --purge -y build-essential && \ |     apt-get remove --purge -y build-essential && \ | ||||||
|     apt-get autoremove --purge && \ |     apt-get autoremove --purge -y && \ | ||||||
|     adduser --system --no-create-home --uid 1000 --group --home /passbook passbook |     # This is quite hacky, but docker has no guaranteed Group ID | ||||||
|  |     # we could instead check for the GID of the socket and add the user dynamically, | ||||||
|  |     # but then we have to drop permmissions later | ||||||
|  |     groupadd -g 998 docker_998 && \ | ||||||
|  |     groupadd -g 999 docker_999 && \ | ||||||
|  |     adduser --system --no-create-home --uid 1000 --group --home /passbook passbook && \ | ||||||
|  |     usermod -a -G docker_998 passbook && \ | ||||||
|  |     usermod -a -G docker_999 passbook && \ | ||||||
|  |     mkdir /backups && \ | ||||||
|  |     chown passbook:passbook /backups | ||||||
|  |  | ||||||
| COPY ./passbook/ /passbook | COPY ./passbook/ /passbook | ||||||
| COPY ./manage.py / | COPY ./manage.py / | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						| @ -11,8 +11,8 @@ lint-fix: | |||||||
| 	black passbook e2e lifecycle | 	black passbook e2e lifecycle | ||||||
|  |  | ||||||
| lint: | lint: | ||||||
| 	pyright pyright e2e lifecycle | 	pyright passbook e2e lifecycle | ||||||
| 	bandit -r passbook e2e lifecycle | 	bandit -r passbook e2e lifecycle -x node_modules | ||||||
| 	pylint passbook e2e lifecycle | 	pylint passbook e2e lifecycle | ||||||
| 	prospector | 	prospector | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						| @ -17,10 +17,10 @@ django-otp = "*" | |||||||
| django-prometheus = "*" | django-prometheus = "*" | ||||||
| django-recaptcha = "*" | django-recaptcha = "*" | ||||||
| django-redis = "*" | django-redis = "*" | ||||||
| django-rest-framework = "*" | djangorestframework = "*" | ||||||
| django-storages = "*" | django-storages = "*" | ||||||
| djangorestframework-guardian = "*" | djangorestframework-guardian = "*" | ||||||
| drf-yasg = "*" | drf_yasg2 = "*" | ||||||
| facebook-sdk = "*" | facebook-sdk = "*" | ||||||
| ldap3 = "*" | ldap3 = "*" | ||||||
| lxml = "*" | lxml = "*" | ||||||
| @ -28,7 +28,7 @@ packaging = "*" | |||||||
| psycopg2-binary = "*" | psycopg2-binary = "*" | ||||||
| pycryptodome = "*" | pycryptodome = "*" | ||||||
| pyjwkest = "*" | pyjwkest = "*" | ||||||
| uvicorn = "*" | uvicorn = {extras = ["standard"],version = "*"} | ||||||
| gunicorn = "*" | gunicorn = "*" | ||||||
| pyyaml = "*" | pyyaml = "*" | ||||||
| qrcode = "*" | qrcode = "*" | ||||||
| @ -43,6 +43,7 @@ dacite = "*" | |||||||
| channels = "*" | channels = "*" | ||||||
| channels-redis = "*" | channels-redis = "*" | ||||||
| kubernetes = "*" | kubernetes = "*" | ||||||
|  | docker = "*" | ||||||
|  |  | ||||||
| [requires] | [requires] | ||||||
| python_version = "3.8" | python_version = "3.8" | ||||||
| @ -50,12 +51,11 @@ python_version = "3.8" | |||||||
| [dev-packages] | [dev-packages] | ||||||
| autopep8 = "*" | autopep8 = "*" | ||||||
| bandit = "*" | bandit = "*" | ||||||
| black = "==19.10b0" | black = "==20.8b1" | ||||||
| bumpversion = "*" | bumpversion = "*" | ||||||
| colorama = "*" | colorama = "*" | ||||||
| coverage = "*" | coverage = "*" | ||||||
| django-debug-toolbar = "*" | django-debug-toolbar = "*" | ||||||
| docker = "*" |  | ||||||
| pylint = "*" | pylint = "*" | ||||||
| pylint-django = "*" | pylint-django = "*" | ||||||
| selenium = "*" | selenium = "*" | ||||||
|  | |||||||
							
								
								
									
										474
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "_meta": { |     "_meta": { | ||||||
|         "hash": { |         "hash": { | ||||||
|             "sha256": "80570636236962f4b934a884817292de9f7bb48520aa964afc2959b0f795fb57" |             "sha256": "d1a9883d864e25f18e34b298b72b58db333a037571c7a20cefb7ba7a4037a434" | ||||||
|         }, |         }, | ||||||
|         "pipfile-spec": 6, |         "pipfile-spec": 6, | ||||||
|         "requires": { |         "requires": { | ||||||
| @ -74,18 +74,17 @@ | |||||||
|         }, |         }, | ||||||
|         "boto3": { |         "boto3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0c464a7de522f88b581ca0d41ffa71e9be5e17fbb0456c275421f65b7c5f6a55", |                 "sha256:163bf25d7c28be24392744485cf180bee4ff5e84c4ee891a32501e89dc352982" | ||||||
|                 "sha256:0fce548e19d6db8e11fd0e2ae7809e1e3282080636b4062b2452bfa20e4f0233" |  | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.15.5" |             "version": "==1.16.5" | ||||||
|         }, |         }, | ||||||
|         "botocore": { |         "botocore": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7ce7a05b98ffb3170396960273383e8aade9be6026d5a762f5f40969d5d6b761", |                 "sha256:2bacdfb1eb66c885be18b7557202160f9eb48d81e16881e1ce5176881cc9d8aa", | ||||||
|                 "sha256:e3bf44fba058f6df16006b94a67650418a080a525c82521abb3cb516a4cba362" |                 "sha256:50e6b9b0e082a9af620552b5d1b5966f1da09202d6ccd03fd2c9856ae0578db5" | ||||||
|             ], |             ], | ||||||
|             "version": "==1.18.5" |             "version": "==1.19.5" | ||||||
|         }, |         }, | ||||||
|         "cachetools": { |         "cachetools": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -96,11 +95,11 @@ | |||||||
|         }, |         }, | ||||||
|         "celery": { |         "celery": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:313930fddde703d8e37029a304bf91429cd11aeef63c57de6daca9d958e1f255", |                 "sha256:7aa4ee46ed318bc177900ae7c01500354aee62d723255b0925db0754bcd4d390", | ||||||
|                 "sha256:72138dc3887f68dc58e1a2397e477256f80f1894c69fa4337f8ed70be460375b" |                 "sha256:e3e8956d74af986b1e9770e0a294338b259618bf70283d6157416328e50c2bd6" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==5.0.0" |             "version": "==5.0.1" | ||||||
|         }, |         }, | ||||||
|         "certifi": { |         "certifi": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -263,11 +262,11 @@ | |||||||
|         }, |         }, | ||||||
|         "django": { |         "django": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:59c8125ca873ed3bdae9c12b146fbbd6ed8d0f743e4cf5f5817af50c51f1fc2f", |                 "sha256:a2127ad0150ec6966655bedf15dbbff9697cc86d61653db2da1afa506c0b04cc", | ||||||
|                 "sha256:b5fbb818e751f660fa2d576d9f40c34a4c615c8b48dd383f5216e609f383371f" |                 "sha256:c93c28ccf1d094cbd00d860e83128a39e45d2c571d3b54361713aaaf9a94cac4" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==3.1.1" |             "version": "==3.1.2" | ||||||
|         }, |         }, | ||||||
|         "django-cors-middleware": { |         "django-cors-middleware": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -286,11 +285,11 @@ | |||||||
|         }, |         }, | ||||||
|         "django-filter": { |         "django-filter": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:11e63dd759835d9ba7a763926ffb2662cf8a6dcb4c7971a95064de34dbc7e5af", |                 "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06", | ||||||
|                 "sha256:616848eab6fc50193a1b3730140c49b60c57a3eda1f7fc57fa8505ac156c6c75" |                 "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==2.3.0" |             "version": "==2.4.0" | ||||||
|         }, |         }, | ||||||
|         "django-guardian": { |         "django-guardian": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -310,11 +309,11 @@ | |||||||
|         }, |         }, | ||||||
|         "django-otp": { |         "django-otp": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0c9edbb3f4abc9ac6e43daf0a9e0e293e99ad917641cf8d7dbc49d613bcb5cd4", |                 "sha256:8ba5ab9bd2738c7321376c349d7cce49cf4404e79f6804e0a3cc462a91728e18", | ||||||
|                 "sha256:ace831f3a0f2c2267e4f7219c78deeb3b41c2dc8ae44b03daebb4fb85dabeb43" |                 "sha256:f523fb9dec420f28a29d3e2ad72ac06f64588956ed4f2b5b430d8e957ebb8287" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.0.0" |             "version": "==1.0.2" | ||||||
|         }, |         }, | ||||||
|         "django-prometheus": { |         "django-prometheus": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -339,13 +338,6 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==4.12.1" |             "version": "==4.12.1" | ||||||
|         }, |         }, | ||||||
|         "django-rest-framework": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:47a8f496fa69e3b6bd79f68dd7a1527d907d6b77f009e9db7cf9bb21cc565e4a" |  | ||||||
|             ], |  | ||||||
|             "index": "pypi", |  | ||||||
|             "version": "==0.1.0" |  | ||||||
|         }, |  | ||||||
|         "django-storages": { |         "django-storages": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c", |                 "sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c", | ||||||
| @ -356,10 +348,11 @@ | |||||||
|         }, |         }, | ||||||
|         "djangorestframework": { |         "djangorestframework": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32", |                 "sha256:5c5071fcbad6dce16f566d492015c829ddb0df42965d488b878594aabc3aed21", | ||||||
|                 "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b" |                 "sha256:d54452aedebb4b650254ca092f9f4f5df947cb1de6ab245d817b08b4f4156249" | ||||||
|             ], |             ], | ||||||
|             "version": "==3.11.1" |             "index": "pypi", | ||||||
|  |             "version": "==3.12.1" | ||||||
|         }, |         }, | ||||||
|         "djangorestframework-guardian": { |         "djangorestframework-guardian": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -369,13 +362,21 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==0.3.0" |             "version": "==0.3.0" | ||||||
|         }, |         }, | ||||||
|         "drf-yasg": { |         "docker": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:5572e9d5baab9f6b49318169df9789f7399d0e3c7bdac8fdb8dfccf1d5d2b1ca", |                 "sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828", | ||||||
|                 "sha256:7d7af27ad16e18507e9392b2afd6b218fbffc432ec8dbea053099a2241e184ff" |                 "sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.17.1" |             "version": "==4.3.1" | ||||||
|  |         }, | ||||||
|  |         "drf-yasg2": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:65826bf19e5222d38b84380468303c8c389d0b9e2335ee6efa4151ba87ca0a3f", | ||||||
|  |                 "sha256:6c662de6e0ffd4f74c49c06a88b8a9d1eb4bc9d7bfe82dac9f80a51a23cacecb" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "version": "==1.19.3" | ||||||
|         }, |         }, | ||||||
|         "eight": { |         "eight": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -400,10 +401,10 @@ | |||||||
|         }, |         }, | ||||||
|         "google-auth": { |         "google-auth": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:31941bf019fb242c04d0de32845da10180788bfddb0de87d78c4bdf55555dda1", |                 "sha256:712dd7d140a9a1ea218e5688c7fcb04af71b431a29ec9ce433e384c60e387b98", | ||||||
|                 "sha256:873051a6317294b083795cffc467bcd05b6df483ef542bfe0069ddbfbac0a096" |                 "sha256:9c0f71789438d703f77b94aad4ea545afaec9a65f10e6cc1bc8b89ce242244bb" | ||||||
|             ], |             ], | ||||||
|             "version": "==1.21.3" |             "version": "==1.22.1" | ||||||
|         }, |         }, | ||||||
|         "gunicorn": { |         "gunicorn": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -415,10 +416,10 @@ | |||||||
|         }, |         }, | ||||||
|         "h11": { |         "h11": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1", |                 "sha256:3c6c61d69c6f13d41f1b80ab0322f1872702a3ba26e12aa864c928f6a43fbaab", | ||||||
|                 "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1" |                 "sha256:ab6c335e1b6ef34b205d5ca3e228c9299cc7218b049819ec84a388c2525e5d87" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.9.0" |             "version": "==0.11.0" | ||||||
|         }, |         }, | ||||||
|         "hiredis": { |         "hiredis": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -486,7 +487,6 @@ | |||||||
|                 "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4", |                 "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4", | ||||||
|                 "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437" |                 "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437" | ||||||
|             ], |             ], | ||||||
|             "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", |  | ||||||
|             "version": "==0.1.1" |             "version": "==0.1.1" | ||||||
|         }, |         }, | ||||||
|         "hyperlink": { |         "hyperlink": { | ||||||
| @ -554,11 +554,11 @@ | |||||||
|         }, |         }, | ||||||
|         "kubernetes": { |         "kubernetes": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:1a2472f8b01bc6aa87e3a34781f859bded5a5c8ff791a53d889a8bd6cc550430", |                 "sha256:72f095a1cd593401ff26b3b8d71749340394ca6d8413770ea28ce18efd5bcf4c", | ||||||
|                 "sha256:4af81201520977139a143f96123fb789fa351879df37f122916b9b6ed050bbaf" |                 "sha256:9a339a32d6c79e6461cb6050c3662cb4e33058b508d8d34ee5d5206add395828" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==11.0.0" |             "version": "==12.0.0" | ||||||
|         }, |         }, | ||||||
|         "ldap3": { |         "ldap3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -570,40 +570,45 @@ | |||||||
|         }, |         }, | ||||||
|         "lxml": { |         "lxml": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f", |                 "sha256:0e89f5d422988c65e6936e4ec0fe54d6f73f3128c80eb7ecc3b87f595523607b", | ||||||
|                 "sha256:08fc93257dcfe9542c0a6883a25ba4971d78297f63d7a5a26ffa34861ca78730", |                 "sha256:189ad47203e846a7a4951c17694d845b6ade7917c47c64b29b86526eefc3adf5", | ||||||
|                 "sha256:107781b213cf7201ec3806555657ccda67b1fccc4261fb889ef7fc56976db81f", |                 "sha256:1d87936cb5801c557f3e981c9c193861264c01209cb3ad0964a16310ca1b3301", | ||||||
|                 "sha256:121b665b04083a1e85ff1f5243d4a93aa1aaba281bc12ea334d5a187278ceaf1", |                 "sha256:211b3bcf5da70c2d4b84d09232534ad1d78320762e2c59dedc73bf01cb1fc45b", | ||||||
|                 "sha256:1fa21263c3aba2b76fd7c45713d4428dbcc7644d73dcf0650e9d344e433741b3", |                 "sha256:2358809cc64394617f2719147a58ae26dac9e21bae772b45cfb80baa26bfca5d", | ||||||
|                 "sha256:2b30aa2bcff8e958cd85d907d5109820b01ac511eae5b460803430a7404e34d7", |                 "sha256:23c83112b4dada0b75789d73f949dbb4e8f29a0a3511647024a398ebd023347b", | ||||||
|                 "sha256:4b4a111bcf4b9c948e020fd207f915c24a6de3f1adc7682a2d92660eb4e84f1a", |                 "sha256:24e811118aab6abe3ce23ff0d7d38932329c513f9cef849d3ee88b0f848f2aa9", | ||||||
|                 "sha256:5591c4164755778e29e69b86e425880f852464a21c7bb53c7ea453bbe2633bbe", |                 "sha256:2d5896ddf5389560257bbe89317ca7bcb4e54a02b53a3e572e1ce4226512b51b", | ||||||
|                 "sha256:59daa84aef650b11bccd18f99f64bfe44b9f14a08a28259959d33676554065a1", |                 "sha256:2d6571c48328be4304aee031d2d5046cbc8aed5740c654575613c5a4f5a11311", | ||||||
|                 "sha256:5a9c8d11aa2c8f8b6043d845927a51eb9102eb558e3f936df494e96393f5fd3e", |                 "sha256:2e311a10f3e85250910a615fe194839a04a0f6bc4e8e5bb5cac221344e3a7891", | ||||||
|                 "sha256:5dd20538a60c4cc9a077d3b715bb42307239fcd25ef1ca7286775f95e9e9a46d", |                 "sha256:302160eb6e9764168e01d8c9ec6becddeb87776e81d3fcb0d97954dd51d48e0a", | ||||||
|                 "sha256:74f48ec98430e06c1fa8949b49ebdd8d27ceb9df8d3d1c92e1fdc2773f003f20", |                 "sha256:3a7a380bfecc551cfd67d6e8ad9faa91289173bdf12e9cfafbd2bdec0d7b1ec1", | ||||||
|                 "sha256:786aad2aa20de3dbff21aab86b2fb6a7be68064cbbc0219bde414d3a30aa47ae", |                 "sha256:3d9b2b72eb0dbbdb0e276403873ecfae870599c83ba22cadff2db58541e72856", | ||||||
|                 "sha256:7ad7906e098ccd30d8f7068030a0b16668ab8aa5cda6fcd5146d8d20cbaa71b5", |                 "sha256:475325e037fdf068e0c2140b818518cf6bc4aa72435c407a798b2db9f8e90810", | ||||||
|                 "sha256:80a38b188d20c0524fe8959c8ce770a8fdf0e617c6912d23fc97c68301bb9aba", |                 "sha256:4b7572145054330c8e324a72d808c8c8fbe12be33368db28c39a255ad5f7fb51", | ||||||
|                 "sha256:8f0ec6b9b3832e0bd1d57af41f9238ea7709bbd7271f639024f2fc9d3bb01293", |                 "sha256:4fff34721b628cce9eb4538cf9a73d02e0f3da4f35a515773cce6f5fe413b360", | ||||||
|                 "sha256:92282c83547a9add85ad658143c76a64a8d339028926d7dc1998ca029c88ea6a", |                 "sha256:56eff8c6fb7bc4bcca395fdff494c52712b7a57486e4fbde34c31bb9da4c6cc4", | ||||||
|                 "sha256:94150231f1e90c9595ccc80d7d2006c61f90a5995db82bccbca7944fd457f0f6", |                 "sha256:573b2f5496c7e9f4985de70b9bbb4719ffd293d5565513e04ac20e42e6e5583f", | ||||||
|                 "sha256:9dc9006dcc47e00a8a6a029eb035c8f696ad38e40a27d073a003d7d1443f5d88", |                 "sha256:7ecaef52fd9b9535ae5f01a1dd2651f6608e4ec9dc136fc4dfe7ebe3c3ddb230", | ||||||
|                 "sha256:a76979f728dd845655026ab991df25d26379a1a8fc1e9e68e25c7eda43004bed", |                 "sha256:803a80d72d1f693aa448566be46ffd70882d1ad8fc689a2e22afe63035eb998a", | ||||||
|                 "sha256:aa8eba3db3d8761db161003e2d0586608092e217151d7458206e243be5a43843", |                 "sha256:8862d1c2c020cb7a03b421a9a7b4fe046a208db30994fc8ff68c627a7915987f", | ||||||
|                 "sha256:bea760a63ce9bba566c23f726d72b3c0250e2fa2569909e2d83cda1534c79443", |                 "sha256:9b06690224258db5cd39a84e993882a6874676f5de582da57f3df3a82ead9174", | ||||||
|                 "sha256:c3f511a3c58676147c277eff0224c061dd5a6a8e1373572ac817ac6324f1b1e0", |                 "sha256:a71400b90b3599eb7bf241f947932e18a066907bf84617d80817998cee81e4bf", | ||||||
|                 "sha256:c9d317efde4bafbc1561509bfa8a23c5cab66c44d49ab5b63ff690f5159b2304", |                 "sha256:bb252f802f91f59767dcc559744e91efa9df532240a502befd874b54571417bd", | ||||||
|                 "sha256:cc411ad324a4486b142c41d9b2b6a722c534096963688d879ea6fa8a35028258", |                 "sha256:be1ebf9cc25ab5399501c9046a7dcdaa9e911802ed0e12b7d620cd4bbf0518b3", | ||||||
|                 "sha256:cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6", |                 "sha256:be7c65e34d1b50ab7093b90427cbc488260e4b3a38ef2435d65b62e9fa3d798a", | ||||||
|                 "sha256:cfd7c5dd3c35c19cec59c63df9571c67c6d6e5c92e0fe63517920e97f61106d1", |                 "sha256:c0dac835c1a22621ffa5e5f999d57359c790c52bbd1c687fe514ae6924f65ef5", | ||||||
|                 "sha256:e1cacf4796b20865789083252186ce9dc6cc59eca0c2e79cca332bdff24ac481", |                 "sha256:c152b2e93b639d1f36ec5a8ca24cde4a8eefb2b6b83668fcd8e83a67badcb367", | ||||||
|                 "sha256:e70d4e467e243455492f5de463b72151cc400710ac03a0678206a5f27e79ddef", |                 "sha256:d182eada8ea0de61a45a526aa0ae4bcd222f9673424e65315c35820291ff299c", | ||||||
|                 "sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd", |                 "sha256:d18331ea905a41ae71596502bd4c9a2998902328bbabd29e3d0f5f8569fabad1", | ||||||
|                 "sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee" |                 "sha256:d20d32cbb31d731def4b1502294ca2ee99f9249b63bc80e03e67e8f8e126dea8", | ||||||
|  |                 "sha256:d4ad7fd3269281cb471ad6c7bafca372e69789540d16e3755dd717e9e5c9d82f", | ||||||
|  |                 "sha256:d6f8c23f65a4bfe4300b85f1f40f6c32569822d08901db3b6454ab785d9117cc", | ||||||
|  |                 "sha256:d84d741c6e35c9f3e7406cb7c4c2e08474c2a6441d59322a00dcae65aac6315d", | ||||||
|  |                 "sha256:e65c221b2115a91035b55a593b6eb94aa1206fa3ab374f47c6dc10d364583ff9", | ||||||
|  |                 "sha256:f98b6f256be6cec8dd308a8563976ddaff0bdc18b730720f6f4bee927ffe926f" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==4.5.2" |             "version": "==4.6.1" | ||||||
|         }, |         }, | ||||||
|         "markupsafe": { |         "markupsafe": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -690,10 +695,10 @@ | |||||||
|         }, |         }, | ||||||
|         "prompt-toolkit": { |         "prompt-toolkit": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:822f4605f28f7d2ba6b0b09a31e25e140871e96364d1d377667b547bb3bf4489", |                 "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c", | ||||||
|                 "sha256:83074ee28ad4ba6af190593d4d4c607ff525272a504eb159199b6dd9f950c950" |                 "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63" | ||||||
|             ], |             ], | ||||||
|             "version": "==3.0.7" |             "version": "==3.0.8" | ||||||
|         }, |         }, | ||||||
|         "psycopg2-binary": { |         "psycopg2-binary": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -707,6 +712,7 @@ | |||||||
|                 "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", |                 "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", | ||||||
|                 "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", |                 "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", | ||||||
|                 "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", |                 "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", | ||||||
|  |                 "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", | ||||||
|                 "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", |                 "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", | ||||||
|                 "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", |                 "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", | ||||||
|                 "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", |                 "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", | ||||||
| @ -876,6 +882,13 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==2.8.1" |             "version": "==2.8.1" | ||||||
|         }, |         }, | ||||||
|  |         "python-dotenv": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", | ||||||
|  |                 "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" | ||||||
|  |             ], | ||||||
|  |             "version": "==0.14.0" | ||||||
|  |         }, | ||||||
|         "pytz": { |         "pytz": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", |                 "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", | ||||||
| @ -966,6 +979,8 @@ | |||||||
|                 "sha256:a873e4d4954f865dcb60bdc4914af7eaae48fb56b60ed6daa1d6251c72f5337c", |                 "sha256:a873e4d4954f865dcb60bdc4914af7eaae48fb56b60ed6daa1d6251c72f5337c", | ||||||
|                 "sha256:ab845f1f51f7eb750a78937be9f79baea4a42c7960f5a94dde34e69f3cce1988", |                 "sha256:ab845f1f51f7eb750a78937be9f79baea4a42c7960f5a94dde34e69f3cce1988", | ||||||
|                 "sha256:b1e981fe1aff1fd11627f531524826a4dcc1f26c726235a52fcb62ded27d150f", |                 "sha256:b1e981fe1aff1fd11627f531524826a4dcc1f26c726235a52fcb62ded27d150f", | ||||||
|  |                 "sha256:b4b0d31f2052b3f9f9b5327024dc629a253a83d8649d4734ca7f35b60ec3e9e5", | ||||||
|  |                 "sha256:c6ac7e45367b1317e56f1461719c853fd6825226f45b835df7436bb04031fd8a", | ||||||
|                 "sha256:daf21aa33ee9b351f66deed30a3d450ab55c14242cfdfcd377798e2c0d25c9f1", |                 "sha256:daf21aa33ee9b351f66deed30a3d450ab55c14242cfdfcd377798e2c0d25c9f1", | ||||||
|                 "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2", |                 "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2", | ||||||
|                 "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f" |                 "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f" | ||||||
| @ -982,11 +997,11 @@ | |||||||
|         }, |         }, | ||||||
|         "sentry-sdk": { |         "sentry-sdk": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:c9c0fa1412bad87104c4eee8dd36c7bbf60b0d92ae917ab519094779b22e6d9a", |                 "sha256:0eea248408d36e8e7037c7b73827bea20b13a4375bf1719c406cae6fcbc094e3", | ||||||
|                 "sha256:e159f7c919d19ae86e5a4ff370fccc45149fab461fbeb93fb5a735a0b33a9cb1" |                 "sha256:5cf36eb6b1dc62d55f3c64289792cbaebc8ffa5a9da14474f49b46d20caa7fc8" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==0.17.8" |             "version": "==0.19.1" | ||||||
|         }, |         }, | ||||||
|         "service-identity": { |         "service-identity": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1013,10 +1028,10 @@ | |||||||
|         }, |         }, | ||||||
|         "sqlparse": { |         "sqlparse": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", |                 "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", | ||||||
|                 "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" |                 "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.3.1" |             "version": "==0.4.1" | ||||||
|         }, |         }, | ||||||
|         "structlog": { |         "structlog": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1084,20 +1099,23 @@ | |||||||
|                 "secure" |                 "secure" | ||||||
|             ], |             ], | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", |                 "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", | ||||||
|                 "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" |                 "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": null, |             "markers": null, | ||||||
|             "version": "==1.25.10" |             "version": "==1.25.11" | ||||||
|         }, |         }, | ||||||
|         "uvicorn": { |         "uvicorn": { | ||||||
|  |             "extras": [ | ||||||
|  |                 "standard" | ||||||
|  |             ], | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:46a83e371f37ea7ff29577d00015f02c942410288fb57def6440f2653fff1d26", |                 "sha256:8ff7495c74b8286a341526ff9efa3988ebab9a4b2f561c7438c3cb420992d7dd", | ||||||
|                 "sha256:4b70ddb4c1946e39db9f3082d53e323dfd50634b95fd83625d778729ef1730ef" |                 "sha256:e5dbed4a8a44c7b04376021021d63798d6a7bcfae9c654a0b153577b93854fba" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==0.11.8" |             "version": "==0.12.2" | ||||||
|         }, |         }, | ||||||
|         "uvloop": { |         "uvloop": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1111,7 +1129,6 @@ | |||||||
|                 "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", |                 "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", | ||||||
|                 "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" |                 "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" | ||||||
|             ], |             ], | ||||||
|             "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", |  | ||||||
|             "version": "==0.14.0" |             "version": "==0.14.0" | ||||||
|         }, |         }, | ||||||
|         "vine": { |         "vine": { | ||||||
| @ -1121,6 +1138,13 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==5.0.0" |             "version": "==5.0.0" | ||||||
|         }, |         }, | ||||||
|  |         "watchgod": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:59700dab7445aa8e6067a5b94f37bae90fc367554549b1ed2e9d0f4f38a90d2a", | ||||||
|  |                 "sha256:e9cca0ab9c63f17fc85df9fd8bd18156ff00aff04ebe5976cee473f4968c6858" | ||||||
|  |             ], | ||||||
|  |             "version": "==0.6" | ||||||
|  |         }, | ||||||
|         "wcwidth": { |         "wcwidth": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", |                 "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", | ||||||
| @ -1164,48 +1188,48 @@ | |||||||
|         }, |         }, | ||||||
|         "zope.interface": { |         "zope.interface": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0103cba5ed09f27d2e3de7e48bb320338592e2fabc5ce1432cf33808eb2dfd8b", |                 "sha256:040f833694496065147e76581c0bf32b229a8b8c5eda120a0293afb008222387", | ||||||
|                 "sha256:14415d6979356629f1c386c8c4249b4d0082f2ea7f75871ebad2e29584bd16c5", |                 "sha256:11198b44e4a3d8c7a80cc20bbdd65522258a4d82fe467cd310c9fcce8ffe2ed2", | ||||||
|                 "sha256:1ae4693ccee94c6e0c88a4568fb3b34af8871c60f5ba30cf9f94977ed0e53ddd", |                 "sha256:121a9dccfe0c34be9c33b2c28225f0284f9b8e090580ffdff26c38fa16c7ffe1", | ||||||
|                 "sha256:1b87ed2dc05cb835138f6a6e3595593fea3564d712cb2eb2de963a41fd35758c", |                 "sha256:15f3082575e7e19581a80b866664f843719b647a7f7189c811ba7f9ab3309f83", | ||||||
|                 "sha256:269b27f60bcf45438e8683269f8ecd1235fa13e5411de93dae3b9ee4fe7f7bc7", |                 "sha256:1d73d8986f948525536956ddd902e8a587a6846ebf4492117db16daba2865ddf", | ||||||
|                 "sha256:27d287e61639d692563d9dab76bafe071fbeb26818dd6a32a0022f3f7ca884b5", |                 "sha256:208e82f73b242275b8566ac07a25158e7b21fa2f14e642a7881048430612d1a6", | ||||||
|                 "sha256:39106649c3082972106f930766ae23d1464a73b7d30b3698c986f74bf1256a34", |                 "sha256:2557833df892558123d791d6ff80ac4a2a0351f69c7421c7d5f0c07db72c8865", | ||||||
|                 "sha256:40e4c42bd27ed3c11b2c983fecfb03356fae1209de10686d03c02c8696a1d90e", |                 "sha256:25ea6906f9987d42546329d06f9750e69f0ee62307a2e7092955ed0758e64f09", | ||||||
|                 "sha256:461d4339b3b8f3335d7e2c90ce335eb275488c587b61aca4b305196dde2ff086", |                 "sha256:2c867914f7608674a555ac8daf20265644ac7be709e1da7d818089eebdfe544e", | ||||||
|                 "sha256:4f98f70328bc788c86a6a1a8a14b0ea979f81ae6015dd6c72978f1feff70ecda", |                 "sha256:2eadac20711a795d3bb7a2bfc87c04091cb5274d9c3281b43088a1227099b662", | ||||||
|                 "sha256:558a20a0845d1a5dc6ff87cd0f63d7dac982d7c3be05d2ffb6322a87c17fa286", |                 "sha256:37999d5ebd5d7bcd32438b725ca3470df05a7de8b1e9c0395bef24296b31ca99", | ||||||
|                 "sha256:562dccd37acec149458c1791da459f130c6cf8902c94c93b8d47c6337b9fb826", |                 "sha256:3ae8946d51789779f76e4fa326fd6676d8c19c1c3b4c4c5e9342807185264875", | ||||||
|                 "sha256:5e86c66a6dea8ab6152e83b0facc856dc4d435fe0f872f01d66ce0a2131b7f1d", |                 "sha256:5636cd7e60583b1608044ae4405e91575399430e66a5e1812f4bf30bcc55864e", | ||||||
|                 "sha256:60a207efcd8c11d6bbeb7862e33418fba4e4ad79846d88d160d7231fcb42a5ee", |                 "sha256:570e637cb6509998555f7e4af13006d89fad6c09cfc5c4795855385391063e4b", | ||||||
|                 "sha256:645a7092b77fdbc3f68d3cc98f9d3e71510e419f54019d6e282328c0dd140dcd", |                 "sha256:590a40447ff3803c44050ce3c17c3958f11ca028dae3eacdd7b96775184394fa", | ||||||
|                 "sha256:6874367586c020705a44eecdad5d6b587c64b892e34305bb6ed87c9bbe22a5e9", |                 "sha256:5aab51b9c1af1b8a84f40aa49ffe1684d41810b18d6c3e94aa50194e0a563f01", | ||||||
|                 "sha256:74bf0a4f9091131de09286f9a605db449840e313753949fe07c8d0fe7659ad1e", |                 "sha256:5ffe4e0753393bcbcfc9a58133ed3d3a584634cc7cc2e667f8e3e6fbcbb2155d", | ||||||
|                 "sha256:7b726194f938791a6691c7592c8b9e805fc6d1b9632a833b9c0640828cd49cbc", |                 "sha256:663982381bd428a275a841009e52983cc69c471a4979ce01344fadbf72cf353d", | ||||||
|                 "sha256:8149ded7f90154fdc1a40e0c8975df58041a6f693b8f7edcd9348484e9dc17fe", |                 "sha256:6d06bf8e24dd6c473c4fbd8e16a83bd2e6d74add6ba25169043deb46d497b211", | ||||||
|                 "sha256:8cccf7057c7d19064a9e27660f5aec4e5c4001ffcf653a47531bde19b5aa2a8a", |                 "sha256:6e5b9a4bf133cf1887b4a04c21c10ca9f548114f19c83957b2820d5c84254940", | ||||||
|                 "sha256:911714b08b63d155f9c948da2b5534b223a1a4fc50bb67139ab68b277c938578", |                 "sha256:70a2aed9615645bbe9d82c0f52bc7e676d2c0f8a63933d68418e0cb307f30536", | ||||||
|                 "sha256:a5f8f85986197d1dd6444763c4a15c991bfed86d835a1f6f7d476f7198d5f56a", |                 "sha256:7750746421c4395e3d2cc3d805919f4f57bb9f2a9a0ccd955566a9341050a1b4", | ||||||
|                 "sha256:a744132d0abaa854d1aad50ba9bc64e79c6f835b3e92521db4235a1991176813", |                 "sha256:7fc8708bc996e50fc7a9a2ad394e1f015348e389da26789fa6916630237143d7", | ||||||
|                 "sha256:af2c14efc0bb0e91af63d00080ccc067866fb8cbbaca2b0438ab4105f5e0f08d", |                 "sha256:91abd2f080065a7c007540f6bbd93ef7bdbbffa6df4a4cfab3892d8623b83c98", | ||||||
|                 "sha256:b054eb0a8aa712c8e9030065a59b5e6a5cf0746ecdb5f087cca5ec7685690c19", |                 "sha256:988f8b2281f3d95c66c01bdb141cefef1cc97db0d473c25c3fe2927ef00293b9", | ||||||
|                 "sha256:b0becb75418f8a130e9d465e718316cd17c7a8acce6fe8fe07adc72762bee425", |                 "sha256:9f56121d8a676802044584e6cc41250bbcde069d8adf725b9b817a6b0fd87f09", | ||||||
|                 "sha256:b1d2ed1cbda2ae107283befd9284e650d840f8f7568cb9060b5466d25dc48975", |                 "sha256:a0f51536ce6e817a7aa25b0dca8b62feb210d4dc22cabfe8d1a92d47979372cd", | ||||||
|                 "sha256:ba4261c8ad00b49d48bbb3b5af388bb7576edfc0ca50a49c11dcb77caa1d897e", |                 "sha256:a1cdd7390d7f66ddcebf545203ca3728c4890d605f9f2697bc8e31437906e8e7", | ||||||
|                 "sha256:d1fe9d7d09bb07228650903d6a9dc48ea649e3b8c69b1d263419cc722b3938e8", |                 "sha256:b10eb4d0a77609679bf5f23708e20b1cd461a1643bd8ea42b1ca4149b1a5406c", | ||||||
|                 "sha256:d7804f6a71fc2dda888ef2de266727ec2f3915373d5a785ed4ddc603bbc91e08", |                 "sha256:b274ac8e511b55ffb62e8292316bd2baa80c10e9fe811b1aa5ce81da6b6697d8", | ||||||
|                 "sha256:da2844fba024dd58eaa712561da47dcd1e7ad544a257482392472eae1c86d5e5", |                 "sha256:c75b502af2c83fcfa2ee9c2257c1ba5806634a91a50db6129ff70e67c42c7e7b", | ||||||
|                 "sha256:dcefc97d1daf8d55199420e9162ab584ed0893a109f45e438b9794ced44c9fd0", |                 "sha256:c9c8e53a5472b77f6a391b515c771105011f4b40740ce53af8428d1c8ca20004", | ||||||
|                 "sha256:dd98c436a1fc56f48c70882cc243df89ad036210d871c7427dc164b31500dc11", |                 "sha256:d867998a56c5133b9d31992beb699892e33b72150a8bf40f86cb52b8c606c83f", | ||||||
|                 "sha256:e74671e43ed4569fbd7989e5eecc7d06dc134b571872ab1d5a88f4a123814e9f", |                 "sha256:eb566cab630ec176b2d6115ed08b2cf4d921b47caa7f02cca1b4a9525223ee94", | ||||||
|                 "sha256:eb9b92f456ff3ec746cd4935b73c1117538d6124b8617bc0fe6fda0b3816e345", |                 "sha256:f61e6b95b414431ffe9dc460928fe9f351095fde074e2c2f5c6dda7b67a2192d", | ||||||
|                 "sha256:ebb4e637a1fb861c34e48a00d03cffa9234f42bef923aec44e5625ffb9a8e8f9", |                 "sha256:f718675fd071bcce4f7cbf9250cbaaf64e2e91ef1b0b32a1af596e7412647556", | ||||||
|                 "sha256:ef739fe89e7f43fb6494a43b1878a36273e5924869ba1d866f752c5812ae8d58", |                 "sha256:f9d4bfbd015e4b80dbad11c97049975f94592a6a0440e903ee647309f6252a1f", | ||||||
|                 "sha256:f40db0e02a8157d2b90857c24d89b6310f9b6c3642369852cdc3b5ac49b92afc", |                 "sha256:fae50fc12a5e8541f6f1cc4ed744ca8f76a9543876cf63f618fb0e6aca8f8375", | ||||||
|                 "sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6", |                 "sha256:fcf9c8edda7f7b2fd78069e97f4197815df5e871ec47b0f22580d330c6dec561", | ||||||
|                 "sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8" |                 "sha256:fdedce3bc5360bd29d4bb90396e8d4d3c09af49bc0383909fe84c7233c5ee675" | ||||||
|             ], |             ], | ||||||
|             "version": "==5.1.0" |             "version": "==5.1.2" | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "develop": { |     "develop": { | ||||||
| @ -1254,18 +1278,17 @@ | |||||||
|         }, |         }, | ||||||
|         "black": { |         "black": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", |                 "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" | ||||||
|                 "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" |  | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==19.10b0" |             "version": "==20.8b1" | ||||||
|         }, |         }, | ||||||
|         "bump2version": { |         "bump2version": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:477f0e18a0d58e50bb3dbc9af7fcda464fd0ebfc7a6151d8888602d7153171a0", |                 "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410", | ||||||
|                 "sha256:cd4f3a231305e405ed8944d8ff35bd742d9bc740ad62f483bd0ca21ce7131984" |                 "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6" | ||||||
|             ], |             ], | ||||||
|             "version": "==1.0.0" |             "version": "==1.0.1" | ||||||
|         }, |         }, | ||||||
|         "bumpversion": { |         "bumpversion": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1275,20 +1298,6 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==0.6.0" |             "version": "==0.6.0" | ||||||
|         }, |         }, | ||||||
|         "certifi": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", |  | ||||||
|                 "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" |  | ||||||
|             ], |  | ||||||
|             "version": "==2020.6.20" |  | ||||||
|         }, |  | ||||||
|         "chardet": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", |  | ||||||
|                 "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" |  | ||||||
|             ], |  | ||||||
|             "version": "==3.0.4" |  | ||||||
|         }, |  | ||||||
|         "click": { |         "click": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", |                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", | ||||||
| @ -1298,11 +1307,11 @@ | |||||||
|         }, |         }, | ||||||
|         "colorama": { |         "colorama": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", |                 "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", | ||||||
|                 "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" |                 "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==0.4.3" |             "version": "==0.4.4" | ||||||
|         }, |         }, | ||||||
|         "coverage": { |         "coverage": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1346,11 +1355,11 @@ | |||||||
|         }, |         }, | ||||||
|         "django": { |         "django": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:59c8125ca873ed3bdae9c12b146fbbd6ed8d0f743e4cf5f5817af50c51f1fc2f", |                 "sha256:a2127ad0150ec6966655bedf15dbbff9697cc86d61653db2da1afa506c0b04cc", | ||||||
|                 "sha256:b5fbb818e751f660fa2d576d9f40c34a4c615c8b48dd383f5216e609f383371f" |                 "sha256:c93c28ccf1d094cbd00d860e83128a39e45d2c571d3b54361713aaaf9a94cac4" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==3.1.1" |             "version": "==3.1.2" | ||||||
|         }, |         }, | ||||||
|         "django-debug-toolbar": { |         "django-debug-toolbar": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1360,14 +1369,6 @@ | |||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==3.1.1" |             "version": "==3.1.1" | ||||||
|         }, |         }, | ||||||
|         "docker": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828", |  | ||||||
|                 "sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2" |  | ||||||
|             ], |  | ||||||
|             "index": "pypi", |  | ||||||
|             "version": "==4.3.1" |  | ||||||
|         }, |  | ||||||
|         "dodgy": { |         "dodgy": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a", |                 "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a", | ||||||
| @ -1377,10 +1378,10 @@ | |||||||
|         }, |         }, | ||||||
|         "flake8": { |         "flake8": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", |                 "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", | ||||||
|                 "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" |                 "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" | ||||||
|             ], |             ], | ||||||
|             "version": "==3.8.3" |             "version": "==3.8.4" | ||||||
|         }, |         }, | ||||||
|         "flake8-polyfill": { |         "flake8-polyfill": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1398,24 +1399,17 @@ | |||||||
|         }, |         }, | ||||||
|         "gitpython": { |         "gitpython": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:080bf8e2cf1a2b907634761c2eaefbe83b69930c94c66ad11b65a8252959f912", |                 "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b", | ||||||
|                 "sha256:1858f4fd089abe92ae465f01d5aaaf55e937eca565fb2c1fce35a51b5f85c910" |                 "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8" | ||||||
|             ], |             ], | ||||||
|             "version": "==3.1.8" |             "version": "==3.1.11" | ||||||
|         }, |  | ||||||
|         "idna": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", |  | ||||||
|                 "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" |  | ||||||
|             ], |  | ||||||
|             "version": "==2.10" |  | ||||||
|         }, |         }, | ||||||
|         "iniconfig": { |         "iniconfig": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", |                 "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", | ||||||
|                 "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" |                 "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" | ||||||
|             ], |             ], | ||||||
|             "version": "==1.0.1" |             "version": "==1.1.1" | ||||||
|         }, |         }, | ||||||
|         "isort": { |         "isort": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1457,12 +1451,12 @@ | |||||||
|             ], |             ], | ||||||
|             "version": "==0.6.1" |             "version": "==0.6.1" | ||||||
|         }, |         }, | ||||||
|         "more-itertools": { |         "mypy-extensions": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20", |                 "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", | ||||||
|                 "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c" |                 "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" | ||||||
|             ], |             ], | ||||||
|             "version": "==8.5.0" |             "version": "==0.4.3" | ||||||
|         }, |         }, | ||||||
|         "packaging": { |         "packaging": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1481,10 +1475,10 @@ | |||||||
|         }, |         }, | ||||||
|         "pbr": { |         "pbr": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea", |                 "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9", | ||||||
|                 "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15" |                 "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00" | ||||||
|             ], |             ], | ||||||
|             "version": "==5.5.0" |             "version": "==5.5.1" | ||||||
|         }, |         }, | ||||||
|         "pep8-naming": { |         "pep8-naming": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1579,19 +1573,19 @@ | |||||||
|         }, |         }, | ||||||
|         "pytest": { |         "pytest": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40", |                 "sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9", | ||||||
|                 "sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043" |                 "sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==6.0.2" |             "version": "==6.1.1" | ||||||
|         }, |         }, | ||||||
|         "pytest-django": { |         "pytest-django": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:4de6dbd077ed8606616958f77655fed0d5e3ee45159475671c7fa67596c6dba6", |                 "sha256:10e384e6b8912ded92db64c58be8139d9ae23fb8361e5fc139d8e4f8fc601bc2", | ||||||
|                 "sha256:c33e3d3da14d8409b125d825d4e74da17bb252191bf6fc3da6856e27a8b73ea4" |                 "sha256:26f02c16d36fd4c8672390deebe3413678d89f30720c16efb8b2a6bf63b9041f" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==3.10.0" |             "version": "==4.1.0" | ||||||
|         }, |         }, | ||||||
|         "pytz": { |         "pytz": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1619,36 +1613,35 @@ | |||||||
|         }, |         }, | ||||||
|         "regex": { |         "regex": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", |                 "sha256:0cb23ed0e327c18fb7eac61ebbb3180ebafed5b9b86ca2e15438201e5903b5dd", | ||||||
|                 "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", |                 "sha256:1a065e7a6a1b4aa851a0efa1a2579eabc765246b8b3a5fd74000aaa3134b8b4e", | ||||||
|                 "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", |                 "sha256:1a511470db3aa97432ac8c1bf014fcc6c9fbfd0f4b1313024d342549cf86bcd6", | ||||||
|                 "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", |                 "sha256:1c447b0d108cddc69036b1b3910fac159f2b51fdeec7f13872e059b7bc932be1", | ||||||
|                 "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", |                 "sha256:2278453c6a76280b38855a263198961938108ea2333ee145c5168c36b8e2b376", | ||||||
|                 "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", |                 "sha256:240509721a663836b611fa13ca1843079fc52d0b91ef3f92d9bba8da12e768a0", | ||||||
|                 "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", |                 "sha256:4e21340c07090ddc8c16deebfd82eb9c9e1ec5e62f57bb86194a2595fd7b46e0", | ||||||
|                 "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", |                 "sha256:570e916a44a361d4e85f355aacd90e9113319c78ce3c2d098d2ddf9631b34505", | ||||||
|                 "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", |                 "sha256:59d5c6302d22c16d59611a9fd53556554010db1d47e9df5df37be05007bebe75", | ||||||
|                 "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", |                 "sha256:6a46eba253cedcbe8a6469f881f014f0a98819d99d341461630885139850e281", | ||||||
|                 "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", |                 "sha256:6f567df0601e9c7434958143aebea47a9c4b45434ea0ae0286a4ec19e9877169", | ||||||
|                 "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", |                 "sha256:781906e45ef1d10a0ed9ec8ab83a09b5e0d742de70e627b20d61ccb1b1d3964d", | ||||||
|                 "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", |                 "sha256:8469377a437dbc31e480993399fd1fd15fe26f382dc04c51c9cb73e42965cc06", | ||||||
|                 "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", |                 "sha256:8cd0d587aaac74194ad3e68029124c06245acaeddaae14cb45844e5c9bebeea4", | ||||||
|                 "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", |                 "sha256:97a023f97cddf00831ba04886d1596ef10f59b93df7f855856f037190936e868", | ||||||
|                 "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", |                 "sha256:a973d5a7a324e2a5230ad7c43f5e1383cac51ef4903bf274936a5634b724b531", | ||||||
|                 "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", |                 "sha256:af360e62a9790e0a96bc9ac845d87bfa0e4ee0ee68547ae8b5a9c1030517dbef", | ||||||
|                 "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", |                 "sha256:b706c70070eea03411b1761fff3a2675da28d042a1ab7d0863b3efe1faa125c9", | ||||||
|                 "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", |                 "sha256:bfd7a9fddd11d116a58b62ee6c502fd24cfe22a4792261f258f886aa41c2a899", | ||||||
|                 "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", |                 "sha256:c30d8766a055c22e39dd7e1a4f98f6266169f2de05db737efe509c2fb9c8a3c8", | ||||||
|                 "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" |                 "sha256:c53dc8ee3bb7b7e28ee9feb996a0c999137be6c1d3b02cb6b3c4cba4f9e5ed09", | ||||||
|  |                 "sha256:c95d514093b80e5309bdca5dd99e51bcf82c44043b57c34594d9d7556bd04d05", | ||||||
|  |                 "sha256:d43cf21df524283daa80ecad551c306b7f52881c8d0fe4e3e76a96b626b6d8d8", | ||||||
|  |                 "sha256:d62205f00f461fe8b24ade07499454a3b7adf3def1225e258b994e2215fd15c5", | ||||||
|  |                 "sha256:e289a857dca3b35d3615c3a6a438622e20d1bf0abcb82c57d866c8d0be3f44c4", | ||||||
|  |                 "sha256:e5f6aa56dda92472e9d6f7b1e6331f4e2d51a67caafff4d4c5121cadac03941e", | ||||||
|  |                 "sha256:f4b1c65ee86bfbf7d0c3dfd90592a9e3d6e9ecd36c367c884094c050d4c35d04" | ||||||
|             ], |             ], | ||||||
|             "version": "==2020.7.14" |             "version": "==2020.10.23" | ||||||
|         }, |  | ||||||
|         "requests": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", |  | ||||||
|                 "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" |  | ||||||
|             ], |  | ||||||
|             "version": "==2.24.0" |  | ||||||
|         }, |         }, | ||||||
|         "requirements-detector": { |         "requirements-detector": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1693,10 +1686,10 @@ | |||||||
|         }, |         }, | ||||||
|         "sqlparse": { |         "sqlparse": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", |                 "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", | ||||||
|                 "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" |                 "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" | ||||||
|             ], |             ], | ||||||
|             "version": "==0.3.1" |             "version": "==0.4.1" | ||||||
|         }, |         }, | ||||||
|         "stevedore": { |         "stevedore": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1716,9 +1709,11 @@ | |||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", |                 "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", | ||||||
|                 "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", |                 "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", | ||||||
|  |                 "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", | ||||||
|                 "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", |                 "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", | ||||||
|                 "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", |                 "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", | ||||||
|                 "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", |                 "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", | ||||||
|  |                 "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", | ||||||
|                 "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", |                 "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", | ||||||
|                 "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", |                 "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", | ||||||
|                 "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", |                 "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", | ||||||
| @ -1727,35 +1722,40 @@ | |||||||
|                 "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", |                 "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", | ||||||
|                 "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", |                 "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", | ||||||
|                 "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", |                 "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", | ||||||
|  |                 "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", | ||||||
|                 "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", |                 "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", | ||||||
|  |                 "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", | ||||||
|                 "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", |                 "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", | ||||||
|                 "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", |                 "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", | ||||||
|                 "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", |                 "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", | ||||||
|                 "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", |                 "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", | ||||||
|                 "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", |                 "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", | ||||||
|  |                 "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", | ||||||
|  |                 "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", | ||||||
|                 "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", |                 "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", | ||||||
|                 "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" |                 "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" | ||||||
|             ], |             ], | ||||||
|             "version": "==1.4.1" |             "version": "==1.4.1" | ||||||
|         }, |         }, | ||||||
|  |         "typing-extensions": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", | ||||||
|  |                 "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", | ||||||
|  |                 "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" | ||||||
|  |             ], | ||||||
|  |             "version": "==3.7.4.3" | ||||||
|  |         }, | ||||||
|         "urllib3": { |         "urllib3": { | ||||||
|             "extras": [ |             "extras": [ | ||||||
|                 "secure" |                 "secure" | ||||||
|             ], |             ], | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", |                 "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", | ||||||
|                 "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" |                 "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": null, |             "markers": null, | ||||||
|             "version": "==1.25.10" |             "version": "==1.25.11" | ||||||
|         }, |  | ||||||
|         "websocket-client": { |  | ||||||
|             "hashes": [ |  | ||||||
|                 "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549", |  | ||||||
|                 "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010" |  | ||||||
|             ], |  | ||||||
|             "version": "==0.57.0" |  | ||||||
|         }, |         }, | ||||||
|         "wrapt": { |         "wrapt": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|  | |||||||
| @ -6,8 +6,9 @@ As passbook is currently in a pre-stable, only the latest "stable" version is su | |||||||
|  |  | ||||||
| | Version  | Supported          | | | Version  | Supported          | | ||||||
| | -------- | ------------------ | | | -------- | ------------------ | | ||||||
| | 0.9.x    | :white_check_mark: | |  | ||||||
| | 0.10.x   | :white_check_mark: | | | 0.10.x   | :white_check_mark: | | ||||||
|  | | 0.11.x   | :white_check_mark: | | ||||||
|  | | 0.12.x   | :white_check_mark: | | ||||||
|  |  | ||||||
| ## Reporting a Vulnerability | ## Reporting a Vulnerability | ||||||
|  |  | ||||||
|  | |||||||
| @ -89,7 +89,7 @@ stages: | |||||||
|               versionSpec: '3.8' |               versionSpec: '3.8' | ||||||
|           - task: CmdLine@2 |           - task: CmdLine@2 | ||||||
|             inputs: |             inputs: | ||||||
|               script: npm install -g pyright |               script: npm install -g pyright@1.1.79 | ||||||
|           - task: CmdLine@2 |           - task: CmdLine@2 | ||||||
|             inputs: |             inputs: | ||||||
|               script: | |               script: | | ||||||
| @ -169,6 +169,13 @@ stages: | |||||||
|               dockerComposeFile: 'scripts/ci.docker-compose.yml' |               dockerComposeFile: 'scripts/ci.docker-compose.yml' | ||||||
|               action: 'Run services' |               action: 'Run services' | ||||||
|               buildImages: false |               buildImages: false | ||||||
|  |           - task: CmdLine@2 | ||||||
|  |             displayName: Install K3d and prepare | ||||||
|  |             inputs: | ||||||
|  |               script: | | ||||||
|  |                 wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash | ||||||
|  |                 k3d cluster create | ||||||
|  |                 k3d kubeconfig write -o ~/.kube/config --overwrite | ||||||
|           - task: CmdLine@2 |           - task: CmdLine@2 | ||||||
|             inputs: |             inputs: | ||||||
|               script: | |               script: | | ||||||
| @ -178,6 +185,7 @@ stages: | |||||||
|             displayName: Run full test suite |             displayName: Run full test suite | ||||||
|             inputs: |             inputs: | ||||||
|               script: | |               script: | | ||||||
|  |                 export PB_TEST_K8S=true | ||||||
|                 pipenv run coverage run ./manage.py test passbook -v 3 |                 pipenv run coverage run ./manage.py test passbook -v 3 | ||||||
|           - task: CmdLine@2 |           - task: CmdLine@2 | ||||||
|             inputs: |             inputs: | ||||||
| @ -203,6 +211,13 @@ stages: | |||||||
|               dockerComposeFile: 'scripts/ci.docker-compose.yml' |               dockerComposeFile: 'scripts/ci.docker-compose.yml' | ||||||
|               action: 'Run services' |               action: 'Run services' | ||||||
|               buildImages: false |               buildImages: false | ||||||
|  |           - task: CmdLine@2 | ||||||
|  |             displayName: Install K3d and prepare | ||||||
|  |             inputs: | ||||||
|  |               script: | | ||||||
|  |                 wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash | ||||||
|  |                 k3d cluster create | ||||||
|  |                 k3d kubeconfig write -o ~/.kube/config --overwrite | ||||||
|           - task: CmdLine@2 |           - task: CmdLine@2 | ||||||
|             inputs: |             inputs: | ||||||
|               script: | |               script: | | ||||||
| @ -219,11 +234,13 @@ stages: | |||||||
|             inputs: |             inputs: | ||||||
|               script: | |               script: | | ||||||
|                 cd passbook/static/static |                 cd passbook/static/static | ||||||
|                 yarn |                 npm i | ||||||
|  |                 npm run build | ||||||
|           - task: CmdLine@2 |           - task: CmdLine@2 | ||||||
|             displayName: Run full test suite |             displayName: Run full test suite | ||||||
|             inputs: |             inputs: | ||||||
|               script: | |               script: | | ||||||
|  |                 export PB_TEST_K8S=true | ||||||
|                 pipenv run coverage run ./manage.py test e2e -v 3 --failfast |                 pipenv run coverage run ./manage.py test e2e -v 3 --failfast | ||||||
|           - task: CmdLine@2 |           - task: CmdLine@2 | ||||||
|             condition: always() |             condition: always() | ||||||
| @ -231,6 +248,7 @@ stages: | |||||||
|             inputs: |             inputs: | ||||||
|               script: | |               script: | | ||||||
|                 docker stop $(docker ps -aq) |                 docker stop $(docker ps -aq) | ||||||
|  |                 docker container prune -f | ||||||
|           - task: CmdLine@2 |           - task: CmdLine@2 | ||||||
|             displayName: Prepare unittests and coverage for upload |             displayName: Prepare unittests and coverage for upload | ||||||
|             inputs: |             inputs: | ||||||
| @ -332,19 +350,3 @@ stages: | |||||||
|             repository: 'beryju/passbook-static' |             repository: 'beryju/passbook-static' | ||||||
|             command: 'push' |             command: 'push' | ||||||
|             tags: "gh-${{ variables.branchName }}" |             tags: "gh-${{ variables.branchName }}" | ||||||
|   - stage: Deploy |  | ||||||
|     jobs: |  | ||||||
|       - job: deploy_dev |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|           steps: |  | ||||||
|           - task: HelmDeploy@0 |  | ||||||
|             inputs: |  | ||||||
|               connectionType: 'Kubernetes Service Connection' |  | ||||||
|               kubernetesServiceConnection: 'k8s-beryjuorg-prd' |  | ||||||
|               namespace: 'passbook-dev' |  | ||||||
|               command: 'upgrade' |  | ||||||
|               chartType: 'FilePath' |  | ||||||
|               chartPath: 'helm/' |  | ||||||
|               releaseName: 'passbook-dev' |  | ||||||
|               recreate: true |  | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ version: '3.2' | |||||||
|  |  | ||||||
| services: | services: | ||||||
|   postgresql: |   postgresql: | ||||||
|     image: postgres |     image: postgres:12 | ||||||
|     volumes: |     volumes: | ||||||
|       - database:/var/lib/postgresql/data |       - database:/var/lib/postgresql/data | ||||||
|     networks: |     networks: | ||||||
| @ -12,59 +12,68 @@ services: | |||||||
|       - POSTGRES_PASSWORD=${PG_PASS:-thisisnotagoodpassword} |       - POSTGRES_PASSWORD=${PG_PASS:-thisisnotagoodpassword} | ||||||
|       - POSTGRES_USER=passbook |       - POSTGRES_USER=passbook | ||||||
|       - POSTGRES_DB=passbook |       - POSTGRES_DB=passbook | ||||||
|     labels: |  | ||||||
|       - traefik.enable=false |  | ||||||
|     env_file: |     env_file: | ||||||
|       - .env |       - .env | ||||||
|   redis: |   redis: | ||||||
|     image: redis |     image: redis | ||||||
|     networks: |     networks: | ||||||
|       - internal |       - internal | ||||||
|     labels: |  | ||||||
|       - traefik.enable=false |  | ||||||
|   server: |   server: | ||||||
|     image: beryju/passbook:${PASSBOOK_TAG:-0.10.7-stable} |     image: beryju/passbook:${PASSBOOK_TAG:-0.12.7-stable} | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
|       PASSBOOK_REDIS__HOST: redis |       PASSBOOK_REDIS__HOST: redis | ||||||
|       PASSBOOK_POSTGRESQL__HOST: postgresql |       PASSBOOK_POSTGRESQL__HOST: postgresql | ||||||
|       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS} |       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS} | ||||||
|       PASSBOOK_LOG_LEVEL: debug |  | ||||||
|     ports: |     ports: | ||||||
|       - 8000 |       - 8000 | ||||||
|     networks: |     networks: | ||||||
|       - internal |       - internal | ||||||
|     labels: |     labels: | ||||||
|       - traefik.port=8000 |       traefik.enable: 'true' | ||||||
|       - traefik.docker.network=internal |       traefik.docker.network: internal | ||||||
|       - traefik.frontend.rule=PathPrefix:/ |       traefik.http.routers.app-router.rule: PathPrefix(`/`) | ||||||
|  |       traefik.http.routers.app-router.service: app-service | ||||||
|  |       traefik.http.routers.app-router.tls: 'true' | ||||||
|  |       traefik.http.services.app-service.loadbalancer.healthcheck.hostname: passbook-healthcheck-host | ||||||
|  |       traefik.http.services.app-service.loadbalancer.server.port: '8000' | ||||||
|     env_file: |     env_file: | ||||||
|       - .env |       - .env | ||||||
|   worker: |   worker: | ||||||
|     image: beryju/passbook:${PASSBOOK_TAG:-0.10.7-stable} |     image: beryju/passbook:${PASSBOOK_TAG:-0.12.7-stable} | ||||||
|     command: worker |     command: worker | ||||||
|     networks: |     networks: | ||||||
|       - internal |       - internal | ||||||
|     labels: |  | ||||||
|       - traefik.enable=false |  | ||||||
|     environment: |     environment: | ||||||
|       PASSBOOK_REDIS__HOST: redis |       PASSBOOK_REDIS__HOST: redis | ||||||
|       PASSBOOK_POSTGRESQL__HOST: postgresql |       PASSBOOK_POSTGRESQL__HOST: postgresql | ||||||
|       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS} |       PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS} | ||||||
|       PASSBOOK_LOG_LEVEL: debug |     volumes: | ||||||
|  |       - ./backups:/backups | ||||||
|  |       - /var/run/docker.sock:/var/run/docker.sock | ||||||
|     env_file: |     env_file: | ||||||
|       - .env |       - .env | ||||||
|   static: |   static: | ||||||
|     image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.7-stable} |     image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.7-stable} | ||||||
|     networks: |     networks: | ||||||
|       - internal |       - internal | ||||||
|     labels: |     labels: | ||||||
|       - traefik.frontend.rule=PathPrefix:/static, /robots.txt, /favicon.ico |       traefik.enable: 'true' | ||||||
|       - traefik.port=80 |       traefik.docker.network: internal | ||||||
|       - traefik.docker.network=internal |       traefik.http.routers.static-router.rule: PathPrefix(`/static`, `/robots.txt`, `/favicon.ico`) | ||||||
|  |       traefik.http.routers.static-router.tls: 'true' | ||||||
|  |       traefik.http.routers.static-router.service: static-service | ||||||
|  |       traefik.http.services.static-service.loadbalancer.healthcheck.path: / | ||||||
|  |       traefik.http.services.static-service.loadbalancer.server.port: '80' | ||||||
|   traefik: |   traefik: | ||||||
|     image: traefik:1.7 |     image: traefik:2.3 | ||||||
|     command: --api --docker --defaultentrypoints=https --entryPoints='Name:http Address::80 Redirect.EntryPoint:https' --entryPoints='Name:https Address::443 TLS' |     command: | ||||||
|  |       - "--log.format=json" | ||||||
|  |       - "--api.insecure=true" | ||||||
|  |       - "--providers.docker=true" | ||||||
|  |       - "--providers.docker.exposedbydefault=false" | ||||||
|  |       - "--entrypoints.http.address=:80" | ||||||
|  |       - "--entrypoints.https.address=:443" | ||||||
|     volumes: |     volumes: | ||||||
|       - /var/run/docker.sock:/var/run/docker.sock:ro |       - /var/run/docker.sock:/var/run/docker.sock:ro | ||||||
|     ports: |     ports: | ||||||
|  | |||||||
| @ -4,18 +4,23 @@ The User object has the following attributes: | |||||||
|  |  | ||||||
| - `username`: User's username. | - `username`: User's username. | ||||||
| - `email` User's email. | - `email` User's email. | ||||||
|  - `name` User's display mame. | - `name` User's display name. | ||||||
| - `is_staff` Boolean field if user is staff. | - `is_staff` Boolean field if user is staff. | ||||||
| - `is_active` Boolean field if user is active. | - `is_active` Boolean field if user is active. | ||||||
| - `date_joined` Date user joined/was created. | - `date_joined` Date user joined/was created. | ||||||
| - `password_change_date` Date password was last changed. | - `password_change_date` Date password was last changed. | ||||||
| - `attributes` Dynamic attributes. | - `attributes` Dynamic attributes. | ||||||
|  | - `pb_groups` This is a queryset of all the user's groups. | ||||||
|  |  | ||||||
|  |     You can do additional filtering like `user.pb_groups.filter(name__startswith='test')`, see [here](https://docs.djangoproject.com/en/3.1/ref/models/querysets/#id4) | ||||||
|  |  | ||||||
|  |     To get the name of all groups, you can do `[group.name for group in user.pb_groups.all()]` | ||||||
|  |  | ||||||
| ## Examples | ## Examples | ||||||
|  |  | ||||||
| List all the User's group names: | List all the User's group names: | ||||||
|  |  | ||||||
| ```python | ```python | ||||||
| for group in user.groups.all(): | for group in user.pb_groups.all(): | ||||||
|     yield group.name |     yield group.name | ||||||
| ``` | ``` | ||||||
|  | |||||||
| @ -117,7 +117,7 @@ | |||||||
|             }, |             }, | ||||||
|             "model": "passbook_stages_user_login.userloginstage", |             "model": "passbook_stages_user_login.userloginstage", | ||||||
|             "attrs": { |             "attrs": { | ||||||
|                 "session_duration": 0 |                 "session_duration": "seconds=-1" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|  | |||||||
| @ -136,7 +136,7 @@ | |||||||
|             }, |             }, | ||||||
|             "model": "passbook_stages_user_login.userloginstage", |             "model": "passbook_stages_user_login.userloginstage", | ||||||
|             "attrs": { |             "attrs": { | ||||||
|                 "session_duration": 0 |                 "session_duration": "seconds=-1" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ | |||||||
|             }, |             }, | ||||||
|             "model": "passbook_stages_user_login.userloginstage", |             "model": "passbook_stages_user_login.userloginstage", | ||||||
|             "attrs": { |             "attrs": { | ||||||
|                 "session_duration": 0 |                 "session_duration": "seconds=-1" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ | |||||||
|             }, |             }, | ||||||
|             "model": "passbook_stages_user_login.userloginstage", |             "model": "passbook_stages_user_login.userloginstage", | ||||||
|             "attrs": { |             "attrs": { | ||||||
|                 "session_duration": 0 |                 "session_duration": "seconds=-1" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
| @ -95,7 +95,8 @@ | |||||||
|             }, |             }, | ||||||
|             "model": "passbook_flows.flowstagebinding", |             "model": "passbook_flows.flowstagebinding", | ||||||
|             "attrs": { |             "attrs": { | ||||||
|                 "re_evaluate_policies": false |                 "evaluate_on_plan": false, | ||||||
|  |                 "re_evaluate_policies": true | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|  | |||||||
| @ -101,7 +101,7 @@ | |||||||
|         { |         { | ||||||
|             "identifiers": { |             "identifiers": { | ||||||
|                 "pk": "975d5502-1e22-4d10-b560-fbc5bd70ff4d", |                 "pk": "975d5502-1e22-4d10-b560-fbc5bd70ff4d", | ||||||
|                 "name": "default-password-change-prompt" |                 "name": "Change your password" | ||||||
|             }, |             }, | ||||||
|             "model": "passbook_stages_prompt.promptstage", |             "model": "passbook_stages_prompt.promptstage", | ||||||
|             "attrs": { |             "attrs": { | ||||||
| @ -118,7 +118,7 @@ | |||||||
|             }, |             }, | ||||||
|             "model": "passbook_stages_user_login.userloginstage", |             "model": "passbook_stages_user_login.userloginstage", | ||||||
|             "attrs": { |             "attrs": { | ||||||
|                 "session_duration": 0 |                 "session_duration": "seconds=-1" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|  | |||||||
| Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 373 KiB | 
| Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 450 KiB | 
| @ -13,7 +13,7 @@ Download the latest `docker-compose.yml` from [here](https://raw.githubuserconte | |||||||
|  |  | ||||||
| To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env` | To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env` | ||||||
|  |  | ||||||
| To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.7-stable >> .env` | To optionally deploy a different version run `echo PASSBOOK_TAG=0.12.7-stable >> .env` | ||||||
|  |  | ||||||
| If this is a fresh passbook install run the following commands to generate a password: | If this is a fresh passbook install run the following commands to generate a password: | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,39 +4,52 @@ For a mid to high-load installation, Kubernetes is recommended. passbook is inst | |||||||
|  |  | ||||||
| This installation automatically applies database migrations on startup. After the installation is done, you can use `pbadmin` as username and password. | This installation automatically applies database migrations on startup. After the installation is done, you can use `pbadmin` as username and password. | ||||||
|  |  | ||||||
| ``` | ```yaml | ||||||
| ################################### | ################################### | ||||||
| # Values directly affecting passbook | # Values directly affecting passbook | ||||||
| ################################### | ################################### | ||||||
| image: | image: | ||||||
|   name: beryju/passbook |   name: beryju/passbook | ||||||
|   name_static: beryju/passbook-static |   name_static: beryju/passbook-static | ||||||
|   tag: 0.10.7-stable |   tag: 0.12.7-stable | ||||||
|  |  | ||||||
| nameOverride: "" |  | ||||||
|  |  | ||||||
| serverReplicas: 1 | serverReplicas: 1 | ||||||
| workerReplicas: 1 | workerReplicas: 1 | ||||||
|  |  | ||||||
|  | # Enable the Kubernetes integration which lets passbook deploy outposts into kubernetes | ||||||
|  | kubernetesIntegration: true | ||||||
|  |  | ||||||
| config: | config: | ||||||
|   # Optionally specify fixed secret_key, otherwise generated automatically |   # Optionally specify fixed secret_key, otherwise generated automatically | ||||||
|   # secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o |   # secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o | ||||||
|   # Enable error reporting |   # Enable error reporting | ||||||
|   error_reporting: |   errorReporting: | ||||||
|     enabled: false |     enabled: false | ||||||
|     environment: customer |     environment: customer | ||||||
|     send_pii: false |     sendPii: false | ||||||
|   # Log level used by web and worker |   # Log level used by web and worker | ||||||
|   # Can be either debug, info, warning, error |   # Can be either debug, info, warning, error | ||||||
|   log_level: warning |   logLevel: warning | ||||||
|  |  | ||||||
| # Enable Database Backups to S3 | # Enable Database Backups to S3 | ||||||
| # backup: | # backup: | ||||||
| #   access_key: access-key | #   accessKey: access-key | ||||||
| #   secret_key: secret-key | #   secretKey: secret-key | ||||||
| #   bucket: s3-bucket | #   bucket: s3-bucket | ||||||
|  | #   region: eu-central-1 | ||||||
| #   host: s3-host | #   host: s3-host | ||||||
|  |  | ||||||
|  | ingress: | ||||||
|  |   annotations: {} | ||||||
|  |     # kubernetes.io/ingress.class: nginx | ||||||
|  |     # kubernetes.io/tls-acme: "true" | ||||||
|  |   hosts: | ||||||
|  |     - passbook.k8s.local | ||||||
|  |   tls: [] | ||||||
|  |   #  - secretName: chart-example-tls | ||||||
|  |   #    hosts: | ||||||
|  |   #      - passbook.k8s.local | ||||||
|  |  | ||||||
| ################################### | ################################### | ||||||
| # Values controlling dependencies | # Values controlling dependencies | ||||||
| ################################### | ################################### | ||||||
| @ -57,16 +70,4 @@ redis: | |||||||
|       enabled: false |       enabled: false | ||||||
|     # https://stackoverflow.com/a/59189742 |     # https://stackoverflow.com/a/59189742 | ||||||
|     disableCommands: [] |     disableCommands: [] | ||||||
|  |  | ||||||
| ingress: |  | ||||||
|   annotations: {} |  | ||||||
|     # kubernetes.io/ingress.class: nginx |  | ||||||
|     # kubernetes.io/tls-acme: "true" |  | ||||||
|   path: / |  | ||||||
|   hosts: |  | ||||||
|     - passbook.k8s.local |  | ||||||
|   tls: [] |  | ||||||
|   #  - secretName: chart-example-tls |  | ||||||
|   #    hosts: |  | ||||||
|   #      - passbook.k8s.local |  | ||||||
| ``` | ``` | ||||||
|  | |||||||
							
								
								
									
										43
									
								
								docs/installation/reverse-proxy.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,43 @@ | |||||||
|  | # passbook behind a reverse-proxy | ||||||
|  |  | ||||||
|  | If you want to access passbook behind a reverse-proxy, use a config like this. It is important that Websocket is enabled, so that Outposts can connect. | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | map $http_upgrade $connection_upgrade { | ||||||
|  |     default upgrade; | ||||||
|  |     ''      close; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | server { | ||||||
|  |     # Server config | ||||||
|  |     listen 80; | ||||||
|  |     server_name sso.domain.tld; | ||||||
|  |  | ||||||
|  |     # 301 to SSL | ||||||
|  |     location / { | ||||||
|  |             return 301 https://$host$request_uri; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | server { | ||||||
|  |     # Server config | ||||||
|  |     listen 443 ssl http2; | ||||||
|  |     server_name sso.domain.tld; | ||||||
|  |  | ||||||
|  |     # SSL Certs | ||||||
|  |     ssl_certificate /etc/letsencrypt/live/domain.tld/fullchain.pem; | ||||||
|  |     ssl_certificate_key /etc/letsencrypt/live/domain.tld/privkey.pem; | ||||||
|  |  | ||||||
|  |     # Proxy site | ||||||
|  |     location / { | ||||||
|  |         proxy_pass https://<hostname of your passbook server>; | ||||||
|  |         proxy_http_version 1.1; | ||||||
|  |         proxy_set_header X-Forwarded-Proto https; | ||||||
|  |         proxy_set_header X-Forwarded-Port 443; | ||||||
|  |         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||||||
|  |         # This needs to be set inside the location block, very important. | ||||||
|  |         proxy_set_header Host $host; | ||||||
|  |         proxy_set_header Upgrade $http_upgrade; | ||||||
|  |         proxy_set_header Connection $connection_upgrade; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | ``` | ||||||
							
								
								
									
										48
									
								
								docs/integrations/services/home-assistant/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,48 @@ | |||||||
|  | # Home-Assistant Integration | ||||||
|  |  | ||||||
|  | ## What is Home-Assistant | ||||||
|  |  | ||||||
|  | From https://www.home-assistant.io/ | ||||||
|  |  | ||||||
|  | !!! note "" | ||||||
|  |     Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Preparation | ||||||
|  |  | ||||||
|  | The following placeholders will be used: | ||||||
|  |  | ||||||
|  | - `hass.company` is the FQDN of the Home-Assistant install. | ||||||
|  | - `passbook.company` is the FQDN of the passbook install. | ||||||
|  |  | ||||||
|  | !!! note | ||||||
|  |  | ||||||
|  |     This setup uses https://github.com/BeryJu/hass-auth-header and the passbook proxy for authentication. When this [PR](https://github.com/home-assistant/core/pull/32926) is merged, this will no longer be necessary. | ||||||
|  |  | ||||||
|  | ## Home-Assistant | ||||||
|  |  | ||||||
|  | This guide requires https://github.com/BeryJu/hass-auth-header, which can be installed as described in the Readme. | ||||||
|  |  | ||||||
|  | Afterwards, make sure the `trusted_proxies` setting contains the IP(s) of the Host(s) passbook is running on. | ||||||
|  |  | ||||||
|  | ## passbook | ||||||
|  |  | ||||||
|  | Create a Proxy Provider with the following values | ||||||
|  |  | ||||||
|  | - Internal host | ||||||
|  |  | ||||||
|  |     If Home-Assistant is running in docker, and you're deploying the passbook proxy on the same host, set the value to `http://homeassistant:8123`, where Home-Assistant is the name of your container. | ||||||
|  |  | ||||||
|  |     If Home-Assistant is running on a different server than where you are deploying the passbook proxy, set the value to `http://hass.company:8123`. | ||||||
|  |  | ||||||
|  | - External host | ||||||
|  |  | ||||||
|  |     Set this to the external URL you will be accessing Home-Assistant from. | ||||||
|  |  | ||||||
|  | Create an application in passbook and select the provider you've created above. | ||||||
|  |  | ||||||
|  | ## Deployment | ||||||
|  |  | ||||||
|  | Create an outpost deployment for the provider you've created above, as described [here](../../../outposts/outposts.md). Deploy this Outpost either on the same host or a different host that can access Home-Assistant. | ||||||
|  |  | ||||||
|  | The outpost will connect to passbook and configure itself. | ||||||
| @ -18,7 +18,7 @@ The following placeholders will be used: | |||||||
| - `sonarr.company` is the FQDN of the Sonarr install. | - `sonarr.company` is the FQDN of the Sonarr install. | ||||||
| - `passbook.company` is the FQDN of the passbook install. | - `passbook.company` is the FQDN of the passbook install. | ||||||
|  |  | ||||||
| Create an application in passbook. Create a Proxy Provider with the following values | Create a Proxy Provider with the following values | ||||||
|  |  | ||||||
| - Internal host | - Internal host | ||||||
|  |  | ||||||
| @ -30,6 +30,8 @@ Create an application in passbook. Create a Proxy Provider with the following va | |||||||
|  |  | ||||||
|     Set this to the external URL you will be accessing Sonarr from. |     Set this to the external URL you will be accessing Sonarr from. | ||||||
|  |  | ||||||
|  | Create an application in passbook and select the provider you've created above. | ||||||
|  |  | ||||||
| ## Deployment | ## Deployment | ||||||
|  |  | ||||||
| Create an outpost deployment for the provider you've created above, as described [here](../../../outposts/outposts.md). Deploy this Outpost either on the same host or a different host that can access Sonarr. | Create an outpost deployment for the provider you've created above, as described [here](../../../outposts/outposts.md). Deploy this Outpost either on the same host or a different host that can access Sonarr. | ||||||
|  | |||||||
							
								
								
									
										50
									
								
								docs/integrations/services/tautulli/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,50 @@ | |||||||
|  | # Tautulli Integration | ||||||
|  |  | ||||||
|  | ## What is Tautulli | ||||||
|  |  | ||||||
|  | From https://tautulli.com/ | ||||||
|  |  | ||||||
|  | !!! note | ||||||
|  |     Tautulli is a 3rd party application that you can run alongside your Plex Media Server to monitor activity and track various statistics. Most importantly, these statistics include what has been watched, who watched it, when and where they watched it, and how it was watched. The only thing missing is "why they watched it", but who am I to question your 42 plays of Frozen. All statistics are presented in a nice and clean interface with many tables and graphs, which makes it easy to brag about your server to everyone else. | ||||||
|  |  | ||||||
|  | ## Preparation | ||||||
|  |  | ||||||
|  | The following placeholders will be used: | ||||||
|  |  | ||||||
|  | - `tautulli.company` is the FQDN of the Tautulli install. | ||||||
|  | - `passbook.company` is the FQDN of the passbook install. | ||||||
|  |  | ||||||
|  | ## passbook Setup | ||||||
|  |  | ||||||
|  | Because Tautulli requires valid HTTP Basic credentials, you must save your HTTP Basic Credentials in passbook. The recommended way to do this is to create a Group. Name the group "Tautulli Users", for example. For this group, add the following attributes: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | tautulli_user: username | ||||||
|  | tautulli_password: password | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Add all Tautulli users to the Group. You should also create a Group Membership Policy to limit access to the application. | ||||||
|  |  | ||||||
|  | Create an application in passbook. Create a Proxy provider with the following parameters: | ||||||
|  |  | ||||||
|  | - Internal host | ||||||
|  |  | ||||||
|  |     If Tautulli is running in docker, and you're deploying the passbook proxy on the same host, set the value to `http://tautulli:3579`, where tautulli is the name of your container. | ||||||
|  |  | ||||||
|  |     If Tautulli is running on a different server to where you are deploying the passbook proxy, set the value to `http://tautulli.company:3579`. | ||||||
|  |  | ||||||
|  | - External host | ||||||
|  |  | ||||||
|  |     Set this to the external URL you will be accessing Tautulli from. | ||||||
|  |  | ||||||
|  | Enable the `Set HTTP-Basic Authentication` option. Set and `HTTP-Basic Username` and `HTTP-Basic Password` to `tautulli_user` and `tautulli_password` respectively. These values can be chosen freely, `tautulli_` is just used as a prefix for clarity. | ||||||
|  |  | ||||||
|  | ## Tautulli Setup | ||||||
|  |  | ||||||
|  | In Tautulli, navigate to Settings and enable the "Show Advanced" option. Navigate to "Web Interface" on the sidebar, and ensure the Option `Use Basic Authentication` is checked. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Save the settings, and restart Tautulli if prompted. | ||||||
|  |  | ||||||
|  | Afterwards, you need to deploy an Outpost in front of Tautulli, as descried [here](../sonarr/index.md) | ||||||
							
								
								
									
										
											BIN
										
									
								
								docs/integrations/services/tautulli/tautulli.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 90 KiB | 
| @ -16,6 +16,10 @@ From https://en.wikipedia.org/wiki/VCenter | |||||||
|  |  | ||||||
|     This requires VMware vCenter 7.0.0 or newer. |     This requires VMware vCenter 7.0.0 or newer. | ||||||
|  |  | ||||||
|  | !!! note | ||||||
|  |  | ||||||
|  |     It seems that the vCenter still needs to be joined to the Active Directory Domain, otherwise group membership does not work correctly. We're working on a fix for this, for the meantime your vCenter should be part of your Domain. | ||||||
|  |  | ||||||
| ## Preparation | ## Preparation | ||||||
|  |  | ||||||
| The following placeholders will be used: | The following placeholders will be used: | ||||||
| @ -60,6 +64,8 @@ Under *Providers*, create an OAuth2/OpenID Provider with these settings: | |||||||
|  |  | ||||||
| Create an application which uses this provider. Optionally apply access restrictions to the application. | Create an application which uses this provider. Optionally apply access restrictions to the application. | ||||||
|  |  | ||||||
|  | Set the Launch URL to `https://vcenter.company/ui/login/oauth2`. This will skip vCenter's User Prompt and directly log you in. | ||||||
|  |  | ||||||
| ## vCenter Setup | ## vCenter Setup | ||||||
|  |  | ||||||
| Login as local Administrator account (most likely ends with vsphere.local). Using the Menu in the Navigation bar, navigate to *Administration -> Single Sing-on -> Configuration*. | Login as local Administrator account (most likely ends with vsphere.local). Using the Menu in the Navigation bar, navigate to *Administration -> Single Sing-on -> Configuration*. | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								docs/integrations/sources/active-directory/01_user_create.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 26 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/integrations/sources/active-directory/02_delegate.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 31 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/integrations/sources/active-directory/03_pb_status.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 64 KiB | 
							
								
								
									
										55
									
								
								docs/integrations/sources/active-directory/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,55 @@ | |||||||
|  | # Active Directory Integration | ||||||
|  |  | ||||||
|  | ## Preparation | ||||||
|  |  | ||||||
|  | The following placeholders will be used: | ||||||
|  |  | ||||||
|  |  - `ad.company` is the Name of the Active Directory domain. | ||||||
|  |  - `passbook.company` is the FQDN of the passbook install. | ||||||
|  |  | ||||||
|  | ## Active Directory Setup | ||||||
|  |  | ||||||
|  | 1. Open Active Directory Users and Computers | ||||||
|  |  | ||||||
|  | 2. Create a user in Active Directory, matching your naming scheme | ||||||
|  |  | ||||||
|  |      | ||||||
|  |  | ||||||
|  | 3. Give the User a password, generated using for example `pwgen 64 1`. | ||||||
|  |  | ||||||
|  | 4. Open the Delegation of Control Wizard by right-clicking the domain. | ||||||
|  |  | ||||||
|  | 5. Select the passbook service user you've just created. | ||||||
|  |  | ||||||
|  | 6. Ensure the "Reset user password and force password change at next logon" Option is checked. | ||||||
|  |  | ||||||
|  |      | ||||||
|  |  | ||||||
|  | ## passbook Setup | ||||||
|  |  | ||||||
|  | In passbook, create a new LDAP Source in Administration -> Sources. | ||||||
|  |  | ||||||
|  | Use these settings: | ||||||
|  |  | ||||||
|  | - Server URI: `ldap://ad.company` | ||||||
|  |  | ||||||
|  |     For passbook to be able to write passwords back to Active Directory, make sure to use `ldaps://` | ||||||
|  |  | ||||||
|  | - Bind CN: `<name of your service user>@ad.company` | ||||||
|  | - Bind Password: The password you've given the user above | ||||||
|  | - Base DN: The base DN which you want passbook to sync | ||||||
|  | - Property Mappings: Select all and click the right arrow | ||||||
|  |  | ||||||
|  | The other settings might need to be adjusted based on the setup of your domain. | ||||||
|  |  | ||||||
|  | - Addition User/Group DN: Additional DN which is *prepended* to your Base DN for user synchronization. | ||||||
|  | - Addition Group DN: Additional DN which is *prepended* to your Base DN for group synchronization. | ||||||
|  | - User object filter: Which objects should be considered users. | ||||||
|  | - Group object filter: Which objects should be considered groups. | ||||||
|  | - User group membership field: Which user field saves the group membership | ||||||
|  | - Object uniqueness field: A user field which contains a unique Identifier | ||||||
|  | - Sync parent group: If enabled, all synchronized groups will be given this group as a parent. | ||||||
|  |  | ||||||
|  | After you save the source, a synchronization will start in the background. When its done, you cen see the summary on the System Tasks page. | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										102
									
								
								docs/maintenance/backups/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,102 @@ | |||||||
|  | # Backup and restore | ||||||
|  |  | ||||||
|  | !!! warning | ||||||
|  |  | ||||||
|  |     Local backups are only supported for docker-compose installs. If you want to backup a Kubernetes instance locally, use an S3-compatible server such as [minio](https://min.io/) | ||||||
|  |  | ||||||
|  | ### Backup | ||||||
|  |  | ||||||
|  | !!! notice | ||||||
|  |  | ||||||
|  |     Local backups are **enabled** by default, and will be run daily at 00:00 | ||||||
|  |  | ||||||
|  | Local backups can be created by running the following command in your passbook installation directory | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | docker-compose run --rm worker backup | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | This will dump the current database into the `./backups` folder. By defaults, the last 10 Backups are kept. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Restore | ||||||
|  |  | ||||||
|  | Run this command in your passbook installation directory | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | docker-compose run --rm worker restore | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | This will prompt you to restore from your last backup. If you want to restore from a specific file, use the `-i` flag with the filename: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | docker-compose run --rm worker restore -i default-2020-10-03-115557.psql | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | After you've restored the backup, it is recommended to restart all services with `docker-compose restart`. | ||||||
|  |  | ||||||
|  | ### S3 Configuration | ||||||
|  |  | ||||||
|  | #### Preparation | ||||||
|  |  | ||||||
|  | passbook expects the bucket you select to already exist. The IAM User given to passbook should have the following permissions | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |     "Version": "2012-10-17", | ||||||
|  |     "Statement": [ | ||||||
|  |         { | ||||||
|  |             "Sid": "VisualEditor0", | ||||||
|  |             "Effect": "Allow", | ||||||
|  |             "Action": [ | ||||||
|  |                 "s3:PutObject", | ||||||
|  |                 "s3:GetObjectAcl", | ||||||
|  |                 "s3:GetObject", | ||||||
|  |                 "s3:ListBucket", | ||||||
|  |                 "s3:DeleteObject", | ||||||
|  |                 "s3:PutObjectAcl" | ||||||
|  |             ], | ||||||
|  |             "Principal": { | ||||||
|  |                 "AWS": "arn:aws:iam::example-AWS-account-ID:user/example-user-name" | ||||||
|  |             }, | ||||||
|  |             "Resource": [ | ||||||
|  |                 "arn:aws:s3:::example-bucket-name/*", | ||||||
|  |                 "arn:aws:s3:::example-bucket-name" | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### docker-compose | ||||||
|  |  | ||||||
|  | Set the following values in your `.env` file. | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | PASSBOOK_POSTGRESQL__S3_BACKUP__ACCESS_KEY= | ||||||
|  | PASSBOOK_POSTGRESQL__S3_BACKUP__SECRET_KEY= | ||||||
|  | PASSBOOK_POSTGRESQL__S3_BACKUP__BUCKET= | ||||||
|  | PASSBOOK_POSTGRESQL__S3_BACKUP__REGION= | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | If you want to backup to an S3-compatible server, like [minio](https://min.io/), use this setting: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | PASSBOOK_POSTGRESQL__S3_BACKUP__HOST=http://play.min.io | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### Kubernetes | ||||||
|  |  | ||||||
|  | Simply enable these options in your values.yaml file | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | # Enable Database Backups to S3 | ||||||
|  | backup: | ||||||
|  |   accessKey: access-key | ||||||
|  |   secretKey: secret-key | ||||||
|  |   bucket: s3-bucket | ||||||
|  |   region: eu-central-1 | ||||||
|  |   host: s3-host | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Afterwards, run a `helm upgrade` to update the ConfigMap. Backups are done automatically as above, at 00:00 every day. | ||||||
| @ -5,7 +5,7 @@ To deploy an outpost with docker-compose, use  this snippet in your docker-compo | |||||||
| You can also run the outpost in a separate docker-compose project, you just have to ensure that the outpost container can reach your application container. | You can also run the outpost in a separate docker-compose project, you just have to ensure that the outpost container can reach your application container. | ||||||
|  |  | ||||||
| ```yaml | ```yaml | ||||||
| version: 3.5 | version: '3.5' | ||||||
|  |  | ||||||
| services: | services: | ||||||
|   passbook_proxy: |   passbook_proxy: | ||||||
|  | |||||||
| @ -26,7 +26,11 @@ return False | |||||||
|     - `request.obj`: A Django Model instance. This is only set if the policy is ran against an object. |     - `request.obj`: A Django Model instance. This is only set if the policy is ran against an object. | ||||||
|     - `request.context`: A dictionary with dynamic data. This depends on the origin of the execution. |     - `request.context`: A dictionary with dynamic data. This depends on the origin of the execution. | ||||||
| - `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external provider. | - `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external provider. | ||||||
| - `pb_client_ip`: Client's IP Address or '255.255.255.255' if no IP Address could be extracted. Can be [compared](../expressions/index.md#comparing-ip-addresses) | - `pb_client_ip`: Client's IP Address or 255.255.255.255 if no IP Address could be extracted. Can be [compared](../expressions/index.md#comparing-ip-addresses), for example | ||||||
|  |  | ||||||
|  |     ```python | ||||||
|  |     return pb_client_ip in ip_network('10.0.0.0/24') | ||||||
|  |     ``` | ||||||
|  |  | ||||||
| Additionally, when the policy is executed from a flow, every variable from the flow's current context is accessible under the `context` object. | Additionally, when the policy is executed from a flow, every variable from the flow's current context is accessible under the `context` object. | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								docs/upgrading/to-0.11.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,20 @@ | |||||||
|  | # Upgrading to 0.11 | ||||||
|  |  | ||||||
|  | This update brings these headline features: | ||||||
|  |  | ||||||
|  | - Add Backup and Restore, currently only externally schedulable, documented [here](https://passbook.beryju.org/maintenance/backups/) | ||||||
|  | - New Admin Dashboard with more metrics and Charts | ||||||
|  |  | ||||||
|  |   Shows successful and failed logins from the last 24 hours, as well as the most used applications | ||||||
|  | - Add search to all table views | ||||||
|  | - Outpost now supports a Docker Controller, which installs the Outpost on the same host as passbook, updates and manages it | ||||||
|  | - Add Token Identifier | ||||||
|  |  | ||||||
|  |   Tokens now have an identifier which is used to reference to them, so the Primary key is not shown in URLs | ||||||
|  | - `core/applications/list` API now shows applications the user has access to via policies | ||||||
|  |  | ||||||
|  | ## Upgrading | ||||||
|  |  | ||||||
|  | This upgrade can be done as with minor upgrades, the only external change is the new docker-compose file, which enabled the Docker Integration for Outposts. To use this feature, please download the latest docker-compose from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). | ||||||
|  |  | ||||||
|  | Afterwards, you can simply run `docker-compose up -d` and then the normal upgrade command of `docker-compose run --rm server migrate`. | ||||||
							
								
								
									
										63
									
								
								docs/upgrading/to-0.12.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,63 @@ | |||||||
|  | # Upgrading to 0.12 | ||||||
|  |  | ||||||
|  | This update brings these headline features: | ||||||
|  |  | ||||||
|  | - Rewrite Outpost state Logic, which now supports multiple concurrent Outpost instances. | ||||||
|  | - Add Kubernetes Integration for Outposts, which deploys and maintains Outposts with High Availability in a Kubernetes Cluster | ||||||
|  | - Add System Task Overview to see all background tasks, their status, the log output, and retry them | ||||||
|  | - Alerts now disappear automatically | ||||||
|  | - Audit Logs are now searchable | ||||||
|  | - Users can now create their own Tokens to access the API | ||||||
|  | - docker-compose deployment now uses traefik 2.3 | ||||||
|  |  | ||||||
|  | Fixes: | ||||||
|  |  | ||||||
|  | - Fix high CPU Usage of the proxy when Websocket connections fail | ||||||
|  |  | ||||||
|  | ## Upgrading | ||||||
|  |  | ||||||
|  | ### docker-compose | ||||||
|  |  | ||||||
|  | Docker-compose users should download the latest docker-compose file from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). This includes the new traefik 2.3. | ||||||
|  |  | ||||||
|  | Afterwards, you can simply run `docker-compose up -d` and then the normal upgrade command of `docker-compose run --rm server migrate`. | ||||||
|  |  | ||||||
|  | ### Kubernetes | ||||||
|  |  | ||||||
|  | For Kubernetes users, there are some changes to the helm values. | ||||||
|  |  | ||||||
|  | The values change from | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | config: | ||||||
|  |   # Optionally specify fixed secret_key, otherwise generated automatically | ||||||
|  |   # secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o | ||||||
|  |   # Enable error reporting | ||||||
|  |   error_reporting: | ||||||
|  |     enabled: false | ||||||
|  |     environment: customer | ||||||
|  |     send_pii: false | ||||||
|  |   # Log level used by web and worker | ||||||
|  |   # Can be either debug, info, warning, error | ||||||
|  |   log_level: warning | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | to | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | config: | ||||||
|  |   # Optionally specify fixed secret_key, otherwise generated automatically | ||||||
|  |   # secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o | ||||||
|  |   # Enable error reporting | ||||||
|  |   errorReporting: | ||||||
|  |     enabled: false | ||||||
|  |     environment: customer | ||||||
|  |     sendPii: false | ||||||
|  |   # Log level used by web and worker | ||||||
|  |   # Can be either debug, info, warning, error | ||||||
|  |   logLevel: warning | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | in order to be consistent with the rest of the settings. | ||||||
|  |  | ||||||
|  | There is also a new setting called `kubernetesIntegration`, which controls the Kubernetes integration for passbook. When enabled (the default), a Service Account is created, which allows passbook to deploy and update Outposts. | ||||||
| @ -8,7 +8,7 @@ from docker.types import Healthcheck | |||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
|  |  | ||||||
| from e2e.utils import USER, SeleniumTestCase | from e2e.utils import USER, SeleniumTestCase, retry | ||||||
| from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding | from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
| from passbook.stages.email.models import EmailStage, EmailTemplates | from passbook.stages.email.models import EmailStage, EmailTemplates | ||||||
| from passbook.stages.identification.models import IdentificationStage | from passbook.stages.identification.models import IdentificationStage | ||||||
| @ -23,7 +23,7 @@ class TestFlowsEnroll(SeleniumTestCase): | |||||||
|  |  | ||||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: |     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||||
|         return { |         return { | ||||||
|             "image": "mailhog/mailhog:v1.0.1", |             "image": "docker.beryju.org/proxy/mailhog/mailhog:v1.0.1", | ||||||
|             "detach": True, |             "detach": True, | ||||||
|             "network_mode": "host", |             "network_mode": "host", | ||||||
|             "auto_remove": True, |             "auto_remove": True, | ||||||
| @ -34,6 +34,7 @@ class TestFlowsEnroll(SeleniumTestCase): | |||||||
|             ), |             ), | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_enroll_2_step(self): |     def test_enroll_2_step(self): | ||||||
|         """Test 2-step enroll flow""" |         """Test 2-step enroll flow""" | ||||||
|         # First stage fields |         # First stage fields | ||||||
| @ -104,7 +105,7 @@ class TestFlowsEnroll(SeleniumTestCase): | |||||||
|  |  | ||||||
|         self.wait_for_url(self.url("passbook_core:user-settings")) |         self.wait_for_url(self.url("passbook_core:user-settings")) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, |             self.driver.find_element(By.ID, "user-settings").text, | ||||||
|             "foo", |             "foo", | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
| @ -119,6 +120,7 @@ class TestFlowsEnroll(SeleniumTestCase): | |||||||
|             "foo@bar.baz", |             "foo@bar.baz", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     @override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend") |     @override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend") | ||||||
|     def test_enroll_email(self): |     def test_enroll_email(self): | ||||||
|         """Test enroll with Email verification""" |         """Test enroll with Email verification""" | ||||||
| @ -205,15 +207,11 @@ class TestFlowsEnroll(SeleniumTestCase): | |||||||
|         self.driver.switch_to.window(self.driver.window_handles[0]) |         self.driver.switch_to.window(self.driver.window_handles[0]) | ||||||
|  |  | ||||||
|         # We're now logged in |         # We're now logged in | ||||||
|         self.wait.until( |         self.wait.until(ec.presence_of_element_located((By.ID, "user-settings"))) | ||||||
|             ec.presence_of_element_located( |         self.driver.find_element(By.ID, "user-settings").click() | ||||||
|                 (By.XPATH, "//a[contains(@href, '/-/user/')]") |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click() |  | ||||||
|  |  | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, |             self.driver.find_element(By.ID, "user-settings").text, | ||||||
|             "foo", |             "foo", | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|  | |||||||
| @ -5,13 +5,14 @@ from unittest.case import skipUnless | |||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.common.keys import Keys | from selenium.webdriver.common.keys import Keys | ||||||
|  |  | ||||||
| from e2e.utils import USER, SeleniumTestCase | from e2e.utils import USER, SeleniumTestCase, retry | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| class TestFlowsLogin(SeleniumTestCase): | class TestFlowsLogin(SeleniumTestCase): | ||||||
|     """test default login flow""" |     """test default login flow""" | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_login(self): |     def test_login(self): | ||||||
|         """test default login flow""" |         """test default login flow""" | ||||||
|         self.driver.get(f"{self.live_server_url}/flows/default-authentication-flow/") |         self.driver.get(f"{self.live_server_url}/flows/default-authentication-flow/") | ||||||
| @ -21,6 +22,6 @@ class TestFlowsLogin(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, |             self.driver.find_element(By.ID, "user-settings").text, | ||||||
|             USER().username, |             USER().username, | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -10,8 +10,9 @@ from django_otp.plugins.otp_static.models import StaticDevice, StaticToken | |||||||
| from django_otp.plugins.otp_totp.models import TOTPDevice | from django_otp.plugins.otp_totp.models import TOTPDevice | ||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.common.keys import Keys | from selenium.webdriver.common.keys import Keys | ||||||
|  | from selenium.webdriver.support import expected_conditions as ec | ||||||
|  |  | ||||||
| from e2e.utils import USER, SeleniumTestCase | from e2e.utils import USER, SeleniumTestCase, retry | ||||||
| from passbook.flows.models import Flow, FlowStageBinding | from passbook.flows.models import Flow, FlowStageBinding | ||||||
| from passbook.stages.otp_validate.models import OTPValidateStage | from passbook.stages.otp_validate.models import OTPValidateStage | ||||||
|  |  | ||||||
| @ -20,6 +21,7 @@ from passbook.stages.otp_validate.models import OTPValidateStage | |||||||
| class TestFlowsOTP(SeleniumTestCase): | class TestFlowsOTP(SeleniumTestCase): | ||||||
|     """test flow with otp stages""" |     """test flow with otp stages""" | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_otp_validate(self): |     def test_otp_validate(self): | ||||||
|         """test flow with otp stages""" |         """test flow with otp stages""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -45,11 +47,13 @@ class TestFlowsOTP(SeleniumTestCase): | |||||||
|         totp = TOTP(device.bin_key, device.step, device.t0, device.digits, device.drift) |         totp = TOTP(device.bin_key, device.step, device.t0, device.digits, device.drift) | ||||||
|         self.driver.find_element(By.ID, "id_code").send_keys(totp.token()) |         self.driver.find_element(By.ID, "id_code").send_keys(totp.token()) | ||||||
|         self.driver.find_element(By.ID, "id_code").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_code").send_keys(Keys.ENTER) | ||||||
|  |         self.wait_for_url(self.url("passbook_core:overview")) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, |             self.driver.find_element(By.ID, "user-settings").text, | ||||||
|             USER().username, |             USER().username, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_otp_totp_setup(self): |     def test_otp_totp_setup(self): | ||||||
|         """test TOTP Setup stage""" |         """test TOTP Setup stage""" | ||||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") |         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||||
| @ -61,13 +65,12 @@ class TestFlowsOTP(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, |             self.driver.find_element(By.ID, "user-settings").text, | ||||||
|             USER().username, |             USER().username, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click() |         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click() | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click() |         self.driver.get(self.url("passbook_core:user-settings")) | ||||||
|         self.wait_for_url(self.url("passbook_core:user-settings")) |  | ||||||
|  |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Time-based OTP").click() |         self.driver.find_element(By.LINK_TEXT, "Time-based OTP").click() | ||||||
|  |  | ||||||
| @ -78,9 +81,8 @@ class TestFlowsOTP(SeleniumTestCase): | |||||||
|             By.CSS_SELECTOR, ".pf-c-card__body a.pf-c-button" |             By.CSS_SELECTOR, ".pf-c-card__body a.pf-c-button" | ||||||
|         ).click() |         ).click() | ||||||
|  |  | ||||||
|         otp_uri = self.driver.find_element( |         self.wait.until(ec.presence_of_element_located((By.ID, "qr"))) | ||||||
|             By.CSS_SELECTOR, "#flow-body > div > form > div:nth-child(3) > div" |         otp_uri = self.driver.find_element(By.ID, "qr").get_attribute("data-otpuri") | ||||||
|         ).get_attribute("aria-label") |  | ||||||
|  |  | ||||||
|         # Parse the OTP URI, extract the secret and get the next token |         # Parse the OTP URI, extract the secret and get the next token | ||||||
|         otp_args = urlparse(otp_uri) |         otp_args = urlparse(otp_uri) | ||||||
| @ -98,6 +100,7 @@ class TestFlowsOTP(SeleniumTestCase): | |||||||
|  |  | ||||||
|         self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists()) |         self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists()) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_otp_static_setup(self): |     def test_otp_static_setup(self): | ||||||
|         """test Static OTP Setup stage""" |         """test Static OTP Setup stage""" | ||||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") |         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||||
| @ -109,12 +112,12 @@ class TestFlowsOTP(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, |             self.driver.find_element(By.ID, "user-settings").text, | ||||||
|             USER().username, |             USER().username, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click() |         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click() | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click() |         self.driver.find_element(By.ID, "user-settings").click() | ||||||
|         self.wait_for_url(self.url("passbook_core:user-settings")) |         self.wait_for_url(self.url("passbook_core:user-settings")) | ||||||
|  |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Static OTP").click() |         self.driver.find_element(By.LINK_TEXT, "Static OTP").click() | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ from unittest.case import skipUnless | |||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.common.keys import Keys | from selenium.webdriver.common.keys import Keys | ||||||
|  |  | ||||||
| from e2e.utils import USER, SeleniumTestCase | from e2e.utils import USER, SeleniumTestCase, retry | ||||||
| from passbook.core.models import User | from passbook.core.models import User | ||||||
| from passbook.flows.models import Flow, FlowDesignation | from passbook.flows.models import Flow, FlowDesignation | ||||||
| from passbook.providers.oauth2.generators import generate_client_secret | from passbook.providers.oauth2.generators import generate_client_secret | ||||||
| @ -16,6 +16,7 @@ from passbook.stages.password.models import PasswordStage | |||||||
| class TestFlowsStageSetup(SeleniumTestCase): | class TestFlowsStageSetup(SeleniumTestCase): | ||||||
|     """test stage setup flows""" |     """test stage setup flows""" | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_password_change(self): |     def test_password_change(self): | ||||||
|         """test password change flow""" |         """test password change flow""" | ||||||
|         # Ensure that password stage has change_flow set |         # Ensure that password stage has change_flow set | ||||||
| @ -38,7 +39,7 @@ class TestFlowsStageSetup(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click() |         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click() | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click() |         self.driver.find_element(By.ID, "user-settings").click() | ||||||
|         self.wait_for_url(self.url("passbook_core:user-settings")) |         self.wait_for_url(self.url("passbook_core:user-settings")) | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Change password").click() |         self.driver.find_element(By.LINK_TEXT, "Change password").click() | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(new_password) |         self.driver.find_element(By.ID, "id_password").send_keys(new_password) | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ from selenium.webdriver.common.by import By | |||||||
| from selenium.webdriver.common.keys import Keys | from selenium.webdriver.common.keys import Keys | ||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
|  |  | ||||||
| from e2e.utils import USER, SeleniumTestCase | from e2e.utils import USER, SeleniumTestCase, retry | ||||||
| from passbook.core.models import Application | from passbook.core.models import Application | ||||||
| from passbook.flows.models import Flow | from passbook.flows.models import Flow | ||||||
| from passbook.policies.expression.models import ExpressionPolicy | from passbook.policies.expression.models import ExpressionPolicy | ||||||
| @ -33,7 +33,7 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: |     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||||
|         """Setup client grafana container which we test OAuth against""" |         """Setup client grafana container which we test OAuth against""" | ||||||
|         return { |         return { | ||||||
|             "image": "grafana/grafana:7.1.0", |             "image": "docker.beryju.org/proxy/grafana/grafana:7.1.0", | ||||||
|             "detach": True, |             "detach": True, | ||||||
|             "network_mode": "host", |             "network_mode": "host", | ||||||
|             "auto_remove": True, |             "auto_remove": True, | ||||||
| @ -61,6 +61,7 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|             }, |             }, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_authorization_consent_implied(self): |     def test_authorization_consent_implied(self): | ||||||
|         """test OAuth Provider flow (default authorization flow with implied consent)""" |         """test OAuth Provider flow (default authorization flow with implied consent)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -77,7 +78,9 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         Application.objects.create( |         Application.objects.create( | ||||||
|             name="Grafana", slug="grafana", provider=provider, |             name="Grafana", | ||||||
|  |             slug="grafana", | ||||||
|  |             provider=provider, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         self.driver.get("http://localhost:3000") |         self.driver.get("http://localhost:3000") | ||||||
| @ -89,7 +92,7 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|  |  | ||||||
|         self.wait_for_url("http://localhost:3000/?orgId=1") |         self.wait_for_url("http://localhost:3000/?orgId=1") | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click() |         self.driver.get("http://localhost:3000/profile") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.CLASS_NAME, "page-header__title").text, |             self.driver.find_element(By.CLASS_NAME, "page-header__title").text, | ||||||
|             USER().username, |             USER().username, | ||||||
| @ -113,6 +116,7 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|             USER().username, |             USER().username, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_authorization_consent_explicit(self): |     def test_authorization_consent_explicit(self): | ||||||
|         """test OAuth Provider flow (default authorization flow with explicit consent)""" |         """test OAuth Provider flow (default authorization flow with explicit consent)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -129,7 +133,9 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         app = Application.objects.create( |         app = Application.objects.create( | ||||||
|             name="Grafana", slug="grafana", provider=provider, |             name="Grafana", | ||||||
|  |             slug="grafana", | ||||||
|  |             provider=provider, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         self.driver.get("http://localhost:3000") |         self.driver.get("http://localhost:3000") | ||||||
| @ -142,28 +148,21 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|  |  | ||||||
|         sleep(1) |         sleep(1) | ||||||
|  |  | ||||||
|         self.assertIn( |         self.assertEqual( | ||||||
|             app.name, |             app.name, | ||||||
|             self.driver.find_element( |             self.driver.find_element(By.ID, "application-name").text, | ||||||
|                 By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]" |  | ||||||
|             ).text, |  | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             "GitHub Compatibility: Access you Email addresses", |             "GitHub Compatibility: Access you Email addresses", | ||||||
|             self.driver.find_element( |             self.driver.find_element(By.ID, "scope-user:email").text, | ||||||
|                 By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]" |  | ||||||
|             ).text, |  | ||||||
|         ) |         ) | ||||||
|         self.driver.find_element( |         self.driver.find_element( | ||||||
|             By.CSS_SELECTOR, |             By.CSS_SELECTOR, | ||||||
|             ( |             ("[type=submit]"), | ||||||
|                 "form[action='/flows/b/default-provider-authorization-explicit-consent/'] " |  | ||||||
|                 "[type=submit]" |  | ||||||
|             ), |  | ||||||
|         ).click() |         ).click() | ||||||
|  |  | ||||||
|         self.wait_for_url("http://localhost:3000/?orgId=1") |         self.wait_for_url("http://localhost:3000/?orgId=1") | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click() |         self.driver.get("http://localhost:3000/profile") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.CLASS_NAME, "page-header__title").text, |             self.driver.find_element(By.CLASS_NAME, "page-header__title").text, | ||||||
|             USER().username, |             USER().username, | ||||||
| @ -187,6 +186,7 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|             USER().username, |             USER().username, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_denied(self): |     def test_denied(self): | ||||||
|         """test OAuth Provider flow (default authorization flow, denied)""" |         """test OAuth Provider flow (default authorization flow, denied)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -203,7 +203,9 @@ class TestProviderOAuth2Github(SeleniumTestCase): | |||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|         ) |         ) | ||||||
|         app = Application.objects.create( |         app = Application.objects.create( | ||||||
|             name="Grafana", slug="grafana", provider=provider, |             name="Grafana", | ||||||
|  |             slug="grafana", | ||||||
|  |             provider=provider, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         negative_policy = ExpressionPolicy.objects.create( |         negative_policy = ExpressionPolicy.objects.create( | ||||||
|  | |||||||
							
								
								
									
										380
									
								
								e2e/test_provider_oauth2_grafana.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,380 @@ | |||||||
|  | """test OAuth2 OpenID Provider flow""" | ||||||
|  | from sys import platform | ||||||
|  | from time import sleep | ||||||
|  | from typing import Any, Dict, Optional | ||||||
|  | from unittest.case import skipUnless | ||||||
|  |  | ||||||
|  | from docker.types import Healthcheck | ||||||
|  | from selenium.webdriver.common.by import By | ||||||
|  | from selenium.webdriver.common.keys import Keys | ||||||
|  | from selenium.webdriver.support import expected_conditions as ec | ||||||
|  | from structlog import get_logger | ||||||
|  |  | ||||||
|  | from e2e.utils import USER, SeleniumTestCase, retry | ||||||
|  | from passbook.core.models import Application | ||||||
|  | from passbook.crypto.models import CertificateKeyPair | ||||||
|  | from passbook.flows.models import Flow | ||||||
|  | from passbook.policies.expression.models import ExpressionPolicy | ||||||
|  | from passbook.policies.models import PolicyBinding | ||||||
|  | from passbook.providers.oauth2.constants import ( | ||||||
|  |     SCOPE_OPENID, | ||||||
|  |     SCOPE_OPENID_EMAIL, | ||||||
|  |     SCOPE_OPENID_PROFILE, | ||||||
|  | ) | ||||||
|  | from passbook.providers.oauth2.generators import ( | ||||||
|  |     generate_client_id, | ||||||
|  |     generate_client_secret, | ||||||
|  | ) | ||||||
|  | from passbook.providers.oauth2.models import ( | ||||||
|  |     ClientTypes, | ||||||
|  |     OAuth2Provider, | ||||||
|  |     ResponseTypes, | ||||||
|  |     ScopeMapping, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | LOGGER = get_logger() | ||||||
|  | APPLICATION_SLUG = "grafana" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
|  | class TestProviderOAuth2OAuth(SeleniumTestCase): | ||||||
|  |     """test OAuth with OAuth Provider flow""" | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         self.client_id = generate_client_id() | ||||||
|  |         self.client_secret = generate_client_secret() | ||||||
|  |         super().setUp() | ||||||
|  |  | ||||||
|  |     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||||
|  |         return { | ||||||
|  |             "image": "docker.beryju.org/proxy/grafana/grafana:7.1.0", | ||||||
|  |             "detach": True, | ||||||
|  |             "network_mode": "host", | ||||||
|  |             "auto_remove": True, | ||||||
|  |             "healthcheck": Healthcheck( | ||||||
|  |                 test=["CMD", "wget", "--spider", "http://localhost:3000"], | ||||||
|  |                 interval=5 * 100 * 1000000, | ||||||
|  |                 start_period=1 * 100 * 1000000, | ||||||
|  |             ), | ||||||
|  |             "environment": { | ||||||
|  |                 "GF_AUTH_GENERIC_OAUTH_ENABLED": "true", | ||||||
|  |                 "GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id, | ||||||
|  |                 "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret, | ||||||
|  |                 "GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile", | ||||||
|  |                 "GF_AUTH_GENERIC_OAUTH_AUTH_URL": ( | ||||||
|  |                     self.url("passbook_providers_oauth2:authorize") | ||||||
|  |                 ), | ||||||
|  |                 "GF_AUTH_GENERIC_OAUTH_TOKEN_URL": ( | ||||||
|  |                     self.url("passbook_providers_oauth2:token") | ||||||
|  |                 ), | ||||||
|  |                 "GF_AUTH_GENERIC_OAUTH_API_URL": ( | ||||||
|  |                     self.url("passbook_providers_oauth2:userinfo") | ||||||
|  |                 ), | ||||||
|  |                 "GF_AUTH_SIGNOUT_REDIRECT_URL": ( | ||||||
|  |                     self.url( | ||||||
|  |                         "passbook_providers_oauth2:end-session", | ||||||
|  |                         application_slug=APPLICATION_SLUG, | ||||||
|  |                     ) | ||||||
|  |                 ), | ||||||
|  |                 "GF_LOG_LEVEL": "debug", | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|  |     def test_redirect_uri_error(self): | ||||||
|  |         """test OpenID Provider flow (invalid redirect URI, check error message)""" | ||||||
|  |         sleep(1) | ||||||
|  |         # Bootstrap all needed objects | ||||||
|  |         authorization_flow = Flow.objects.get( | ||||||
|  |             slug="default-provider-authorization-implicit-consent" | ||||||
|  |         ) | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="grafana", | ||||||
|  |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|  |             client_id=self.client_id, | ||||||
|  |             client_secret=self.client_secret, | ||||||
|  |             rsa_key=CertificateKeyPair.objects.first(), | ||||||
|  |             redirect_uris="http://localhost:3000/", | ||||||
|  |             authorization_flow=authorization_flow, | ||||||
|  |             response_type=ResponseTypes.CODE, | ||||||
|  |         ) | ||||||
|  |         provider.property_mappings.set( | ||||||
|  |             ScopeMapping.objects.filter( | ||||||
|  |                 scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE] | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         provider.save() | ||||||
|  |         Application.objects.create( | ||||||
|  |             name="Grafana", | ||||||
|  |             slug=APPLICATION_SLUG, | ||||||
|  |             provider=provider, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.driver.get("http://localhost:3000") | ||||||
|  |         self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|  |         sleep(2) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CLASS_NAME, "pf-c-title").text, | ||||||
|  |             "Redirect URI Error", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|  |     def test_authorization_consent_implied(self): | ||||||
|  |         """test OpenID Provider flow (default authorization flow with implied consent)""" | ||||||
|  |         sleep(1) | ||||||
|  |         # Bootstrap all needed objects | ||||||
|  |         authorization_flow = Flow.objects.get( | ||||||
|  |             slug="default-provider-authorization-implicit-consent" | ||||||
|  |         ) | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="grafana", | ||||||
|  |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|  |             client_id=self.client_id, | ||||||
|  |             client_secret=self.client_secret, | ||||||
|  |             rsa_key=CertificateKeyPair.objects.first(), | ||||||
|  |             redirect_uris="http://localhost:3000/login/generic_oauth", | ||||||
|  |             authorization_flow=authorization_flow, | ||||||
|  |             response_type=ResponseTypes.CODE, | ||||||
|  |         ) | ||||||
|  |         provider.property_mappings.set( | ||||||
|  |             ScopeMapping.objects.filter( | ||||||
|  |                 scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE] | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         provider.save() | ||||||
|  |         Application.objects.create( | ||||||
|  |             name="Grafana", | ||||||
|  |             slug=APPLICATION_SLUG, | ||||||
|  |             provider=provider, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.driver.get("http://localhost:3000") | ||||||
|  |         self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|  |         self.wait_for_url("http://localhost:3000/?orgId=1") | ||||||
|  |         self.driver.get("http://localhost:3000/profile") | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CLASS_NAME, "page-header__title").text, | ||||||
|  |             USER().name, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute( | ||||||
|  |                 "value" | ||||||
|  |             ), | ||||||
|  |             USER().name, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element( | ||||||
|  |                 By.CSS_SELECTOR, "input[name=email]" | ||||||
|  |             ).get_attribute("value"), | ||||||
|  |             USER().email, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element( | ||||||
|  |                 By.CSS_SELECTOR, "input[name=login]" | ||||||
|  |             ).get_attribute("value"), | ||||||
|  |             USER().email, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|  |     def test_authorization_logout(self): | ||||||
|  |         """test OpenID Provider flow with logout""" | ||||||
|  |         sleep(1) | ||||||
|  |         # Bootstrap all needed objects | ||||||
|  |         authorization_flow = Flow.objects.get( | ||||||
|  |             slug="default-provider-authorization-implicit-consent" | ||||||
|  |         ) | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="grafana", | ||||||
|  |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|  |             client_id=self.client_id, | ||||||
|  |             client_secret=self.client_secret, | ||||||
|  |             rsa_key=CertificateKeyPair.objects.first(), | ||||||
|  |             redirect_uris="http://localhost:3000/login/generic_oauth", | ||||||
|  |             authorization_flow=authorization_flow, | ||||||
|  |             response_type=ResponseTypes.CODE, | ||||||
|  |         ) | ||||||
|  |         provider.property_mappings.set( | ||||||
|  |             ScopeMapping.objects.filter( | ||||||
|  |                 scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE] | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         provider.save() | ||||||
|  |         Application.objects.create( | ||||||
|  |             name="Grafana", | ||||||
|  |             slug=APPLICATION_SLUG, | ||||||
|  |             provider=provider, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.driver.get("http://localhost:3000") | ||||||
|  |         self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|  |         self.wait_for_url("http://localhost:3000/?orgId=1") | ||||||
|  |         self.driver.get("http://localhost:3000/profile") | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CLASS_NAME, "page-header__title").text, | ||||||
|  |             USER().name, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute( | ||||||
|  |                 "value" | ||||||
|  |             ), | ||||||
|  |             USER().name, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element( | ||||||
|  |                 By.CSS_SELECTOR, "input[name=email]" | ||||||
|  |             ).get_attribute("value"), | ||||||
|  |             USER().email, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element( | ||||||
|  |                 By.CSS_SELECTOR, "input[name=login]" | ||||||
|  |             ).get_attribute("value"), | ||||||
|  |             USER().email, | ||||||
|  |         ) | ||||||
|  |         self.driver.get("http://localhost:3000/logout") | ||||||
|  |         self.wait_for_url( | ||||||
|  |             self.url( | ||||||
|  |                 "passbook_providers_oauth2:end-session", | ||||||
|  |                 application_slug=APPLICATION_SLUG, | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         self.driver.find_element(By.ID, "logout").click() | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|  |     def test_authorization_consent_explicit(self): | ||||||
|  |         """test OpenID Provider flow (default authorization flow with explicit consent)""" | ||||||
|  |         sleep(1) | ||||||
|  |         # Bootstrap all needed objects | ||||||
|  |         authorization_flow = Flow.objects.get( | ||||||
|  |             slug="default-provider-authorization-explicit-consent" | ||||||
|  |         ) | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="grafana", | ||||||
|  |             authorization_flow=authorization_flow, | ||||||
|  |             response_type=ResponseTypes.CODE, | ||||||
|  |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|  |             client_id=self.client_id, | ||||||
|  |             client_secret=self.client_secret, | ||||||
|  |             rsa_key=CertificateKeyPair.objects.first(), | ||||||
|  |             redirect_uris="http://localhost:3000/login/generic_oauth", | ||||||
|  |         ) | ||||||
|  |         provider.property_mappings.set( | ||||||
|  |             ScopeMapping.objects.filter( | ||||||
|  |                 scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE] | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         provider.save() | ||||||
|  |         app = Application.objects.create( | ||||||
|  |             name="Grafana", | ||||||
|  |             slug=APPLICATION_SLUG, | ||||||
|  |             provider=provider, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.driver.get("http://localhost:3000") | ||||||
|  |         self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|  |  | ||||||
|  |         self.assertEqual( | ||||||
|  |             app.name, | ||||||
|  |             self.driver.find_element(By.ID, "application-name").text, | ||||||
|  |         ) | ||||||
|  |         self.wait.until( | ||||||
|  |             ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]")) | ||||||
|  |         ) | ||||||
|  |         sleep(1) | ||||||
|  |         self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() | ||||||
|  |  | ||||||
|  |         self.wait_for_url("http://localhost:3000/?orgId=1") | ||||||
|  |         self.driver.get("http://localhost:3000/profile") | ||||||
|  |  | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CLASS_NAME, "page-header__title").text, | ||||||
|  |             USER().name, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute( | ||||||
|  |                 "value" | ||||||
|  |             ), | ||||||
|  |             USER().name, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element( | ||||||
|  |                 By.CSS_SELECTOR, "input[name=email]" | ||||||
|  |             ).get_attribute("value"), | ||||||
|  |             USER().email, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element( | ||||||
|  |                 By.CSS_SELECTOR, "input[name=login]" | ||||||
|  |             ).get_attribute("value"), | ||||||
|  |             USER().email, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|  |     def test_authorization_denied(self): | ||||||
|  |         """test OpenID Provider flow (default authorization with access deny)""" | ||||||
|  |         sleep(1) | ||||||
|  |         # Bootstrap all needed objects | ||||||
|  |         authorization_flow = Flow.objects.get( | ||||||
|  |             slug="default-provider-authorization-explicit-consent" | ||||||
|  |         ) | ||||||
|  |         provider = OAuth2Provider.objects.create( | ||||||
|  |             name="grafana", | ||||||
|  |             authorization_flow=authorization_flow, | ||||||
|  |             response_type=ResponseTypes.CODE, | ||||||
|  |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|  |             client_id=self.client_id, | ||||||
|  |             client_secret=self.client_secret, | ||||||
|  |             rsa_key=CertificateKeyPair.objects.first(), | ||||||
|  |             redirect_uris="http://localhost:3000/login/generic_oauth", | ||||||
|  |         ) | ||||||
|  |         provider.property_mappings.set( | ||||||
|  |             ScopeMapping.objects.filter( | ||||||
|  |                 scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE] | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         provider.save() | ||||||
|  |         app = Application.objects.create( | ||||||
|  |             name="Grafana", | ||||||
|  |             slug=APPLICATION_SLUG, | ||||||
|  |             provider=provider, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         negative_policy = ExpressionPolicy.objects.create( | ||||||
|  |             name="negative-static", expression="return False" | ||||||
|  |         ) | ||||||
|  |         PolicyBinding.objects.create(target=app, policy=negative_policy, order=0) | ||||||
|  |         self.driver.get("http://localhost:3000") | ||||||
|  |         self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|  |  | ||||||
|  |         self.wait.until( | ||||||
|  |             ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1")) | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.CSS_SELECTOR, "header > h1").text, | ||||||
|  |             "Permission denied", | ||||||
|  |         ) | ||||||
| @ -1,16 +1,18 @@ | |||||||
| """test OAuth2 OpenID Provider flow""" | """test OAuth2 OpenID Provider flow""" | ||||||
|  | from json import loads | ||||||
| from sys import platform | from sys import platform | ||||||
| from time import sleep | from time import sleep | ||||||
| from typing import Any, Dict, Optional |  | ||||||
| from unittest.case import skipUnless | from unittest.case import skipUnless | ||||||
|  |  | ||||||
|  | from docker import DockerClient, from_env | ||||||
|  | from docker.models.containers import Container | ||||||
| from docker.types import Healthcheck | from docker.types import Healthcheck | ||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.common.keys import Keys | from selenium.webdriver.common.keys import Keys | ||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
| from structlog import get_logger | from structlog import get_logger | ||||||
|  |  | ||||||
| from e2e.utils import USER, SeleniumTestCase | from e2e.utils import USER, SeleniumTestCase, retry | ||||||
| from passbook.core.models import Application | from passbook.core.models import Application | ||||||
| from passbook.crypto.models import CertificateKeyPair | from passbook.crypto.models import CertificateKeyPair | ||||||
| from passbook.flows.models import Flow | from passbook.flows.models import Flow | ||||||
| @ -33,7 +35,6 @@ from passbook.providers.oauth2.models import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| APPLICATION_SLUG = "grafana" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
| @ -43,43 +44,39 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.client_id = generate_client_id() |         self.client_id = generate_client_id() | ||||||
|         self.client_secret = generate_client_secret() |         self.client_secret = generate_client_secret() | ||||||
|  |         self.application_slug = "test" | ||||||
|         super().setUp() |         super().setUp() | ||||||
|  |  | ||||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: |     def setup_client(self) -> Container: | ||||||
|         return { |         """Setup client saml-sp container which we test SAML against""" | ||||||
|             "image": "grafana/grafana:7.1.0", |         sleep(1) | ||||||
|             "detach": True, |         client: DockerClient = from_env() | ||||||
|             "network_mode": "host", |         client.images.pull("beryju/oidc-test-client") | ||||||
|             "auto_remove": True, |         container = client.containers.run( | ||||||
|             "healthcheck": Healthcheck( |             image="docker.beryju.org/proxy/beryju/oidc-test-client", | ||||||
|                 test=["CMD", "wget", "--spider", "http://localhost:3000"], |             detach=True, | ||||||
|  |             network_mode="host", | ||||||
|  |             auto_remove=True, | ||||||
|  |             healthcheck=Healthcheck( | ||||||
|  |                 test=["CMD", "wget", "--spider", "http://localhost:9009/health"], | ||||||
|                 interval=5 * 100 * 1000000, |                 interval=5 * 100 * 1000000, | ||||||
|                 start_period=1 * 100 * 1000000, |                 start_period=1 * 100 * 1000000, | ||||||
|             ), |             ), | ||||||
|             "environment": { |             environment={ | ||||||
|                 "GF_AUTH_GENERIC_OAUTH_ENABLED": "true", |                 "OIDC_CLIENT_ID": self.client_id, | ||||||
|                 "GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id, |                 "OIDC_CLIENT_SECRET": self.client_secret, | ||||||
|                 "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret, |                 "OIDC_PROVIDER": f"{self.live_server_url}/application/o/{self.application_slug}/", | ||||||
|                 "GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile", |  | ||||||
|                 "GF_AUTH_GENERIC_OAUTH_AUTH_URL": ( |  | ||||||
|                     self.url("passbook_providers_oauth2:authorize") |  | ||||||
|                 ), |  | ||||||
|                 "GF_AUTH_GENERIC_OAUTH_TOKEN_URL": ( |  | ||||||
|                     self.url("passbook_providers_oauth2:token") |  | ||||||
|                 ), |  | ||||||
|                 "GF_AUTH_GENERIC_OAUTH_API_URL": ( |  | ||||||
|                     self.url("passbook_providers_oauth2:userinfo") |  | ||||||
|                 ), |  | ||||||
|                 "GF_AUTH_SIGNOUT_REDIRECT_URL": ( |  | ||||||
|                     self.url( |  | ||||||
|                         "passbook_providers_oauth2:end-session", |  | ||||||
|                         application_slug=APPLICATION_SLUG, |  | ||||||
|                     ) |  | ||||||
|                 ), |  | ||||||
|                 "GF_LOG_LEVEL": "debug", |  | ||||||
|             }, |             }, | ||||||
|         } |         ) | ||||||
|  |         while True: | ||||||
|  |             container.reload() | ||||||
|  |             status = container.attrs.get("State", {}).get("Health", {}).get("Status") | ||||||
|  |             if status == "healthy": | ||||||
|  |                 return container | ||||||
|  |             LOGGER.info("Container failed healthcheck") | ||||||
|  |             sleep(1) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_redirect_uri_error(self): |     def test_redirect_uri_error(self): | ||||||
|         """test OpenID Provider flow (invalid redirect URI, check error message)""" |         """test OpenID Provider flow (invalid redirect URI, check error message)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -88,12 +85,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|             slug="default-provider-authorization-implicit-consent" |             slug="default-provider-authorization-implicit-consent" | ||||||
|         ) |         ) | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name="grafana", |             name=self.application_slug, | ||||||
|             client_type=ClientTypes.CONFIDENTIAL, |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             rsa_key=CertificateKeyPair.objects.first(), |             rsa_key=CertificateKeyPair.objects.first(), | ||||||
|             redirect_uris="http://localhost:3000/", |             redirect_uris="http://localhost:9009/", | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|             response_type=ResponseTypes.CODE, |             response_type=ResponseTypes.CODE, | ||||||
|         ) |         ) | ||||||
| @ -104,11 +101,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|         provider.save() |         provider.save() | ||||||
|         Application.objects.create( |         Application.objects.create( | ||||||
|             name="Grafana", slug=APPLICATION_SLUG, provider=provider, |             name=self.application_slug, | ||||||
|  |             slug=self.application_slug, | ||||||
|  |             provider=provider, | ||||||
|         ) |         ) | ||||||
|  |         self.container = self.setup_client() | ||||||
|  |  | ||||||
|  |         self.driver.get("http://localhost:9009") | ||||||
|  |  | ||||||
|         self.driver.get("http://localhost:3000") |  | ||||||
|         self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").click() |         self.driver.find_element(By.ID, "id_uid_field").click() | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
| @ -120,6 +120,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|             "Redirect URI Error", |             "Redirect URI Error", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_authorization_consent_implied(self): |     def test_authorization_consent_implied(self): | ||||||
|         """test OpenID Provider flow (default authorization flow with implied consent)""" |         """test OpenID Provider flow (default authorization flow with implied consent)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -128,12 +129,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|             slug="default-provider-authorization-implicit-consent" |             slug="default-provider-authorization-implicit-consent" | ||||||
|         ) |         ) | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name="grafana", |             name=self.application_slug, | ||||||
|             client_type=ClientTypes.CONFIDENTIAL, |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             rsa_key=CertificateKeyPair.objects.first(), |             rsa_key=CertificateKeyPair.objects.first(), | ||||||
|             redirect_uris="http://localhost:3000/login/generic_oauth", |             redirect_uris="http://localhost:9009/auth/callback", | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|             response_type=ResponseTypes.CODE, |             response_type=ResponseTypes.CODE, | ||||||
|         ) |         ) | ||||||
| @ -144,106 +145,33 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|         provider.save() |         provider.save() | ||||||
|         Application.objects.create( |         Application.objects.create( | ||||||
|             name="Grafana", slug=APPLICATION_SLUG, provider=provider, |             name=self.application_slug, | ||||||
|  |             slug=self.application_slug, | ||||||
|  |             provider=provider, | ||||||
|         ) |         ) | ||||||
|  |         self.container = self.setup_client() | ||||||
|  |  | ||||||
|  |         self.driver.get("http://localhost:9009") | ||||||
|  |  | ||||||
|         self.driver.get("http://localhost:3000") |  | ||||||
|         self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").click() |         self.driver.find_element(By.ID, "id_uid_field").click() | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click() |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.CLASS_NAME, "page-header__title").text, |  | ||||||
|             USER().name, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute( |  | ||||||
|                 "value" |  | ||||||
|             ), |  | ||||||
|             USER().name, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element( |  | ||||||
|                 By.CSS_SELECTOR, "input[name=email]" |  | ||||||
|             ).get_attribute("value"), |  | ||||||
|             USER().email, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element( |  | ||||||
|                 By.CSS_SELECTOR, "input[name=login]" |  | ||||||
|             ).get_attribute("value"), |  | ||||||
|             USER().email, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def test_authorization_logout(self): |         self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre"))) | ||||||
|         """test OpenID Provider flow with logout""" |         body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text) | ||||||
|         sleep(1) |  | ||||||
|         # Bootstrap all needed objects |  | ||||||
|         authorization_flow = Flow.objects.get( |  | ||||||
|             slug="default-provider-authorization-implicit-consent" |  | ||||||
|         ) |  | ||||||
|         provider = OAuth2Provider.objects.create( |  | ||||||
|             name="grafana", |  | ||||||
|             client_type=ClientTypes.CONFIDENTIAL, |  | ||||||
|             client_id=self.client_id, |  | ||||||
|             client_secret=self.client_secret, |  | ||||||
|             rsa_key=CertificateKeyPair.objects.first(), |  | ||||||
|             redirect_uris="http://localhost:3000/login/generic_oauth", |  | ||||||
|             authorization_flow=authorization_flow, |  | ||||||
|             response_type=ResponseTypes.CODE, |  | ||||||
|         ) |  | ||||||
|         provider.property_mappings.set( |  | ||||||
|             ScopeMapping.objects.filter( |  | ||||||
|                 scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE] |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         provider.save() |  | ||||||
|         Application.objects.create( |  | ||||||
|             name="Grafana", slug=APPLICATION_SLUG, provider=provider, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         self.driver.get("http://localhost:3000") |         self.assertEqual(body["IDTokenClaims"]["nickname"], USER().username) | ||||||
|         self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() |         self.assertEqual(body["UserInfo"]["nickname"], USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) |  | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) |  | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |  | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |  | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click() |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.CLASS_NAME, "page-header__title").text, |  | ||||||
|             USER().name, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute( |  | ||||||
|                 "value" |  | ||||||
|             ), |  | ||||||
|             USER().name, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element( |  | ||||||
|                 By.CSS_SELECTOR, "input[name=email]" |  | ||||||
|             ).get_attribute("value"), |  | ||||||
|             USER().email, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element( |  | ||||||
|                 By.CSS_SELECTOR, "input[name=login]" |  | ||||||
|             ).get_attribute("value"), |  | ||||||
|             USER().email, |  | ||||||
|         ) |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, "[href='/logout']").click() |  | ||||||
|         self.wait_for_url( |  | ||||||
|             self.url( |  | ||||||
|                 "passbook_providers_oauth2:end-session", |  | ||||||
|                 application_slug=APPLICATION_SLUG, |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         self.driver.find_element(By.ID, "logout").click() |  | ||||||
|  |  | ||||||
|  |         self.assertEqual(body["IDTokenClaims"]["name"], USER().name) | ||||||
|  |         self.assertEqual(body["UserInfo"]["name"], USER().name) | ||||||
|  |  | ||||||
|  |         self.assertEqual(body["IDTokenClaims"]["email"], USER().email) | ||||||
|  |         self.assertEqual(body["UserInfo"]["email"], USER().email) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_authorization_consent_explicit(self): |     def test_authorization_consent_explicit(self): | ||||||
|         """test OpenID Provider flow (default authorization flow with explicit consent)""" |         """test OpenID Provider flow (default authorization flow with explicit consent)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -252,14 +180,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|             slug="default-provider-authorization-explicit-consent" |             slug="default-provider-authorization-explicit-consent" | ||||||
|         ) |         ) | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name="grafana", |             name=self.application_slug, | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|             response_type=ResponseTypes.CODE, |             response_type=ResponseTypes.CODE, | ||||||
|             client_type=ClientTypes.CONFIDENTIAL, |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             rsa_key=CertificateKeyPair.objects.first(), |             rsa_key=CertificateKeyPair.objects.first(), | ||||||
|             redirect_uris="http://localhost:3000/login/generic_oauth", |             redirect_uris="http://localhost:9009/auth/callback", | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
|             ScopeMapping.objects.filter( |             ScopeMapping.objects.filter( | ||||||
| @ -268,22 +196,23 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|         provider.save() |         provider.save() | ||||||
|         app = Application.objects.create( |         app = Application.objects.create( | ||||||
|             name="Grafana", slug=APPLICATION_SLUG, provider=provider, |             name=self.application_slug, | ||||||
|  |             slug=self.application_slug, | ||||||
|  |             provider=provider, | ||||||
|         ) |         ) | ||||||
|  |         self.container = self.setup_client() | ||||||
|  |  | ||||||
|  |         self.driver.get("http://localhost:9009") | ||||||
|  |  | ||||||
|         self.driver.get("http://localhost:3000") |  | ||||||
|         self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").click() |         self.driver.find_element(By.ID, "id_uid_field").click() | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|  |  | ||||||
|         self.assertIn( |         self.assertEqual( | ||||||
|             app.name, |             app.name, | ||||||
|             self.driver.find_element( |             self.driver.find_element(By.ID, "application-name").text, | ||||||
|                 By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]" |  | ||||||
|             ).text, |  | ||||||
|         ) |         ) | ||||||
|         self.wait.until( |         self.wait.until( | ||||||
|             ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]")) |             ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]")) | ||||||
| @ -291,35 +220,19 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|         sleep(1) |         sleep(1) | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() |         self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() | ||||||
|  |  | ||||||
|         self.wait.until( |         self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre"))) | ||||||
|             ec.presence_of_element_located( |         body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text) | ||||||
|                 (By.XPATH, "//a[contains(@href, '/profile')]") |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click() |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.CLASS_NAME, "page-header__title").text, |  | ||||||
|             USER().name, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute( |  | ||||||
|                 "value" |  | ||||||
|             ), |  | ||||||
|             USER().name, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element( |  | ||||||
|                 By.CSS_SELECTOR, "input[name=email]" |  | ||||||
|             ).get_attribute("value"), |  | ||||||
|             USER().email, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element( |  | ||||||
|                 By.CSS_SELECTOR, "input[name=login]" |  | ||||||
|             ).get_attribute("value"), |  | ||||||
|             USER().email, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|  |         self.assertEqual(body["IDTokenClaims"]["nickname"], USER().username) | ||||||
|  |         self.assertEqual(body["UserInfo"]["nickname"], USER().username) | ||||||
|  |  | ||||||
|  |         self.assertEqual(body["IDTokenClaims"]["name"], USER().name) | ||||||
|  |         self.assertEqual(body["UserInfo"]["name"], USER().name) | ||||||
|  |  | ||||||
|  |         self.assertEqual(body["IDTokenClaims"]["email"], USER().email) | ||||||
|  |         self.assertEqual(body["UserInfo"]["email"], USER().email) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_authorization_denied(self): |     def test_authorization_denied(self): | ||||||
|         """test OpenID Provider flow (default authorization with access deny)""" |         """test OpenID Provider flow (default authorization with access deny)""" | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -328,14 +241,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|             slug="default-provider-authorization-explicit-consent" |             slug="default-provider-authorization-explicit-consent" | ||||||
|         ) |         ) | ||||||
|         provider = OAuth2Provider.objects.create( |         provider = OAuth2Provider.objects.create( | ||||||
|             name="grafana", |             name=self.application_slug, | ||||||
|             authorization_flow=authorization_flow, |             authorization_flow=authorization_flow, | ||||||
|             response_type=ResponseTypes.CODE, |             response_type=ResponseTypes.CODE, | ||||||
|             client_type=ClientTypes.CONFIDENTIAL, |             client_type=ClientTypes.CONFIDENTIAL, | ||||||
|             client_id=self.client_id, |             client_id=self.client_id, | ||||||
|             client_secret=self.client_secret, |             client_secret=self.client_secret, | ||||||
|             rsa_key=CertificateKeyPair.objects.first(), |             rsa_key=CertificateKeyPair.objects.first(), | ||||||
|             redirect_uris="http://localhost:3000/login/generic_oauth", |             redirect_uris="http://localhost:9009/auth/callback", | ||||||
|         ) |         ) | ||||||
|         provider.property_mappings.set( |         provider.property_mappings.set( | ||||||
|             ScopeMapping.objects.filter( |             ScopeMapping.objects.filter( | ||||||
| @ -344,15 +257,19 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|         provider.save() |         provider.save() | ||||||
|         app = Application.objects.create( |         app = Application.objects.create( | ||||||
|             name="Grafana", slug=APPLICATION_SLUG, provider=provider, |             name=self.application_slug, | ||||||
|  |             slug=self.application_slug, | ||||||
|  |             provider=provider, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         negative_policy = ExpressionPolicy.objects.create( |         negative_policy = ExpressionPolicy.objects.create( | ||||||
|             name="negative-static", expression="return False" |             name="negative-static", expression="return False" | ||||||
|         ) |         ) | ||||||
|         PolicyBinding.objects.create(target=app, policy=negative_policy, order=0) |         PolicyBinding.objects.create(target=app, policy=negative_policy, order=0) | ||||||
|         self.driver.get("http://localhost:3000") |  | ||||||
|         self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() |         self.container = self.setup_client() | ||||||
|  |         self.driver.get("http://localhost:9009") | ||||||
|  |  | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").click() |         self.driver.find_element(By.ID, "id_uid_field").click() | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|  | |||||||
| @ -1,18 +1,26 @@ | |||||||
| """Proxy and Outpost e2e tests""" | """Proxy and Outpost e2e tests""" | ||||||
|  | from dataclasses import asdict | ||||||
| from sys import platform | from sys import platform | ||||||
| from time import sleep | from time import sleep | ||||||
| from typing import Any, Dict, Optional | from typing import Any, Dict, Optional | ||||||
| from unittest.case import skipUnless | from unittest.case import skipUnless | ||||||
|  |  | ||||||
|  | from channels.testing import ChannelsLiveServerTestCase | ||||||
| from docker.client import DockerClient, from_env | from docker.client import DockerClient, from_env | ||||||
| from docker.models.containers import Container | from docker.models.containers import Container | ||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.common.keys import Keys | from selenium.webdriver.common.keys import Keys | ||||||
|  |  | ||||||
| from e2e.utils import USER, SeleniumTestCase | from e2e.utils import USER, SeleniumTestCase, retry | ||||||
|  | from passbook import __version__ | ||||||
| from passbook.core.models import Application | from passbook.core.models import Application | ||||||
| from passbook.flows.models import Flow | from passbook.flows.models import Flow | ||||||
| from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType | from passbook.outposts.models import ( | ||||||
|  |     Outpost, | ||||||
|  |     OutpostConfig, | ||||||
|  |     OutpostDeploymentType, | ||||||
|  |     OutpostType, | ||||||
|  | ) | ||||||
| from passbook.providers.proxy.models import ProxyProvider | from passbook.providers.proxy.models import ProxyProvider | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -28,7 +36,7 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|  |  | ||||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: |     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||||
|         return { |         return { | ||||||
|             "image": "traefik/whoami:latest", |             "image": "docker.beryju.org/proxy/traefik/whoami:latest", | ||||||
|             "detach": True, |             "detach": True, | ||||||
|             "network_mode": "host", |             "network_mode": "host", | ||||||
|             "auto_remove": True, |             "auto_remove": True, | ||||||
| @ -38,17 +46,18 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|         """Start proxy container based on outpost created""" |         """Start proxy container based on outpost created""" | ||||||
|         client: DockerClient = from_env() |         client: DockerClient = from_env() | ||||||
|         container = client.containers.run( |         container = client.containers.run( | ||||||
|             image="beryju/passbook-proxy:latest", |             image=f"beryju/passbook-proxy:{__version__}", | ||||||
|             detach=True, |             detach=True, | ||||||
|             network_mode="host", |             network_mode="host", | ||||||
|             auto_remove=True, |             auto_remove=True, | ||||||
|             environment={ |             environment={ | ||||||
|                 "PASSBOOK_HOST": self.live_server_url, |                 "PASSBOOK_HOST": self.live_server_url, | ||||||
|                 "PASSBOOK_TOKEN": outpost.token.token_uuid.hex, |                 "PASSBOOK_TOKEN": outpost.token.key, | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         return container |         return container | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_proxy_simple(self): |     def test_proxy_simple(self): | ||||||
|         """Test simple outpost setup with single provider""" |         """Test simple outpost setup with single provider""" | ||||||
|         proxy: ProxyProvider = ProxyProvider.objects.create( |         proxy: ProxyProvider = ProxyProvider.objects.create( | ||||||
| @ -77,7 +86,9 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|         # Wait until outpost healthcheck succeeds |         # Wait until outpost healthcheck succeeds | ||||||
|         healthcheck_retries = 0 |         healthcheck_retries = 0 | ||||||
|         while healthcheck_retries < 50: |         while healthcheck_retries < 50: | ||||||
|             if outpost.deployment_health: |             if len(outpost.state) > 0: | ||||||
|  |                 state = outpost.state[0] | ||||||
|  |                 if state.last_seen: | ||||||
|                     break |                     break | ||||||
|             healthcheck_retries += 1 |             healthcheck_retries += 1 | ||||||
|             sleep(0.5) |             sleep(0.5) | ||||||
| @ -94,3 +105,53 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|  |  | ||||||
|         full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text |         full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text | ||||||
|         self.assertIn("X-Forwarded-Preferred-Username: pbadmin", full_body_text) |         self.assertIn("X-Forwarded-Preferred-Username: pbadmin", full_body_text) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @skipUnless(platform.startswith("linux"), "requires local docker") | ||||||
|  | class TestProviderProxyConnect(ChannelsLiveServerTestCase): | ||||||
|  |     """Test Proxy connectivity over websockets""" | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|  |     def test_proxy_connectivity(self): | ||||||
|  |         """Test proxy connectivity over websocket""" | ||||||
|  |         SeleniumTestCase().apply_default_data() | ||||||
|  |         proxy: ProxyProvider = ProxyProvider.objects.create( | ||||||
|  |             name="proxy_provider", | ||||||
|  |             authorization_flow=Flow.objects.get( | ||||||
|  |                 slug="default-provider-authorization-implicit-consent" | ||||||
|  |             ), | ||||||
|  |             internal_host="http://localhost:80", | ||||||
|  |             external_host="http://localhost:4180", | ||||||
|  |         ) | ||||||
|  |         # Ensure OAuth2 Params are set | ||||||
|  |         proxy.set_oauth_defaults() | ||||||
|  |         proxy.save() | ||||||
|  |         # we need to create an application to actually access the proxy | ||||||
|  |         Application.objects.create(name="proxy", slug="proxy", provider=proxy) | ||||||
|  |         outpost: Outpost = Outpost.objects.create( | ||||||
|  |             name="proxy_outpost", | ||||||
|  |             type=OutpostType.PROXY, | ||||||
|  |             deployment_type=OutpostDeploymentType.DOCKER, | ||||||
|  |             _config=asdict( | ||||||
|  |                 OutpostConfig(passbook_host=self.live_server_url, log_level="debug") | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |         outpost.providers.add(proxy) | ||||||
|  |         outpost.save() | ||||||
|  |  | ||||||
|  |         # Wait until outpost healthcheck succeeds | ||||||
|  |         healthcheck_retries = 0 | ||||||
|  |         while healthcheck_retries < 50: | ||||||
|  |             if len(outpost.state) > 0: | ||||||
|  |                 state = outpost.state[0] | ||||||
|  |                 if state.last_seen and state.version: | ||||||
|  |                     break | ||||||
|  |             healthcheck_retries += 1 | ||||||
|  |             sleep(0.5) | ||||||
|  |  | ||||||
|  |         state = outpost.state | ||||||
|  |         self.assertTrue(len(state), 1) | ||||||
|  |         self.assertEqual(state[0].version, __version__) | ||||||
|  |  | ||||||
|  |         # Make sure to delete the outpost to remove the container | ||||||
|  |         outpost.delete() | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| """test SAML Provider flow""" | """test SAML Provider flow""" | ||||||
|  | from json import loads | ||||||
| from sys import platform | from sys import platform | ||||||
| from time import sleep | from time import sleep | ||||||
| from unittest.case import skipUnless | from unittest.case import skipUnless | ||||||
| @ -11,7 +12,7 @@ from selenium.webdriver.common.keys import Keys | |||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
| from structlog import get_logger | from structlog import get_logger | ||||||
|  |  | ||||||
| from e2e.utils import USER, SeleniumTestCase | from e2e.utils import USER, SeleniumTestCase, retry | ||||||
| from passbook.core.models import Application | from passbook.core.models import Application | ||||||
| from passbook.crypto.models import CertificateKeyPair | from passbook.crypto.models import CertificateKeyPair | ||||||
| from passbook.flows.models import Flow | from passbook.flows.models import Flow | ||||||
| @ -35,8 +36,9 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|     def setup_client(self, provider: SAMLProvider) -> Container: |     def setup_client(self, provider: SAMLProvider) -> Container: | ||||||
|         """Setup client saml-sp container which we test SAML against""" |         """Setup client saml-sp container which we test SAML against""" | ||||||
|         client: DockerClient = from_env() |         client: DockerClient = from_env() | ||||||
|  |         client.images.pull("beryju/oidc-test-client") | ||||||
|         container = client.containers.run( |         container = client.containers.run( | ||||||
|             image="beryju/saml-test-sp", |             image="docker.beryju.org/proxy/beryju/saml-test-sp", | ||||||
|             detach=True, |             detach=True, | ||||||
|             network_mode="host", |             network_mode="host", | ||||||
|             auto_remove=True, |             auto_remove=True, | ||||||
| @ -64,6 +66,7 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|             LOGGER.info("Container failed healthcheck") |             LOGGER.info("Container failed healthcheck") | ||||||
|             sleep(1) |             sleep(1) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_sp_initiated_implicit(self): |     def test_sp_initiated_implicit(self): | ||||||
|         """test SAML Provider flow SP-initiated flow (implicit consent)""" |         """test SAML Provider flow SP-initiated flow (implicit consent)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -82,7 +85,9 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         provider.property_mappings.set(SAMLPropertyMapping.objects.all()) |         provider.property_mappings.set(SAMLPropertyMapping.objects.all()) | ||||||
|         provider.save() |         provider.save() | ||||||
|         Application.objects.create( |         Application.objects.create( | ||||||
|             name="SAML", slug="passbook-saml", provider=provider, |             name="SAML", | ||||||
|  |             slug="passbook-saml", | ||||||
|  |             provider=provider, | ||||||
|         ) |         ) | ||||||
|         self.container = self.setup_client(provider) |         self.container = self.setup_client(provider) | ||||||
|         self.driver.get("http://localhost:9009") |         self.driver.get("http://localhost:9009") | ||||||
| @ -92,11 +97,16 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|         self.wait_for_url("http://localhost:9009/") |         self.wait_for_url("http://localhost:9009/") | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.XPATH, "/html/body/pre").text, |  | ||||||
|             f"Hello, {USER().name}!", |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|  |         body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text) | ||||||
|  |  | ||||||
|  |         self.assertEqual(body["attr"]["cn"], [USER().name]) | ||||||
|  |         self.assertEqual(body["attr"]["displayName"], [USER().username]) | ||||||
|  |         self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email]) | ||||||
|  |         self.assertEqual(body["attr"]["mail"], [USER().email]) | ||||||
|  |         self.assertEqual(body["attr"]["uid"], [str(USER().pk)]) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_sp_initiated_explicit(self): |     def test_sp_initiated_explicit(self): | ||||||
|         """test SAML Provider flow SP-initiated flow (explicit consent)""" |         """test SAML Provider flow SP-initiated flow (explicit consent)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -115,7 +125,9 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         provider.property_mappings.set(SAMLPropertyMapping.objects.all()) |         provider.property_mappings.set(SAMLPropertyMapping.objects.all()) | ||||||
|         provider.save() |         provider.save() | ||||||
|         app = Application.objects.create( |         app = Application.objects.create( | ||||||
|             name="SAML", slug="passbook-saml", provider=provider, |             name="SAML", | ||||||
|  |             slug="passbook-saml", | ||||||
|  |             provider=provider, | ||||||
|         ) |         ) | ||||||
|         self.container = self.setup_client(provider) |         self.container = self.setup_client(provider) | ||||||
|         self.driver.get("http://localhost:9009") |         self.driver.get("http://localhost:9009") | ||||||
| @ -124,20 +136,23 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|         self.assertIn( |         self.assertEqual( | ||||||
|             app.name, |             app.name, | ||||||
|             self.driver.find_element( |             self.driver.find_element(By.ID, "application-name").text, | ||||||
|                 By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]" |  | ||||||
|             ).text, |  | ||||||
|         ) |         ) | ||||||
|         sleep(1) |         sleep(1) | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() |         self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() | ||||||
|         self.wait_for_url("http://localhost:9009/") |         self.wait_for_url("http://localhost:9009/") | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.XPATH, "/html/body/pre").text, |  | ||||||
|             f"Hello, {USER().name}!", |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|  |         body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text) | ||||||
|  |  | ||||||
|  |         self.assertEqual(body["attr"]["cn"], [USER().name]) | ||||||
|  |         self.assertEqual(body["attr"]["displayName"], [USER().username]) | ||||||
|  |         self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email]) | ||||||
|  |         self.assertEqual(body["attr"]["mail"], [USER().email]) | ||||||
|  |         self.assertEqual(body["attr"]["uid"], [str(USER().pk)]) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_idp_initiated_implicit(self): |     def test_idp_initiated_implicit(self): | ||||||
|         """test SAML Provider flow IdP-initiated flow (implicit consent)""" |         """test SAML Provider flow IdP-initiated flow (implicit consent)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -156,7 +171,9 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         provider.property_mappings.set(SAMLPropertyMapping.objects.all()) |         provider.property_mappings.set(SAMLPropertyMapping.objects.all()) | ||||||
|         provider.save() |         provider.save() | ||||||
|         Application.objects.create( |         Application.objects.create( | ||||||
|             name="SAML", slug="passbook-saml", provider=provider, |             name="SAML", | ||||||
|  |             slug="passbook-saml", | ||||||
|  |             provider=provider, | ||||||
|         ) |         ) | ||||||
|         self.container = self.setup_client(provider) |         self.container = self.setup_client(provider) | ||||||
|         self.driver.get( |         self.driver.get( | ||||||
| @ -170,12 +187,18 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|  |         sleep(1) | ||||||
|         self.wait_for_url("http://localhost:9009/") |         self.wait_for_url("http://localhost:9009/") | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.XPATH, "/html/body/pre").text, |  | ||||||
|             f"Hello, {USER().name}!", |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|  |         body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text) | ||||||
|  |  | ||||||
|  |         self.assertEqual(body["attr"]["cn"], [USER().name]) | ||||||
|  |         self.assertEqual(body["attr"]["displayName"], [USER().username]) | ||||||
|  |         self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email]) | ||||||
|  |         self.assertEqual(body["attr"]["mail"], [USER().email]) | ||||||
|  |         self.assertEqual(body["attr"]["uid"], [str(USER().pk)]) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_sp_initiated_denied(self): |     def test_sp_initiated_denied(self): | ||||||
|         """test SAML Provider flow SP-initiated flow (Policy denies access)""" |         """test SAML Provider flow SP-initiated flow (Policy denies access)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
| @ -197,7 +220,9 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         provider.property_mappings.set(SAMLPropertyMapping.objects.all()) |         provider.property_mappings.set(SAMLPropertyMapping.objects.all()) | ||||||
|         provider.save() |         provider.save() | ||||||
|         app = Application.objects.create( |         app = Application.objects.create( | ||||||
|             name="SAML", slug="passbook-saml", provider=provider, |             name="SAML", | ||||||
|  |             slug="passbook-saml", | ||||||
|  |             provider=provider, | ||||||
|         ) |         ) | ||||||
|         PolicyBinding.objects.create(target=app, policy=negative_policy, order=0) |         PolicyBinding.objects.create(target=app, policy=negative_policy, order=0) | ||||||
|         self.container = self.setup_client(provider) |         self.container = self.setup_client(provider) | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ from selenium.webdriver.support import expected_conditions as ec | |||||||
| from structlog import get_logger | from structlog import get_logger | ||||||
| from yaml import safe_dump | from yaml import safe_dump | ||||||
|  |  | ||||||
| from e2e.utils import SeleniumTestCase | from e2e.utils import SeleniumTestCase, retry | ||||||
| from passbook.flows.models import Flow | from passbook.flows.models import Flow | ||||||
| from passbook.providers.oauth2.generators import ( | from passbook.providers.oauth2.generators import ( | ||||||
|     generate_client_id, |     generate_client_id, | ||||||
| @ -106,6 +106,7 @@ class TestSourceOAuth2(SeleniumTestCase): | |||||||
|             consumer_secret=self.client_secret, |             consumer_secret=self.client_secret, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_oauth_enroll(self): |     def test_oauth_enroll(self): | ||||||
|         """test OAuth Source With With OIDC""" |         """test OAuth Source With With OIDC""" | ||||||
|         self.create_objects() |         self.create_objects() | ||||||
| @ -140,25 +141,26 @@ class TestSourceOAuth2(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER) |         self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER) | ||||||
|  |  | ||||||
|         # Wait until we've loaded the user info page |         # Wait until we've loaded the user info page | ||||||
|         self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo"))) |         self.wait.until(ec.presence_of_element_located((By.ID, "user-settings"))) | ||||||
|         self.driver.find_element(By.LINK_TEXT, "foo").click() |         self.driver.get(self.url("passbook_core:user-settings")) | ||||||
|  |  | ||||||
|         self.wait_for_url(self.url("passbook_core:user-settings")) |  | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, |             self.driver.find_element(By.ID, "user-settings").text, | ||||||
|             "foo", |             "foo", | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo" |             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo" | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin", |             self.driver.find_element(By.ID, "id_name").get_attribute("value"), | ||||||
|  |             "admin", | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.ID, "id_email").get_attribute("value"), |             self.driver.find_element(By.ID, "id_email").get_attribute("value"), | ||||||
|             "admin@example.com", |             "admin@example.com", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     @override_settings(SESSION_COOKIE_SAMESITE="strict") |     @override_settings(SESSION_COOKIE_SAMESITE="strict") | ||||||
|     def test_oauth_samesite_strict(self): |     def test_oauth_samesite_strict(self): | ||||||
|         """test OAuth Source With SameSite set to strict |         """test OAuth Source With SameSite set to strict | ||||||
| @ -195,11 +197,12 @@ class TestSourceOAuth2(SeleniumTestCase): | |||||||
|             "Authentication Failed.", |             "Authentication Failed.", | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_oauth_enroll_auth(self): |     def test_oauth_enroll_auth(self): | ||||||
|         """test OAuth Source With With OIDC (enroll and authenticate again)""" |         """test OAuth Source With With OIDC (enroll and authenticate again)""" | ||||||
|         self.test_oauth_enroll() |         self.test_oauth_enroll() | ||||||
|         # We're logged in at the end of this, log out and re-login |         # We're logged in at the end of this, log out and re-login | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, "[aria-label=logout]").click() |         self.driver.find_element(By.ID, "logout").click() | ||||||
|  |  | ||||||
|         self.wait.until( |         self.wait.until( | ||||||
|             ec.presence_of_element_located( |             ec.presence_of_element_located( | ||||||
| @ -223,20 +226,19 @@ class TestSourceOAuth2(SeleniumTestCase): | |||||||
|         ) |         ) | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click() |         self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click() | ||||||
|  |  | ||||||
|         # Wait until we've loaded the user info page |         self.wait.until(ec.presence_of_element_located((By.ID, "user-settings"))) | ||||||
|         self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo"))) |         self.driver.get(self.url("passbook_core:user-settings")) | ||||||
|         self.driver.find_element(By.LINK_TEXT, "foo").click() |  | ||||||
|  |  | ||||||
|         self.wait_for_url(self.url("passbook_core:user-settings")) |  | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, |             self.driver.find_element(By.ID, "user-settings").text, | ||||||
|             "foo", |             "foo", | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo" |             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo" | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin", |             self.driver.find_element(By.ID, "id_name").get_attribute("value"), | ||||||
|  |             "admin", | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.ID, "id_email").get_attribute("value"), |             self.driver.find_element(By.ID, "id_email").get_attribute("value"), | ||||||
| @ -256,7 +258,7 @@ class TestSourceOAuth1(SeleniumTestCase): | |||||||
|  |  | ||||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: |     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||||
|         return { |         return { | ||||||
|             "image": "beryju/oauth1-test-server", |             "image": "docker.beryju.org/proxy/beryju/oauth1-test-server", | ||||||
|             "detach": True, |             "detach": True, | ||||||
|             "network_mode": "host", |             "network_mode": "host", | ||||||
|             "auto_remove": True, |             "auto_remove": True, | ||||||
| @ -292,6 +294,7 @@ class TestSourceOAuth1(SeleniumTestCase): | |||||||
|             consumer_secret=self.client_secret, |             consumer_secret=self.client_secret, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_oauth_enroll(self): |     def test_oauth_enroll(self): | ||||||
|         """test OAuth Source With With OIDC""" |         """test OAuth Source With With OIDC""" | ||||||
|         self.create_objects() |         self.create_objects() | ||||||
| @ -318,12 +321,12 @@ class TestSourceOAuth1(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.CSS_SELECTOR, "[name='confirm']").click() |         self.driver.find_element(By.CSS_SELECTOR, "[name='confirm']").click() | ||||||
|  |  | ||||||
|         # Wait until we've loaded the user info page |         # Wait until we've loaded the user info page | ||||||
|         self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "example-user"))) |         sleep(2) | ||||||
|         self.driver.find_element(By.LINK_TEXT, "example-user").click() |         self.wait.until(ec.presence_of_element_located((By.ID, "user-settings"))) | ||||||
|  |         self.driver.get(self.url("passbook_core:user-settings")) | ||||||
|  |  | ||||||
|         self.wait_for_url(self.url("passbook_core:user-settings")) |  | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, |             self.driver.find_element(By.ID, "user-settings").text, | ||||||
|             "example-user", |             "example-user", | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ from selenium.webdriver.common.keys import Keys | |||||||
| from selenium.webdriver.support import expected_conditions as ec | from selenium.webdriver.support import expected_conditions as ec | ||||||
| from structlog import get_logger | from structlog import get_logger | ||||||
|  |  | ||||||
| from e2e.utils import SeleniumTestCase | from e2e.utils import SeleniumTestCase, retry | ||||||
| from passbook.crypto.models import CertificateKeyPair | from passbook.crypto.models import CertificateKeyPair | ||||||
| from passbook.flows.models import Flow | from passbook.flows.models import Flow | ||||||
| from passbook.sources.saml.models import SAMLBindingTypes, SAMLSource | from passbook.sources.saml.models import SAMLBindingTypes, SAMLSource | ||||||
| @ -75,7 +75,7 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|  |  | ||||||
|     def get_container_specs(self) -> Optional[Dict[str, Any]]: |     def get_container_specs(self) -> Optional[Dict[str, Any]]: | ||||||
|         return { |         return { | ||||||
|             "image": "kristophjunge/test-saml-idp:1.15", |             "image": "docker.beryju.org/proxy/kristophjunge/test-saml-idp:1.15", | ||||||
|             "detach": True, |             "detach": True, | ||||||
|             "network_mode": "host", |             "network_mode": "host", | ||||||
|             "auto_remove": True, |             "auto_remove": True, | ||||||
| @ -92,13 +92,16 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|             }, |             }, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_idp_redirect(self): |     def test_idp_redirect(self): | ||||||
|         """test SAML Source With redirect binding""" |         """test SAML Source With redirect binding""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
|         authentication_flow = Flow.objects.get(slug="default-source-authentication") |         authentication_flow = Flow.objects.get(slug="default-source-authentication") | ||||||
|         enrollment_flow = Flow.objects.get(slug="default-source-enrollment") |         enrollment_flow = Flow.objects.get(slug="default-source-enrollment") | ||||||
|         keypair = CertificateKeyPair.objects.create( |         keypair = CertificateKeyPair.objects.create( | ||||||
|             name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY, |             name="test-idp-cert", | ||||||
|  |             certificate_data=IDP_CERT, | ||||||
|  |             key_data=IDP_KEY, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         SAMLSource.objects.create( |         SAMLSource.objects.create( | ||||||
| @ -130,12 +133,8 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER) | ||||||
|  |  | ||||||
|         # Wait until we're logged in |         # Wait until we're logged in | ||||||
|         self.wait.until( |         self.wait.until(ec.presence_of_element_located((By.ID, "user-settings"))) | ||||||
|             ec.presence_of_element_located( |         self.driver.get(self.url("passbook_core:user-settings")) | ||||||
|                 (By.XPATH, "//a[contains(@href, '/-/user/')]") |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click() |  | ||||||
|  |  | ||||||
|         # Wait until we've loaded the user info page |         # Wait until we've loaded the user info page | ||||||
|         self.wait.until(ec.presence_of_element_located((By.ID, "id_username"))) |         self.wait.until(ec.presence_of_element_located((By.ID, "id_username"))) | ||||||
| @ -143,13 +142,16 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "" |             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_idp_post(self): |     def test_idp_post(self): | ||||||
|         """test SAML Source With post binding""" |         """test SAML Source With post binding""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
|         authentication_flow = Flow.objects.get(slug="default-source-authentication") |         authentication_flow = Flow.objects.get(slug="default-source-authentication") | ||||||
|         enrollment_flow = Flow.objects.get(slug="default-source-enrollment") |         enrollment_flow = Flow.objects.get(slug="default-source-enrollment") | ||||||
|         keypair = CertificateKeyPair.objects.create( |         keypair = CertificateKeyPair.objects.create( | ||||||
|             name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY, |             name="test-idp-cert", | ||||||
|  |             certificate_data=IDP_CERT, | ||||||
|  |             key_data=IDP_KEY, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         SAMLSource.objects.create( |         SAMLSource.objects.create( | ||||||
| @ -183,12 +185,8 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER) | ||||||
|  |  | ||||||
|         # Wait until we're logged in |         # Wait until we're logged in | ||||||
|         self.wait.until( |         self.wait.until(ec.presence_of_element_located((By.ID, "user-settings"))) | ||||||
|             ec.presence_of_element_located( |         self.driver.get(self.url("passbook_core:user-settings")) | ||||||
|                 (By.XPATH, "//a[contains(@href, '/-/user/')]") |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click() |  | ||||||
|  |  | ||||||
|         # Wait until we've loaded the user info page |         # Wait until we've loaded the user info page | ||||||
|         self.wait.until(ec.presence_of_element_located((By.ID, "id_username"))) |         self.wait.until(ec.presence_of_element_located((By.ID, "id_username"))) | ||||||
| @ -196,13 +194,16 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "" |             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @retry() | ||||||
|     def test_idp_post_auto(self): |     def test_idp_post_auto(self): | ||||||
|         """test SAML Source With post binding (auto redirect)""" |         """test SAML Source With post binding (auto redirect)""" | ||||||
|         # Bootstrap all needed objects |         # Bootstrap all needed objects | ||||||
|         authentication_flow = Flow.objects.get(slug="default-source-authentication") |         authentication_flow = Flow.objects.get(slug="default-source-authentication") | ||||||
|         enrollment_flow = Flow.objects.get(slug="default-source-enrollment") |         enrollment_flow = Flow.objects.get(slug="default-source-enrollment") | ||||||
|         keypair = CertificateKeyPair.objects.create( |         keypair = CertificateKeyPair.objects.create( | ||||||
|             name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY, |             name="test-idp-cert", | ||||||
|  |             certificate_data=IDP_CERT, | ||||||
|  |             key_data=IDP_KEY, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         SAMLSource.objects.create( |         SAMLSource.objects.create( | ||||||
| @ -234,12 +235,8 @@ class TestSourceSAML(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER) | ||||||
|  |  | ||||||
|         # Wait until we're logged in |         # Wait until we're logged in | ||||||
|         self.wait.until( |         self.wait.until(ec.presence_of_element_located((By.ID, "user-settings"))) | ||||||
|             ec.presence_of_element_located( |         self.driver.get(self.url("passbook_core:user-settings")) | ||||||
|                 (By.XPATH, "//a[contains(@href, '/-/user/')]") |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click() |  | ||||||
|  |  | ||||||
|         # Wait until we've loaded the user info page |         # Wait until we've loaded the user info page | ||||||
|         self.wait.until(ec.presence_of_element_located((By.ID, "id_username"))) |         self.wait.until(ec.presence_of_element_located((By.ID, "id_username"))) | ||||||
|  | |||||||
							
								
								
									
										48
									
								
								e2e/utils.py
									
									
									
									
									
								
							
							
						
						| @ -1,19 +1,22 @@ | |||||||
| """passbook e2e testing utilities""" | """passbook e2e testing utilities""" | ||||||
|  | from functools import wraps | ||||||
| from glob import glob | from glob import glob | ||||||
| from importlib.util import module_from_spec, spec_from_file_location | from importlib.util import module_from_spec, spec_from_file_location | ||||||
| from inspect import getmembers, isfunction | from inspect import getmembers, isfunction | ||||||
| from os import environ, makedirs | from os import environ, makedirs | ||||||
| from time import sleep, time | from time import sleep, time | ||||||
| from typing import Any, Dict, Optional | from typing import Any, Callable, Dict, Optional | ||||||
|  |  | ||||||
| from django.apps import apps | from django.apps import apps | ||||||
| from django.contrib.staticfiles.testing import StaticLiveServerTestCase | from django.contrib.staticfiles.testing import StaticLiveServerTestCase | ||||||
| from django.db import connection, transaction | from django.db import connection, transaction | ||||||
| from django.db.utils import IntegrityError | from django.db.utils import IntegrityError | ||||||
| from django.shortcuts import reverse | from django.shortcuts import reverse | ||||||
|  | from django.test.testcases import TransactionTestCase | ||||||
| from docker import DockerClient, from_env | from docker import DockerClient, from_env | ||||||
| from docker.models.containers import Container | from docker.models.containers import Container | ||||||
| from selenium import webdriver | from selenium import webdriver | ||||||
|  | from selenium.common.exceptions import NoSuchElementException, TimeoutException | ||||||
| from selenium.webdriver.common.desired_capabilities import DesiredCapabilities | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities | ||||||
| from selenium.webdriver.remote.webdriver import WebDriver | from selenium.webdriver.remote.webdriver import WebDriver | ||||||
| from selenium.webdriver.support.ui import WebDriverWait | from selenium.webdriver.support.ui import WebDriverWait | ||||||
| @ -38,8 +41,8 @@ class SeleniumTestCase(StaticLiveServerTestCase): | |||||||
|         makedirs("selenium_screenshots/", exist_ok=True) |         makedirs("selenium_screenshots/", exist_ok=True) | ||||||
|         self.driver = self._get_driver() |         self.driver = self._get_driver() | ||||||
|         self.driver.maximize_window() |         self.driver.maximize_window() | ||||||
|         self.driver.implicitly_wait(10) |         self.driver.implicitly_wait(30) | ||||||
|         self.wait = WebDriverWait(self.driver, 30) |         self.wait = WebDriverWait(self.driver, 60) | ||||||
|         self.apply_default_data() |         self.apply_default_data() | ||||||
|         self.logger = get_logger() |         self.logger = get_logger() | ||||||
|         if specs := self.get_container_specs(): |         if specs := self.get_container_specs(): | ||||||
| @ -47,6 +50,7 @@ class SeleniumTestCase(StaticLiveServerTestCase): | |||||||
|  |  | ||||||
|     def _start_container(self, specs: Dict[str, Any]) -> Container: |     def _start_container(self, specs: Dict[str, Any]) -> Container: | ||||||
|         client: DockerClient = from_env() |         client: DockerClient = from_env() | ||||||
|  |         client.images.pull(specs["image"]) | ||||||
|         container = client.containers.run(**specs) |         container = client.containers.run(**specs) | ||||||
|         if "healthcheck" not in specs: |         if "healthcheck" not in specs: | ||||||
|             return container |             return container | ||||||
| @ -122,3 +126,41 @@ class SeleniumTestCase(StaticLiveServerTestCase): | |||||||
|                             func(apps, schema_editor) |                             func(apps, schema_editor) | ||||||
|                         except IntegrityError: |                         except IntegrityError: | ||||||
|                             pass |                             pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def retry(max_retires=3, exceptions=None): | ||||||
|  |     """Retry test multiple times. Default to catching Selenium Timeout Exception""" | ||||||
|  |  | ||||||
|  |     if not exceptions: | ||||||
|  |         exceptions = [TimeoutException, NoSuchElementException] | ||||||
|  |  | ||||||
|  |     logger = get_logger() | ||||||
|  |  | ||||||
|  |     def retry_actual(func: Callable): | ||||||
|  |         """Retry test multiple times""" | ||||||
|  |         count = 1 | ||||||
|  |  | ||||||
|  |         @wraps(func) | ||||||
|  |         def wrapper(self: TransactionTestCase, *args, **kwargs): | ||||||
|  |             """Run test again if we're below max_retries, including tearDown and | ||||||
|  |             setUp. Otherwise raise the error""" | ||||||
|  |             nonlocal count | ||||||
|  |             try: | ||||||
|  |                 return func(self, *args, **kwargs) | ||||||
|  |             # pylint: disable=catching-non-exception | ||||||
|  |             except tuple(exceptions) as exc: | ||||||
|  |                 count += 1 | ||||||
|  |                 if count > max_retires: | ||||||
|  |                     logger.debug("Exceeded retry count", exc=exc, test=self) | ||||||
|  |                     # pylint: disable=raising-non-exception | ||||||
|  |                     raise exc | ||||||
|  |                 logger.debug("Retrying on error", exc=exc, test=self) | ||||||
|  |                 self.tearDown() | ||||||
|  |                 # pylint: disable=protected-access | ||||||
|  |                 self._post_teardown() | ||||||
|  |                 self.setUp() | ||||||
|  |                 return wrapper(self, *args, **kwargs) | ||||||
|  |  | ||||||
|  |         return wrapper | ||||||
|  |  | ||||||
|  |     return retry_actual | ||||||
|  | |||||||
| @ -1,9 +1,11 @@ | |||||||
| apiVersion: v2 | apiVersion: v2 | ||||||
| appVersion: "0.10.7-stable" | description: passbook is an open-source Identity Provider focused on flexibility and versatility. You can use passbook in an existing environment to add support for new protocols. passbook is also a great solution for implementing signup/recovery/etc in your application, so you don't have to deal with it. | ||||||
| description: A Helm chart for passbook. |  | ||||||
| name: passbook | name: passbook | ||||||
| version: "0.10.7-stable" | home: https://passbook.beryju.org | ||||||
| icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg | sources: | ||||||
|  |   - https://github.com/BeryJu/passbook | ||||||
|  | version: "0.12.7-stable" | ||||||
|  | icon: https://raw.githubusercontent.com/BeryJu/passbook/master/docs/images/logo.svg | ||||||
| dependencies: | dependencies: | ||||||
|   - name: postgresql |   - name: postgresql | ||||||
|     version: 9.4.1 |     version: 9.4.1 | ||||||
|  | |||||||
							
								
								
									
										28
									
								
								helm/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,28 @@ | |||||||
|  | # passbook Helm Chart | ||||||
|  |  | ||||||
|  | | Name                              | Default                 | Description | | ||||||
|  | |-----------------------------------|-------------------------|-------------| | ||||||
|  | | image.name                        | beryju/passbook         | Image used to run the passbook server and worker | | ||||||
|  | | image.name_static                 | beryju/passbook-static  | Image used to run the passbook static server (CSS and JS Files) | | ||||||
|  | | image.tag                         | 0.12.5-stable           | Image tag | | ||||||
|  | | serverReplicas                    | 1                       | Replicas for the Server deployment | | ||||||
|  | | workerReplicas                    | 1                       | Replicas for the Worker deployment | | ||||||
|  | | kubernetesIntegration             | true                    | Enable/disable the Kubernetes integration for passbook. This will create a service account for passbook to create and update outposts in passbook | | ||||||
|  | | config.secretKey                  |                         | Secret key used to sign session cookies, generate with `pwgen 50 1` for example. | | ||||||
|  | | config.errorReporting.enabled     | false                   | Enable/disable error reporting | | ||||||
|  | | config.errorReporting.environment | customer                | Environment sent with the error reporting | | ||||||
|  | | config.errorReporting.sendPii     | false                   | Whether to send Personally-identifiable data with the error reporting | | ||||||
|  | | config.logLevel                   | warning                 | Log level of passbook | | ||||||
|  | | backup.accessKey                  |                         | Optionally enable S3 Backup, Access Key | | ||||||
|  | | backup.secretKey                  |                         | Optionally enable S3 Backup, Secret Key | | ||||||
|  | | backup.bucket                     |                         | Optionally enable S3 Backup, Bucket | | ||||||
|  | | backup.region                     |                         | Optionally enable S3 Backup, Region | | ||||||
|  | | backup.host                       |                         | Optionally enable S3 Backup, to custom Endpoint like minio | | ||||||
|  | | ingress.annotations               | {}                      | Annotations for the ingress object | | ||||||
|  | | ingress.hosts                     | [passbook.k8s.local]    | Hosts which the ingress will match | | ||||||
|  | | ingress.tls                       | []                      | TLS Configuration, same as Ingress objects | | ||||||
|  | | install.postgresql                | true                    | Enables/disables the packaged PostgreSQL Chart | ||||||
|  | | install.redis                     | true                    | Enables/disables the packaged Redis Chart | ||||||
|  | | postgresql.postgresqlPassword     |                         | Password used for PostgreSQL, generated automatically. | ||||||
|  |  | ||||||
|  | For more info, see https://passbook.beryju.org/ and https://passbook.beryju.org/installation/kubernetes/ | ||||||
| @ -3,7 +3,7 @@ | |||||||
| Expand the name of the chart. | Expand the name of the chart. | ||||||
| */}} | */}} | ||||||
| {{- define "passbook.name" -}} | {{- define "passbook.name" -}} | ||||||
| {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} | {{- default .Chart.Name | trunc 63 | trimSuffix "-" -}} | ||||||
| {{- end -}} | {{- end -}} | ||||||
|  |  | ||||||
| {{/* | {{/* | ||||||
| @ -12,17 +12,13 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this | |||||||
| If release name contains chart name it will be used as a full name. | If release name contains chart name it will be used as a full name. | ||||||
| */}} | */}} | ||||||
| {{- define "passbook.fullname" -}} | {{- define "passbook.fullname" -}} | ||||||
| {{- if .Values.fullnameOverride -}} | {{- $name := default .Chart.Name -}} | ||||||
| {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} |  | ||||||
| {{- else -}} |  | ||||||
| {{- $name := default .Chart.Name .Values.nameOverride -}} |  | ||||||
| {{- if contains $name .Release.Name -}} | {{- if contains $name .Release.Name -}} | ||||||
| {{- .Release.Name | trunc 63 | trimSuffix "-" -}} | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} | ||||||
| {{- else -}} | {{- else -}} | ||||||
| {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} | ||||||
| {{- end -}} | {{- end -}} | ||||||
| {{- end -}} | {{- end -}} | ||||||
| {{- end -}} |  | ||||||
|  |  | ||||||
| {{/* | {{/* | ||||||
| Create chart name and version as used by the chart label. | Create chart name and version as used by the chart label. | ||||||
|  | |||||||
| @ -7,13 +7,14 @@ data: | |||||||
|   POSTGRESQL__NAME: "{{ .Values.postgresql.postgresqlDatabase }}" |   POSTGRESQL__NAME: "{{ .Values.postgresql.postgresqlDatabase }}" | ||||||
|   POSTGRESQL__USER: "{{ .Values.postgresql.postgresqlUsername }}" |   POSTGRESQL__USER: "{{ .Values.postgresql.postgresqlUsername }}" | ||||||
|   {{- if .Values.backup }} |   {{- if .Values.backup }} | ||||||
|   POSTGRESQL__BACKUP__ACCESS_KEY: "{{ .Values.backup.access_key }}" |   POSTGRESQL__S3_BACKUP__ACCESS_KEY: "{{ .Values.backup.accessKey }}" | ||||||
|   POSTGRESQL__BACKUP__SECRET_KEY: "{{ .Values.backup.secret_key }}" |   POSTGRESQL__S3_BACKUP__SECRET_KEY: "{{ .Values.backup.secretKey }}" | ||||||
|   POSTGRESQL__BACKUP__BUCKET: "{{ .Values.backup.bucket }}" |   POSTGRESQL__S3_BACKUP__BUCKET: "{{ .Values.backup.bucket }}" | ||||||
|   POSTGRESQL__BACKUP__HOST: "{{ .Values.backup.host }}" |   POSTGRESQL__S3_BACKUP__REGION: "{{ .Values.backup.region }}" | ||||||
|  |   POSTGRESQL__S3_BACKUP__HOST: "{{ .Values.backup.host }}" | ||||||
|   {{- end}} |   {{- end}} | ||||||
|   REDIS__HOST: "{{ .Release.Name }}-redis-master" |   REDIS__HOST: "{{ .Release.Name }}-redis-master" | ||||||
|   ERROR_REPORTING__ENABLED: "{{ .Values.config.error_reporting.enabled }}" |   ERROR_REPORTING__ENABLED: "{{ .Values.config.errorReporting.enabled }}" | ||||||
|   ERROR_REPORTING__ENVIRONMENT: "{{ .Values.config.error_reporting.environment }}" |   ERROR_REPORTING__ENVIRONMENT: "{{ .Values.config.errorReporting.environment }}" | ||||||
|   ERROR_REPORTING__SEND_PII: "{{ .Values.config.error_reporting.send_pii }}" |   ERROR_REPORTING__SEND_PII: "{{ .Values.config.errorReporting.sendPii }}" | ||||||
|   LOG_LEVEL: "{{ .Values.config.log_level }}" |   LOG_LEVEL: "{{ .Values.config.logLevel }}" | ||||||
|  | |||||||
| @ -5,8 +5,8 @@ metadata: | |||||||
|   name: {{ include "passbook.fullname" . }}-secret-key |   name: {{ include "passbook.fullname" . }}-secret-key | ||||||
| data: | data: | ||||||
|   monitoring_username: bW9uaXRvcg== # monitor in base64 |   monitoring_username: bW9uaXRvcg== # monitor in base64 | ||||||
|   {{- if .Values.config.secret_key }} |   {{- if .Values.config.secretKey }} | ||||||
|   secret_key: {{ .Values.config.secret_key | b64enc | quote }} |   secret_key: {{ .Values.config.secretKey | b64enc | quote }} | ||||||
|   {{- else }} |   {{- else }} | ||||||
|   secret_key: {{ randAlphaNum 50 | b64enc | quote}} |   secret_key: {{ randAlphaNum 50 | b64enc | quote}} | ||||||
|   {{- end }} |   {{- end }} | ||||||
|  | |||||||
							
								
								
									
										64
									
								
								helm/templates/service-account.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,64 @@ | |||||||
|  | {{- if .Values.kubernetesIntegration }} | ||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: ClusterRole | ||||||
|  | metadata: | ||||||
|  |   name: {{ include "passbook.fullname" . }}-sa-role | ||||||
|  | rules: | ||||||
|  | - apiGroups: | ||||||
|  |     - "" | ||||||
|  |   resources: | ||||||
|  |     - secrets | ||||||
|  |     - services | ||||||
|  |   verbs: | ||||||
|  |     - "get" | ||||||
|  |     - "create" | ||||||
|  |     - "delete" | ||||||
|  |     - "read" | ||||||
|  |     - "patch" | ||||||
|  | - apiGroups: | ||||||
|  |     - "extensions" | ||||||
|  |     - "apps" | ||||||
|  |   resources: | ||||||
|  |     - "deployments" | ||||||
|  |   verbs: | ||||||
|  |     - "get" | ||||||
|  |     - "create" | ||||||
|  |     - "delete" | ||||||
|  |     - "read" | ||||||
|  |     - "patch" | ||||||
|  | - apiGroups: | ||||||
|  |     - "extensions" | ||||||
|  |     - "networking.k8s.io" | ||||||
|  |   resources: | ||||||
|  |     - "ingresses" | ||||||
|  |   verbs: | ||||||
|  |     - "get" | ||||||
|  |     - "create" | ||||||
|  |     - "delete" | ||||||
|  |     - "read" | ||||||
|  |     - "patch" | ||||||
|  | - apiGroups: | ||||||
|  |     - "" | ||||||
|  |   resources: | ||||||
|  |     - namespaces | ||||||
|  |   verbs: | ||||||
|  |     - list | ||||||
|  | --- | ||||||
|  | apiVersion: v1 | ||||||
|  | kind: ServiceAccount | ||||||
|  | metadata: | ||||||
|  |   name: {{ include "passbook.fullname" . }}-sa | ||||||
|  | --- | ||||||
|  | apiVersion: rbac.authorization.k8s.io/v1 | ||||||
|  | kind: ClusterRoleBinding | ||||||
|  | metadata: | ||||||
|  |   name: {{ include "passbook.fullname" . }}-sa-role-binding | ||||||
|  | roleRef: | ||||||
|  |   apiGroup: rbac.authorization.k8s.io | ||||||
|  |   kind: ClusterRole | ||||||
|  |   name: {{ include "passbook.fullname" . }}-sa-role | ||||||
|  | subjects: | ||||||
|  | - kind: ServiceAccount | ||||||
|  |   name: {{ include "passbook.fullname" . }}-sa | ||||||
|  |   namespace: {{ .Release.Namespace }} | ||||||
|  | {{- end }} | ||||||
| @ -100,14 +100,14 @@ spec: | |||||||
|               port: http |               port: http | ||||||
|               httpHeaders: |               httpHeaders: | ||||||
|                 - name: Host |                 - name: Host | ||||||
|                   value: kubernetes-healthcheck-host |                   value: passbook-healthcheck-host | ||||||
|           readinessProbe: |           readinessProbe: | ||||||
|             httpGet: |             httpGet: | ||||||
|               path: / |               path: / | ||||||
|               port: http |               port: http | ||||||
|               httpHeaders: |               httpHeaders: | ||||||
|                 - name: Host |                 - name: Host | ||||||
|                   value: kubernetes-healthcheck-host |                   value: passbook-healthcheck-host | ||||||
|           resources: |           resources: | ||||||
|             requests: |             requests: | ||||||
|               cpu: 100m |               cpu: 100m | ||||||
|  | |||||||
| @ -22,6 +22,9 @@ 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: | ||||||
|  |       {{- if .Values.kubernetesIntegration }} | ||||||
|  |       serviceAccountName: {{ include "passbook.fullname" . }}-sa | ||||||
|  |       {{- end }} | ||||||
|       affinity: |       affinity: | ||||||
|         podAntiAffinity: |         podAntiAffinity: | ||||||
|           preferredDuringSchedulingIgnoredDuringExecution: |           preferredDuringSchedulingIgnoredDuringExecution: | ||||||
|  | |||||||
							
								
								
									
										21
									
								
								helm/values.test.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,21 @@ | |||||||
|  | image: | ||||||
|  |   tag: gh-master | ||||||
|  |  | ||||||
|  | serverReplicas: 1 | ||||||
|  | workerReplicas: 1 | ||||||
|  |  | ||||||
|  | config: | ||||||
|  |   # Log level used by web and worker | ||||||
|  |   # Can be either debug, info, warning, error | ||||||
|  |   logLevel: debug | ||||||
|  |  | ||||||
|  | ingress: | ||||||
|  |   hosts: | ||||||
|  |     - passbook.127.0.0.1.nip.io | ||||||
|  |  | ||||||
|  | # These values influence the bundled postgresql and redis charts, but are also used by passbook to connect | ||||||
|  | postgresql: | ||||||
|  |   postgresqlPassword: EK-5jnKfjrGRm<77 | ||||||
|  |  | ||||||
|  | redis: | ||||||
|  |   password: password | ||||||
| @ -4,32 +4,45 @@ | |||||||
| image: | image: | ||||||
|   name: beryju/passbook |   name: beryju/passbook | ||||||
|   name_static: beryju/passbook-static |   name_static: beryju/passbook-static | ||||||
|   tag: 0.10.7-stable |   tag: 0.12.7-stable | ||||||
|  |  | ||||||
| nameOverride: "" |  | ||||||
|  |  | ||||||
| serverReplicas: 1 | serverReplicas: 1 | ||||||
| workerReplicas: 1 | workerReplicas: 1 | ||||||
|  |  | ||||||
|  | # Enable the Kubernetes integration which lets passbook deploy outposts into kubernetes | ||||||
|  | kubernetesIntegration: true | ||||||
|  |  | ||||||
| config: | config: | ||||||
|   # Optionally specify fixed secret_key, otherwise generated automatically |   # Optionally specify fixed secret_key, otherwise generated automatically | ||||||
|   # secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o |   # secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o | ||||||
|   # Enable error reporting |   # Enable error reporting | ||||||
|   error_reporting: |   errorReporting: | ||||||
|     enabled: false |     enabled: false | ||||||
|     environment: customer |     environment: customer | ||||||
|     send_pii: false |     sendPii: false | ||||||
|   # Log level used by web and worker |   # Log level used by web and worker | ||||||
|   # Can be either debug, info, warning, error |   # Can be either debug, info, warning, error | ||||||
|   log_level: warning |   logLevel: warning | ||||||
|  |  | ||||||
| # Enable Database Backups to S3 | # Enable Database Backups to S3 | ||||||
| # backup: | # backup: | ||||||
| #   access_key: access-key | #   accessKey: access-key | ||||||
| #   secret_key: secret-key | #   secretKey: secret-key | ||||||
| #   bucket: s3-bucket | #   bucket: s3-bucket | ||||||
|  | #   region: eu-central-1 | ||||||
| #   host: s3-host | #   host: s3-host | ||||||
|  |  | ||||||
|  | ingress: | ||||||
|  |   annotations: {} | ||||||
|  |     # kubernetes.io/ingress.class: nginx | ||||||
|  |     # kubernetes.io/tls-acme: "true" | ||||||
|  |   hosts: | ||||||
|  |     - passbook.k8s.local | ||||||
|  |   tls: [] | ||||||
|  |   #  - secretName: chart-example-tls | ||||||
|  |   #    hosts: | ||||||
|  |   #      - passbook.k8s.local | ||||||
|  |  | ||||||
| ################################### | ################################### | ||||||
| # Values controlling dependencies | # Values controlling dependencies | ||||||
| ################################### | ################################### | ||||||
| @ -46,19 +59,5 @@ redis: | |||||||
|   cluster: |   cluster: | ||||||
|     enabled: false |     enabled: false | ||||||
|   master: |   master: | ||||||
|     persistence: |  | ||||||
|       enabled: false |  | ||||||
|     # https://stackoverflow.com/a/59189742 |     # https://stackoverflow.com/a/59189742 | ||||||
|     disableCommands: [] |     disableCommands: [] | ||||||
|  |  | ||||||
| ingress: |  | ||||||
|   annotations: {} |  | ||||||
|     # kubernetes.io/ingress.class: nginx |  | ||||||
|     # kubernetes.io/tls-acme: "true" |  | ||||||
|   path: / |  | ||||||
|   hosts: |  | ||||||
|     - passbook.k8s.local |  | ||||||
|   tls: [] |  | ||||||
|   #  - secretName: chart-example-tls |  | ||||||
|   #    hosts: |  | ||||||
|   #      - passbook.k8s.local |  | ||||||
|  | |||||||
| @ -1,14 +1,20 @@ | |||||||
| #!/bin/bash -e | #!/bin/bash -e | ||||||
| python -m lifecycle.wait_for_db | python -m lifecycle.wait_for_db | ||||||
| printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@" | printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@" > /dev/stderr | ||||||
| if [[ "$1" == "server" ]]; then | if [[ "$1" == "server" ]]; then | ||||||
|     gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application |     gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application | ||||||
| elif [[ "$1" == "worker" ]]; then | elif [[ "$1" == "worker" ]]; then | ||||||
|     celery -A passbook.root.celery worker --autoscale 10,3 -E -B -s /tmp/celerybeat-schedule -Q passbook,passbook_scheduled |     celery -A passbook.root.celery worker --autoscale 3,1 -E -B -s /tmp/celerybeat-schedule -Q passbook,passbook_scheduled | ||||||
| elif [[ "$1" == "migrate" ]]; then | elif [[ "$1" == "migrate" ]]; then | ||||||
|     # Run system migrations first, run normal migrations after |     # Run system migrations first, run normal migrations after | ||||||
|     python -m lifecycle.migrate |     python -m lifecycle.migrate | ||||||
|     python -m manage migrate |     python -m manage migrate | ||||||
|  | elif [[ "$1" == "backup" ]]; then | ||||||
|  |     python -m manage dbbackup --clean | ||||||
|  | elif [[ "$1" == "restore" ]]; then | ||||||
|  |     python -m manage dbrestore ${@:2} | ||||||
|  | elif [[ "$1" == "bash" ]]; then | ||||||
|  |     /bin/bash | ||||||
| else | else | ||||||
|     python -m manage "$@" |     python -m manage "$@" | ||||||
| fi | fi | ||||||
|  | |||||||
| @ -47,7 +47,9 @@ if __name__ == "__main__": | |||||||
|         # pyright: reportGeneralTypeIssues=false |         # pyright: reportGeneralTypeIssues=false | ||||||
|         spec.loader.exec_module(mod) |         spec.loader.exec_module(mod) | ||||||
|  |  | ||||||
|         for _, sub in getmembers(mod, isclass): |         for name, sub in getmembers(mod, isclass): | ||||||
|  |             if name != "Migration": | ||||||
|  |                 continue | ||||||
|             migration = sub(curr, conn) |             migration = sub(curr, conn) | ||||||
|             if migration.needs_migration(): |             if migration.needs_migration(): | ||||||
|                 LOGGER.info("Migration needs to be applied", migration=sub) |                 LOGGER.info("Migration needs to be applied", migration=sub) | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ delete from django_migrations where app = 'passbook_stages_password' and | |||||||
| name = '0002_passwordstage_change_flow';""" | name = '0002_passwordstage_change_flow';""" | ||||||
|  |  | ||||||
|  |  | ||||||
| class To010Migration(BaseMigration): | class Migration(BaseMigration): | ||||||
|     def needs_migration(self) -> bool: |     def needs_migration(self) -> bool: | ||||||
|         self.cur.execute( |         self.cur.execute( | ||||||
|             "select * from information_schema.tables where table_name='oidc_provider_client'" |             "select * from information_schema.tables where table_name='oidc_provider_client'" | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ nav: | |||||||
|   - Installation: |   - Installation: | ||||||
|     - docker-compose: installation/docker-compose.md |     - docker-compose: installation/docker-compose.md | ||||||
|     - Kubernetes: installation/kubernetes.md |     - Kubernetes: installation/kubernetes.md | ||||||
|  |     - Reverse Proxy: installation/reverse-proxy.md | ||||||
|   - Flows: |   - Flows: | ||||||
|       Overview: flow/flows.md |       Overview: flow/flows.md | ||||||
|       Examples: flow/examples/examples.md |       Examples: flow/examples/examples.md | ||||||
| @ -46,6 +47,8 @@ nav: | |||||||
|     - Overview: policies/index.md |     - Overview: policies/index.md | ||||||
|     - Expression: policies/expression.md |     - Expression: policies/expression.md | ||||||
|   - Integrations: |   - Integrations: | ||||||
|  |     - as Source: | ||||||
|  |       - Active Directory: integrations/sources/active-directory/index.md | ||||||
|     - as Provider: |     - as Provider: | ||||||
|       - Amazon Web Services: integrations/services/aws/index.md |       - Amazon Web Services: integrations/services/aws/index.md | ||||||
|       - GitLab: integrations/services/gitlab/index.md |       - GitLab: integrations/services/gitlab/index.md | ||||||
| @ -56,9 +59,14 @@ nav: | |||||||
|       - VMware vCenter: integrations/services/vmware-vcenter/index.md |       - VMware vCenter: integrations/services/vmware-vcenter/index.md | ||||||
|       - Ubuntu Landscape: integrations/services/ubuntu-landscape/index.md |       - Ubuntu Landscape: integrations/services/ubuntu-landscape/index.md | ||||||
|       - Sonarr: integrations/services/sonarr/index.md |       - Sonarr: integrations/services/sonarr/index.md | ||||||
|  |       - Tautulli: integrations/services/tautulli/index.md | ||||||
|  |   - Maintenance: | ||||||
|  |     - Backups: maintenance/backups/index.md | ||||||
|   - 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 | ||||||
|  |     - to 0.11: upgrading/to-0.11.md | ||||||
|  |     - to 0.12: upgrading/to-0.12.md | ||||||
|   - Troubleshooting: |   - Troubleshooting: | ||||||
|     - Access problems: troubleshooting/access.md |     - Access problems: troubleshooting/access.md | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,2 +1,2 @@ | |||||||
| """passbook""" | """passbook""" | ||||||
| __version__ = "0.10.7-stable" | __version__ = "0.12.7-stable" | ||||||
|  | |||||||
							
								
								
									
										0
									
								
								passbook/admin/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										80
									
								
								passbook/admin/api/overview.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,80 @@ | |||||||
|  | """passbook administration overview""" | ||||||
|  | from django.core.cache import cache | ||||||
|  | from django.http import response | ||||||
|  | from drf_yasg2.utils import swagger_auto_schema | ||||||
|  | from rest_framework.fields import SerializerMethodField | ||||||
|  | from rest_framework.permissions import IsAdminUser | ||||||
|  | from rest_framework.request import Request | ||||||
|  | from rest_framework.response import Response | ||||||
|  | from rest_framework.serializers import Serializer | ||||||
|  | from rest_framework.viewsets import ViewSet | ||||||
|  |  | ||||||
|  | from passbook import __version__ | ||||||
|  | from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version | ||||||
|  | from passbook.core.models import Provider | ||||||
|  | from passbook.policies.models import Policy | ||||||
|  | from passbook.root.celery import CELERY_APP | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdministrationOverviewSerializer(Serializer): | ||||||
|  |     """Overview View""" | ||||||
|  |  | ||||||
|  |     version = SerializerMethodField() | ||||||
|  |     version_latest = SerializerMethodField() | ||||||
|  |     worker_count = SerializerMethodField() | ||||||
|  |     providers_without_application = SerializerMethodField() | ||||||
|  |     policies_without_binding = SerializerMethodField() | ||||||
|  |     cached_policies = SerializerMethodField() | ||||||
|  |     cached_flows = SerializerMethodField() | ||||||
|  |  | ||||||
|  |     def get_version(self, _) -> str: | ||||||
|  |         """Get current version""" | ||||||
|  |         return __version__ | ||||||
|  |  | ||||||
|  |     def get_version_latest(self, _) -> str: | ||||||
|  |         """Get latest version from cache""" | ||||||
|  |         version_in_cache = cache.get(VERSION_CACHE_KEY) | ||||||
|  |         if not version_in_cache: | ||||||
|  |             update_latest_version.delay() | ||||||
|  |             return __version__ | ||||||
|  |         return version_in_cache | ||||||
|  |  | ||||||
|  |     def get_worker_count(self, _) -> int: | ||||||
|  |         """Ping workers""" | ||||||
|  |         return len(CELERY_APP.control.ping(timeout=0.5)) | ||||||
|  |  | ||||||
|  |     def get_providers_without_application(self, _) -> int: | ||||||
|  |         """Count of providers without application""" | ||||||
|  |         return len(Provider.objects.filter(application=None)) | ||||||
|  |  | ||||||
|  |     def get_policies_without_binding(self, _) -> int: | ||||||
|  |         """Count of policies not bound or use in prompt stages""" | ||||||
|  |         return len( | ||||||
|  |             Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def get_cached_policies(self, _) -> int: | ||||||
|  |         """Get cached policy count""" | ||||||
|  |         return len(cache.keys("policy_*")) | ||||||
|  |  | ||||||
|  |     def get_cached_flows(self, _) -> int: | ||||||
|  |         """Get cached flow count""" | ||||||
|  |         return len(cache.keys("flow_*")) | ||||||
|  |  | ||||||
|  |     def create(self, request: Request) -> response: | ||||||
|  |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |     def update(self, request: Request) -> Response: | ||||||
|  |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdministrationOverviewViewSet(ViewSet): | ||||||
|  |     """Return single instance of AdministrationOverviewSerializer""" | ||||||
|  |  | ||||||
|  |     permission_classes = [IsAdminUser] | ||||||
|  |  | ||||||
|  |     @swagger_auto_schema(responses={200: AdministrationOverviewSerializer(many=True)}) | ||||||
|  |     def list(self, request: Request) -> Response: | ||||||
|  |         """Return single instance of AdministrationOverviewSerializer""" | ||||||
|  |         serializer = AdministrationOverviewSerializer(True) | ||||||
|  |         return Response(serializer.data) | ||||||
							
								
								
									
										80
									
								
								passbook/admin/api/overview_metrics.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,80 @@ | |||||||
|  | """passbook administration overview""" | ||||||
|  | import time | ||||||
|  | from collections import Counter | ||||||
|  | from datetime import timedelta | ||||||
|  | from typing import Dict, List | ||||||
|  |  | ||||||
|  | from django.db.models import Count, ExpressionWrapper, F | ||||||
|  | from django.db.models.fields import DurationField | ||||||
|  | from django.db.models.functions import ExtractHour | ||||||
|  | from django.http import response | ||||||
|  | from django.utils.timezone import now | ||||||
|  | from drf_yasg2.utils import swagger_auto_schema | ||||||
|  | from rest_framework.fields import SerializerMethodField | ||||||
|  | from rest_framework.permissions import IsAdminUser | ||||||
|  | from rest_framework.request import Request | ||||||
|  | from rest_framework.response import Response | ||||||
|  | from rest_framework.serializers import Serializer | ||||||
|  | from rest_framework.viewsets import ViewSet | ||||||
|  |  | ||||||
|  | from passbook.audit.models import Event, EventAction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdministrationMetricsSerializer(Serializer): | ||||||
|  |     """Overview View""" | ||||||
|  |  | ||||||
|  |     logins_per_1h = SerializerMethodField() | ||||||
|  |     logins_failed_per_1h = SerializerMethodField() | ||||||
|  |  | ||||||
|  |     def get_events_per_1h(self, action: str) -> List[Dict[str, int]]: | ||||||
|  |         """Get event count by hour in the last day, fill with zeros""" | ||||||
|  |         date_from = now() - timedelta(days=1) | ||||||
|  |         result = ( | ||||||
|  |             Event.objects.filter(action=action, created__gte=date_from) | ||||||
|  |             .annotate( | ||||||
|  |                 age=ExpressionWrapper( | ||||||
|  |                     now() - F("created"), output_field=DurationField() | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             .annotate(age_hours=ExtractHour("age")) | ||||||
|  |             .values("age_hours") | ||||||
|  |             .annotate(count=Count("pk")) | ||||||
|  |             .order_by("age_hours") | ||||||
|  |         ) | ||||||
|  |         data = Counter({d["age_hours"]: d["count"] for d in result}) | ||||||
|  |         results = [] | ||||||
|  |         _now = now() | ||||||
|  |         for hour in range(0, -24, -1): | ||||||
|  |             results.append( | ||||||
|  |                 { | ||||||
|  |                     "x": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000, | ||||||
|  |                     "y": data[hour * -1], | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |         return results | ||||||
|  |  | ||||||
|  |     def get_logins_per_1h(self, _): | ||||||
|  |         """Get successful logins per hour for the last 24 hours""" | ||||||
|  |         return self.get_events_per_1h(EventAction.LOGIN) | ||||||
|  |  | ||||||
|  |     def get_logins_failed_per_1h(self, _): | ||||||
|  |         """Get failed logins per hour for the last 24 hours""" | ||||||
|  |         return self.get_events_per_1h(EventAction.LOGIN_FAILED) | ||||||
|  |  | ||||||
|  |     def create(self, request: Request) -> response: | ||||||
|  |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |     def update(self, request: Request) -> Response: | ||||||
|  |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdministrationMetricsViewSet(ViewSet): | ||||||
|  |     """Return single instance of AdministrationMetricsSerializer""" | ||||||
|  |  | ||||||
|  |     permission_classes = [IsAdminUser] | ||||||
|  |  | ||||||
|  |     @swagger_auto_schema(responses={200: AdministrationMetricsSerializer(many=True)}) | ||||||
|  |     def list(self, request: Request) -> Response: | ||||||
|  |         """Return single instance of AdministrationMetricsSerializer""" | ||||||
|  |         serializer = AdministrationMetricsSerializer(True) | ||||||
|  |         return Response(serializer.data) | ||||||
							
								
								
									
										72
									
								
								passbook/admin/api/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,72 @@ | |||||||
|  | """Tasks API""" | ||||||
|  | from importlib import import_module | ||||||
|  |  | ||||||
|  | from django.contrib import messages | ||||||
|  | from django.http.response import Http404 | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  | from drf_yasg2.utils import swagger_auto_schema | ||||||
|  | from rest_framework.decorators import action | ||||||
|  | from rest_framework.fields import CharField, DateTimeField, IntegerField, ListField | ||||||
|  | from rest_framework.permissions import IsAdminUser | ||||||
|  | from rest_framework.request import Request | ||||||
|  | from rest_framework.response import Response | ||||||
|  | from rest_framework.serializers import Serializer | ||||||
|  | from rest_framework.viewsets import ViewSet | ||||||
|  |  | ||||||
|  | from passbook.lib.tasks import TaskInfo | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TaskSerializer(Serializer): | ||||||
|  |     """Serialize TaskInfo and TaskResult""" | ||||||
|  |  | ||||||
|  |     task_name = CharField() | ||||||
|  |     task_description = CharField() | ||||||
|  |     task_finish_timestamp = DateTimeField(source="finish_timestamp") | ||||||
|  |  | ||||||
|  |     status = IntegerField(source="result.status.value") | ||||||
|  |     messages = ListField(source="result.messages") | ||||||
|  |  | ||||||
|  |     def create(self, request: Request) -> Response: | ||||||
|  |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |     def update(self, request: Request) -> Response: | ||||||
|  |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TaskViewSet(ViewSet): | ||||||
|  |     """Read-only view set that returns all background tasks""" | ||||||
|  |  | ||||||
|  |     permission_classes = [IsAdminUser] | ||||||
|  |  | ||||||
|  |     @swagger_auto_schema(responses={200: TaskSerializer(many=True)}) | ||||||
|  |     def list(self, request: Request) -> Response: | ||||||
|  |         """List current messages and pass into Serializer""" | ||||||
|  |         return Response(TaskSerializer(TaskInfo.all().values(), many=True).data) | ||||||
|  |  | ||||||
|  |     @action(detail=True, methods=["post"]) | ||||||
|  |     # pylint: disable=invalid-name | ||||||
|  |     def retry(self, request: Request, pk=None) -> Response: | ||||||
|  |         """Retry task""" | ||||||
|  |         task = TaskInfo.by_name(pk) | ||||||
|  |         if not task: | ||||||
|  |             raise Http404 | ||||||
|  |         try: | ||||||
|  |             task_module = import_module(task.task_call_module) | ||||||
|  |             task_func = getattr(task_module, task.task_call_func) | ||||||
|  |             task_func.delay(*task.task_call_args, **task.task_call_kwargs) | ||||||
|  |             messages.success( | ||||||
|  |                 self.request, | ||||||
|  |                 _( | ||||||
|  |                     "Successfully re-scheduled Task %(name)s!" | ||||||
|  |                     % {"name": task.task_name} | ||||||
|  |                 ), | ||||||
|  |             ) | ||||||
|  |             return Response( | ||||||
|  |                 { | ||||||
|  |                     "successful": True, | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |         except ImportError: | ||||||
|  |             # if we get an import error, the module path has probably changed | ||||||
|  |             task.delete() | ||||||
|  |             return Response({"successful": False}) | ||||||
| @ -1,9 +1,35 @@ | |||||||
| """YAML fields""" | """Additional fields""" | ||||||
| import yaml | import yaml | ||||||
| from django import forms | from django import forms | ||||||
|  | from django.utils.datastructures import MultiValueDict | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ArrayFieldSelectMultiple(forms.SelectMultiple): | ||||||
|  |     """This is a Form Widget for use with a Postgres ArrayField. It implements | ||||||
|  |     a multi-select interface that can be given a set of `choices`. | ||||||
|  |     You can provide a `delimiter` keyword argument to specify the delimeter used. | ||||||
|  |  | ||||||
|  |     https://gist.github.com/stephane/00e73c0002de52b1c601""" | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         # Accept a `delimiter` argument, and grab it (defaulting to a comma) | ||||||
|  |         self.delimiter = kwargs.pop("delimiter", ",") | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  |  | ||||||
|  |     def value_from_datadict(self, data, files, name): | ||||||
|  |         if isinstance(data, MultiValueDict): | ||||||
|  |             # Normally, we'd want a list here, which is what we get from the | ||||||
|  |             # SelectMultiple superclass, but the SimpleArrayField expects to | ||||||
|  |             # get a delimited string, so we're doing a little extra work. | ||||||
|  |             return self.delimiter.join(data.getlist(name)) | ||||||
|  |  | ||||||
|  |         return data.get(name) | ||||||
|  |  | ||||||
|  |     def get_context(self, name, value, attrs): | ||||||
|  |         return super().get_context(name, value.split(self.delimiter), attrs) | ||||||
|  |  | ||||||
|  |  | ||||||
| class CodeMirrorWidget(forms.Textarea): | class CodeMirrorWidget(forms.Textarea): | ||||||
|     """Custom Textarea-based Widget that triggers a CodeMirror editor""" |     """Custom Textarea-based Widget that triggers a CodeMirror editor""" | ||||||
|  |  | ||||||
| @ -49,7 +75,9 @@ class YAMLField(forms.JSONField): | |||||||
|             converted = yaml.safe_load(value) |             converted = yaml.safe_load(value) | ||||||
|         except yaml.YAMLError: |         except yaml.YAMLError: | ||||||
|             raise forms.ValidationError( |             raise forms.ValidationError( | ||||||
|                 self.error_messages["invalid"], code="invalid", params={"value": value}, |                 self.error_messages["invalid"], | ||||||
|  |                 code="invalid", | ||||||
|  |                 params={"value": value}, | ||||||
|             ) |             ) | ||||||
|         if isinstance(converted, str): |         if isinstance(converted, str): | ||||||
|             return YAMLString(converted) |             return YAMLString(converted) | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ from django.core.cache import cache | |||||||
| from requests import RequestException, get | from requests import RequestException, get | ||||||
| from structlog import get_logger | from structlog import get_logger | ||||||
|  |  | ||||||
|  | from passbook.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||||
| from passbook.root.celery import CELERY_APP | from passbook.root.celery import CELERY_APP | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| @ -10,8 +11,8 @@ VERSION_CACHE_KEY = "passbook_latest_version" | |||||||
| VERSION_CACHE_TIMEOUT = 2 * 60 * 60  # 2 hours | VERSION_CACHE_TIMEOUT = 2 * 60 * 60  # 2 hours | ||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task() | @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||||
| def update_latest_version(): | def update_latest_version(self: MonitoredTask): | ||||||
|     """Update latest version info""" |     """Update latest version info""" | ||||||
|     try: |     try: | ||||||
|         data = get( |         data = get( | ||||||
| @ -19,5 +20,11 @@ def update_latest_version(): | |||||||
|         ).json() |         ).json() | ||||||
|         tag_name = data.get("tag_name") |         tag_name = data.get("tag_name") | ||||||
|         cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], VERSION_CACHE_TIMEOUT) |         cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], VERSION_CACHE_TIMEOUT) | ||||||
|     except (RequestException, IndexError): |         self.set_status( | ||||||
|  |             TaskResult( | ||||||
|  |                 TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"] | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     except (RequestException, IndexError) as exc: | ||||||
|         cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT) |         cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT) | ||||||
|  |         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                     <a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|                 </div> |                 </div> | ||||||
| @ -62,18 +63,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon pf-icon-applications pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Applications.' %} |                     {% trans 'No Applications.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any application." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no applications exist. Click the button below to create one.' %} |                     {% trans 'Currently no applications exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                 <a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|             </div> |             </div> | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ | |||||||
|                 <li class="pf-c-nav__item"> |                 <li class="pf-c-nav__item"> | ||||||
|                     <a href="{% url 'passbook_admin:overview' %}" |                     <a href="{% url 'passbook_admin:overview' %}" | ||||||
|                         class="pf-c-nav__link {% is_active 'passbook_admin:overview' %}"> |                         class="pf-c-nav__link {% is_active 'passbook_admin:overview' %}"> | ||||||
|                         {% trans 'System Status' %} |                         {% trans 'Overview' %} | ||||||
|                     </a> |                     </a> | ||||||
|                 </li> |                 </li> | ||||||
|                 <li class="pf-c-nav__item"> |                 <li class="pf-c-nav__item"> | ||||||
| @ -58,7 +58,7 @@ | |||||||
|                         {% trans 'Property Mappings' %} |                         {% trans 'Property Mappings' %} | ||||||
|                     </a> |                     </a> | ||||||
|                 </li> |                 </li> | ||||||
|                 <li class="pf-c-nav__item pf-m-expandable pf-m-expanded"> |                 <li class="pf-c-nav__item pf-m-expanded"> | ||||||
|                     <a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Flows' %} |                     <a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Flows' %} | ||||||
|                         <span class="pf-c-nav__toggle"> |                         <span class="pf-c-nav__toggle"> | ||||||
|                             <i class="fas fa-angle-right" aria-hidden="true"></i> |                             <i class="fas fa-angle-right" aria-hidden="true"></i> | ||||||
| @ -99,7 +99,7 @@ | |||||||
|                         </ul> |                         </ul> | ||||||
|                     </section> |                     </section> | ||||||
|                 </li> |                 </li> | ||||||
|                 <li class="pf-c-nav__item pf-m-expandable pf-m-expanded"> |                 <li class="pf-c-nav__item pf-m-expanded"> | ||||||
|                     <a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Policies' %} |                     <a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Policies' %} | ||||||
|                         <span class="pf-c-nav__toggle"> |                         <span class="pf-c-nav__toggle"> | ||||||
|                             <i class="fas fa-angle-right" aria-hidden="true"></i> |                             <i class="fas fa-angle-right" aria-hidden="true"></i> | ||||||
| @ -146,6 +146,12 @@ | |||||||
|                         {% trans 'Groups' %} |                         {% trans 'Groups' %} | ||||||
|                     </a> |                     </a> | ||||||
|                 </li> |                 </li> | ||||||
|  |                 <li class="pf-c-nav__item"> | ||||||
|  |                     <a href="{% url 'passbook_admin:tasks' %}" | ||||||
|  |                         class="pf-c-nav__link {% is_active 'passbook_admin:tasks' %}"> | ||||||
|  |                         {% trans 'System Tasks' %} | ||||||
|  |                     </a> | ||||||
|  |                 </li> | ||||||
|             </ul> |             </ul> | ||||||
|         </nav> |         </nav> | ||||||
|     </div> |     </div> | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                     <a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|                 </div> |                 </div> | ||||||
| @ -64,18 +65,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon pf-icon-key pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Certificates.' %} |                     {% trans 'No Certificates.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any certificates." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no certificates exist. Click the button below to create one.' %} |                     {% trans 'Currently no certificates exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                 <a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|             </div> |             </div> | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                     <a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|                     <a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-secondary" type="button">{% trans 'Import' %}</a> |                     <a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-secondary" type="button">{% trans 'Import' %}</a> | ||||||
| @ -69,18 +70,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon pf-icon-process-automation pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Flows.' %} |                     {% trans 'No Flows.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any flows." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no flows exist. Click the button below to create one.' %} |                     {% trans 'Currently no flows exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                 <a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|                 <a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Import' %}</a> |                 <a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Import' %}</a> | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}" |                     <a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}" | ||||||
|                         class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                         class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
| @ -61,18 +62,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon pf-icon-users pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Groups.' %} |                     {% trans 'No Groups.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any groups." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no group exist. Click the button below to create one.' %} |                     {% trans 'Currently no group exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                 <a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|             </div> |             </div> | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <a href="{% url 'passbook_admin:outpost-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                     <a href="{% url 'passbook_admin:outpost-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|                 </div> |                 </div> | ||||||
| @ -47,30 +48,41 @@ | |||||||
|                             {{ outpost.providers.all.select_subclasses|join:", " }} |                             {{ outpost.providers.all.select_subclasses|join:", " }} | ||||||
|                         </span> |                         </span> | ||||||
|                     </td> |                     </td> | ||||||
|  |                     {% with states=outpost.state %} | ||||||
|  |                     {% if states|length > 0 %} | ||||||
|                         <td role="cell"> |                         <td role="cell"> | ||||||
|                         {% with health=outpost.deployment_health %} |                             {% for state in states %} | ||||||
|                         {% if health %} |                             <div> | ||||||
|                             <i class="fas fa-check pf-m-success"></i> {{ health|naturaltime }} |                                 {% if state.last_seen %} | ||||||
|  |                                 <i class="fas fa-check pf-m-success"></i> {{ state.last_seen|naturaltime }} | ||||||
|                                 {% else %} |                                 {% else %} | ||||||
|                             <i class="fas fa-times pf-m-danger"></i> Unhealthy |                                 <i class="fas fa-times pf-m-danger"></i> {% trans 'Unhealthy' %} | ||||||
|                                 {% endif %} |                                 {% endif %} | ||||||
|                         {% endwith %} |                             </div> | ||||||
|  |                             {% endfor %} | ||||||
|                         </td> |                         </td> | ||||||
|                         <td role="cell"> |                         <td role="cell"> | ||||||
|                         <span> |                             {% for state in states %} | ||||||
|                             {% with ver=outpost.deployment_version %} |                                 <div> | ||||||
|                             {% if ver.outdated %} |                                     {% if not state.version %} | ||||||
|                                 {% if ver.version == "" %} |                                     <i class="fas fa-question-circle"></i> | ||||||
|                                 <i class="fas fa-times pf-m-danger"></i> - |                                     {% elif state.version_outdated %} | ||||||
|  |                                     <i class="fas fa-times pf-m-danger"></i> {% blocktrans with is=state.version should=state.version_should %}{{ is }}, should be {{ should }}{% endblocktrans %} | ||||||
|                                     {% else %} |                                     {% else %} | ||||||
|                                 <i class="fas fa-times pf-m-danger"></i> {% blocktrans with is=ver.version should=ver.should %}{{ is }}, should be {{ should }}{% endblocktrans %} |                                     <i class="fas fa-check pf-m-success"></i> {{ state.version }} | ||||||
|                                     {% endif %} |                                     {% endif %} | ||||||
|  |                                 </div> | ||||||
|  |                             {% endfor %} | ||||||
|  |                         </td> | ||||||
|                     {% else %} |                     {% else %} | ||||||
|                             <i class="fas fa-check pf-m-success"></i> {{ ver.version }} |                         <td role="cell"> | ||||||
|  |                             <i class="fas fa-question-circle"></i> | ||||||
|  |                         </td> | ||||||
|  |                         <td role="cell"> | ||||||
|  |                             <i class="fas fa-question-circle"></i> | ||||||
|  |                         </td> | ||||||
|                     {% endif %} |                     {% endif %} | ||||||
|                     {% endwith %} |                     {% endwith %} | ||||||
|                         </span> |  | ||||||
|                     </td> |  | ||||||
|                     <td> |                     <td> | ||||||
|                         <a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-update' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> |                         <a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-update' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> | ||||||
|                         <a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-delete' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> |                         <a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-delete' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> | ||||||
| @ -83,18 +95,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="fas fa-map-marker pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Outposts.' %} |                     {% trans 'No Outposts.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any outposts." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no outposts exist. Click the button below to create one.' %} |                     {% trans 'Currently no outposts exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <a href="{% url 'passbook_admin:outpost-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                 <a href="{% url 'passbook_admin:outpost-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|             </div> |             </div> | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| {% extends "administration/base.html" %} | {% extends "administration/base.html" %} | ||||||
|  |  | ||||||
| {% load i18n %} | {% load i18n %} | ||||||
|  | {% load static %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| <section class="pf-c-page__main-section pf-m-light"> | <section class="pf-c-page__main-section pf-m-light"> | ||||||
| @ -10,139 +11,123 @@ | |||||||
| </section> | </section> | ||||||
| <section class="pf-c-page__main-section"> | <section class="pf-c-page__main-section"> | ||||||
|     <div class="pf-l-gallery pf-m-gutter"> |     <div class="pf-l-gallery pf-m-gutter"> | ||||||
|         <a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact"> |         <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 3;grid-row-end: span 2;"> | ||||||
|             <div class="pf-c-card__header"> |             <div class="pf-c-card__header"> | ||||||
|                 <div class="pf-c-card__header-main"> |                 <div class="pf-c-card__header-main"> | ||||||
|                     <i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %} |                     <i class="pf-icon pf-icon-server"></i> {% trans 'Logins over the last 24 hours' %} | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |             <div class="pf-c-card__body" style="position: relative; height:100%; width:100%"> | ||||||
|  |                 <canvas id="logins-last-metrics"></canvas> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 2;grid-row-end: span 3;"> | ||||||
|  |             <div class="pf-c-card__header"> | ||||||
|  |                 <div class="pf-c-card__header-main"> | ||||||
|  |                     <i class="pf-icon pf-icon-server"></i> {% trans 'Apps with most usage' %} | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pf-c-card__body"> |             <div class="pf-c-card__body"> | ||||||
|                 <p class="aggregate-status"> |                 <table class="pf-c-table pf-m-compact" role="grid"> | ||||||
|                     <i class="fa fa-check-circle"></i> {{ application_count }} |                     <thead> | ||||||
|                 </p> |                         <tr role="row"> | ||||||
|  |                             <th role="columnheader" scope="col">{% trans 'Application' %}</th> | ||||||
|  |                             <th role="columnheader" scope="col">{% trans 'Logins' %}</th> | ||||||
|  |                             <th role="columnheader" scope="col"></th> | ||||||
|  |                         </tr> | ||||||
|  |                     </thead> | ||||||
|  |                     <tbody role="rowgroup"> | ||||||
|  |                         {% for app in most_used_applications %} | ||||||
|  |                         <tr role="row"> | ||||||
|  |                             <td role="cell"> | ||||||
|  |                                 {{ app.application.name }} | ||||||
|  |                             </td> | ||||||
|  |                             <td role="cell"> | ||||||
|  |                                 {{ app.total_logins }} | ||||||
|  |                             </td> | ||||||
|  |                             <td role="cell"> | ||||||
|  |                                 <progress value="{{ app.total_logins }}" max="{{ most_used_applications.0.total_logins }}"></progress> | ||||||
|  |                             </td> | ||||||
|  |                         </tr> | ||||||
|  |                         {% endfor %} | ||||||
|  |                     </tbody> | ||||||
|  |                 </table> | ||||||
|  |             </div> | ||||||
|         </div> |         </div> | ||||||
|         </a> |  | ||||||
|  |  | ||||||
|         <a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact"> |         <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact"> | ||||||
|             <div class="pf-c-card__header"> |             <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between"> | ||||||
|                 <div class="pf-c-card__header-main"> |  | ||||||
|                     <i class="pf-icon pf-icon-middleware"></i> {% trans 'Sources' %} |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="pf-c-card__body"> |  | ||||||
|                 <p class="aggregate-status"> |  | ||||||
|                     <i class="fa fa-check-circle"></i> {{ source_count }} |  | ||||||
|                 </p> |  | ||||||
|             </div> |  | ||||||
|         </a> |  | ||||||
|  |  | ||||||
|         <a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact"> |  | ||||||
|             <div class="pf-c-card__header"> |  | ||||||
|                 <div class="pf-c-card__header-main"> |                 <div class="pf-c-card__header-main"> | ||||||
|                     <i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %} |                     <i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %} | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <a href="{% url 'passbook_admin:providers' %}"> | ||||||
|  |                     <i class="fa fa-external-link-alt"> </i> | ||||||
|  |                 </a> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pf-c-card__body"> |             <div class="pf-c-card__body"> | ||||||
|                 {% if providers_without_application.exists %} |                 {% if providers_without_application.exists %} | ||||||
|                 <p class="aggregate-status"> |                 <p class="pb-aggregate-card"> | ||||||
|                     <i class="fa fa-exclamation-triangle"></i> {{ provider_count }} |                     <i class="fa fa-exclamation-triangle"></i> {{ provider_count }} | ||||||
|                 </p> |                 </p> | ||||||
|                 <p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p> |                 <p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p> | ||||||
|                 {% else %} |                 {% else %} | ||||||
|                 <p class="aggregate-status"> |                 <p class="pb-aggregate-card"> | ||||||
|                     <i class="fa fa-check-circle"></i> {{ provider_count }} |                     <i class="fa fa-check-circle"></i> {{ provider_count }} | ||||||
|                 </p> |                 </p> | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
|             </div> |             </div> | ||||||
|         </a> |         </div> | ||||||
|  |  | ||||||
|         <a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact"> |         <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact"> | ||||||
|             <div class="pf-c-card__header"> |             <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between"> | ||||||
|                 <div class="pf-c-card__header-main"> |  | ||||||
|                     <i class="pf-icon pf-icon-plugged"></i> {% trans 'Stages' %} |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="pf-c-card__body"> |  | ||||||
|                 {% if stage_count < 1 %} |  | ||||||
|                 <p class="aggregate-status"> |  | ||||||
|                     <i class="pficon-error-circle-o"></i> {{ stage_count }} |  | ||||||
|                 </p> |  | ||||||
|                 <p>{% trans 'No Stages configured. No Users will be able to login.' %}"></p> |  | ||||||
|                 {% else %} |  | ||||||
|                 <p class="aggregate-status"> |  | ||||||
|                     <i class="fa fa-check-circle"></i> {{ stage_count }} |  | ||||||
|                 </p> |  | ||||||
|                 {% endif %} |  | ||||||
|             </div> |  | ||||||
|         </a> |  | ||||||
|  |  | ||||||
|         <a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact"> |  | ||||||
|             <div class="pf-c-card__header"> |  | ||||||
|                 <div class="pf-c-card__header-main"> |  | ||||||
|                     <i class="pf-icon pf-icon-topology"></i> {% trans 'Flows' %} |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="pf-c-card__body"> |  | ||||||
|                 <p class="aggregate-status"> |  | ||||||
|                     <i class="fa fa-check-circle"></i> {{ flow_count }} |  | ||||||
|                 </p> |  | ||||||
|             </div> |  | ||||||
|         </a> |  | ||||||
|  |  | ||||||
|         <a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact"> |  | ||||||
|             <div class="pf-c-card__header"> |  | ||||||
|                 <div class="pf-c-card__header-main"> |                 <div class="pf-c-card__header-main"> | ||||||
|                     <i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %} |                     <i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %} | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <a href="{% url 'passbook_admin:policies' %}"> | ||||||
|  |                     <i class="fa fa-external-link-alt"> </i> | ||||||
|  |                 </a> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pf-c-card__body"> |             <div class="pf-c-card__body"> | ||||||
|                 {% if policies_without_binding %} |                 {% if policies_without_binding %} | ||||||
|                 <p class="aggregate-status"> |                 <p class="pb-aggregate-card"> | ||||||
|                     <i class="fa fa-exclamation-triangle"></i> {{ policy_count }} |                     <i class="fa fa-exclamation-triangle"></i> {{ policy_count }} | ||||||
|                 </p> |                 </p> | ||||||
|                 <p>{% trans 'Policies without binding exist.' %}</p> |                 <p>{% trans 'Policies without binding exist.' %}</p> | ||||||
|                 {% else %} |                 {% else %} | ||||||
|                 <p class="aggregate-status"> |                 <p class="pb-aggregate-card"> | ||||||
|                     <i class="fa fa-check-circle"></i> {{ policy_count }} |                     <i class="fa fa-check-circle"></i> {{ policy_count }} | ||||||
|                 </p> |                 </p> | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
|             </div> |             </div> | ||||||
|         </a> |         </div> | ||||||
|  |  | ||||||
|         <a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact"> |         <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact"> | ||||||
|             <div class="pf-c-card__header"> |             <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between"> | ||||||
|                 <div class="pf-c-card__header-main"> |  | ||||||
|                     <i class="pf-icon pf-icon-migration"></i> {% trans 'Invitation' %} |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="pf-c-card__body"> |  | ||||||
|                 <p class="aggregate-status"> |  | ||||||
|                     <i class="fa fa-check-circle"></i> {{ invitation_count }} |  | ||||||
|                 </p> |  | ||||||
|             </div> |  | ||||||
|         </a> |  | ||||||
|  |  | ||||||
|         <a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact"> |  | ||||||
|             <div class="pf-c-card__header"> |  | ||||||
|                 <div class="pf-c-card__header-main"> |                 <div class="pf-c-card__header-main"> | ||||||
|                     <i class="pf-icon pf-icon-user"></i> {% trans 'Users' %} |                     <i class="pf-icon pf-icon-user"></i> {% trans 'Users' %} | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <a href="{% url 'passbook_admin:users' %}"> | ||||||
|  |                     <i class="fa fa-external-link-alt"> </i> | ||||||
|  |                 </a> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pf-c-card__body"> |             <div class="pf-c-card__body"> | ||||||
|                 <p class="aggregate-status"> |                 <p class="pb-aggregate-card"> | ||||||
|                     <i class="fa fa-check-circle"></i> {{ user_count }} |                     <i class="fa fa-check-circle"></i> {{ user_count }} | ||||||
|                 </p> |                 </p> | ||||||
|             </div> |             </div> | ||||||
|         </a> |         </div> | ||||||
|  |  | ||||||
|         <div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact"> |         <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact"> | ||||||
|             <div class="pf-c-card__header"> |             <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between"> | ||||||
|                 <div class="pf-c-card__header-main"> |                 <div class="pf-c-card__header-main"> | ||||||
|                     <i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %} |                     <i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %} | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <a href="https://github.com/BeryJu/passbook/releases" target="_blank"> | ||||||
|  |                     <i class="fa fa-external-link-alt"> </i> | ||||||
|  |                 </a> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pf-c-card__body"> |             <div class="pf-c-card__body"> | ||||||
|                 <p class="aggregate-status"> |                 <p class="pb-aggregate-card"> | ||||||
|                     {% if version >= version_latest %} |                     {% if version >= version_latest %} | ||||||
|                     <i class="fa fa-check-circle"></i> {{ version }} |                     <i class="fa fa-check-circle"></i> {{ version }} | ||||||
|                     {% else %} |                     {% else %} | ||||||
| @ -161,97 +146,192 @@ | |||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact"> |         <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact"> | ||||||
|             <div class="pf-c-card__header"> |             <div class="pf-c-card__header"> | ||||||
|                 <div class="pf-c-card__header-main"> |                 <div class="pf-c-card__header-main"> | ||||||
|                     <i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %} |                     <i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %} | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pf-c-card__body"> |             <fetch-fill-slot class="pf-c-card__body" url="{% url 'passbook_api:admin_overview-list' %}" key="worker_count"> | ||||||
|                 {% if worker_count < 1 %} |                 <div slot="value < 1"> | ||||||
|                 <p class="aggregate-status"> |                     <p class="pb-aggregate-card"> | ||||||
|                     <i class="fa fa-exclamation-triangle"></i> {{ worker_count }} |                         <i class="fa fa-exclamation-triangle"></i> <span data-value></span> | ||||||
|                     </p> |                     </p> | ||||||
|                     <p>{% trans 'No workers connected.' %}</p> |                     <p>{% trans 'No workers connected.' %}</p> | ||||||
|                 {% else %} |  | ||||||
|                 <p class="aggregate-status"> |  | ||||||
|                     <i class="fa fa-check-circle"></i> {{ worker_count }} |  | ||||||
|                 </p> |  | ||||||
|                 {% endif %} |  | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <div slot="value >= 1"> | ||||||
|  |                     <p class="pb-aggregate-card"> | ||||||
|  |                         <i class="fa fa-check-circle"></i> <span data-value></span> | ||||||
|  |                     </p> | ||||||
|  |                 </div> | ||||||
|  |                 <div> | ||||||
|  |                     <span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading..."> | ||||||
|  |                         <span class="pf-c-spinner__clipper"></span> | ||||||
|  |                         <span class="pf-c-spinner__lead-ball"></span> | ||||||
|  |                         <span class="pf-c-spinner__tail-ball"></span> | ||||||
|  |                     </span> | ||||||
|  |                 </div> | ||||||
|  |             </fetch-fill-slot> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <a class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot"> |         <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact"> | ||||||
|             <div class="pf-c-card__header"> |             <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between"> | ||||||
|                 <div class="pf-c-card__header-main"> |                 <div class="pf-c-card__header-main"> | ||||||
|                     <i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %} |                     <i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %} | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <a data-target="modal" data-modal="clearPolicyCache"> | ||||||
|  |                     <i class="fa fa-trash"> </i> | ||||||
|  |                 </a> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pf-c-card__body"> |             <div class="pf-c-card__body"> | ||||||
|                 {% if cached_policies < 1 %} |                 {% if cached_policies < 1 %} | ||||||
|                 <p class="aggregate-status"> |                 <p class="pb-aggregate-card"> | ||||||
|                     <i class="fa fa-exclamation-triangle"></i> {{ cached_policies }} |                     <i class="fa fa-exclamation-triangle"></i> {{ cached_policies }} | ||||||
|                 </p> |                 </p> | ||||||
|                 <p>{% trans 'No policies cached. Users may experience slow response times.' %}</p> |                 <p>{% trans 'No policies cached. Users may experience slow response times.' %}</p> | ||||||
|                 {% else %} |                 {% else %} | ||||||
|                 <p class="aggregate-status"> |                 <p class="pb-aggregate-card"> | ||||||
|                     <i class="fa fa-check-circle"></i> {{ cached_policies }} |                     <i class="fa fa-check-circle"></i> {{ cached_policies }} | ||||||
|                 </p> |                 </p> | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
|             </div> |             </div> | ||||||
|         </a> |         </div> | ||||||
|  |  | ||||||
|         <div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact"> |         <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact"> | ||||||
|             <div class="pf-c-card__header"> |             <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between"> | ||||||
|                 <div class="pf-c-card__header-main"> |                 <div class="pf-c-card__header-main"> | ||||||
|                     <i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %} |                     <i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %} | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <a data-target="modal" data-modal="clearFlowCache"> | ||||||
|  |                     <i class="fa fa-trash"> </i> | ||||||
|  |                 </a> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pf-c-card__body"> |             <div class="pf-c-card__body"> | ||||||
|                 {% if cached_flows < 1 %} |                 {% if cached_flows < 1 %} | ||||||
|                 <p class="aggregate-status"> |                 <p class="pb-aggregate-card"> | ||||||
|                     <span class="fa fa-exclamation-triangle"></span> {{ cached_flows }} |                     <span class="fa fa-exclamation-triangle"></span> {{ cached_flows }} | ||||||
|                 </p> |                 </p> | ||||||
|                 <p>{% trans 'No flows cached.' %}</p> |                 <p>{% trans 'No flows cached.' %}</p> | ||||||
|                 {% else %} |                 {% else %} | ||||||
|                 <p class="aggregate-status"> |                 <p class="pb-aggregate-card"> | ||||||
|                     <i class="fa fa-check-circle"></i> {{ cached_flows }} |                     <i class="fa fa-check-circle"></i> {{ cached_flows }} | ||||||
|                 </p> |                 </p> | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|     </section> |  | ||||||
|     </div> |     </div> | ||||||
| <div class="pf-c-backdrop" id="clearCacheModalRoot" hidden> | </section> | ||||||
|  |  | ||||||
|  | <div class="pf-c-backdrop" id="clearPolicyCache" hidden> | ||||||
|     <div class="pf-l-bullseye"> |     <div class="pf-l-bullseye"> | ||||||
|         <div class="pf-c-modal-box pf-m-sm" role="dialog"> |         <div class="pf-c-modal-box pf-m-sm" role="dialog"> | ||||||
|             <button data-modal-close class="pf-c-button pf-m-plain" type="button" aria-label="Close dialog"> |             <button data-modal-close class="pf-c-button pf-m-plain" type="button" aria-label="Close dialog"> | ||||||
|                 <i class="fas fa-times" aria-hidden="true"></i> |                 <i class="fas fa-times" aria-hidden="true"></i> | ||||||
|             </button> |             </button> | ||||||
|             <div class="pf-c-modal-box__header"> |             <div class="pf-c-modal-box__header"> | ||||||
|                 <h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1> |                 <h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Policy Cache' %}?</h1> | ||||||
|             </div> |             </div> | ||||||
|             <div class="pf-c-modal-box__body" id="modal-description"> |             <div class="pf-c-modal-box__body" id="modal-description"> | ||||||
|                 <form method="post" id="clearForm"> |                 <form method="post" id="clear_policies"> | ||||||
|                     {% csrf_token %} |                     {% csrf_token %} | ||||||
|                     <input type="hidden" name="clear"> |                     <input type="hidden" name="clear_policies"> | ||||||
|                     <p> |                     <p> | ||||||
|                         {% blocktrans %} |                         {% blocktrans %} | ||||||
|                         Are you sure you want to clear the cache? This includes all user sessions and all cached Policy results. |                         Are you sure you want to clear the policy cache? This will cause all policies to be re-evaluated on their next usage. | ||||||
|                         {% endblocktrans %} |                         {% endblocktrans %} | ||||||
|                     </p> |                     </p> | ||||||
|                     <h3> |  | ||||||
|                         {% blocktrans %} |  | ||||||
|                         This will also log you out. |  | ||||||
|                         {% endblocktrans %} |  | ||||||
|                     </h3> |  | ||||||
|                 </form> |                 </form> | ||||||
|             </div> |             </div> | ||||||
|             <footer class="pf-c-modal-box__footer pf-m-align-left"> |             <footer class="pf-c-modal-box__footer pf-m-align-left"> | ||||||
|                 <button form="clearForm" class="pf-c-button pf-m-primary" type="submit">{% trans 'Clear' %}</button> |                 <button form="clear_policies" class="pf-c-button pf-m-primary" type="submit">{% trans 'Clear' %}</button> | ||||||
|                 <button data-modal-close class="pf-c-button pf-m-link" type="button">{% trans 'Cancel' %}</button> |                 <button data-modal-close class="pf-c-button pf-m-link" type="button">{% trans 'Cancel' %}</button> | ||||||
|             </footer> |             </footer> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | <div class="pf-c-backdrop" id="clearFlowCache" hidden> | ||||||
|  |     <div class="pf-l-bullseye"> | ||||||
|  |         <div class="pf-c-modal-box pf-m-sm" role="dialog"> | ||||||
|  |             <button data-modal-close class="pf-c-button pf-m-plain" type="button" aria-label="Close dialog"> | ||||||
|  |                 <i class="fas fa-times" aria-hidden="true"></i> | ||||||
|  |             </button> | ||||||
|  |             <div class="pf-c-modal-box__header"> | ||||||
|  |                 <h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Flow Cache' %}?</h1> | ||||||
|  |             </div> | ||||||
|  |             <div class="pf-c-modal-box__body" id="modal-description"> | ||||||
|  |                 <form method="post" id="clear_flows"> | ||||||
|  |                     {% csrf_token %} | ||||||
|  |                     <input type="hidden" name="clear_flows"> | ||||||
|  |                     <p> | ||||||
|  |                         {% blocktrans %} | ||||||
|  |                         Are you sure you want to clear the flow cache? This will cause all flows to be re-evaluated on their next usage. | ||||||
|  |                         {% endblocktrans %} | ||||||
|  |                     </p> | ||||||
|  |                 </form> | ||||||
|  |             </div> | ||||||
|  |             <footer class="pf-c-modal-box__footer pf-m-align-left"> | ||||||
|  |                 <button form="clear_flows" class="pf-c-button pf-m-primary" type="submit">{% trans 'Clear' %}</button> | ||||||
|  |                 <button data-modal-close class="pf-c-button pf-m-link" type="button">{% trans 'Cancel' %}</button> | ||||||
|  |             </footer> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | <script src="{% static 'node_modules/chart.js/dist/Chart.bundle.min.js' %}"></script> | ||||||
|  | <script> | ||||||
|  | var ctx = document.getElementById('logins-last-metrics').getContext('2d'); | ||||||
|  | fetch("{% url 'passbook_api:admin_metrics-list' %}").then(r => r.json()).then(r => { | ||||||
|  |     var myChart = new Chart(ctx, { | ||||||
|  |         type: 'bar', | ||||||
|  |         data: { | ||||||
|  |             datasets: [ | ||||||
|  |                 { | ||||||
|  |                     label: 'Failed Logins', | ||||||
|  |                     backgroundColor: "rgba(201, 25, 11, .5)", | ||||||
|  |                     spanGaps: true, | ||||||
|  |                     data: r.logins_failed_per_1h, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     label: 'Successful Logins', | ||||||
|  |                     backgroundColor: "rgba(189, 229, 184, .5)", | ||||||
|  |                     spanGaps: true, | ||||||
|  |                     data: r.logins_per_1h, | ||||||
|  |                 }, | ||||||
|  |             ] | ||||||
|  |         }, | ||||||
|  |         options: { | ||||||
|  |             maintainAspectRatio: false, | ||||||
|  |             spanGaps: true, | ||||||
|  |             scales: { | ||||||
|  |                 xAxes: [{ | ||||||
|  |                     stacked: true, | ||||||
|  |                     gridLines: { | ||||||
|  |                         color: "rgba(0, 0, 0, 0)", | ||||||
|  |                     }, | ||||||
|  |                     type: 'time', | ||||||
|  |                     offset: true, | ||||||
|  |                     ticks: { | ||||||
|  |                         callback: function (value, index, values) { | ||||||
|  |                             const date = new Date(); | ||||||
|  |                             const delta = (date - values[index].value); | ||||||
|  |                             const ago = Math.round(delta / 1000 / 3600); | ||||||
|  |                             return `${ago} Hours ago`; | ||||||
|  |                         }, | ||||||
|  |                         autoSkip: true, | ||||||
|  |                         maxTicksLimit: 8 | ||||||
|  |                     } | ||||||
|  |                 }], | ||||||
|  |                 yAxes: [{ | ||||||
|  |                     stacked: true, | ||||||
|  |                     gridLines: { | ||||||
|  |                         color: "rgba(0, 0, 0, 0)", | ||||||
|  |                     } | ||||||
|  |                 }] | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <div class="pf-c-dropdown"> |                     <div class="pf-c-dropdown"> | ||||||
|                         <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> |                         <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> | ||||||
| @ -78,18 +79,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon pf-icon-infrastructure pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Policies.' %} |                     {% trans 'No Policies.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any policies." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no policies exist. Click the button below to create one.' %} |                     {% trans 'Currently no policies exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="pf-c-dropdown"> |                 <div class="pf-c-dropdown"> | ||||||
|                     <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> |                     <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> | ||||||
|  | |||||||
| @ -75,7 +75,7 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <div class="pf-c-dropdown"> |                     <div class="pf-c-dropdown"> | ||||||
|                         <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> |                         <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> | ||||||
| @ -72,18 +73,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon pf-icon-blueprint pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Property Mappings.' %} |                     {% trans 'No Property Mappings.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any property mappings." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no property mappings exist. Click the button below to create one.' %} |                     {% trans 'Currently no property mappings exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="pf-c-dropdown"> |                 <div class="pf-c-dropdown"> | ||||||
|                     <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> |                     <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <div class="pf-c-dropdown"> |                     <div class="pf-c-dropdown"> | ||||||
|                         <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> |                         <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> | ||||||
| @ -91,18 +92,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon-integration pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Providers.' %} |                     {% trans 'No Providers.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any providers." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no providers exist. Click the button below to create one.' %} |                     {% trans 'Currently no providers exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="pf-c-dropdown"> |                 <div class="pf-c-dropdown"> | ||||||
|                     <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> |                     <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <div class="pf-c-dropdown"> |                     <div class="pf-c-dropdown"> | ||||||
|                         <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> |                         <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> | ||||||
| @ -85,18 +86,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon pf-icon-middleware pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Sources.' %} |                     {% trans 'No Sources.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any sources." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no sources exist. Click the button below to create one.' %} |                     {% trans 'Currently no sources exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="pf-c-dropdown"> |                 <div class="pf-c-dropdown"> | ||||||
|                     <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> |                     <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> | ||||||
|  | |||||||
| @ -11,8 +11,7 @@ | |||||||
|             <i class="pf-icon pf-icon-plugged"></i> |             <i class="pf-icon pf-icon-plugged"></i> | ||||||
|             {% trans 'Stages' %} |             {% trans 'Stages' %} | ||||||
|         </h1> |         </h1> | ||||||
|         <p>{% trans "Stages are single steps of a Flow that a user is guided through." %} |         <p>{% trans "Stages are single steps of a Flow that a user is guided through." %}</p> | ||||||
|         </p> |  | ||||||
|     </div> |     </div> | ||||||
| </section> | </section> | ||||||
| <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||||
| @ -20,6 +19,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <div class="pf-c-dropdown"> |                     <div class="pf-c-dropdown"> | ||||||
|                         <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> |                         <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> | ||||||
| @ -81,18 +81,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon pf-icon-plugged pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Stages.' %} |                     {% trans 'No Stages.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any stages." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no stages exist. Click the button below to create one.' %} |                     {% trans 'Currently no stages exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="pf-c-dropdown"> |                 <div class="pf-c-dropdown"> | ||||||
|                     <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> |                     <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> | ||||||
|  | |||||||
| @ -81,7 +81,7 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}" |                     <a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}" | ||||||
|                         class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                         class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
| @ -54,18 +55,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon pf-icon-migration pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Invitations.' %} |                     {% trans 'No Invitations.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any invitations." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no invitations exist. Click the button below to create one.' %} |                     {% trans 'Currently no invitations exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                 <a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|             </div> |             </div> | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                     <a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|                 </div> |                 </div> | ||||||
| @ -80,18 +81,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon pf-icon-plugged pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Stage Prompts.' %} |                     {% trans 'No Stage Prompts.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any stage prompts." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no stage prompts exist. Click the button below to create one.' %} |                     {% trans 'Currently no stage prompts exist. Click the button below to create one.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                 <a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|             </div> |             </div> | ||||||
|  | |||||||
							
								
								
									
										77
									
								
								passbook/admin/templates/administration/task/list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,77 @@ | |||||||
|  | {% extends "administration/base.html" %} | ||||||
|  |  | ||||||
|  | {% load i18n %} | ||||||
|  | {% load humanize %} | ||||||
|  | {% load passbook_utils %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <section class="pf-c-page__main-section pf-m-light"> | ||||||
|  |     <div class="pf-c-content"> | ||||||
|  |         <h1> | ||||||
|  |             <i class="pf-icon pf-icon-automation"></i> | ||||||
|  |             {% trans 'System Tasks' %} | ||||||
|  |         </h1> | ||||||
|  |         <p>{% trans "Long-running operations which passbook executes in the background." %}</p> | ||||||
|  |     </div> | ||||||
|  | </section> | ||||||
|  | <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||||
|  |     <div class="pf-c-card"> | ||||||
|  |         <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid"> | ||||||
|  |             <thead> | ||||||
|  |                 <tr role="row"> | ||||||
|  |                     <th role="columnheader" scope="col">{% trans 'Identifier' %}</th> | ||||||
|  |                     <th role="columnheader" scope="col">{% trans 'Description' %}</th> | ||||||
|  |                     <th role="columnheader" scope="col">{% trans 'Last Run' %}</th> | ||||||
|  |                     <th role="columnheader" scope="col">{% trans 'Status' %}</th> | ||||||
|  |                     <th role="columnheader" scope="col">{% trans 'Messages' %}</th> | ||||||
|  |                     <th role="cell"></th> | ||||||
|  |                 </tr> | ||||||
|  |             </thead> | ||||||
|  |             <tbody role="rowgroup"> | ||||||
|  |                 {% for task in object_list %} | ||||||
|  |                 <tr role="row"> | ||||||
|  |                     <th role="columnheader"> | ||||||
|  |                         <pre>{{ task.task_name }}</pre> | ||||||
|  |                     </th> | ||||||
|  |                     <td role="cell"> | ||||||
|  |                         <span> | ||||||
|  |                             {{ task.task_description }} | ||||||
|  |                         </span> | ||||||
|  |                     </td> | ||||||
|  |                     <td role="cell"> | ||||||
|  |                         <span> | ||||||
|  |                             {{ task.finish_timestamp|naturaltime }} | ||||||
|  |                         </span> | ||||||
|  |                     </td> | ||||||
|  |                     <td role="cell"> | ||||||
|  |                         <span> | ||||||
|  |                             {% if task.result.status == task_successful %} | ||||||
|  |                             <i class="fas fa-check pf-m-success"></i> {% trans 'Successful' %} | ||||||
|  |                             {% elif task.result.status == task_warning %} | ||||||
|  |                             <i class="fas fa-exclamation-triangle pf-m-warning"></i> {% trans 'Warning' %} | ||||||
|  |                             {% elif task.result.status == task_error %} | ||||||
|  |                             <i class="fas fa-times pf-m-danger"></i> {% trans 'Error' %} | ||||||
|  |                             {% else %} | ||||||
|  |                             <i class="fas fa-question-circle"></i> {% trans 'Unknown' %} | ||||||
|  |                             {% endif %} | ||||||
|  |                         </span> | ||||||
|  |                     </td> | ||||||
|  |                     <td> | ||||||
|  |                         {% for message in task.result.messages %} | ||||||
|  |                         <div> | ||||||
|  |                             {{ message }} | ||||||
|  |                         </div> | ||||||
|  |                         {% endfor %} | ||||||
|  |                     </td> | ||||||
|  |                     <td> | ||||||
|  |                         <button is="action-button" class="pf-c-button pf-m-primary" url="{% url 'passbook_api:admin_system_tasks-retry' pk=task.task_name %}"> | ||||||
|  |                             {% trans 'Retry Task' %} | ||||||
|  |                         </button> | ||||||
|  |                     </td> | ||||||
|  |                 </tr> | ||||||
|  |                 {% endfor %} | ||||||
|  |             </tbody> | ||||||
|  |         </table> | ||||||
|  |     </div> | ||||||
|  | </section> | ||||||
|  | {% endblock %} | ||||||
| @ -18,13 +18,14 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 {% include 'partials/pagination.html' %} |                 {% include 'partials/pagination.html' %} | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|         <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid"> |         <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid"> | ||||||
|             <thead> |             <thead> | ||||||
|                 <tr role="row"> |                 <tr role="row"> | ||||||
|                     <th role="columnheader" scope="col">{% trans 'Token' %}</th> |                     <th role="columnheader" scope="col">{% trans 'Identifier' %}</th> | ||||||
|                     <th role="columnheader" scope="col">{% trans 'User' %}</th> |                     <th role="columnheader" scope="col">{% trans 'User' %}</th> | ||||||
|                     <th role="columnheader" scope="col">{% trans 'Expires?' %}</th> |                     <th role="columnheader" scope="col">{% trans 'Expires?' %}</th> | ||||||
|                     <th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th> |                     <th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th> | ||||||
| @ -35,9 +36,7 @@ | |||||||
|                 {% for token in object_list %} |                 {% for token in object_list %} | ||||||
|                 <tr role="row"> |                 <tr role="row"> | ||||||
|                     <th role="columnheader"> |                     <th role="columnheader"> | ||||||
|                         <div> |                         <div>{{ token.identifier }}</div> | ||||||
|                             <div>{{ token.pk.hex }}</div> |  | ||||||
|                         </div> |  | ||||||
|                     </th> |                     </th> | ||||||
|                     <td role="cell"> |                     <td role="cell"> | ||||||
|                         <span> |                         <span> | ||||||
| @ -65,18 +64,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="fas fa-key pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Tokens.' %} |                     {% trans 'No Tokens.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any token." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no tokens exist.' %} |                     {% trans 'Currently no tokens exist.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|  | |||||||
							
								
								
									
										42
									
								
								passbook/admin/templates/administration/user/disable.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,42 @@ | |||||||
|  | {% extends "administration/base.html" %} | ||||||
|  |  | ||||||
|  | {% load i18n %} | ||||||
|  | {% load passbook_utils %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <section class="pf-c-page__main-section pf-m-light"> | ||||||
|  |     <div class="pf-c-content"> | ||||||
|  |         {% block above_form %} | ||||||
|  |         <h1> | ||||||
|  |             {% blocktrans with object_type=object|verbose_name %} | ||||||
|  |             Disable {{ object_type }} | ||||||
|  |             {% endblocktrans %} | ||||||
|  |         </h1> | ||||||
|  |         {% endblock %} | ||||||
|  |     </div> | ||||||
|  | </section> | ||||||
|  | <section class="pf-c-page__main-section"> | ||||||
|  |     <div class="pf-l-stack"> | ||||||
|  |         <div class="pf-l-stack__item"> | ||||||
|  |             <div class="pf-c-card"> | ||||||
|  |                 <div class="pf-c-card__body"> | ||||||
|  |                     <form action="" method="post" class="pf-c-form"> | ||||||
|  |                         {% csrf_token %} | ||||||
|  |                         <p> | ||||||
|  |                             {% blocktrans with object_type=object|verbose_name name=object %} | ||||||
|  |                             Are you sure you want to disable {{ object_type }} "{{ object }}"? | ||||||
|  |                             {% endblocktrans %} | ||||||
|  |                         </p> | ||||||
|  |                         <div class="pf-c-form__group pf-m-action"> | ||||||
|  |                             <div class="pf-c-form__actions"> | ||||||
|  |                                 <input class="pf-c-button pf-m-danger" type="submit" value="{% trans 'Disable' %}" /> | ||||||
|  |                                 <a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Back" %}</a> | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                     </form> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </section> | ||||||
|  | {% endblock %} | ||||||
| @ -17,6 +17,7 @@ | |||||||
|         {% if object_list %} |         {% if object_list %} | ||||||
|         <div class="pf-c-toolbar"> |         <div class="pf-c-toolbar"> | ||||||
|             <div class="pf-c-toolbar__content"> |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|                 <div class="pf-c-toolbar__bulk-select"> |                 <div class="pf-c-toolbar__bulk-select"> | ||||||
|                     <a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                     <a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|                 </div> |                 </div> | ||||||
| @ -53,7 +54,11 @@ | |||||||
|                     </td> |                     </td> | ||||||
|                     <td> |                     <td> | ||||||
|                         <a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> |                         <a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> | ||||||
|                         <a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> |                         {% if user.is_active %} | ||||||
|  |                         <a class="pf-c-button pf-m-warning" href="{% url 'passbook_admin:user-disable' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Disable' %}</a> | ||||||
|  |                         {% else %} | ||||||
|  |                         <a class="pf-c-button pf-m-primary" href="{% url 'passbook_admin:user-enable' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Enable' %}</a> | ||||||
|  |                         {% endif %} | ||||||
|                         <a class="pf-c-button pf-m-tertiary" href="{% url 'passbook_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a> |                         <a class="pf-c-button pf-m-tertiary" href="{% url 'passbook_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a> | ||||||
|                         <a class="pf-c-button pf-m-tertiary" href="{% url 'passbook_core:impersonate-init' user_id=user.pk %}">{% trans 'Impersonate' %}</a> |                         <a class="pf-c-button pf-m-tertiary" href="{% url 'passbook_core:impersonate-init' user_id=user.pk %}">{% trans 'Impersonate' %}</a> | ||||||
|                     </td> |                     </td> | ||||||
| @ -61,18 +66,27 @@ | |||||||
|                 {% endfor %} |                 {% endfor %} | ||||||
|             </tbody> |             </tbody> | ||||||
|         </table> |         </table> | ||||||
|         <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom"> |         <div class="pf-c-pagination pf-m-bottom"> | ||||||
|             {% include 'partials/pagination.html' %} |             {% include 'partials/pagination.html' %} | ||||||
|         </div> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|  |         <div class="pf-c-toolbar"> | ||||||
|  |             <div class="pf-c-toolbar__content"> | ||||||
|  |                 {% include 'partials/toolbar_search.html' %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="pf-c-empty-state"> |         <div class="pf-c-empty-state"> | ||||||
|             <div class="pf-c-empty-state__content"> |             <div class="pf-c-empty-state__content"> | ||||||
|                 <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i> |                 <i class="pf-icon pf-icon-user pf-c-empty-state__icon" aria-hidden="true"></i> | ||||||
|                 <h1 class="pf-c-title pf-m-lg"> |                 <h1 class="pf-c-title pf-m-lg"> | ||||||
|                     {% trans 'No Users.' %} |                     {% trans 'No Users.' %} | ||||||
|                 </h1> |                 </h1> | ||||||
|                 <div class="pf-c-empty-state__body"> |                 <div class="pf-c-empty-state__body"> | ||||||
|  |                 {% if request.GET.search != "" %} | ||||||
|  |                     {% trans "Your search query doesn't match any users." %} | ||||||
|  |                 {% else %} | ||||||
|                     {% trans 'Currently no users exist. How did you even get here.' %} |                     {% trans 'Currently no users exist. How did you even get here.' %} | ||||||
|  |                 {% endif %} | ||||||
|                 </div> |                 </div> | ||||||
|                 <a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> |                 <a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a> | ||||||
|             </div> |             </div> | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| {% extends "administration/base.html" %} | {% extends container_template|default:"administration/base.html" %} | ||||||
|  |  | ||||||
| {% load i18n %} | {% load i18n %} | ||||||
| {% load passbook_utils %} | {% load passbook_utils %} | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ from passbook.admin.views import ( | |||||||
|     stages_bindings, |     stages_bindings, | ||||||
|     stages_invitations, |     stages_invitations, | ||||||
|     stages_prompts, |     stages_prompts, | ||||||
|  |     tasks, | ||||||
|     tokens, |     tokens, | ||||||
|     users, |     users, | ||||||
| ) | ) | ||||||
| @ -191,10 +192,20 @@ urlpatterns = [ | |||||||
|     ), |     ), | ||||||
|     # Flows |     # Flows | ||||||
|     path("flows/", flows.FlowListView.as_view(), name="flows"), |     path("flows/", flows.FlowListView.as_view(), name="flows"), | ||||||
|     path("flows/create/", flows.FlowCreateView.as_view(), name="flow-create",), |  | ||||||
|     path("flows/import/", flows.FlowImportView.as_view(), name="flow-import",), |  | ||||||
|     path( |     path( | ||||||
|         "flows/<uuid:pk>/update/", flows.FlowUpdateView.as_view(), name="flow-update", |         "flows/create/", | ||||||
|  |         flows.FlowCreateView.as_view(), | ||||||
|  |         name="flow-create", | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         "flows/import/", | ||||||
|  |         flows.FlowImportView.as_view(), | ||||||
|  |         name="flow-import", | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         "flows/<uuid:pk>/update/", | ||||||
|  |         flows.FlowUpdateView.as_view(), | ||||||
|  |         name="flow-update", | ||||||
|     ), |     ), | ||||||
|     path( |     path( | ||||||
|         "flows/<uuid:pk>/execute/", |         "flows/<uuid:pk>/execute/", | ||||||
| @ -202,10 +213,14 @@ urlpatterns = [ | |||||||
|         name="flow-execute", |         name="flow-execute", | ||||||
|     ), |     ), | ||||||
|     path( |     path( | ||||||
|         "flows/<uuid:pk>/export/", flows.FlowExportView.as_view(), name="flow-export", |         "flows/<uuid:pk>/export/", | ||||||
|  |         flows.FlowExportView.as_view(), | ||||||
|  |         name="flow-export", | ||||||
|     ), |     ), | ||||||
|     path( |     path( | ||||||
|         "flows/<uuid:pk>/delete/", flows.FlowDeleteView.as_view(), name="flow-delete", |         "flows/<uuid:pk>/delete/", | ||||||
|  |         flows.FlowDeleteView.as_view(), | ||||||
|  |         name="flow-delete", | ||||||
|     ), |     ), | ||||||
|     # Property Mappings |     # Property Mappings | ||||||
|     path( |     path( | ||||||
| @ -233,6 +248,10 @@ urlpatterns = [ | |||||||
|     path("users/create/", users.UserCreateView.as_view(), name="user-create"), |     path("users/create/", users.UserCreateView.as_view(), name="user-create"), | ||||||
|     path("users/<int:pk>/update/", users.UserUpdateView.as_view(), name="user-update"), |     path("users/<int:pk>/update/", users.UserUpdateView.as_view(), name="user-update"), | ||||||
|     path("users/<int:pk>/delete/", users.UserDeleteView.as_view(), name="user-delete"), |     path("users/<int:pk>/delete/", users.UserDeleteView.as_view(), name="user-delete"), | ||||||
|  |     path( | ||||||
|  |         "users/<int:pk>/disable/", users.UserDisableView.as_view(), name="user-disable" | ||||||
|  |     ), | ||||||
|  |     path("users/<int:pk>/enable/", users.UserEnableView.as_view(), name="user-enable"), | ||||||
|     path( |     path( | ||||||
|         "users/<int:pk>/reset/", |         "users/<int:pk>/reset/", | ||||||
|         users.UserPasswordResetView.as_view(), |         users.UserPasswordResetView.as_view(), | ||||||
| @ -273,9 +292,15 @@ urlpatterns = [ | |||||||
|         name="certificatekeypair-delete", |         name="certificatekeypair-delete", | ||||||
|     ), |     ), | ||||||
|     # Outposts |     # Outposts | ||||||
|     path("outposts/", outposts.OutpostListView.as_view(), name="outposts",), |  | ||||||
|     path( |     path( | ||||||
|         "outposts/create/", outposts.OutpostCreateView.as_view(), name="outpost-create", |         "outposts/", | ||||||
|  |         outposts.OutpostListView.as_view(), | ||||||
|  |         name="outposts", | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         "outposts/create/", | ||||||
|  |         outposts.OutpostCreateView.as_view(), | ||||||
|  |         name="outpost-create", | ||||||
|     ), |     ), | ||||||
|     path( |     path( | ||||||
|         "outposts/<uuid:pk>/update/", |         "outposts/<uuid:pk>/update/", | ||||||
| @ -287,4 +312,10 @@ urlpatterns = [ | |||||||
|         outposts.OutpostDeleteView.as_view(), |         outposts.OutpostDeleteView.as_view(), | ||||||
|         name="outpost-delete", |         name="outpost-delete", | ||||||
|     ), |     ), | ||||||
|  |     # Tasks | ||||||
|  |     path( | ||||||
|  |         "tasks/", | ||||||
|  |         tasks.TaskListView.as_view(), | ||||||
|  |         name="tasks", | ||||||
|  |     ), | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | |||||||
| from passbook.admin.views.utils import ( | from passbook.admin.views.utils import ( | ||||||
|     BackSuccessUrlMixin, |     BackSuccessUrlMixin, | ||||||
|     DeleteMessageView, |     DeleteMessageView, | ||||||
|  |     SearchListMixin, | ||||||
|     UserPaginateListMixin, |     UserPaginateListMixin, | ||||||
| ) | ) | ||||||
| from passbook.core.forms.applications import ApplicationForm | from passbook.core.forms.applications import ApplicationForm | ||||||
| @ -20,7 +21,11 @@ from passbook.lib.views import CreateAssignPermView | |||||||
|  |  | ||||||
|  |  | ||||||
| class ApplicationListView( | class ApplicationListView( | ||||||
|     LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView |     LoginRequiredMixin, | ||||||
|  |     PermissionListMixin, | ||||||
|  |     UserPaginateListMixin, | ||||||
|  |     SearchListMixin, | ||||||
|  |     ListView, | ||||||
| ): | ): | ||||||
|     """Show list of all applications""" |     """Show list of all applications""" | ||||||
|  |  | ||||||
| @ -29,6 +34,15 @@ class ApplicationListView( | |||||||
|     ordering = "name" |     ordering = "name" | ||||||
|     template_name = "administration/application/list.html" |     template_name = "administration/application/list.html" | ||||||
|  |  | ||||||
|  |     search_fields = [ | ||||||
|  |         "name", | ||||||
|  |         "slug", | ||||||
|  |         "meta_launch_url", | ||||||
|  |         "meta_icon_url", | ||||||
|  |         "meta_description", | ||||||
|  |         "meta_publisher", | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class ApplicationCreateView( | class ApplicationCreateView( | ||||||
|     SuccessMessageMixin, |     SuccessMessageMixin, | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | |||||||
| from passbook.admin.views.utils import ( | from passbook.admin.views.utils import ( | ||||||
|     BackSuccessUrlMixin, |     BackSuccessUrlMixin, | ||||||
|     DeleteMessageView, |     DeleteMessageView, | ||||||
|  |     SearchListMixin, | ||||||
|     UserPaginateListMixin, |     UserPaginateListMixin, | ||||||
| ) | ) | ||||||
| from passbook.crypto.forms import CertificateKeyPairForm | from passbook.crypto.forms import CertificateKeyPairForm | ||||||
| @ -20,7 +21,11 @@ from passbook.lib.views import CreateAssignPermView | |||||||
|  |  | ||||||
|  |  | ||||||
| class CertificateKeyPairListView( | class CertificateKeyPairListView( | ||||||
|     LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView |     LoginRequiredMixin, | ||||||
|  |     PermissionListMixin, | ||||||
|  |     UserPaginateListMixin, | ||||||
|  |     SearchListMixin, | ||||||
|  |     ListView, | ||||||
| ): | ): | ||||||
|     """Show list of all keypairs""" |     """Show list of all keypairs""" | ||||||
|  |  | ||||||
| @ -29,6 +34,8 @@ class CertificateKeyPairListView( | |||||||
|     ordering = "name" |     ordering = "name" | ||||||
|     template_name = "administration/certificatekeypair/list.html" |     template_name = "administration/certificatekeypair/list.html" | ||||||
|  |  | ||||||
|  |     search_fields = ["name"] | ||||||
|  |  | ||||||
|  |  | ||||||
| class CertificateKeyPairCreateView( | class CertificateKeyPairCreateView( | ||||||
|     SuccessMessageMixin, |     SuccessMessageMixin, | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | |||||||
| from passbook.admin.views.utils import ( | from passbook.admin.views.utils import ( | ||||||
|     BackSuccessUrlMixin, |     BackSuccessUrlMixin, | ||||||
|     DeleteMessageView, |     DeleteMessageView, | ||||||
|  |     SearchListMixin, | ||||||
|     UserPaginateListMixin, |     UserPaginateListMixin, | ||||||
| ) | ) | ||||||
| from passbook.flows.forms import FlowForm, FlowImportForm | from passbook.flows.forms import FlowForm, FlowImportForm | ||||||
| @ -28,7 +29,11 @@ from passbook.lib.views import CreateAssignPermView | |||||||
|  |  | ||||||
|  |  | ||||||
| class FlowListView( | class FlowListView( | ||||||
|     LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView |     LoginRequiredMixin, | ||||||
|  |     PermissionListMixin, | ||||||
|  |     UserPaginateListMixin, | ||||||
|  |     SearchListMixin, | ||||||
|  |     ListView, | ||||||
| ): | ): | ||||||
|     """Show list of all flows""" |     """Show list of all flows""" | ||||||
|  |  | ||||||
| @ -36,6 +41,7 @@ class FlowListView( | |||||||
|     permission_required = "passbook_flows.view_flow" |     permission_required = "passbook_flows.view_flow" | ||||||
|     ordering = "name" |     ordering = "name" | ||||||
|     template_name = "administration/flow/list.html" |     template_name = "administration/flow/list.html" | ||||||
|  |     search_fields = ["name", "slug", "designation", "title"] | ||||||
|  |  | ||||||
|  |  | ||||||
| class FlowCreateView( | class FlowCreateView( | ||||||
| @ -100,7 +106,9 @@ class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi | |||||||
|         plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user}) |         plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user}) | ||||||
|         self.request.session[SESSION_KEY_PLAN] = plan |         self.request.session[SESSION_KEY_PLAN] = plan | ||||||
|         return redirect_with_qs( |         return redirect_with_qs( | ||||||
|             "passbook_flows:flow-executor-shell", self.request.GET, flow_slug=flow.slug, |             "passbook_flows:flow-executor-shell", | ||||||
|  |             self.request.GET, | ||||||
|  |             flow_slug=flow.slug, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | |||||||
| from passbook.admin.views.utils import ( | from passbook.admin.views.utils import ( | ||||||
|     BackSuccessUrlMixin, |     BackSuccessUrlMixin, | ||||||
|     DeleteMessageView, |     DeleteMessageView, | ||||||
|  |     SearchListMixin, | ||||||
|     UserPaginateListMixin, |     UserPaginateListMixin, | ||||||
| ) | ) | ||||||
| from passbook.core.forms.groups import GroupForm | from passbook.core.forms.groups import GroupForm | ||||||
| @ -20,7 +21,11 @@ from passbook.lib.views import CreateAssignPermView | |||||||
|  |  | ||||||
|  |  | ||||||
| class GroupListView( | class GroupListView( | ||||||
|     LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView |     LoginRequiredMixin, | ||||||
|  |     PermissionListMixin, | ||||||
|  |     UserPaginateListMixin, | ||||||
|  |     SearchListMixin, | ||||||
|  |     ListView, | ||||||
| ): | ): | ||||||
|     """Show list of all groups""" |     """Show list of all groups""" | ||||||
|  |  | ||||||
| @ -28,6 +33,7 @@ class GroupListView( | |||||||
|     permission_required = "passbook_core.view_group" |     permission_required = "passbook_core.view_group" | ||||||
|     ordering = "name" |     ordering = "name" | ||||||
|     template_name = "administration/group/list.html" |     template_name = "administration/group/list.html" | ||||||
|  |     search_fields = ["name", "attributes"] | ||||||
|  |  | ||||||
|  |  | ||||||
| class GroupCreateView( | class GroupCreateView( | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | |||||||
| from passbook.admin.views.utils import ( | from passbook.admin.views.utils import ( | ||||||
|     BackSuccessUrlMixin, |     BackSuccessUrlMixin, | ||||||
|     DeleteMessageView, |     DeleteMessageView, | ||||||
|  |     SearchListMixin, | ||||||
|     UserPaginateListMixin, |     UserPaginateListMixin, | ||||||
| ) | ) | ||||||
| from passbook.lib.views import CreateAssignPermView | from passbook.lib.views import CreateAssignPermView | ||||||
| @ -23,7 +24,11 @@ from passbook.outposts.models import Outpost, OutpostConfig | |||||||
|  |  | ||||||
|  |  | ||||||
| class OutpostListView( | class OutpostListView( | ||||||
|     LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView |     LoginRequiredMixin, | ||||||
|  |     PermissionListMixin, | ||||||
|  |     UserPaginateListMixin, | ||||||
|  |     SearchListMixin, | ||||||
|  |     ListView, | ||||||
| ): | ): | ||||||
|     """Show list of all outposts""" |     """Show list of all outposts""" | ||||||
|  |  | ||||||
| @ -31,6 +36,7 @@ class OutpostListView( | |||||||
|     permission_required = "passbook_outposts.view_outpost" |     permission_required = "passbook_outposts.view_outpost" | ||||||
|     ordering = "name" |     ordering = "name" | ||||||
|     template_name = "administration/outpost/list.html" |     template_name = "administration/outpost/list.html" | ||||||
|  |     search_fields = ["name", "_config"] | ||||||
|  |  | ||||||
|  |  | ||||||
| class OutpostCreateView( | class OutpostCreateView( | ||||||
|  | |||||||
| @ -1,19 +1,22 @@ | |||||||
| """passbook administration overview""" | """passbook administration overview""" | ||||||
| from typing import Union | from typing import Union | ||||||
|  |  | ||||||
|  | from django.conf import settings | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.shortcuts import redirect, reverse | from django.db.models import Count | ||||||
|  | from django.db.models.fields.json import KeyTextTransform | ||||||
| from django.views.generic import TemplateView | from django.views.generic import TemplateView | ||||||
| from packaging.version import LegacyVersion, Version, parse | from packaging.version import LegacyVersion, Version, parse | ||||||
|  | from structlog import get_logger | ||||||
|  |  | ||||||
| from passbook import __version__ | from passbook import __version__ | ||||||
| from passbook.admin.mixins import AdminRequiredMixin | from passbook.admin.mixins import AdminRequiredMixin | ||||||
| from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version | from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version | ||||||
| from passbook.core.models import Application, Provider, Source, User | from passbook.audit.models import Event, EventAction | ||||||
| from passbook.flows.models import Flow, Stage | from passbook.core.models import Provider, User | ||||||
| from passbook.policies.models import Policy | from passbook.policies.models import Policy | ||||||
| from passbook.root.celery import CELERY_APP |  | ||||||
| from passbook.stages.invitation.models import Invitation | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
| class AdministrationOverviewView(AdminRequiredMixin, TemplateView): | class AdministrationOverviewView(AdminRequiredMixin, TemplateView): | ||||||
| @ -23,31 +26,46 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView): | |||||||
|  |  | ||||||
|     def post(self, *args, **kwargs): |     def post(self, *args, **kwargs): | ||||||
|         """Handle post (clear cache from modal)""" |         """Handle post (clear cache from modal)""" | ||||||
|         if "clear" in self.request.POST: |         if "clear_policies" in self.request.POST: | ||||||
|             cache.clear() |             keys = cache.keys("policy_*") | ||||||
|             return redirect(reverse("passbook_flows:default-authentication")) |             cache.delete_many(keys) | ||||||
|  |             LOGGER.debug("Cleared Policy cache", keys=len(keys)) | ||||||
|  |         if "clear_flows" in self.request.POST: | ||||||
|  |             keys = cache.keys("flow_*") | ||||||
|  |             cache.delete_many(keys) | ||||||
|  |             LOGGER.debug("Cleared flow cache", keys=len(keys)) | ||||||
|         return self.get(*args, **kwargs) |         return self.get(*args, **kwargs) | ||||||
|  |  | ||||||
|     def get_latest_version(self) -> Union[LegacyVersion, Version]: |     def get_latest_version(self) -> Union[LegacyVersion, Version]: | ||||||
|         """Get latest version from cache""" |         """Get latest version from cache""" | ||||||
|         version_in_cache = cache.get(VERSION_CACHE_KEY) |         version_in_cache = cache.get(VERSION_CACHE_KEY) | ||||||
|         if not version_in_cache: |         if not version_in_cache: | ||||||
|  |             if not settings.DEBUG: | ||||||
|                 update_latest_version.delay() |                 update_latest_version.delay() | ||||||
|             return parse(__version__) |             return parse(__version__) | ||||||
|         return parse(version_in_cache) |         return parse(version_in_cache) | ||||||
|  |  | ||||||
|  |     def get_most_used_applications(self): | ||||||
|  |         """Get Most used applications, total login counts and unique users that have used them.""" | ||||||
|  |         return ( | ||||||
|  |             Event.objects.filter(action=EventAction.AUTHORIZE_APPLICATION) | ||||||
|  |             .exclude(context__authorized_application=None) | ||||||
|  |             .annotate(application=KeyTextTransform("authorized_application", "context")) | ||||||
|  |             .annotate(user_pk=KeyTextTransform("pk", "user")) | ||||||
|  |             .values("application") | ||||||
|  |             .annotate(total_logins=Count("application")) | ||||||
|  |             .annotate(unique_users=Count("user_pk", distinct=True)) | ||||||
|  |             .values("unique_users", "application", "total_logins") | ||||||
|  |             .order_by("-total_logins")[:15] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def get_context_data(self, **kwargs): |     def get_context_data(self, **kwargs): | ||||||
|         kwargs["application_count"] = len(Application.objects.all()) |  | ||||||
|         kwargs["policy_count"] = len(Policy.objects.all()) |         kwargs["policy_count"] = len(Policy.objects.all()) | ||||||
|         kwargs["user_count"] = len(User.objects.all()) - 1  # Remove anonymous user |         kwargs["user_count"] = len(User.objects.all()) - 1  # Remove anonymous user | ||||||
|         kwargs["provider_count"] = len(Provider.objects.all()) |         kwargs["provider_count"] = len(Provider.objects.all()) | ||||||
|         kwargs["source_count"] = len(Source.objects.all()) |  | ||||||
|         kwargs["stage_count"] = len(Stage.objects.all()) |  | ||||||
|         kwargs["flow_count"] = len(Flow.objects.all()) |  | ||||||
|         kwargs["invitation_count"] = len(Invitation.objects.all()) |  | ||||||
|         kwargs["version"] = parse(__version__) |         kwargs["version"] = parse(__version__) | ||||||
|         kwargs["version_latest"] = self.get_latest_version() |         kwargs["version_latest"] = self.get_latest_version() | ||||||
|         kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5)) |         kwargs["most_used_applications"] = self.get_most_used_applications() | ||||||
|         kwargs["providers_without_application"] = Provider.objects.filter( |         kwargs["providers_without_application"] = Provider.objects.filter( | ||||||
|             application=None |             application=None | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ from passbook.admin.views.utils import ( | |||||||
|     InheritanceCreateView, |     InheritanceCreateView, | ||||||
|     InheritanceListView, |     InheritanceListView, | ||||||
|     InheritanceUpdateView, |     InheritanceUpdateView, | ||||||
|  |     SearchListMixin, | ||||||
|     UserPaginateListMixin, |     UserPaginateListMixin, | ||||||
| ) | ) | ||||||
| from passbook.policies.models import Policy, PolicyBinding | from passbook.policies.models import Policy, PolicyBinding | ||||||
| @ -29,7 +30,11 @@ from passbook.policies.process import PolicyProcess, PolicyRequest | |||||||
|  |  | ||||||
|  |  | ||||||
| class PolicyListView( | class PolicyListView( | ||||||
|     LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView |     LoginRequiredMixin, | ||||||
|  |     PermissionListMixin, | ||||||
|  |     UserPaginateListMixin, | ||||||
|  |     SearchListMixin, | ||||||
|  |     InheritanceListView, | ||||||
| ): | ): | ||||||
|     """Show list of all policies""" |     """Show list of all policies""" | ||||||
|  |  | ||||||
| @ -37,6 +42,7 @@ class PolicyListView( | |||||||
|     permission_required = "passbook_policies.view_policy" |     permission_required = "passbook_policies.view_policy" | ||||||
|     ordering = "name" |     ordering = "name" | ||||||
|     template_name = "administration/policy/list.html" |     template_name = "administration/policy/list.html" | ||||||
|  |     search_fields = ["name"] | ||||||
|  |  | ||||||
|  |  | ||||||
| class PolicyCreateView( | class PolicyCreateView( | ||||||
|  | |||||||
| @ -14,13 +14,18 @@ from passbook.admin.views.utils import ( | |||||||
|     InheritanceCreateView, |     InheritanceCreateView, | ||||||
|     InheritanceListView, |     InheritanceListView, | ||||||
|     InheritanceUpdateView, |     InheritanceUpdateView, | ||||||
|  |     SearchListMixin, | ||||||
|     UserPaginateListMixin, |     UserPaginateListMixin, | ||||||
| ) | ) | ||||||
| from passbook.core.models import PropertyMapping | from passbook.core.models import PropertyMapping | ||||||
|  |  | ||||||
|  |  | ||||||
| class PropertyMappingListView( | class PropertyMappingListView( | ||||||
|     LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView |     LoginRequiredMixin, | ||||||
|  |     PermissionListMixin, | ||||||
|  |     UserPaginateListMixin, | ||||||
|  |     SearchListMixin, | ||||||
|  |     InheritanceListView, | ||||||
| ): | ): | ||||||
|     """Show list of all property_mappings""" |     """Show list of all property_mappings""" | ||||||
|  |  | ||||||
| @ -28,6 +33,7 @@ class PropertyMappingListView( | |||||||
|     permission_required = "passbook_core.view_propertymapping" |     permission_required = "passbook_core.view_propertymapping" | ||||||
|     template_name = "administration/property_mapping/list.html" |     template_name = "administration/property_mapping/list.html" | ||||||
|     ordering = "name" |     ordering = "name" | ||||||
|  |     search_fields = ["name", "expression"] | ||||||
|  |  | ||||||
|  |  | ||||||
| class PropertyMappingCreateView( | class PropertyMappingCreateView( | ||||||
|  | |||||||
| @ -14,13 +14,18 @@ from passbook.admin.views.utils import ( | |||||||
|     InheritanceCreateView, |     InheritanceCreateView, | ||||||
|     InheritanceListView, |     InheritanceListView, | ||||||
|     InheritanceUpdateView, |     InheritanceUpdateView, | ||||||
|  |     SearchListMixin, | ||||||
|     UserPaginateListMixin, |     UserPaginateListMixin, | ||||||
| ) | ) | ||||||
| from passbook.core.models import Provider | from passbook.core.models import Provider | ||||||
|  |  | ||||||
|  |  | ||||||
| class ProviderListView( | class ProviderListView( | ||||||
|     LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView |     LoginRequiredMixin, | ||||||
|  |     PermissionListMixin, | ||||||
|  |     UserPaginateListMixin, | ||||||
|  |     SearchListMixin, | ||||||
|  |     InheritanceListView, | ||||||
| ): | ): | ||||||
|     """Show list of all providers""" |     """Show list of all providers""" | ||||||
|  |  | ||||||
| @ -28,6 +33,7 @@ class ProviderListView( | |||||||
|     permission_required = "passbook_core.add_provider" |     permission_required = "passbook_core.add_provider" | ||||||
|     template_name = "administration/provider/list.html" |     template_name = "administration/provider/list.html" | ||||||
|     ordering = "id" |     ordering = "id" | ||||||
|  |     search_fields = ["id", "name"] | ||||||
|  |  | ||||||
|  |  | ||||||
| class ProviderCreateView( | class ProviderCreateView( | ||||||
|  | |||||||
