Compare commits
	
		
			238 Commits
		
	
	
		
			version/0.
			...
			version/0.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 251672a67d | |||
| 4ffc0e2a08 | |||
| 4e1808632d | |||
| 791627d3ce | |||
| f3df3a0157 | |||
| 6aaae53a19 | |||
| 4d84f6d598 | |||
| 4e2349b6d9 | |||
| cd57b8f7f3 | |||
| 40b1fc06b0 | |||
| 02fa217e28 | |||
| 6652514358 | |||
| dcd3dc9744 | |||
| 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 | 
@ -1,5 +1,5 @@
 | 
			
		||||
[bumpversion]
 | 
			
		||||
current_version = 0.10.8-stable
 | 
			
		||||
current_version = 0.12.8-stable
 | 
			
		||||
tag = True
 | 
			
		||||
commit = True
 | 
			
		||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -6,12 +6,16 @@ updates:
 | 
			
		||||
    interval: daily
 | 
			
		||||
    time: "04:00"
 | 
			
		||||
  open-pull-requests-limit: 10
 | 
			
		||||
  assignees:
 | 
			
		||||
  - BeryJu
 | 
			
		||||
- package-ecosystem: npm
 | 
			
		||||
  directory: "/passbook/static/static"
 | 
			
		||||
  schedule:
 | 
			
		||||
    interval: daily
 | 
			
		||||
    time: "04:00"
 | 
			
		||||
  open-pull-requests-limit: 10
 | 
			
		||||
  assignees:
 | 
			
		||||
  - BeryJu
 | 
			
		||||
- package-ecosystem: pip
 | 
			
		||||
  directory: "/"
 | 
			
		||||
  schedule:
 | 
			
		||||
@ -20,3 +24,19 @@ updates:
 | 
			
		||||
  open-pull-requests-limit: 10
 | 
			
		||||
  assignees:
 | 
			
		||||
  - 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
 | 
			
		||||
        run: docker build
 | 
			
		||||
          --no-cache
 | 
			
		||||
          -t beryju/passbook:0.10.8-stable
 | 
			
		||||
          -t beryju/passbook:0.12.8-stable
 | 
			
		||||
          -t beryju/passbook:latest
 | 
			
		||||
          -f Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook:0.10.8-stable
 | 
			
		||||
        run: docker push beryju/passbook:0.12.8-stable
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/passbook:latest
 | 
			
		||||
  build-proxy:
 | 
			
		||||
@ -48,11 +48,11 @@ jobs:
 | 
			
		||||
          cd proxy
 | 
			
		||||
          docker build \
 | 
			
		||||
          --no-cache \
 | 
			
		||||
          -t beryju/passbook-proxy:0.10.8-stable \
 | 
			
		||||
          -t beryju/passbook-proxy:0.12.8-stable \
 | 
			
		||||
          -t beryju/passbook-proxy:latest \
 | 
			
		||||
          -f Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook-proxy:0.10.8-stable
 | 
			
		||||
        run: docker push beryju/passbook-proxy:0.12.8-stable
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/passbook-proxy:latest
 | 
			
		||||
  build-static:
 | 
			
		||||
@ -77,11 +77,11 @@ jobs:
 | 
			
		||||
        run: docker build
 | 
			
		||||
          --no-cache
 | 
			
		||||
          --network=$(docker network ls | grep github | awk '{print $1}')
 | 
			
		||||
          -t beryju/passbook-static:0.10.8-stable
 | 
			
		||||
          -t beryju/passbook-static:0.12.8-stable
 | 
			
		||||
          -t beryju/passbook-static:latest
 | 
			
		||||
          -f static.Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook-static:0.10.8-stable
 | 
			
		||||
        run: docker push beryju/passbook-static:0.12.8-stable
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/passbook-static:latest
 | 
			
		||||
  test-release:
 | 
			
		||||
@ -114,5 +114,5 @@ jobs:
 | 
			
		||||
          SENTRY_PROJECT: passbook
 | 
			
		||||
          SENTRY_URL: https://sentry.beryju.org
 | 
			
		||||
        with:
 | 
			
		||||
          tagName: 0.10.8-stable
 | 
			
		||||
          tagName: 0.12.8-stable
 | 
			
		||||
          environment: beryjuorg-prod
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						@ -16,12 +16,25 @@ COPY --from=locker /app/requirements.txt /
 | 
			
		||||
COPY --from=locker /app/requirements-dev.txt /
 | 
			
		||||
 | 
			
		||||
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 && \
 | 
			
		||||
    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 && \
 | 
			
		||||
    apt-get remove --purge -y build-essential && \
 | 
			
		||||
    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 ./manage.py /
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						@ -11,8 +11,8 @@ lint-fix:
 | 
			
		||||
	black passbook e2e lifecycle
 | 
			
		||||
 | 
			
		||||
lint:
 | 
			
		||||
	pyright pyright e2e lifecycle
 | 
			
		||||
	bandit -r passbook e2e lifecycle
 | 
			
		||||
	pyright passbook e2e lifecycle
 | 
			
		||||
	bandit -r passbook e2e lifecycle -x node_modules
 | 
			
		||||
	pylint passbook e2e lifecycle
 | 
			
		||||
	prospector
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						@ -17,10 +17,10 @@ django-otp = "*"
 | 
			
		||||
django-prometheus = "*"
 | 
			
		||||
django-recaptcha = "*"
 | 
			
		||||
django-redis = "*"
 | 
			
		||||
djangorestframework = "==3.11.1"
 | 
			
		||||
djangorestframework = "*"
 | 
			
		||||
django-storages = "*"
 | 
			
		||||
djangorestframework-guardian = "*"
 | 
			
		||||
drf-yasg = "*"
 | 
			
		||||
drf_yasg2 = "*"
 | 
			
		||||
facebook-sdk = "*"
 | 
			
		||||
ldap3 = "*"
 | 
			
		||||
lxml = "*"
 | 
			
		||||
@ -28,7 +28,7 @@ packaging = "*"
 | 
			
		||||
psycopg2-binary = "*"
 | 
			
		||||
pycryptodome = "*"
 | 
			
		||||
pyjwkest = "*"
 | 
			
		||||
uvicorn = "*"
 | 
			
		||||
uvicorn = {extras = ["standard"],version = "*"}
 | 
			
		||||
gunicorn = "*"
 | 
			
		||||
pyyaml = "*"
 | 
			
		||||
qrcode = "*"
 | 
			
		||||
@ -43,6 +43,7 @@ dacite = "*"
 | 
			
		||||
channels = "*"
 | 
			
		||||
channels-redis = "*"
 | 
			
		||||
kubernetes = "*"
 | 
			
		||||
docker = "*"
 | 
			
		||||
 | 
			
		||||
[requires]
 | 
			
		||||
python_version = "3.8"
 | 
			
		||||
@ -50,12 +51,11 @@ python_version = "3.8"
 | 
			
		||||
[dev-packages]
 | 
			
		||||
autopep8 = "*"
 | 
			
		||||
bandit = "*"
 | 
			
		||||
black = "==19.10b0"
 | 
			
		||||
black = "==20.8b1"
 | 
			
		||||
bumpversion = "*"
 | 
			
		||||
colorama = "*"
 | 
			
		||||
coverage = "*"
 | 
			
		||||
django-debug-toolbar = "*"
 | 
			
		||||
docker = "*"
 | 
			
		||||
pylint = "*"
 | 
			
		||||
pylint-django = "*"
 | 
			
		||||
selenium = "*"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										574
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "_meta": {
 | 
			
		||||
        "hash": {
 | 
			
		||||
            "sha256": "39e0a747699dc7e528a215395cc505b380e40e6bd0295fdf4c373a871a9bde96"
 | 
			
		||||
            "sha256": "d1a9883d864e25f18e34b298b72b58db333a037571c7a20cefb7ba7a4037a434"
 | 
			
		||||
        },
 | 
			
		||||
        "pipfile-spec": 6,
 | 
			
		||||
        "requires": {
 | 
			
		||||
@ -16,24 +16,6 @@
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    "default": {
 | 
			
		||||
        "aiohttp": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e",
 | 
			
		||||
                "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326",
 | 
			
		||||
                "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a",
 | 
			
		||||
                "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654",
 | 
			
		||||
                "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a",
 | 
			
		||||
                "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4",
 | 
			
		||||
                "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17",
 | 
			
		||||
                "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec",
 | 
			
		||||
                "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd",
 | 
			
		||||
                "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48",
 | 
			
		||||
                "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59",
 | 
			
		||||
                "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '3.6'",
 | 
			
		||||
            "version": "==3.6.2"
 | 
			
		||||
        },
 | 
			
		||||
        "aioredis": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",
 | 
			
		||||
@ -92,18 +74,18 @@
 | 
			
		||||
        },
 | 
			
		||||
        "boto3": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:348cddfd56be6c8b759544f99d20d633cc6a65d346651700ad8ac93a5214c032",
 | 
			
		||||
                "sha256:c4ccd1f260660603f965bcc145de87e09dd1229040784fe119cd08caeb00dbe9"
 | 
			
		||||
                "sha256:2cabcdc217a128832d6c948cae22cbd3af03ae0736efcb59749f1f11f528be54",
 | 
			
		||||
                "sha256:b378c28c2db3be96abc2ca460c2f08424da8960b87d5d430cb7d6b712ec255b2"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.15.8"
 | 
			
		||||
            "version": "==1.16.7"
 | 
			
		||||
        },
 | 
			
		||||
        "botocore": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:07b399997d8050d3ed1150d4d657b46558999f75246eb5b02cee78b9798b3bd5",
 | 
			
		||||
                "sha256:53a778e6b715ad2ae39bf98e088962e8d524133fb458d83f080964254adc9885"
 | 
			
		||||
                "sha256:1481d6d3ccb77cb7cd97395110408238f3ab93b0d823156c7a2fb697604eb50d",
 | 
			
		||||
                "sha256:ab59f842797cbd09ee7d9e3f353bb9546f428853d94db448977dd554320620b3"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.18.8"
 | 
			
		||||
            "version": "==1.19.7"
 | 
			
		||||
        },
 | 
			
		||||
        "cachetools": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -114,11 +96,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "celery": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:313930fddde703d8e37029a304bf91429cd11aeef63c57de6daca9d958e1f255",
 | 
			
		||||
                "sha256:72138dc3887f68dc58e1a2397e477256f80f1894c69fa4337f8ed70be460375b"
 | 
			
		||||
                "sha256:7aa4ee46ed318bc177900ae7c01500354aee62d723255b0925db0754bcd4d390",
 | 
			
		||||
                "sha256:e3e8956d74af986b1e9770e0a294338b259618bf70283d6157416328e50c2bd6"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==5.0.0"
 | 
			
		||||
            "version": "==5.0.1"
 | 
			
		||||
        },
 | 
			
		||||
        "certifi": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -281,11 +263,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:59c8125ca873ed3bdae9c12b146fbbd6ed8d0f743e4cf5f5817af50c51f1fc2f",
 | 
			
		||||
                "sha256:b5fbb818e751f660fa2d576d9f40c34a4c615c8b48dd383f5216e609f383371f"
 | 
			
		||||
                "sha256:a2127ad0150ec6966655bedf15dbbff9697cc86d61653db2da1afa506c0b04cc",
 | 
			
		||||
                "sha256:c93c28ccf1d094cbd00d860e83128a39e45d2c571d3b54361713aaaf9a94cac4"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.1.1"
 | 
			
		||||
            "version": "==3.1.2"
 | 
			
		||||
        },
 | 
			
		||||
        "django-cors-middleware": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -328,11 +310,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django-otp": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0c9edbb3f4abc9ac6e43daf0a9e0e293e99ad917641cf8d7dbc49d613bcb5cd4",
 | 
			
		||||
                "sha256:ace831f3a0f2c2267e4f7219c78deeb3b41c2dc8ae44b03daebb4fb85dabeb43"
 | 
			
		||||
                "sha256:8ba5ab9bd2738c7321376c349d7cce49cf4404e79f6804e0a3cc462a91728e18",
 | 
			
		||||
                "sha256:f523fb9dec420f28a29d3e2ad72ac06f64588956ed4f2b5b430d8e957ebb8287"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.0.0"
 | 
			
		||||
            "version": "==1.0.2"
 | 
			
		||||
        },
 | 
			
		||||
        "django-prometheus": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -367,11 +349,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "djangorestframework": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32",
 | 
			
		||||
                "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b"
 | 
			
		||||
                "sha256:5c5071fcbad6dce16f566d492015c829ddb0df42965d488b878594aabc3aed21",
 | 
			
		||||
                "sha256:d54452aedebb4b650254ca092f9f4f5df947cb1de6ab245d817b08b4f4156249"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.11.1"
 | 
			
		||||
            "version": "==3.12.1"
 | 
			
		||||
        },
 | 
			
		||||
        "djangorestframework-guardian": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -381,13 +363,21 @@
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==0.3.0"
 | 
			
		||||
        },
 | 
			
		||||
        "drf-yasg": {
 | 
			
		||||
        "docker": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:5572e9d5baab9f6b49318169df9789f7399d0e3c7bdac8fdb8dfccf1d5d2b1ca",
 | 
			
		||||
                "sha256:7d7af27ad16e18507e9392b2afd6b218fbffc432ec8dbea053099a2241e184ff"
 | 
			
		||||
                "sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828",
 | 
			
		||||
                "sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.17.1"
 | 
			
		||||
            "version": "==4.3.1"
 | 
			
		||||
        },
 | 
			
		||||
        "drf-yasg2": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7037a8041eb5d1073fa504a284fc889685f93d0bfd008a963db1b366db786734",
 | 
			
		||||
                "sha256:75e661ca5cf15eb44fcfab408c7b864f87c20794f564aa08b3a31817a857f19d"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.19.4"
 | 
			
		||||
        },
 | 
			
		||||
        "eight": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -412,10 +402,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "google-auth": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:a73e6fb6d232ed1293ef9a5301e6f8aada7880d19c65d7f63e130dc50ec05593",
 | 
			
		||||
                "sha256:e86e72142d939a8d90a772947268aacc127ab7a1d1d6f3e0fecca7a8d74d8257"
 | 
			
		||||
                "sha256:712dd7d140a9a1ea218e5688c7fcb04af71b431a29ec9ce433e384c60e387b98",
 | 
			
		||||
                "sha256:9c0f71789438d703f77b94aad4ea545afaec9a65f10e6cc1bc8b89ce242244bb"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.22.0"
 | 
			
		||||
            "version": "==1.22.1"
 | 
			
		||||
        },
 | 
			
		||||
        "gunicorn": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -427,10 +417,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "h11": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:311dc5478c2568cc07262e0381cdfc5b9c6ba19775905736c87e81ae6662b9fd",
 | 
			
		||||
                "sha256:9eecfbafc980976dbff26a01dd3487644dd5d00f8038584451fc64a660f7c502"
 | 
			
		||||
                "sha256:3c6c61d69c6f13d41f1b80ab0322f1872702a3ba26e12aa864c928f6a43fbaab",
 | 
			
		||||
                "sha256:ab6c335e1b6ef34b205d5ca3e228c9299cc7218b049819ec84a388c2525e5d87"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.10.0"
 | 
			
		||||
            "version": "==0.11.0"
 | 
			
		||||
        },
 | 
			
		||||
        "hiredis": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -483,6 +473,23 @@
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.1.0"
 | 
			
		||||
        },
 | 
			
		||||
        "httptools": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be",
 | 
			
		||||
                "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d",
 | 
			
		||||
                "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce",
 | 
			
		||||
                "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2",
 | 
			
		||||
                "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6",
 | 
			
		||||
                "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f",
 | 
			
		||||
                "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009",
 | 
			
		||||
                "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce",
 | 
			
		||||
                "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a",
 | 
			
		||||
                "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c",
 | 
			
		||||
                "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4",
 | 
			
		||||
                "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.1.1"
 | 
			
		||||
        },
 | 
			
		||||
        "hyperlink": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af",
 | 
			
		||||
@ -548,11 +555,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "kubernetes": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:1a2472f8b01bc6aa87e3a34781f859bded5a5c8ff791a53d889a8bd6cc550430",
 | 
			
		||||
                "sha256:4af81201520977139a143f96123fb789fa351879df37f122916b9b6ed050bbaf"
 | 
			
		||||
                "sha256:72f095a1cd593401ff26b3b8d71749340394ca6d8413770ea28ce18efd5bcf4c",
 | 
			
		||||
                "sha256:9a339a32d6c79e6461cb6050c3662cb4e33058b508d8d34ee5d5206add395828"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==11.0.0"
 | 
			
		||||
            "version": "==12.0.0"
 | 
			
		||||
        },
 | 
			
		||||
        "ldap3": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -564,40 +571,45 @@
 | 
			
		||||
        },
 | 
			
		||||
        "lxml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f",
 | 
			
		||||
                "sha256:08fc93257dcfe9542c0a6883a25ba4971d78297f63d7a5a26ffa34861ca78730",
 | 
			
		||||
                "sha256:107781b213cf7201ec3806555657ccda67b1fccc4261fb889ef7fc56976db81f",
 | 
			
		||||
                "sha256:121b665b04083a1e85ff1f5243d4a93aa1aaba281bc12ea334d5a187278ceaf1",
 | 
			
		||||
                "sha256:1fa21263c3aba2b76fd7c45713d4428dbcc7644d73dcf0650e9d344e433741b3",
 | 
			
		||||
                "sha256:2b30aa2bcff8e958cd85d907d5109820b01ac511eae5b460803430a7404e34d7",
 | 
			
		||||
                "sha256:4b4a111bcf4b9c948e020fd207f915c24a6de3f1adc7682a2d92660eb4e84f1a",
 | 
			
		||||
                "sha256:5591c4164755778e29e69b86e425880f852464a21c7bb53c7ea453bbe2633bbe",
 | 
			
		||||
                "sha256:59daa84aef650b11bccd18f99f64bfe44b9f14a08a28259959d33676554065a1",
 | 
			
		||||
                "sha256:5a9c8d11aa2c8f8b6043d845927a51eb9102eb558e3f936df494e96393f5fd3e",
 | 
			
		||||
                "sha256:5dd20538a60c4cc9a077d3b715bb42307239fcd25ef1ca7286775f95e9e9a46d",
 | 
			
		||||
                "sha256:74f48ec98430e06c1fa8949b49ebdd8d27ceb9df8d3d1c92e1fdc2773f003f20",
 | 
			
		||||
                "sha256:786aad2aa20de3dbff21aab86b2fb6a7be68064cbbc0219bde414d3a30aa47ae",
 | 
			
		||||
                "sha256:7ad7906e098ccd30d8f7068030a0b16668ab8aa5cda6fcd5146d8d20cbaa71b5",
 | 
			
		||||
                "sha256:80a38b188d20c0524fe8959c8ce770a8fdf0e617c6912d23fc97c68301bb9aba",
 | 
			
		||||
                "sha256:8f0ec6b9b3832e0bd1d57af41f9238ea7709bbd7271f639024f2fc9d3bb01293",
 | 
			
		||||
                "sha256:92282c83547a9add85ad658143c76a64a8d339028926d7dc1998ca029c88ea6a",
 | 
			
		||||
                "sha256:94150231f1e90c9595ccc80d7d2006c61f90a5995db82bccbca7944fd457f0f6",
 | 
			
		||||
                "sha256:9dc9006dcc47e00a8a6a029eb035c8f696ad38e40a27d073a003d7d1443f5d88",
 | 
			
		||||
                "sha256:a76979f728dd845655026ab991df25d26379a1a8fc1e9e68e25c7eda43004bed",
 | 
			
		||||
                "sha256:aa8eba3db3d8761db161003e2d0586608092e217151d7458206e243be5a43843",
 | 
			
		||||
                "sha256:bea760a63ce9bba566c23f726d72b3c0250e2fa2569909e2d83cda1534c79443",
 | 
			
		||||
                "sha256:c3f511a3c58676147c277eff0224c061dd5a6a8e1373572ac817ac6324f1b1e0",
 | 
			
		||||
                "sha256:c9d317efde4bafbc1561509bfa8a23c5cab66c44d49ab5b63ff690f5159b2304",
 | 
			
		||||
                "sha256:cc411ad324a4486b142c41d9b2b6a722c534096963688d879ea6fa8a35028258",
 | 
			
		||||
                "sha256:cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6",
 | 
			
		||||
                "sha256:cfd7c5dd3c35c19cec59c63df9571c67c6d6e5c92e0fe63517920e97f61106d1",
 | 
			
		||||
                "sha256:e1cacf4796b20865789083252186ce9dc6cc59eca0c2e79cca332bdff24ac481",
 | 
			
		||||
                "sha256:e70d4e467e243455492f5de463b72151cc400710ac03a0678206a5f27e79ddef",
 | 
			
		||||
                "sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd",
 | 
			
		||||
                "sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee"
 | 
			
		||||
                "sha256:0e89f5d422988c65e6936e4ec0fe54d6f73f3128c80eb7ecc3b87f595523607b",
 | 
			
		||||
                "sha256:189ad47203e846a7a4951c17694d845b6ade7917c47c64b29b86526eefc3adf5",
 | 
			
		||||
                "sha256:1d87936cb5801c557f3e981c9c193861264c01209cb3ad0964a16310ca1b3301",
 | 
			
		||||
                "sha256:211b3bcf5da70c2d4b84d09232534ad1d78320762e2c59dedc73bf01cb1fc45b",
 | 
			
		||||
                "sha256:2358809cc64394617f2719147a58ae26dac9e21bae772b45cfb80baa26bfca5d",
 | 
			
		||||
                "sha256:23c83112b4dada0b75789d73f949dbb4e8f29a0a3511647024a398ebd023347b",
 | 
			
		||||
                "sha256:24e811118aab6abe3ce23ff0d7d38932329c513f9cef849d3ee88b0f848f2aa9",
 | 
			
		||||
                "sha256:2d5896ddf5389560257bbe89317ca7bcb4e54a02b53a3e572e1ce4226512b51b",
 | 
			
		||||
                "sha256:2d6571c48328be4304aee031d2d5046cbc8aed5740c654575613c5a4f5a11311",
 | 
			
		||||
                "sha256:2e311a10f3e85250910a615fe194839a04a0f6bc4e8e5bb5cac221344e3a7891",
 | 
			
		||||
                "sha256:302160eb6e9764168e01d8c9ec6becddeb87776e81d3fcb0d97954dd51d48e0a",
 | 
			
		||||
                "sha256:3a7a380bfecc551cfd67d6e8ad9faa91289173bdf12e9cfafbd2bdec0d7b1ec1",
 | 
			
		||||
                "sha256:3d9b2b72eb0dbbdb0e276403873ecfae870599c83ba22cadff2db58541e72856",
 | 
			
		||||
                "sha256:475325e037fdf068e0c2140b818518cf6bc4aa72435c407a798b2db9f8e90810",
 | 
			
		||||
                "sha256:4b7572145054330c8e324a72d808c8c8fbe12be33368db28c39a255ad5f7fb51",
 | 
			
		||||
                "sha256:4fff34721b628cce9eb4538cf9a73d02e0f3da4f35a515773cce6f5fe413b360",
 | 
			
		||||
                "sha256:56eff8c6fb7bc4bcca395fdff494c52712b7a57486e4fbde34c31bb9da4c6cc4",
 | 
			
		||||
                "sha256:573b2f5496c7e9f4985de70b9bbb4719ffd293d5565513e04ac20e42e6e5583f",
 | 
			
		||||
                "sha256:7ecaef52fd9b9535ae5f01a1dd2651f6608e4ec9dc136fc4dfe7ebe3c3ddb230",
 | 
			
		||||
                "sha256:803a80d72d1f693aa448566be46ffd70882d1ad8fc689a2e22afe63035eb998a",
 | 
			
		||||
                "sha256:8862d1c2c020cb7a03b421a9a7b4fe046a208db30994fc8ff68c627a7915987f",
 | 
			
		||||
                "sha256:9b06690224258db5cd39a84e993882a6874676f5de582da57f3df3a82ead9174",
 | 
			
		||||
                "sha256:a71400b90b3599eb7bf241f947932e18a066907bf84617d80817998cee81e4bf",
 | 
			
		||||
                "sha256:bb252f802f91f59767dcc559744e91efa9df532240a502befd874b54571417bd",
 | 
			
		||||
                "sha256:be1ebf9cc25ab5399501c9046a7dcdaa9e911802ed0e12b7d620cd4bbf0518b3",
 | 
			
		||||
                "sha256:be7c65e34d1b50ab7093b90427cbc488260e4b3a38ef2435d65b62e9fa3d798a",
 | 
			
		||||
                "sha256:c0dac835c1a22621ffa5e5f999d57359c790c52bbd1c687fe514ae6924f65ef5",
 | 
			
		||||
                "sha256:c152b2e93b639d1f36ec5a8ca24cde4a8eefb2b6b83668fcd8e83a67badcb367",
 | 
			
		||||
                "sha256:d182eada8ea0de61a45a526aa0ae4bcd222f9673424e65315c35820291ff299c",
 | 
			
		||||
                "sha256:d18331ea905a41ae71596502bd4c9a2998902328bbabd29e3d0f5f8569fabad1",
 | 
			
		||||
                "sha256:d20d32cbb31d731def4b1502294ca2ee99f9249b63bc80e03e67e8f8e126dea8",
 | 
			
		||||
                "sha256:d4ad7fd3269281cb471ad6c7bafca372e69789540d16e3755dd717e9e5c9d82f",
 | 
			
		||||
                "sha256:d6f8c23f65a4bfe4300b85f1f40f6c32569822d08901db3b6454ab785d9117cc",
 | 
			
		||||
                "sha256:d84d741c6e35c9f3e7406cb7c4c2e08474c2a6441d59322a00dcae65aac6315d",
 | 
			
		||||
                "sha256:e65c221b2115a91035b55a593b6eb94aa1206fa3ab374f47c6dc10d364583ff9",
 | 
			
		||||
                "sha256:f98b6f256be6cec8dd308a8563976ddaff0bdc18b730720f6f4bee927ffe926f"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==4.5.2"
 | 
			
		||||
            "version": "==4.6.1"
 | 
			
		||||
        },
 | 
			
		||||
        "markupsafe": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -660,28 +672,6 @@
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.0.0"
 | 
			
		||||
        },
 | 
			
		||||
        "multidict": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a",
 | 
			
		||||
                "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000",
 | 
			
		||||
                "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2",
 | 
			
		||||
                "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507",
 | 
			
		||||
                "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5",
 | 
			
		||||
                "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7",
 | 
			
		||||
                "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d",
 | 
			
		||||
                "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463",
 | 
			
		||||
                "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19",
 | 
			
		||||
                "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3",
 | 
			
		||||
                "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b",
 | 
			
		||||
                "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c",
 | 
			
		||||
                "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87",
 | 
			
		||||
                "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7",
 | 
			
		||||
                "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430",
 | 
			
		||||
                "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255",
 | 
			
		||||
                "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==4.7.6"
 | 
			
		||||
        },
 | 
			
		||||
        "oauthlib": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
 | 
			
		||||
@ -706,10 +696,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "prompt-toolkit": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:822f4605f28f7d2ba6b0b09a31e25e140871e96364d1d377667b547bb3bf4489",
 | 
			
		||||
                "sha256:83074ee28ad4ba6af190593d4d4c607ff525272a504eb159199b6dd9f950c950"
 | 
			
		||||
                "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c",
 | 
			
		||||
                "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.0.7"
 | 
			
		||||
            "version": "==3.0.8"
 | 
			
		||||
        },
 | 
			
		||||
        "psycopg2-binary": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -723,6 +713,7 @@
 | 
			
		||||
                "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
 | 
			
		||||
                "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
 | 
			
		||||
                "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
 | 
			
		||||
                "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83",
 | 
			
		||||
                "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
 | 
			
		||||
                "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
 | 
			
		||||
                "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
 | 
			
		||||
@ -892,6 +883,13 @@
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.8.1"
 | 
			
		||||
        },
 | 
			
		||||
        "python-dotenv": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e",
 | 
			
		||||
                "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.15.0"
 | 
			
		||||
        },
 | 
			
		||||
        "pytz": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
 | 
			
		||||
@ -982,6 +980,8 @@
 | 
			
		||||
                "sha256:a873e4d4954f865dcb60bdc4914af7eaae48fb56b60ed6daa1d6251c72f5337c",
 | 
			
		||||
                "sha256:ab845f1f51f7eb750a78937be9f79baea4a42c7960f5a94dde34e69f3cce1988",
 | 
			
		||||
                "sha256:b1e981fe1aff1fd11627f531524826a4dcc1f26c726235a52fcb62ded27d150f",
 | 
			
		||||
                "sha256:b4b0d31f2052b3f9f9b5327024dc629a253a83d8649d4734ca7f35b60ec3e9e5",
 | 
			
		||||
                "sha256:c6ac7e45367b1317e56f1461719c853fd6825226f45b835df7436bb04031fd8a",
 | 
			
		||||
                "sha256:daf21aa33ee9b351f66deed30a3d450ab55c14242cfdfcd377798e2c0d25c9f1",
 | 
			
		||||
                "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2",
 | 
			
		||||
                "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f"
 | 
			
		||||
@ -998,11 +998,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "sentry-sdk": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:1d91a0059d2d8bb980bec169578035c2f2d4b93cd8a4fb5b85c81904d33e221a",
 | 
			
		||||
                "sha256:6222cf623e404c3e62b8e0e81c6db866ac2d12a663b7c1f7963350e3f397522a"
 | 
			
		||||
                "sha256:0eea248408d36e8e7037c7b73827bea20b13a4375bf1719c406cae6fcbc094e3",
 | 
			
		||||
                "sha256:5cf36eb6b1dc62d55f3c64289792cbaebc8ffa5a9da14474f49b46d20caa7fc8"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==0.18.0"
 | 
			
		||||
            "version": "==0.19.1"
 | 
			
		||||
        },
 | 
			
		||||
        "service-identity": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1029,10 +1029,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "sqlparse": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
 | 
			
		||||
                "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
 | 
			
		||||
                "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
 | 
			
		||||
                "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.3.1"
 | 
			
		||||
            "version": "==0.4.1"
 | 
			
		||||
        },
 | 
			
		||||
        "structlog": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1100,20 +1100,37 @@
 | 
			
		||||
                "secure"
 | 
			
		||||
            ],
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
 | 
			
		||||
                "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
 | 
			
		||||
                "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
 | 
			
		||||
                "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "markers": null,
 | 
			
		||||
            "version": "==1.25.10"
 | 
			
		||||
            "version": "==1.25.11"
 | 
			
		||||
        },
 | 
			
		||||
        "uvicorn": {
 | 
			
		||||
            "extras": [
 | 
			
		||||
                "standard"
 | 
			
		||||
            ],
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:9a8f3501d977dedf77a540a0ec3cfadf409fe48eafca2c100d45d843ac62bc7b",
 | 
			
		||||
                "sha256:fbe9d1b764bc1f4599e1f150a0974feea0fd6380bec889c0d907ebd0a2e896a7"
 | 
			
		||||
                "sha256:8ff7495c74b8286a341526ff9efa3988ebab9a4b2f561c7438c3cb420992d7dd",
 | 
			
		||||
                "sha256:e5dbed4a8a44c7b04376021021d63798d6a7bcfae9c654a0b153577b93854fba"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==0.12.0"
 | 
			
		||||
            "version": "==0.12.2"
 | 
			
		||||
        },
 | 
			
		||||
        "uvloop": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd",
 | 
			
		||||
                "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e",
 | 
			
		||||
                "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09",
 | 
			
		||||
                "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726",
 | 
			
		||||
                "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891",
 | 
			
		||||
                "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7",
 | 
			
		||||
                "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5",
 | 
			
		||||
                "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95",
 | 
			
		||||
                "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.14.0"
 | 
			
		||||
        },
 | 
			
		||||
        "vine": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1122,6 +1139,13 @@
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==5.0.0"
 | 
			
		||||
        },
 | 
			
		||||
        "watchgod": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:59700dab7445aa8e6067a5b94f37bae90fc367554549b1ed2e9d0f4f38a90d2a",
 | 
			
		||||
                "sha256:e9cca0ab9c63f17fc85df9fd8bd18156ff00aff04ebe5976cee473f4968c6858"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.6"
 | 
			
		||||
        },
 | 
			
		||||
        "wcwidth": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
 | 
			
		||||
@ -1136,72 +1160,77 @@
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.57.0"
 | 
			
		||||
        },
 | 
			
		||||
        "yarl": {
 | 
			
		||||
        "websockets": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:04a54f126a0732af75e5edc9addeaa2113e2ca7c6fce8974a63549a70a25e50e",
 | 
			
		||||
                "sha256:3cc860d72ed989f3b1f3abbd6ecf38e412de722fb38b8f1b1a086315cf0d69c5",
 | 
			
		||||
                "sha256:5d84cc36981eb5a8533be79d6c43454c8e6a39ee3118ceaadbd3c029ab2ee580",
 | 
			
		||||
                "sha256:5e447e7f3780f44f890360ea973418025e8c0cdcd7d6a1b221d952600fd945dc",
 | 
			
		||||
                "sha256:61d3ea3c175fe45f1498af868879c6ffeb989d4143ac542163c45538ba5ec21b",
 | 
			
		||||
                "sha256:67c5ea0970da882eaf9efcf65b66792557c526f8e55f752194eff8ec722c75c2",
 | 
			
		||||
                "sha256:6f6898429ec3c4cfbef12907047136fd7b9e81a6ee9f105b45505e633427330a",
 | 
			
		||||
                "sha256:7ce35944e8e61927a8f4eb78f5bc5d1e6da6d40eadd77e3f79d4e9399e263921",
 | 
			
		||||
                "sha256:b7c199d2cbaf892ba0f91ed36d12ff41ecd0dde46cbf64ff4bfe997a3ebc925e",
 | 
			
		||||
                "sha256:c15d71a640fb1f8e98a1423f9c64d7f1f6a3a168f803042eaf3a5b5022fde0c1",
 | 
			
		||||
                "sha256:c22607421f49c0cb6ff3ed593a49b6a99c6ffdeaaa6c944cdda83c2393c8864d",
 | 
			
		||||
                "sha256:c604998ab8115db802cc55cb1b91619b2831a6128a62ca7eea577fc8ea4d3131",
 | 
			
		||||
                "sha256:d088ea9319e49273f25b1c96a3763bf19a882cff774d1792ae6fba34bd40550a",
 | 
			
		||||
                "sha256:db9eb8307219d7e09b33bcb43287222ef35cbcf1586ba9472b0a4b833666ada1",
 | 
			
		||||
                "sha256:e31fef4e7b68184545c3d68baec7074532e077bd1906b040ecfba659737df188",
 | 
			
		||||
                "sha256:e32f0fb443afcfe7f01f95172b66f279938fbc6bdaebe294b0ff6747fb6db020",
 | 
			
		||||
                "sha256:fcbe419805c9b20db9a51d33b942feddbf6e7fb468cb20686fd7089d4164c12a"
 | 
			
		||||
                "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5",
 | 
			
		||||
                "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5",
 | 
			
		||||
                "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308",
 | 
			
		||||
                "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb",
 | 
			
		||||
                "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a",
 | 
			
		||||
                "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c",
 | 
			
		||||
                "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170",
 | 
			
		||||
                "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422",
 | 
			
		||||
                "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8",
 | 
			
		||||
                "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485",
 | 
			
		||||
                "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f",
 | 
			
		||||
                "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8",
 | 
			
		||||
                "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc",
 | 
			
		||||
                "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779",
 | 
			
		||||
                "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989",
 | 
			
		||||
                "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1",
 | 
			
		||||
                "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092",
 | 
			
		||||
                "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824",
 | 
			
		||||
                "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d",
 | 
			
		||||
                "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55",
 | 
			
		||||
                "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36",
 | 
			
		||||
                "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.6.0"
 | 
			
		||||
            "version": "==8.1"
 | 
			
		||||
        },
 | 
			
		||||
        "zope.interface": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0103cba5ed09f27d2e3de7e48bb320338592e2fabc5ce1432cf33808eb2dfd8b",
 | 
			
		||||
                "sha256:14415d6979356629f1c386c8c4249b4d0082f2ea7f75871ebad2e29584bd16c5",
 | 
			
		||||
                "sha256:1ae4693ccee94c6e0c88a4568fb3b34af8871c60f5ba30cf9f94977ed0e53ddd",
 | 
			
		||||
                "sha256:1b87ed2dc05cb835138f6a6e3595593fea3564d712cb2eb2de963a41fd35758c",
 | 
			
		||||
                "sha256:269b27f60bcf45438e8683269f8ecd1235fa13e5411de93dae3b9ee4fe7f7bc7",
 | 
			
		||||
                "sha256:27d287e61639d692563d9dab76bafe071fbeb26818dd6a32a0022f3f7ca884b5",
 | 
			
		||||
                "sha256:39106649c3082972106f930766ae23d1464a73b7d30b3698c986f74bf1256a34",
 | 
			
		||||
                "sha256:40e4c42bd27ed3c11b2c983fecfb03356fae1209de10686d03c02c8696a1d90e",
 | 
			
		||||
                "sha256:461d4339b3b8f3335d7e2c90ce335eb275488c587b61aca4b305196dde2ff086",
 | 
			
		||||
                "sha256:4f98f70328bc788c86a6a1a8a14b0ea979f81ae6015dd6c72978f1feff70ecda",
 | 
			
		||||
                "sha256:558a20a0845d1a5dc6ff87cd0f63d7dac982d7c3be05d2ffb6322a87c17fa286",
 | 
			
		||||
                "sha256:562dccd37acec149458c1791da459f130c6cf8902c94c93b8d47c6337b9fb826",
 | 
			
		||||
                "sha256:5e86c66a6dea8ab6152e83b0facc856dc4d435fe0f872f01d66ce0a2131b7f1d",
 | 
			
		||||
                "sha256:60a207efcd8c11d6bbeb7862e33418fba4e4ad79846d88d160d7231fcb42a5ee",
 | 
			
		||||
                "sha256:645a7092b77fdbc3f68d3cc98f9d3e71510e419f54019d6e282328c0dd140dcd",
 | 
			
		||||
                "sha256:6874367586c020705a44eecdad5d6b587c64b892e34305bb6ed87c9bbe22a5e9",
 | 
			
		||||
                "sha256:74bf0a4f9091131de09286f9a605db449840e313753949fe07c8d0fe7659ad1e",
 | 
			
		||||
                "sha256:7b726194f938791a6691c7592c8b9e805fc6d1b9632a833b9c0640828cd49cbc",
 | 
			
		||||
                "sha256:8149ded7f90154fdc1a40e0c8975df58041a6f693b8f7edcd9348484e9dc17fe",
 | 
			
		||||
                "sha256:8cccf7057c7d19064a9e27660f5aec4e5c4001ffcf653a47531bde19b5aa2a8a",
 | 
			
		||||
                "sha256:911714b08b63d155f9c948da2b5534b223a1a4fc50bb67139ab68b277c938578",
 | 
			
		||||
                "sha256:a5f8f85986197d1dd6444763c4a15c991bfed86d835a1f6f7d476f7198d5f56a",
 | 
			
		||||
                "sha256:a744132d0abaa854d1aad50ba9bc64e79c6f835b3e92521db4235a1991176813",
 | 
			
		||||
                "sha256:af2c14efc0bb0e91af63d00080ccc067866fb8cbbaca2b0438ab4105f5e0f08d",
 | 
			
		||||
                "sha256:b054eb0a8aa712c8e9030065a59b5e6a5cf0746ecdb5f087cca5ec7685690c19",
 | 
			
		||||
                "sha256:b0becb75418f8a130e9d465e718316cd17c7a8acce6fe8fe07adc72762bee425",
 | 
			
		||||
                "sha256:b1d2ed1cbda2ae107283befd9284e650d840f8f7568cb9060b5466d25dc48975",
 | 
			
		||||
                "sha256:ba4261c8ad00b49d48bbb3b5af388bb7576edfc0ca50a49c11dcb77caa1d897e",
 | 
			
		||||
                "sha256:d1fe9d7d09bb07228650903d6a9dc48ea649e3b8c69b1d263419cc722b3938e8",
 | 
			
		||||
                "sha256:d7804f6a71fc2dda888ef2de266727ec2f3915373d5a785ed4ddc603bbc91e08",
 | 
			
		||||
                "sha256:da2844fba024dd58eaa712561da47dcd1e7ad544a257482392472eae1c86d5e5",
 | 
			
		||||
                "sha256:dcefc97d1daf8d55199420e9162ab584ed0893a109f45e438b9794ced44c9fd0",
 | 
			
		||||
                "sha256:dd98c436a1fc56f48c70882cc243df89ad036210d871c7427dc164b31500dc11",
 | 
			
		||||
                "sha256:e74671e43ed4569fbd7989e5eecc7d06dc134b571872ab1d5a88f4a123814e9f",
 | 
			
		||||
                "sha256:eb9b92f456ff3ec746cd4935b73c1117538d6124b8617bc0fe6fda0b3816e345",
 | 
			
		||||
                "sha256:ebb4e637a1fb861c34e48a00d03cffa9234f42bef923aec44e5625ffb9a8e8f9",
 | 
			
		||||
                "sha256:ef739fe89e7f43fb6494a43b1878a36273e5924869ba1d866f752c5812ae8d58",
 | 
			
		||||
                "sha256:f40db0e02a8157d2b90857c24d89b6310f9b6c3642369852cdc3b5ac49b92afc",
 | 
			
		||||
                "sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6",
 | 
			
		||||
                "sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8"
 | 
			
		||||
                "sha256:040f833694496065147e76581c0bf32b229a8b8c5eda120a0293afb008222387",
 | 
			
		||||
                "sha256:11198b44e4a3d8c7a80cc20bbdd65522258a4d82fe467cd310c9fcce8ffe2ed2",
 | 
			
		||||
                "sha256:121a9dccfe0c34be9c33b2c28225f0284f9b8e090580ffdff26c38fa16c7ffe1",
 | 
			
		||||
                "sha256:15f3082575e7e19581a80b866664f843719b647a7f7189c811ba7f9ab3309f83",
 | 
			
		||||
                "sha256:1d73d8986f948525536956ddd902e8a587a6846ebf4492117db16daba2865ddf",
 | 
			
		||||
                "sha256:208e82f73b242275b8566ac07a25158e7b21fa2f14e642a7881048430612d1a6",
 | 
			
		||||
                "sha256:2557833df892558123d791d6ff80ac4a2a0351f69c7421c7d5f0c07db72c8865",
 | 
			
		||||
                "sha256:25ea6906f9987d42546329d06f9750e69f0ee62307a2e7092955ed0758e64f09",
 | 
			
		||||
                "sha256:2c867914f7608674a555ac8daf20265644ac7be709e1da7d818089eebdfe544e",
 | 
			
		||||
                "sha256:2eadac20711a795d3bb7a2bfc87c04091cb5274d9c3281b43088a1227099b662",
 | 
			
		||||
                "sha256:37999d5ebd5d7bcd32438b725ca3470df05a7de8b1e9c0395bef24296b31ca99",
 | 
			
		||||
                "sha256:3ae8946d51789779f76e4fa326fd6676d8c19c1c3b4c4c5e9342807185264875",
 | 
			
		||||
                "sha256:5636cd7e60583b1608044ae4405e91575399430e66a5e1812f4bf30bcc55864e",
 | 
			
		||||
                "sha256:570e637cb6509998555f7e4af13006d89fad6c09cfc5c4795855385391063e4b",
 | 
			
		||||
                "sha256:590a40447ff3803c44050ce3c17c3958f11ca028dae3eacdd7b96775184394fa",
 | 
			
		||||
                "sha256:5aab51b9c1af1b8a84f40aa49ffe1684d41810b18d6c3e94aa50194e0a563f01",
 | 
			
		||||
                "sha256:5ffe4e0753393bcbcfc9a58133ed3d3a584634cc7cc2e667f8e3e6fbcbb2155d",
 | 
			
		||||
                "sha256:663982381bd428a275a841009e52983cc69c471a4979ce01344fadbf72cf353d",
 | 
			
		||||
                "sha256:6d06bf8e24dd6c473c4fbd8e16a83bd2e6d74add6ba25169043deb46d497b211",
 | 
			
		||||
                "sha256:6e5b9a4bf133cf1887b4a04c21c10ca9f548114f19c83957b2820d5c84254940",
 | 
			
		||||
                "sha256:70a2aed9615645bbe9d82c0f52bc7e676d2c0f8a63933d68418e0cb307f30536",
 | 
			
		||||
                "sha256:7750746421c4395e3d2cc3d805919f4f57bb9f2a9a0ccd955566a9341050a1b4",
 | 
			
		||||
                "sha256:7fc8708bc996e50fc7a9a2ad394e1f015348e389da26789fa6916630237143d7",
 | 
			
		||||
                "sha256:91abd2f080065a7c007540f6bbd93ef7bdbbffa6df4a4cfab3892d8623b83c98",
 | 
			
		||||
                "sha256:988f8b2281f3d95c66c01bdb141cefef1cc97db0d473c25c3fe2927ef00293b9",
 | 
			
		||||
                "sha256:9f56121d8a676802044584e6cc41250bbcde069d8adf725b9b817a6b0fd87f09",
 | 
			
		||||
                "sha256:a0f51536ce6e817a7aa25b0dca8b62feb210d4dc22cabfe8d1a92d47979372cd",
 | 
			
		||||
                "sha256:a1cdd7390d7f66ddcebf545203ca3728c4890d605f9f2697bc8e31437906e8e7",
 | 
			
		||||
                "sha256:b10eb4d0a77609679bf5f23708e20b1cd461a1643bd8ea42b1ca4149b1a5406c",
 | 
			
		||||
                "sha256:b274ac8e511b55ffb62e8292316bd2baa80c10e9fe811b1aa5ce81da6b6697d8",
 | 
			
		||||
                "sha256:c75b502af2c83fcfa2ee9c2257c1ba5806634a91a50db6129ff70e67c42c7e7b",
 | 
			
		||||
                "sha256:c9c8e53a5472b77f6a391b515c771105011f4b40740ce53af8428d1c8ca20004",
 | 
			
		||||
                "sha256:d867998a56c5133b9d31992beb699892e33b72150a8bf40f86cb52b8c606c83f",
 | 
			
		||||
                "sha256:eb566cab630ec176b2d6115ed08b2cf4d921b47caa7f02cca1b4a9525223ee94",
 | 
			
		||||
                "sha256:f61e6b95b414431ffe9dc460928fe9f351095fde074e2c2f5c6dda7b67a2192d",
 | 
			
		||||
                "sha256:f718675fd071bcce4f7cbf9250cbaaf64e2e91ef1b0b32a1af596e7412647556",
 | 
			
		||||
                "sha256:f9d4bfbd015e4b80dbad11c97049975f94592a6a0440e903ee647309f6252a1f",
 | 
			
		||||
                "sha256:fae50fc12a5e8541f6f1cc4ed744ca8f76a9543876cf63f618fb0e6aca8f8375",
 | 
			
		||||
                "sha256:fcf9c8edda7f7b2fd78069e97f4197815df5e871ec47b0f22580d330c6dec561",
 | 
			
		||||
                "sha256:fdedce3bc5360bd29d4bb90396e8d4d3c09af49bc0383909fe84c7233c5ee675"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==5.1.0"
 | 
			
		||||
            "version": "==5.1.2"
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "develop": {
 | 
			
		||||
@ -1250,18 +1279,17 @@
 | 
			
		||||
        },
 | 
			
		||||
        "black": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
 | 
			
		||||
                "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
 | 
			
		||||
                "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==19.10b0"
 | 
			
		||||
            "version": "==20.8b1"
 | 
			
		||||
        },
 | 
			
		||||
        "bump2version": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:477f0e18a0d58e50bb3dbc9af7fcda464fd0ebfc7a6151d8888602d7153171a0",
 | 
			
		||||
                "sha256:cd4f3a231305e405ed8944d8ff35bd742d9bc740ad62f483bd0ca21ce7131984"
 | 
			
		||||
                "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410",
 | 
			
		||||
                "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.0.0"
 | 
			
		||||
            "version": "==1.0.1"
 | 
			
		||||
        },
 | 
			
		||||
        "bumpversion": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1271,20 +1299,6 @@
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "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": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
 | 
			
		||||
@ -1294,11 +1308,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "colorama": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff",
 | 
			
		||||
                "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
 | 
			
		||||
                "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
 | 
			
		||||
                "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==0.4.3"
 | 
			
		||||
            "version": "==0.4.4"
 | 
			
		||||
        },
 | 
			
		||||
        "coverage": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1342,11 +1356,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:59c8125ca873ed3bdae9c12b146fbbd6ed8d0f743e4cf5f5817af50c51f1fc2f",
 | 
			
		||||
                "sha256:b5fbb818e751f660fa2d576d9f40c34a4c615c8b48dd383f5216e609f383371f"
 | 
			
		||||
                "sha256:a2127ad0150ec6966655bedf15dbbff9697cc86d61653db2da1afa506c0b04cc",
 | 
			
		||||
                "sha256:c93c28ccf1d094cbd00d860e83128a39e45d2c571d3b54361713aaaf9a94cac4"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.1.1"
 | 
			
		||||
            "version": "==3.1.2"
 | 
			
		||||
        },
 | 
			
		||||
        "django-debug-toolbar": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1356,14 +1370,6 @@
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.1.1"
 | 
			
		||||
        },
 | 
			
		||||
        "docker": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828",
 | 
			
		||||
                "sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==4.3.1"
 | 
			
		||||
        },
 | 
			
		||||
        "dodgy": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a",
 | 
			
		||||
@ -1373,10 +1379,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "flake8": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c",
 | 
			
		||||
                "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"
 | 
			
		||||
                "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
 | 
			
		||||
                "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.8.3"
 | 
			
		||||
            "version": "==3.8.4"
 | 
			
		||||
        },
 | 
			
		||||
        "flake8-polyfill": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1394,24 +1400,17 @@
 | 
			
		||||
        },
 | 
			
		||||
        "gitpython": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:080bf8e2cf1a2b907634761c2eaefbe83b69930c94c66ad11b65a8252959f912",
 | 
			
		||||
                "sha256:1858f4fd089abe92ae465f01d5aaaf55e937eca565fb2c1fce35a51b5f85c910"
 | 
			
		||||
                "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
 | 
			
		||||
                "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.1.8"
 | 
			
		||||
        },
 | 
			
		||||
        "idna": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
 | 
			
		||||
                "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.10"
 | 
			
		||||
            "version": "==3.1.11"
 | 
			
		||||
        },
 | 
			
		||||
        "iniconfig": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
 | 
			
		||||
                "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"
 | 
			
		||||
                "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
 | 
			
		||||
                "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.0.1"
 | 
			
		||||
            "version": "==1.1.1"
 | 
			
		||||
        },
 | 
			
		||||
        "isort": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1453,6 +1452,13 @@
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.6.1"
 | 
			
		||||
        },
 | 
			
		||||
        "mypy-extensions": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
 | 
			
		||||
                "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.4.3"
 | 
			
		||||
        },
 | 
			
		||||
        "packaging": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
 | 
			
		||||
@ -1470,10 +1476,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pbr": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea",
 | 
			
		||||
                "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15"
 | 
			
		||||
                "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
 | 
			
		||||
                "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==5.5.0"
 | 
			
		||||
            "version": "==5.5.1"
 | 
			
		||||
        },
 | 
			
		||||
        "pep8-naming": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1568,19 +1574,19 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pytest": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:1cd09785c0a50f9af72220dd12aa78cfa49cbffc356c61eab009ca189e018a33",
 | 
			
		||||
                "sha256:d010e24666435b39a4cf48740b039885642b6c273a3f77be3e7e03554d2806b7"
 | 
			
		||||
                "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
 | 
			
		||||
                "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==6.1.0"
 | 
			
		||||
            "version": "==6.1.2"
 | 
			
		||||
        },
 | 
			
		||||
        "pytest-django": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:4de6dbd077ed8606616958f77655fed0d5e3ee45159475671c7fa67596c6dba6",
 | 
			
		||||
                "sha256:c33e3d3da14d8409b125d825d4e74da17bb252191bf6fc3da6856e27a8b73ea4"
 | 
			
		||||
                "sha256:10e384e6b8912ded92db64c58be8139d9ae23fb8361e5fc139d8e4f8fc601bc2",
 | 
			
		||||
                "sha256:26f02c16d36fd4c8672390deebe3413678d89f30720c16efb8b2a6bf63b9041f"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.10.0"
 | 
			
		||||
            "version": "==4.1.0"
 | 
			
		||||
        },
 | 
			
		||||
        "pytz": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1608,36 +1614,34 @@
 | 
			
		||||
        },
 | 
			
		||||
        "regex": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef",
 | 
			
		||||
                "sha256:1fe0a41437bbd06063aa184c34804efa886bcc128222e9916310c92cd54c3b4c",
 | 
			
		||||
                "sha256:41bb65f54bba392643557e617316d0d899ed5b4946dccee1cb6696152b29844b",
 | 
			
		||||
                "sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c",
 | 
			
		||||
                "sha256:4707f3695b34335afdfb09be3802c87fa0bc27030471dbc082f815f23688bc63",
 | 
			
		||||
                "sha256:5533a959a1748a5c042a6da71fe9267a908e21eded7a4f373efd23a2cbdb0ecc",
 | 
			
		||||
                "sha256:5f18875ac23d9aa2f060838e8b79093e8bb2313dbaaa9f54c6d8e52a5df097be",
 | 
			
		||||
                "sha256:60b0e9e6dc45683e569ec37c55ac20c582973841927a85f2d8a7d20ee80216ab",
 | 
			
		||||
                "sha256:84e9407db1b2eb368b7ecc283121b5e592c9aaedbe8c78b1a2f1102eb2e21d19",
 | 
			
		||||
                "sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637",
 | 
			
		||||
                "sha256:9a02d0ae31d35e1ec12a4ea4d4cca990800f66a917d0fb997b20fbc13f5321fc",
 | 
			
		||||
                "sha256:9bc13e0d20b97ffb07821aa3e113f9998e84994fe4d159ffa3d3a9d1b805043b",
 | 
			
		||||
                "sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d",
 | 
			
		||||
                "sha256:ae91972f8ac958039920ef6e8769277c084971a142ce2b660691793ae44aae6b",
 | 
			
		||||
                "sha256:c570f6fa14b9c4c8a4924aaad354652366577b4f98213cf76305067144f7b100",
 | 
			
		||||
                "sha256:d23a18037313714fb3bb5a94434d3151ee4300bae631894b1ac08111abeaa4a3",
 | 
			
		||||
                "sha256:eaf548d117b6737df379fdd53bdde4f08870e66d7ea653e230477f071f861121",
 | 
			
		||||
                "sha256:ebbe29186a3d9b0c591e71b7393f1ae08c83cb2d8e517d2a822b8f7ec99dfd8b",
 | 
			
		||||
                "sha256:eda4771e0ace7f67f58bc5b560e27fb20f32a148cbc993b0c3835970935c2707",
 | 
			
		||||
                "sha256:f1b3afc574a3db3b25c89161059d857bd4909a1269b0b3cb3c904677c8c4a3f7",
 | 
			
		||||
                "sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f"
 | 
			
		||||
                "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a",
 | 
			
		||||
                "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f",
 | 
			
		||||
                "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb",
 | 
			
		||||
                "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5",
 | 
			
		||||
                "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c",
 | 
			
		||||
                "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53",
 | 
			
		||||
                "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12",
 | 
			
		||||
                "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c",
 | 
			
		||||
                "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd",
 | 
			
		||||
                "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504",
 | 
			
		||||
                "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b",
 | 
			
		||||
                "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e",
 | 
			
		||||
                "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582",
 | 
			
		||||
                "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0",
 | 
			
		||||
                "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c",
 | 
			
		||||
                "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0",
 | 
			
		||||
                "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd",
 | 
			
		||||
                "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e",
 | 
			
		||||
                "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786",
 | 
			
		||||
                "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de",
 | 
			
		||||
                "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789",
 | 
			
		||||
                "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520",
 | 
			
		||||
                "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa",
 | 
			
		||||
                "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b",
 | 
			
		||||
                "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625",
 | 
			
		||||
                "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2020.9.27"
 | 
			
		||||
        },
 | 
			
		||||
        "requests": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
 | 
			
		||||
                "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.24.0"
 | 
			
		||||
            "version": "==2020.10.28"
 | 
			
		||||
        },
 | 
			
		||||
        "requirements-detector": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1682,10 +1686,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "sqlparse": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
 | 
			
		||||
                "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
 | 
			
		||||
                "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
 | 
			
		||||
                "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.3.1"
 | 
			
		||||
            "version": "==0.4.1"
 | 
			
		||||
        },
 | 
			
		||||
        "stevedore": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1705,46 +1709,56 @@
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
 | 
			
		||||
                "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
 | 
			
		||||
                "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d",
 | 
			
		||||
                "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
 | 
			
		||||
                "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
 | 
			
		||||
                "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
 | 
			
		||||
                "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c",
 | 
			
		||||
                "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
 | 
			
		||||
                "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
 | 
			
		||||
                "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
 | 
			
		||||
                "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
 | 
			
		||||
                "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
 | 
			
		||||
                "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
 | 
			
		||||
                "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d",
 | 
			
		||||
                "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
 | 
			
		||||
                "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
 | 
			
		||||
                "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c",
 | 
			
		||||
                "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
 | 
			
		||||
                "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395",
 | 
			
		||||
                "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
 | 
			
		||||
                "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
 | 
			
		||||
                "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
 | 
			
		||||
                "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
 | 
			
		||||
                "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
 | 
			
		||||
                "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072",
 | 
			
		||||
                "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298",
 | 
			
		||||
                "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91",
 | 
			
		||||
                "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
 | 
			
		||||
                "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f",
 | 
			
		||||
                "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.4.1"
 | 
			
		||||
        },
 | 
			
		||||
        "typing-extensions": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
 | 
			
		||||
                "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
 | 
			
		||||
                "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.7.4.3"
 | 
			
		||||
        },
 | 
			
		||||
        "urllib3": {
 | 
			
		||||
            "extras": [
 | 
			
		||||
                "secure"
 | 
			
		||||
            ],
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
 | 
			
		||||
                "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
 | 
			
		||||
                "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
 | 
			
		||||
                "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "markers": null,
 | 
			
		||||
            "version": "==1.25.10"
 | 
			
		||||
        },
 | 
			
		||||
        "websocket-client": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
 | 
			
		||||
                "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.57.0"
 | 
			
		||||
            "version": "==1.25.11"
 | 
			
		||||
        },
 | 
			
		||||
        "wrapt": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,9 @@ As passbook is currently in a pre-stable, only the latest "stable" version is su
 | 
			
		||||
 | 
			
		||||
| Version  | Supported          |
 | 
			
		||||
| -------- | ------------------ |
 | 
			
		||||
| 0.9.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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -89,7 +89,7 @@ stages:
 | 
			
		||||
              versionSpec: '3.8'
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: npm install -g pyright
 | 
			
		||||
              script: npm install -g pyright@1.1.79
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: |
 | 
			
		||||
@ -169,6 +169,13 @@ stages:
 | 
			
		||||
              dockerComposeFile: 'scripts/ci.docker-compose.yml'
 | 
			
		||||
              action: 'Run services'
 | 
			
		||||
              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
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: |
 | 
			
		||||
@ -178,6 +185,7 @@ stages:
 | 
			
		||||
            displayName: Run full test suite
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: |
 | 
			
		||||
                export PB_TEST_K8S=true
 | 
			
		||||
                pipenv run coverage run ./manage.py test passbook -v 3
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            inputs:
 | 
			
		||||
@ -203,6 +211,13 @@ stages:
 | 
			
		||||
              dockerComposeFile: 'scripts/ci.docker-compose.yml'
 | 
			
		||||
              action: 'Run services'
 | 
			
		||||
              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
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: |
 | 
			
		||||
@ -219,11 +234,13 @@ stages:
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: |
 | 
			
		||||
                cd passbook/static/static
 | 
			
		||||
                yarn
 | 
			
		||||
                npm i
 | 
			
		||||
                npm run build
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            displayName: Run full test suite
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: |
 | 
			
		||||
                export PB_TEST_K8S=true
 | 
			
		||||
                pipenv run coverage run ./manage.py test e2e -v 3 --failfast
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            condition: always()
 | 
			
		||||
@ -231,6 +248,7 @@ stages:
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: |
 | 
			
		||||
                docker stop $(docker ps -aq)
 | 
			
		||||
                docker container prune -f
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            displayName: Prepare unittests and coverage for upload
 | 
			
		||||
            inputs:
 | 
			
		||||
@ -332,19 +350,3 @@ stages:
 | 
			
		||||
            repository: 'beryju/passbook-static'
 | 
			
		||||
            command: 'push'
 | 
			
		||||
            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:
 | 
			
		||||
  postgresql:
 | 
			
		||||
    image: postgres
 | 
			
		||||
    image: postgres:12
 | 
			
		||||
    volumes:
 | 
			
		||||
      - database:/var/lib/postgresql/data
 | 
			
		||||
    networks:
 | 
			
		||||
@ -12,18 +12,14 @@ services:
 | 
			
		||||
      - POSTGRES_PASSWORD=${PG_PASS:-thisisnotagoodpassword}
 | 
			
		||||
      - POSTGRES_USER=passbook
 | 
			
		||||
      - POSTGRES_DB=passbook
 | 
			
		||||
    labels:
 | 
			
		||||
      - traefik.enable=false
 | 
			
		||||
    env_file:
 | 
			
		||||
      - .env
 | 
			
		||||
  redis:
 | 
			
		||||
    image: redis
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal
 | 
			
		||||
    labels:
 | 
			
		||||
      - traefik.enable=false
 | 
			
		||||
  server:
 | 
			
		||||
    image: beryju/passbook:${PASSBOOK_TAG:-0.10.8-stable}
 | 
			
		||||
    image: beryju/passbook:${PASSBOOK_TAG:-0.12.8-stable}
 | 
			
		||||
    command: server
 | 
			
		||||
    environment:
 | 
			
		||||
      PASSBOOK_REDIS__HOST: redis
 | 
			
		||||
@ -34,35 +30,50 @@ services:
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal
 | 
			
		||||
    labels:
 | 
			
		||||
      - traefik.port=8000
 | 
			
		||||
      - traefik.docker.network=internal
 | 
			
		||||
      - traefik.frontend.rule=PathPrefix:/
 | 
			
		||||
      traefik.enable: 'true'
 | 
			
		||||
      traefik.docker.network: internal
 | 
			
		||||
      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
 | 
			
		||||
  worker:
 | 
			
		||||
    image: beryju/passbook:${PASSBOOK_TAG:-0.10.8-stable}
 | 
			
		||||
    image: beryju/passbook:${PASSBOOK_TAG:-0.12.8-stable}
 | 
			
		||||
    command: worker
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal
 | 
			
		||||
    labels:
 | 
			
		||||
      - traefik.enable=false
 | 
			
		||||
    environment:
 | 
			
		||||
      PASSBOOK_REDIS__HOST: redis
 | 
			
		||||
      PASSBOOK_POSTGRESQL__HOST: postgresql
 | 
			
		||||
      PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./backups:/backups
 | 
			
		||||
      - /var/run/docker.sock:/var/run/docker.sock
 | 
			
		||||
    env_file:
 | 
			
		||||
      - .env
 | 
			
		||||
  static:
 | 
			
		||||
    image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.8-stable}
 | 
			
		||||
    image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.8-stable}
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal
 | 
			
		||||
    labels:
 | 
			
		||||
      - traefik.frontend.rule=PathPrefix:/static, /robots.txt, /favicon.ico
 | 
			
		||||
      - traefik.port=80
 | 
			
		||||
      - traefik.docker.network=internal
 | 
			
		||||
      traefik.enable: 'true'
 | 
			
		||||
      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:
 | 
			
		||||
    image: traefik:1.7
 | 
			
		||||
    command: --api --docker --defaultentrypoints=https --entryPoints='Name:http Address::80 Redirect.EntryPoint:https' --entryPoints='Name:https Address::443 TLS'
 | 
			
		||||
    image: traefik:2.3
 | 
			
		||||
    command:
 | 
			
		||||
      - "--log.format=json"
 | 
			
		||||
      - "--api.insecure=true"
 | 
			
		||||
      - "--providers.docker=true"
 | 
			
		||||
      - "--providers.docker.exposedbydefault=false"
 | 
			
		||||
      - "--entrypoints.http.address=:80"
 | 
			
		||||
      - "--entrypoints.https.address=:443"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - /var/run/docker.sock:/var/run/docker.sock:ro
 | 
			
		||||
    ports:
 | 
			
		||||
 | 
			
		||||
@ -4,18 +4,23 @@ The User object has the following attributes:
 | 
			
		||||
 | 
			
		||||
- `username`: User's username.
 | 
			
		||||
- `email` User's email.
 | 
			
		||||
 - `name` User's display mame.
 | 
			
		||||
- `name` User's display name.
 | 
			
		||||
- `is_staff` Boolean field if user is staff.
 | 
			
		||||
- `is_active` Boolean field if user is active.
 | 
			
		||||
- `date_joined` Date user joined/was created.
 | 
			
		||||
- `password_change_date` Date password was last changed.
 | 
			
		||||
- `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
 | 
			
		||||
 | 
			
		||||
List all the User's group names:
 | 
			
		||||
 | 
			
		||||
```python
 | 
			
		||||
for group in user.groups.all():
 | 
			
		||||
for group in user.pb_groups.all():
 | 
			
		||||
    yield group.name
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -117,7 +117,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_stages_user_login.userloginstage",
 | 
			
		||||
            "attrs": {
 | 
			
		||||
                "session_duration": 0
 | 
			
		||||
                "session_duration": "seconds=-1"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -136,7 +136,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_stages_user_login.userloginstage",
 | 
			
		||||
            "attrs": {
 | 
			
		||||
                "session_duration": 0
 | 
			
		||||
                "session_duration": "seconds=-1"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_stages_user_login.userloginstage",
 | 
			
		||||
            "attrs": {
 | 
			
		||||
                "session_duration": 0
 | 
			
		||||
                "session_duration": "seconds=-1"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_stages_user_login.userloginstage",
 | 
			
		||||
            "attrs": {
 | 
			
		||||
                "session_duration": 0
 | 
			
		||||
                "session_duration": "seconds=-1"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
@ -95,7 +95,8 @@
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_flows.flowstagebinding",
 | 
			
		||||
            "attrs": {
 | 
			
		||||
                "re_evaluate_policies": false
 | 
			
		||||
                "evaluate_on_plan": false,
 | 
			
		||||
                "re_evaluate_policies": true
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -101,7 +101,7 @@
 | 
			
		||||
        {
 | 
			
		||||
            "identifiers": {
 | 
			
		||||
                "pk": "975d5502-1e22-4d10-b560-fbc5bd70ff4d",
 | 
			
		||||
                "name": "default-password-change-prompt"
 | 
			
		||||
                "name": "Change your password"
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_stages_prompt.promptstage",
 | 
			
		||||
            "attrs": {
 | 
			
		||||
@ -118,7 +118,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_stages_user_login.userloginstage",
 | 
			
		||||
            "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 deploy a different version run `echo PASSBOOK_TAG=0.10.8-stable >> .env`
 | 
			
		||||
To optionally deploy a different version run `echo PASSBOOK_TAG=0.12.8-stable >> .env`
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
```yaml
 | 
			
		||||
###################################
 | 
			
		||||
# Values directly affecting passbook
 | 
			
		||||
###################################
 | 
			
		||||
image:
 | 
			
		||||
  name: beryju/passbook
 | 
			
		||||
  name_static: beryju/passbook-static
 | 
			
		||||
  tag: 0.10.8-stable
 | 
			
		||||
 | 
			
		||||
nameOverride: ""
 | 
			
		||||
  tag: 0.12.8-stable
 | 
			
		||||
 | 
			
		||||
serverReplicas: 1
 | 
			
		||||
workerReplicas: 1
 | 
			
		||||
 | 
			
		||||
# Enable the Kubernetes integration which lets passbook deploy outposts into kubernetes
 | 
			
		||||
kubernetesIntegration: true
 | 
			
		||||
 | 
			
		||||
config:
 | 
			
		||||
  # 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
 | 
			
		||||
  error_reporting:
 | 
			
		||||
  errorReporting:
 | 
			
		||||
    enabled: false
 | 
			
		||||
    environment: customer
 | 
			
		||||
    send_pii: false
 | 
			
		||||
    sendPii: false
 | 
			
		||||
  # Log level used by web and worker
 | 
			
		||||
  # Can be either debug, info, warning, error
 | 
			
		||||
  log_level: warning
 | 
			
		||||
  logLevel: warning
 | 
			
		||||
 | 
			
		||||
# Enable Database Backups to S3
 | 
			
		||||
# backup:
 | 
			
		||||
#   access_key: access-key
 | 
			
		||||
#   secret_key: secret-key
 | 
			
		||||
#   accessKey: access-key
 | 
			
		||||
#   secretKey: secret-key
 | 
			
		||||
#   bucket: s3-bucket
 | 
			
		||||
#   region: eu-central-1
 | 
			
		||||
#   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
 | 
			
		||||
###################################
 | 
			
		||||
@ -57,16 +70,4 @@ redis:
 | 
			
		||||
      enabled: false
 | 
			
		||||
    # https://stackoverflow.com/a/59189742
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										59
									
								
								docs/integrations/services/home-assistant/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,59 @@
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
With the default Header of `X-Forwarded-Preferred-Username` matching is done on a username basis, so your Name in Home-Assistant and your username in passbook have to match.
 | 
			
		||||
 | 
			
		||||
If this is not the case, you can simply add an additional header for your user, which contains the Home-Assistant Name and authenticate based on that.
 | 
			
		||||
 | 
			
		||||
For example add this to your user's properties and set the Header to `X-pb-hass-user`.
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
additionalHeaders:
 | 
			
		||||
  X-pb-hass-user: some other name
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 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.
 | 
			
		||||
- `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
 | 
			
		||||
 | 
			
		||||
@ -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.
 | 
			
		||||
 | 
			
		||||
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 Sonarr.
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ The following placeholders will be used:
 | 
			
		||||
 | 
			
		||||
## 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, called for example "Tautulli Users". For this group, add the following attributes:
 | 
			
		||||
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
 | 
			
		||||
@ -31,13 +31,13 @@ Create an application in passbook. Create a Proxy provider with the following pa
 | 
			
		||||
 | 
			
		||||
    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 than where you are deploying the passbook proxy, set the value to `http://tautulli.company:3579`.
 | 
			
		||||
    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 a prefix for clarity.
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@ -47,4 +47,4 @@ In Tautulli, navigate to Settings and enable the "Show Advanced" option. Navigat
 | 
			
		||||
 | 
			
		||||
Save the settings, and restart Tautulli if prompted.
 | 
			
		||||
 | 
			
		||||
Afterwards, you need to deploy an Outpost in front of Tautulli, just like descried [here](../sonarr/index.md)
 | 
			
		||||
Afterwards, you need to deploy an Outpost in front of Tautulli, as descried [here](../sonarr/index.md)
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,10 @@ From https://en.wikipedia.org/wiki/VCenter
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
version: 3.5
 | 
			
		||||
version: '3.5'
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  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.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_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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,14 @@ The Proxy these extra headers to the application:
 | 
			
		||||
 | 
			
		||||
Header Name | Value
 | 
			
		||||
-------------|-------
 | 
			
		||||
X-Auth-Request-User | The user's unique identifier
 | 
			
		||||
X-Auth-Request-Email | The user's email address
 | 
			
		||||
X-Auth-Request-Preferred-Username | The user's username
 | 
			
		||||
X-Forwarded-User | The user's unique identifier (**not the username**)
 | 
			
		||||
X-Forwarded-Email | The user's email address
 | 
			
		||||
X-Forwarded-Preferred-Username | The user's username
 | 
			
		||||
X-Auth-Username | The user's username
 | 
			
		||||
 | 
			
		||||
Additionally, you can add more custom headers using `additionalHeaders` in the User or Group Properties, for example
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
additionalHeaders:
 | 
			
		||||
  X-additional-header: bar
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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.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.stages.email.models import EmailStage, EmailTemplates
 | 
			
		||||
from passbook.stages.identification.models import IdentificationStage
 | 
			
		||||
@ -23,7 +23,7 @@ class TestFlowsEnroll(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "mailhog/mailhog:v1.0.1",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/mailhog/mailhog:v1.0.1",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
@ -34,6 +34,7 @@ class TestFlowsEnroll(SeleniumTestCase):
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_enroll_2_step(self):
 | 
			
		||||
        """Test 2-step enroll flow"""
 | 
			
		||||
        # First stage fields
 | 
			
		||||
@ -104,7 +105,8 @@ class TestFlowsEnroll(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
        self.wait_for_url(self.url("passbook_core:user-settings"))
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text, "foo",
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text,
 | 
			
		||||
            "foo",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
 | 
			
		||||
@ -118,6 +120,7 @@ class TestFlowsEnroll(SeleniumTestCase):
 | 
			
		||||
            "foo@bar.baz",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    @override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend")
 | 
			
		||||
    def test_enroll_email(self):
 | 
			
		||||
        """Test enroll with Email verification"""
 | 
			
		||||
@ -208,7 +211,8 @@ class TestFlowsEnroll(SeleniumTestCase):
 | 
			
		||||
        self.driver.find_element(By.ID, "user-settings").click()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text, "foo",
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text,
 | 
			
		||||
            "foo",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
 | 
			
		||||
 | 
			
		||||
@ -5,13 +5,14 @@ from unittest.case import skipUnless
 | 
			
		||||
from selenium.webdriver.common.by import By
 | 
			
		||||
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")
 | 
			
		||||
class TestFlowsLogin(SeleniumTestCase):
 | 
			
		||||
    """test default login flow"""
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_login(self):
 | 
			
		||||
        """test default login flow"""
 | 
			
		||||
        self.driver.get(f"{self.live_server_url}/flows/default-authentication-flow/")
 | 
			
		||||
@ -21,5 +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(Keys.ENTER)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text, USER().username,
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text,
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ from selenium.webdriver.common.by import By
 | 
			
		||||
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.stages.otp_validate.models import OTPValidateStage
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ from passbook.stages.otp_validate.models import OTPValidateStage
 | 
			
		||||
class TestFlowsOTP(SeleniumTestCase):
 | 
			
		||||
    """test flow with otp stages"""
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_otp_validate(self):
 | 
			
		||||
        """test flow with otp stages"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
@ -48,9 +49,11 @@ class TestFlowsOTP(SeleniumTestCase):
 | 
			
		||||
        self.driver.find_element(By.ID, "id_code").send_keys(Keys.ENTER)
 | 
			
		||||
        self.wait_for_url(self.url("passbook_core:overview"))
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text, USER().username,
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text,
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_otp_totp_setup(self):
 | 
			
		||||
        """test TOTP Setup stage"""
 | 
			
		||||
        flow: Flow = Flow.objects.get(slug="default-authentication-flow")
 | 
			
		||||
@ -62,7 +65,8 @@ 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(Keys.ENTER)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text, USER().username,
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text,
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
 | 
			
		||||
@ -96,6 +100,7 @@ class TestFlowsOTP(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists())
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_otp_static_setup(self):
 | 
			
		||||
        """test Static OTP Setup stage"""
 | 
			
		||||
        flow: Flow = Flow.objects.get(slug="default-authentication-flow")
 | 
			
		||||
@ -107,7 +112,8 @@ 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(Keys.ENTER)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text, USER().username,
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text,
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ from unittest.case import skipUnless
 | 
			
		||||
from selenium.webdriver.common.by import By
 | 
			
		||||
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.flows.models import Flow, FlowDesignation
 | 
			
		||||
from passbook.providers.oauth2.generators import generate_client_secret
 | 
			
		||||
@ -16,6 +16,7 @@ from passbook.stages.password.models import PasswordStage
 | 
			
		||||
class TestFlowsStageSetup(SeleniumTestCase):
 | 
			
		||||
    """test stage setup flows"""
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_password_change(self):
 | 
			
		||||
        """test password change flow"""
 | 
			
		||||
        # Ensure that password stage has change_flow set
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ from selenium.webdriver.common.by import By
 | 
			
		||||
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.core.models import Application
 | 
			
		||||
from passbook.flows.models import Flow
 | 
			
		||||
from passbook.policies.expression.models import ExpressionPolicy
 | 
			
		||||
@ -33,7 +33,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        """Setup client grafana container which we test OAuth against"""
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "grafana/grafana:7.1.0",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/grafana/grafana:7.1.0",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
@ -61,6 +61,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_authorization_consent_implied(self):
 | 
			
		||||
        """test OAuth Provider flow (default authorization flow with implied consent)"""
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
@ -77,7 +78,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
 | 
			
		||||
            authorization_flow=authorization_flow,
 | 
			
		||||
        )
 | 
			
		||||
        Application.objects.create(
 | 
			
		||||
            name="Grafana", slug="grafana", provider=provider,
 | 
			
		||||
            name="Grafana",
 | 
			
		||||
            slug="grafana",
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get("http://localhost:3000")
 | 
			
		||||
@ -113,6 +116,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_authorization_consent_explicit(self):
 | 
			
		||||
        """test OAuth Provider flow (default authorization flow with explicit consent)"""
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
@ -129,7 +133,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
 | 
			
		||||
            authorization_flow=authorization_flow,
 | 
			
		||||
        )
 | 
			
		||||
        app = Application.objects.create(
 | 
			
		||||
            name="Grafana", slug="grafana", provider=provider,
 | 
			
		||||
            name="Grafana",
 | 
			
		||||
            slug="grafana",
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get("http://localhost:3000")
 | 
			
		||||
@ -143,13 +149,17 @@ class TestProviderOAuth2Github(SeleniumTestCase):
 | 
			
		||||
        sleep(1)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            app.name, self.driver.find_element(By.ID, "application-name").text,
 | 
			
		||||
            app.name,
 | 
			
		||||
            self.driver.find_element(By.ID, "application-name").text,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            "GitHub Compatibility: Access you Email addresses",
 | 
			
		||||
            self.driver.find_element(By.ID, "scope-user:email").text,
 | 
			
		||||
        )
 | 
			
		||||
        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:3000/?orgId=1")
 | 
			
		||||
        self.driver.get("http://localhost:3000/profile")
 | 
			
		||||
@ -176,6 +186,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_denied(self):
 | 
			
		||||
        """test OAuth Provider flow (default authorization flow, denied)"""
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
@ -192,7 +203,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
 | 
			
		||||
            authorization_flow=authorization_flow,
 | 
			
		||||
        )
 | 
			
		||||
        app = Application.objects.create(
 | 
			
		||||
            name="Grafana", slug="grafana", provider=provider,
 | 
			
		||||
            name="Grafana",
 | 
			
		||||
            slug="grafana",
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        negative_policy = ExpressionPolicy.objects.create(
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ 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
 | 
			
		||||
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
 | 
			
		||||
@ -47,7 +47,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "grafana/grafana:7.1.0",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/grafana/grafana:7.1.0",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
@ -80,6 +80,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_redirect_uri_error(self):
 | 
			
		||||
        """test OpenID Provider flow (invalid redirect URI, check error message)"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
@ -104,7 +105,9 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        Application.objects.create(
 | 
			
		||||
            name="Grafana", slug=APPLICATION_SLUG, provider=provider,
 | 
			
		||||
            name="Grafana",
 | 
			
		||||
            slug=APPLICATION_SLUG,
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get("http://localhost:3000")
 | 
			
		||||
@ -120,6 +123,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
            "Redirect URI Error",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_authorization_consent_implied(self):
 | 
			
		||||
        """test OpenID Provider flow (default authorization flow with implied consent)"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
@ -144,7 +148,9 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        Application.objects.create(
 | 
			
		||||
            name="Grafana", slug=APPLICATION_SLUG, provider=provider,
 | 
			
		||||
            name="Grafana",
 | 
			
		||||
            slug=APPLICATION_SLUG,
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get("http://localhost:3000")
 | 
			
		||||
@ -179,6 +185,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
            USER().email,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_authorization_logout(self):
 | 
			
		||||
        """test OpenID Provider flow with logout"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
@ -203,7 +210,9 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        Application.objects.create(
 | 
			
		||||
            name="Grafana", slug=APPLICATION_SLUG, provider=provider,
 | 
			
		||||
            name="Grafana",
 | 
			
		||||
            slug=APPLICATION_SLUG,
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get("http://localhost:3000")
 | 
			
		||||
@ -246,6 +255,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        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)
 | 
			
		||||
@ -270,7 +280,9 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        app = Application.objects.create(
 | 
			
		||||
            name="Grafana", slug=APPLICATION_SLUG, provider=provider,
 | 
			
		||||
            name="Grafana",
 | 
			
		||||
            slug=APPLICATION_SLUG,
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get("http://localhost:3000")
 | 
			
		||||
@ -282,7 +294,8 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
        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,
 | 
			
		||||
            app.name,
 | 
			
		||||
            self.driver.find_element(By.ID, "application-name").text,
 | 
			
		||||
        )
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
            ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
 | 
			
		||||
@ -316,6 +329,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
            USER().email,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_authorization_denied(self):
 | 
			
		||||
        """test OpenID Provider flow (default authorization with access deny)"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
@ -340,7 +354,9 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        app = Application.objects.create(
 | 
			
		||||
            name="Grafana", slug=APPLICATION_SLUG, provider=provider,
 | 
			
		||||
            name="Grafana",
 | 
			
		||||
            slug=APPLICATION_SLUG,
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        negative_policy = ExpressionPolicy.objects.create(
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ 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
 | 
			
		||||
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
 | 
			
		||||
@ -53,7 +53,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        client: DockerClient = from_env()
 | 
			
		||||
        client.images.pull("beryju/oidc-test-client")
 | 
			
		||||
        container = client.containers.run(
 | 
			
		||||
            image="beryju/oidc-test-client",
 | 
			
		||||
            image="docker.beryju.org/proxy/beryju/oidc-test-client",
 | 
			
		||||
            detach=True,
 | 
			
		||||
            network_mode="host",
 | 
			
		||||
            auto_remove=True,
 | 
			
		||||
@ -76,6 +76,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
            LOGGER.info("Container failed healthcheck")
 | 
			
		||||
            sleep(1)
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_redirect_uri_error(self):
 | 
			
		||||
        """test OpenID Provider flow (invalid redirect URI, check error message)"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
@ -100,7 +101,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        Application.objects.create(
 | 
			
		||||
            name=self.application_slug, slug=self.application_slug, provider=provider,
 | 
			
		||||
            name=self.application_slug,
 | 
			
		||||
            slug=self.application_slug,
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
        self.container = self.setup_client()
 | 
			
		||||
 | 
			
		||||
@ -117,6 +120,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
            "Redirect URI Error",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_authorization_consent_implied(self):
 | 
			
		||||
        """test OpenID Provider flow (default authorization flow with implied consent)"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
@ -141,7 +145,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        Application.objects.create(
 | 
			
		||||
            name=self.application_slug, slug=self.application_slug, provider=provider,
 | 
			
		||||
            name=self.application_slug,
 | 
			
		||||
            slug=self.application_slug,
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
        self.container = self.setup_client()
 | 
			
		||||
 | 
			
		||||
@ -165,6 +171,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
 | 
			
		||||
        self.assertEqual(body["UserInfo"]["email"], USER().email)
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_authorization_consent_explicit(self):
 | 
			
		||||
        """test OpenID Provider flow (default authorization flow with explicit consent)"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
@ -189,7 +196,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        app = Application.objects.create(
 | 
			
		||||
            name=self.application_slug, slug=self.application_slug, provider=provider,
 | 
			
		||||
            name=self.application_slug,
 | 
			
		||||
            slug=self.application_slug,
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
        self.container = self.setup_client()
 | 
			
		||||
 | 
			
		||||
@ -202,7 +211,8 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        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,
 | 
			
		||||
            app.name,
 | 
			
		||||
            self.driver.find_element(By.ID, "application-name").text,
 | 
			
		||||
        )
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
            ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
 | 
			
		||||
@ -222,6 +232,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
 | 
			
		||||
        self.assertEqual(body["UserInfo"]["email"], USER().email)
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_authorization_denied(self):
 | 
			
		||||
        """test OpenID Provider flow (default authorization with access deny)"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
@ -246,7 +257,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider.save()
 | 
			
		||||
        app = Application.objects.create(
 | 
			
		||||
            name=self.application_slug, slug=self.application_slug, provider=provider,
 | 
			
		||||
            name=self.application_slug,
 | 
			
		||||
            slug=self.application_slug,
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        negative_policy = ExpressionPolicy.objects.create(
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
"""Proxy and Outpost e2e tests"""
 | 
			
		||||
from dataclasses import asdict
 | 
			
		||||
from sys import platform
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Any, Dict, Optional
 | 
			
		||||
@ -10,11 +11,16 @@ from docker.models.containers import Container
 | 
			
		||||
from selenium.webdriver.common.by import By
 | 
			
		||||
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.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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -30,7 +36,7 @@ class TestProviderProxy(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "traefik/whoami:latest",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/traefik/whoami:latest",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
@ -39,19 +45,19 @@ class TestProviderProxy(SeleniumTestCase):
 | 
			
		||||
    def start_proxy(self, outpost: Outpost) -> Container:
 | 
			
		||||
        """Start proxy container based on outpost created"""
 | 
			
		||||
        client: DockerClient = from_env()
 | 
			
		||||
        client.images.pull("beryju/oidc-test-client")
 | 
			
		||||
        container = client.containers.run(
 | 
			
		||||
            image="beryju/passbook-proxy:latest",
 | 
			
		||||
            image=f"beryju/passbook-proxy:{__version__}",
 | 
			
		||||
            detach=True,
 | 
			
		||||
            network_mode="host",
 | 
			
		||||
            auto_remove=True,
 | 
			
		||||
            environment={
 | 
			
		||||
                "PASSBOOK_HOST": self.live_server_url,
 | 
			
		||||
                "PASSBOOK_TOKEN": outpost.token.token_uuid.hex,
 | 
			
		||||
                "PASSBOOK_TOKEN": outpost.token.key,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        return container
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_proxy_simple(self):
 | 
			
		||||
        """Test simple outpost setup with single provider"""
 | 
			
		||||
        proxy: ProxyProvider = ProxyProvider.objects.create(
 | 
			
		||||
@ -80,7 +86,9 @@ class TestProviderProxy(SeleniumTestCase):
 | 
			
		||||
        # Wait until outpost healthcheck succeeds
 | 
			
		||||
        healthcheck_retries = 0
 | 
			
		||||
        while healthcheck_retries < 50:
 | 
			
		||||
            if outpost.deployment_health:
 | 
			
		||||
            if len(outpost.state) > 0:
 | 
			
		||||
                state = outpost.state[0]
 | 
			
		||||
                if state.last_seen:
 | 
			
		||||
                    break
 | 
			
		||||
            healthcheck_retries += 1
 | 
			
		||||
            sleep(0.5)
 | 
			
		||||
@ -103,28 +111,7 @@ class TestProviderProxy(SeleniumTestCase):
 | 
			
		||||
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
 | 
			
		||||
    """Test Proxy connectivity over websockets"""
 | 
			
		||||
 | 
			
		||||
    proxy_container: Container
 | 
			
		||||
 | 
			
		||||
    def tearDown(self) -> None:
 | 
			
		||||
        self.proxy_container.kill()
 | 
			
		||||
        super().tearDown()
 | 
			
		||||
 | 
			
		||||
    def start_proxy(self, outpost: Outpost) -> Container:
 | 
			
		||||
        """Start proxy container based on outpost created"""
 | 
			
		||||
        client: DockerClient = from_env()
 | 
			
		||||
        client.images.pull("beryju/oidc-test-client")
 | 
			
		||||
        container = client.containers.run(
 | 
			
		||||
            image="beryju/passbook-proxy:latest",
 | 
			
		||||
            detach=True,
 | 
			
		||||
            network_mode="host",
 | 
			
		||||
            auto_remove=True,
 | 
			
		||||
            environment={
 | 
			
		||||
                "PASSBOOK_HOST": self.live_server_url,
 | 
			
		||||
                "PASSBOOK_TOKEN": outpost.token.token_uuid.hex,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        return container
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_proxy_connectivity(self):
 | 
			
		||||
        """Test proxy connectivity over websocket"""
 | 
			
		||||
        SeleniumTestCase().apply_default_data()
 | 
			
		||||
@ -144,20 +131,27 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
 | 
			
		||||
        outpost: Outpost = Outpost.objects.create(
 | 
			
		||||
            name="proxy_outpost",
 | 
			
		||||
            type=OutpostType.PROXY,
 | 
			
		||||
            deployment_type=OutpostDeploymentType.CUSTOM,
 | 
			
		||||
            deployment_type=OutpostDeploymentType.DOCKER,
 | 
			
		||||
            _config=asdict(
 | 
			
		||||
                OutpostConfig(passbook_host=self.live_server_url, log_level="debug")
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        outpost.providers.add(proxy)
 | 
			
		||||
        outpost.save()
 | 
			
		||||
 | 
			
		||||
        self.proxy_container = self.start_proxy(outpost)
 | 
			
		||||
 | 
			
		||||
        # Wait until outpost healthcheck succeeds
 | 
			
		||||
        healthcheck_retries = 0
 | 
			
		||||
        while healthcheck_retries < 50:
 | 
			
		||||
            if outpost.deployment_health:
 | 
			
		||||
            if len(outpost.state) > 0:
 | 
			
		||||
                state = outpost.state[0]
 | 
			
		||||
                if state.last_seen and state.version:
 | 
			
		||||
                    break
 | 
			
		||||
            healthcheck_retries += 1
 | 
			
		||||
            sleep(0.5)
 | 
			
		||||
 | 
			
		||||
        self.assertIsNotNone(outpost.deployment_health)
 | 
			
		||||
        self.assertEqual(outpost.deployment_version.get("version"), __version__)
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ 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
 | 
			
		||||
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
 | 
			
		||||
@ -38,7 +38,7 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        client: DockerClient = from_env()
 | 
			
		||||
        client.images.pull("beryju/oidc-test-client")
 | 
			
		||||
        container = client.containers.run(
 | 
			
		||||
            image="beryju/saml-test-sp",
 | 
			
		||||
            image="docker.beryju.org/proxy/beryju/saml-test-sp",
 | 
			
		||||
            detach=True,
 | 
			
		||||
            network_mode="host",
 | 
			
		||||
            auto_remove=True,
 | 
			
		||||
@ -66,6 +66,7 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
            LOGGER.info("Container failed healthcheck")
 | 
			
		||||
            sleep(1)
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_sp_initiated_implicit(self):
 | 
			
		||||
        """test SAML Provider flow SP-initiated flow (implicit consent)"""
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
@ -84,7 +85,9 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        provider.property_mappings.set(SAMLPropertyMapping.objects.all())
 | 
			
		||||
        provider.save()
 | 
			
		||||
        Application.objects.create(
 | 
			
		||||
            name="SAML", slug="passbook-saml", provider=provider,
 | 
			
		||||
            name="SAML",
 | 
			
		||||
            slug="passbook-saml",
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
        self.container = self.setup_client(provider)
 | 
			
		||||
        self.driver.get("http://localhost:9009")
 | 
			
		||||
@ -103,6 +106,7 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        self.assertEqual(body["attr"]["mail"], [USER().email])
 | 
			
		||||
        self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_sp_initiated_explicit(self):
 | 
			
		||||
        """test SAML Provider flow SP-initiated flow (explicit consent)"""
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
@ -121,7 +125,9 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        provider.property_mappings.set(SAMLPropertyMapping.objects.all())
 | 
			
		||||
        provider.save()
 | 
			
		||||
        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.driver.get("http://localhost:9009")
 | 
			
		||||
@ -131,7 +137,8 @@ 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(Keys.ENTER)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            app.name, self.driver.find_element(By.ID, "application-name").text,
 | 
			
		||||
            app.name,
 | 
			
		||||
            self.driver.find_element(By.ID, "application-name").text,
 | 
			
		||||
        )
 | 
			
		||||
        sleep(1)
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
 | 
			
		||||
@ -145,6 +152,7 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        self.assertEqual(body["attr"]["mail"], [USER().email])
 | 
			
		||||
        self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_idp_initiated_implicit(self):
 | 
			
		||||
        """test SAML Provider flow IdP-initiated flow (implicit consent)"""
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
@ -163,7 +171,9 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        provider.property_mappings.set(SAMLPropertyMapping.objects.all())
 | 
			
		||||
        provider.save()
 | 
			
		||||
        Application.objects.create(
 | 
			
		||||
            name="SAML", slug="passbook-saml", provider=provider,
 | 
			
		||||
            name="SAML",
 | 
			
		||||
            slug="passbook-saml",
 | 
			
		||||
            provider=provider,
 | 
			
		||||
        )
 | 
			
		||||
        self.container = self.setup_client(provider)
 | 
			
		||||
        self.driver.get(
 | 
			
		||||
@ -188,6 +198,7 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        self.assertEqual(body["attr"]["mail"], [USER().email])
 | 
			
		||||
        self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_sp_initiated_denied(self):
 | 
			
		||||
        """test SAML Provider flow SP-initiated flow (Policy denies access)"""
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
@ -209,7 +220,9 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        provider.property_mappings.set(SAMLPropertyMapping.objects.all())
 | 
			
		||||
        provider.save()
 | 
			
		||||
        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)
 | 
			
		||||
        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 yaml import safe_dump
 | 
			
		||||
 | 
			
		||||
from e2e.utils import SeleniumTestCase
 | 
			
		||||
from e2e.utils import SeleniumTestCase, retry
 | 
			
		||||
from passbook.flows.models import Flow
 | 
			
		||||
from passbook.providers.oauth2.generators import (
 | 
			
		||||
    generate_client_id,
 | 
			
		||||
@ -106,6 +106,7 @@ class TestSourceOAuth2(SeleniumTestCase):
 | 
			
		||||
            consumer_secret=self.client_secret,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_oauth_enroll(self):
 | 
			
		||||
        """test OAuth Source With With OIDC"""
 | 
			
		||||
        self.create_objects()
 | 
			
		||||
@ -144,19 +145,22 @@ class TestSourceOAuth2(SeleniumTestCase):
 | 
			
		||||
        self.driver.get(self.url("passbook_core:user-settings"))
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text, "foo",
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text,
 | 
			
		||||
            "foo",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
 | 
			
		||||
        )
 | 
			
		||||
        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.driver.find_element(By.ID, "id_email").get_attribute("value"),
 | 
			
		||||
            "admin@example.com",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    @override_settings(SESSION_COOKIE_SAMESITE="strict")
 | 
			
		||||
    def test_oauth_samesite_strict(self):
 | 
			
		||||
        """test OAuth Source With SameSite set to strict
 | 
			
		||||
@ -193,6 +197,7 @@ class TestSourceOAuth2(SeleniumTestCase):
 | 
			
		||||
            "Authentication Failed.",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_oauth_enroll_auth(self):
 | 
			
		||||
        """test OAuth Source With With OIDC (enroll and authenticate again)"""
 | 
			
		||||
        self.test_oauth_enroll()
 | 
			
		||||
@ -225,13 +230,15 @@ class TestSourceOAuth2(SeleniumTestCase):
 | 
			
		||||
        self.driver.get(self.url("passbook_core:user-settings"))
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text, "foo",
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text,
 | 
			
		||||
            "foo",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
 | 
			
		||||
        )
 | 
			
		||||
        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.driver.find_element(By.ID, "id_email").get_attribute("value"),
 | 
			
		||||
@ -251,7 +258,7 @@ class TestSourceOAuth1(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "beryju/oauth1-test-server",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/beryju/oauth1-test-server",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
@ -287,6 +294,7 @@ class TestSourceOAuth1(SeleniumTestCase):
 | 
			
		||||
            consumer_secret=self.client_secret,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_oauth_enroll(self):
 | 
			
		||||
        """test OAuth Source With With OIDC"""
 | 
			
		||||
        self.create_objects()
 | 
			
		||||
@ -313,11 +321,13 @@ class TestSourceOAuth1(SeleniumTestCase):
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, "[name='confirm']").click()
 | 
			
		||||
 | 
			
		||||
        # Wait until we've loaded the user info page
 | 
			
		||||
        sleep(2)
 | 
			
		||||
        self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
 | 
			
		||||
        self.driver.get(self.url("passbook_core:user-settings"))
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text, "example-user",
 | 
			
		||||
            self.driver.find_element(By.ID, "user-settings").text,
 | 
			
		||||
            "example-user",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"),
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ 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 SeleniumTestCase
 | 
			
		||||
from e2e.utils import SeleniumTestCase, retry
 | 
			
		||||
from passbook.crypto.models import CertificateKeyPair
 | 
			
		||||
from passbook.flows.models import Flow
 | 
			
		||||
from passbook.sources.saml.models import SAMLBindingTypes, SAMLSource
 | 
			
		||||
@ -75,7 +75,7 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "kristophjunge/test-saml-idp:1.15",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/kristophjunge/test-saml-idp:1.15",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
@ -92,13 +92,16 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_idp_redirect(self):
 | 
			
		||||
        """test SAML Source With redirect binding"""
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
        authentication_flow = Flow.objects.get(slug="default-source-authentication")
 | 
			
		||||
        enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
 | 
			
		||||
        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(
 | 
			
		||||
@ -139,13 +142,16 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_idp_post(self):
 | 
			
		||||
        """test SAML Source With post binding"""
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
        authentication_flow = Flow.objects.get(slug="default-source-authentication")
 | 
			
		||||
        enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
 | 
			
		||||
        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(
 | 
			
		||||
@ -188,13 +194,16 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @retry()
 | 
			
		||||
    def test_idp_post_auto(self):
 | 
			
		||||
        """test SAML Source With post binding (auto redirect)"""
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
        authentication_flow = Flow.objects.get(slug="default-source-authentication")
 | 
			
		||||
        enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
 | 
			
		||||
        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(
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										43
									
								
								e2e/utils.py
									
									
									
									
									
								
							
							
						
						@ -1,19 +1,22 @@
 | 
			
		||||
"""passbook e2e testing utilities"""
 | 
			
		||||
from functools import wraps
 | 
			
		||||
from glob import glob
 | 
			
		||||
from importlib.util import module_from_spec, spec_from_file_location
 | 
			
		||||
from inspect import getmembers, isfunction
 | 
			
		||||
from os import environ, makedirs
 | 
			
		||||
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.contrib.staticfiles.testing import StaticLiveServerTestCase
 | 
			
		||||
from django.db import connection, transaction
 | 
			
		||||
from django.db.utils import IntegrityError
 | 
			
		||||
from django.shortcuts import reverse
 | 
			
		||||
from django.test.testcases import TransactionTestCase
 | 
			
		||||
from docker import DockerClient, from_env
 | 
			
		||||
from docker.models.containers import Container
 | 
			
		||||
from selenium import webdriver
 | 
			
		||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException
 | 
			
		||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
 | 
			
		||||
from selenium.webdriver.remote.webdriver import WebDriver
 | 
			
		||||
from selenium.webdriver.support.ui import WebDriverWait
 | 
			
		||||
@ -123,3 +126,41 @@ class SeleniumTestCase(StaticLiveServerTestCase):
 | 
			
		||||
                            func(apps, schema_editor)
 | 
			
		||||
                        except IntegrityError:
 | 
			
		||||
                            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
 | 
			
		||||
appVersion: "0.10.8-stable"
 | 
			
		||||
description: A Helm chart for passbook.
 | 
			
		||||
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.
 | 
			
		||||
name: passbook
 | 
			
		||||
version: "0.10.8-stable"
 | 
			
		||||
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
 | 
			
		||||
home: https://passbook.beryju.org
 | 
			
		||||
sources:
 | 
			
		||||
  - https://github.com/BeryJu/passbook
 | 
			
		||||
version: "0.12.8-stable"
 | 
			
		||||
icon: https://raw.githubusercontent.com/BeryJu/passbook/master/docs/images/logo.svg
 | 
			
		||||
dependencies:
 | 
			
		||||
  - name: postgresql
 | 
			
		||||
    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.
 | 
			
		||||
*/}}
 | 
			
		||||
{{- define "passbook.name" -}}
 | 
			
		||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
 | 
			
		||||
{{- default .Chart.Name | trunc 63 | trimSuffix "-" -}}
 | 
			
		||||
{{- 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.
 | 
			
		||||
*/}}
 | 
			
		||||
{{- define "passbook.fullname" -}}
 | 
			
		||||
{{- if .Values.fullnameOverride -}}
 | 
			
		||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
 | 
			
		||||
{{- else -}}
 | 
			
		||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
 | 
			
		||||
{{- $name := default .Chart.Name -}}
 | 
			
		||||
{{- if contains $name .Release.Name -}}
 | 
			
		||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
 | 
			
		||||
{{- else -}}
 | 
			
		||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
 | 
			
		||||
{{- end -}}
 | 
			
		||||
{{- end -}}
 | 
			
		||||
{{- end -}}
 | 
			
		||||
 | 
			
		||||
{{/*
 | 
			
		||||
Create chart name and version as used by the chart label.
 | 
			
		||||
 | 
			
		||||
@ -7,13 +7,14 @@ data:
 | 
			
		||||
  POSTGRESQL__NAME: "{{ .Values.postgresql.postgresqlDatabase }}"
 | 
			
		||||
  POSTGRESQL__USER: "{{ .Values.postgresql.postgresqlUsername }}"
 | 
			
		||||
  {{- if .Values.backup }}
 | 
			
		||||
  POSTGRESQL__BACKUP__ACCESS_KEY: "{{ .Values.backup.access_key }}"
 | 
			
		||||
  POSTGRESQL__BACKUP__SECRET_KEY: "{{ .Values.backup.secret_key }}"
 | 
			
		||||
  POSTGRESQL__BACKUP__BUCKET: "{{ .Values.backup.bucket }}"
 | 
			
		||||
  POSTGRESQL__BACKUP__HOST: "{{ .Values.backup.host }}"
 | 
			
		||||
  POSTGRESQL__S3_BACKUP__ACCESS_KEY: "{{ .Values.backup.accessKey }}"
 | 
			
		||||
  POSTGRESQL__S3_BACKUP__SECRET_KEY: "{{ .Values.backup.secretKey }}"
 | 
			
		||||
  POSTGRESQL__S3_BACKUP__BUCKET: "{{ .Values.backup.bucket }}"
 | 
			
		||||
  POSTGRESQL__S3_BACKUP__REGION: "{{ .Values.backup.region }}"
 | 
			
		||||
  POSTGRESQL__S3_BACKUP__HOST: "{{ .Values.backup.host }}"
 | 
			
		||||
  {{- end}}
 | 
			
		||||
  REDIS__HOST: "{{ .Release.Name }}-redis-master"
 | 
			
		||||
  ERROR_REPORTING__ENABLED: "{{ .Values.config.error_reporting.enabled }}"
 | 
			
		||||
  ERROR_REPORTING__ENVIRONMENT: "{{ .Values.config.error_reporting.environment }}"
 | 
			
		||||
  ERROR_REPORTING__SEND_PII: "{{ .Values.config.error_reporting.send_pii }}"
 | 
			
		||||
  LOG_LEVEL: "{{ .Values.config.log_level }}"
 | 
			
		||||
  ERROR_REPORTING__ENABLED: "{{ .Values.config.errorReporting.enabled }}"
 | 
			
		||||
  ERROR_REPORTING__ENVIRONMENT: "{{ .Values.config.errorReporting.environment }}"
 | 
			
		||||
  ERROR_REPORTING__SEND_PII: "{{ .Values.config.errorReporting.sendPii }}"
 | 
			
		||||
  LOG_LEVEL: "{{ .Values.config.logLevel }}"
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,8 @@ metadata:
 | 
			
		||||
  name: {{ include "passbook.fullname" . }}-secret-key
 | 
			
		||||
data:
 | 
			
		||||
  monitoring_username: bW9uaXRvcg== # monitor in base64
 | 
			
		||||
  {{- if .Values.config.secret_key }}
 | 
			
		||||
  secret_key: {{ .Values.config.secret_key | b64enc | quote }}
 | 
			
		||||
  {{- if .Values.config.secretKey }}
 | 
			
		||||
  secret_key: {{ .Values.config.secretKey | b64enc | quote }}
 | 
			
		||||
  {{- else }}
 | 
			
		||||
  secret_key: {{ randAlphaNum 50 | b64enc | quote}}
 | 
			
		||||
  {{- 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
 | 
			
		||||
              httpHeaders:
 | 
			
		||||
                - name: Host
 | 
			
		||||
                  value: kubernetes-healthcheck-host
 | 
			
		||||
                  value: passbook-healthcheck-host
 | 
			
		||||
          readinessProbe:
 | 
			
		||||
            httpGet:
 | 
			
		||||
              path: /
 | 
			
		||||
              port: http
 | 
			
		||||
              httpHeaders:
 | 
			
		||||
                - name: Host
 | 
			
		||||
                  value: kubernetes-healthcheck-host
 | 
			
		||||
                  value: passbook-healthcheck-host
 | 
			
		||||
          resources:
 | 
			
		||||
            requests:
 | 
			
		||||
              cpu: 100m
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,9 @@ spec:
 | 
			
		||||
        app.kubernetes.io/instance: {{ .Release.Name }}
 | 
			
		||||
        k8s.passbook.beryju.org/component: worker
 | 
			
		||||
    spec:
 | 
			
		||||
      {{- if .Values.kubernetesIntegration }}
 | 
			
		||||
      serviceAccountName: {{ include "passbook.fullname" . }}-sa
 | 
			
		||||
      {{- end }}
 | 
			
		||||
      affinity:
 | 
			
		||||
        podAntiAffinity:
 | 
			
		||||
          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:
 | 
			
		||||
  name: beryju/passbook
 | 
			
		||||
  name_static: beryju/passbook-static
 | 
			
		||||
  tag: 0.10.8-stable
 | 
			
		||||
 | 
			
		||||
nameOverride: ""
 | 
			
		||||
  tag: 0.12.8-stable
 | 
			
		||||
 | 
			
		||||
serverReplicas: 1
 | 
			
		||||
workerReplicas: 1
 | 
			
		||||
 | 
			
		||||
# Enable the Kubernetes integration which lets passbook deploy outposts into kubernetes
 | 
			
		||||
kubernetesIntegration: true
 | 
			
		||||
 | 
			
		||||
config:
 | 
			
		||||
  # 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
 | 
			
		||||
  error_reporting:
 | 
			
		||||
  errorReporting:
 | 
			
		||||
    enabled: false
 | 
			
		||||
    environment: customer
 | 
			
		||||
    send_pii: false
 | 
			
		||||
    sendPii: false
 | 
			
		||||
  # Log level used by web and worker
 | 
			
		||||
  # Can be either debug, info, warning, error
 | 
			
		||||
  log_level: warning
 | 
			
		||||
  logLevel: warning
 | 
			
		||||
 | 
			
		||||
# Enable Database Backups to S3
 | 
			
		||||
# backup:
 | 
			
		||||
#   access_key: access-key
 | 
			
		||||
#   secret_key: secret-key
 | 
			
		||||
#   accessKey: access-key
 | 
			
		||||
#   secretKey: secret-key
 | 
			
		||||
#   bucket: s3-bucket
 | 
			
		||||
#   region: eu-central-1
 | 
			
		||||
#   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
 | 
			
		||||
###################################
 | 
			
		||||
@ -46,19 +59,5 @@ redis:
 | 
			
		||||
  cluster:
 | 
			
		||||
    enabled: false
 | 
			
		||||
  master:
 | 
			
		||||
    persistence:
 | 
			
		||||
      enabled: false
 | 
			
		||||
    # https://stackoverflow.com/a/59189742
 | 
			
		||||
    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
 | 
			
		||||
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
 | 
			
		||||
    gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
 | 
			
		||||
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
 | 
			
		||||
    # Run system migrations first, run normal migrations after
 | 
			
		||||
    python -m lifecycle.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
 | 
			
		||||
    python -m manage "$@"
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,9 @@ if __name__ == "__main__":
 | 
			
		||||
        # pyright: reportGeneralTypeIssues=false
 | 
			
		||||
        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)
 | 
			
		||||
            if migration.needs_migration():
 | 
			
		||||
                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';"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class To010Migration(BaseMigration):
 | 
			
		||||
class Migration(BaseMigration):
 | 
			
		||||
    def needs_migration(self) -> bool:
 | 
			
		||||
        self.cur.execute(
 | 
			
		||||
            "select * from information_schema.tables where table_name='oidc_provider_client'"
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ nav:
 | 
			
		||||
  - Installation:
 | 
			
		||||
    - docker-compose: installation/docker-compose.md
 | 
			
		||||
    - Kubernetes: installation/kubernetes.md
 | 
			
		||||
    - Reverse Proxy: installation/reverse-proxy.md
 | 
			
		||||
  - Flows:
 | 
			
		||||
      Overview: flow/flows.md
 | 
			
		||||
      Examples: flow/examples/examples.md
 | 
			
		||||
@ -46,6 +47,8 @@ nav:
 | 
			
		||||
    - Overview: policies/index.md
 | 
			
		||||
    - Expression: policies/expression.md
 | 
			
		||||
  - Integrations:
 | 
			
		||||
    - as Source:
 | 
			
		||||
      - Active Directory: integrations/sources/active-directory/index.md
 | 
			
		||||
    - as Provider:
 | 
			
		||||
      - Amazon Web Services: integrations/services/aws/index.md
 | 
			
		||||
      - GitLab: integrations/services/gitlab/index.md
 | 
			
		||||
@ -57,9 +60,13 @@ nav:
 | 
			
		||||
      - Ubuntu Landscape: integrations/services/ubuntu-landscape/index.md
 | 
			
		||||
      - Sonarr: integrations/services/sonarr/index.md
 | 
			
		||||
      - Tautulli: integrations/services/tautulli/index.md
 | 
			
		||||
  - Maintenance:
 | 
			
		||||
    - Backups: maintenance/backups/index.md
 | 
			
		||||
  - Upgrading:
 | 
			
		||||
    - to 0.9: upgrading/to-0.9.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:
 | 
			
		||||
    - Access problems: troubleshooting/access.md
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook"""
 | 
			
		||||
__version__ = "0.10.8-stable"
 | 
			
		||||
__version__ = "0.12.8-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
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.utils.datastructures import MultiValueDict
 | 
			
		||||
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):
 | 
			
		||||
    """Custom Textarea-based Widget that triggers a CodeMirror editor"""
 | 
			
		||||
 | 
			
		||||
@ -49,7 +75,9 @@ class YAMLField(forms.JSONField):
 | 
			
		||||
            converted = yaml.safe_load(value)
 | 
			
		||||
        except yaml.YAMLError:
 | 
			
		||||
            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):
 | 
			
		||||
            return YAMLString(converted)
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ from django.core.cache import cache
 | 
			
		||||
from requests import RequestException, get
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
			
		||||
from passbook.root.celery import CELERY_APP
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
@ -10,8 +11,8 @@ VERSION_CACHE_KEY = "passbook_latest_version"
 | 
			
		||||
VERSION_CACHE_TIMEOUT = 2 * 60 * 60  # 2 hours
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task()
 | 
			
		||||
def update_latest_version():
 | 
			
		||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
			
		||||
def update_latest_version(self: MonitoredTask):
 | 
			
		||||
    """Update latest version info"""
 | 
			
		||||
    try:
 | 
			
		||||
        data = get(
 | 
			
		||||
@ -19,5 +20,11 @@ def update_latest_version():
 | 
			
		||||
        ).json()
 | 
			
		||||
        tag_name = data.get("tag_name")
 | 
			
		||||
        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)
 | 
			
		||||
        self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <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>
 | 
			
		||||
                </div>
 | 
			
		||||
@ -62,18 +63,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Applications.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </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>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:overview' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:overview' %}">
 | 
			
		||||
                        {% trans 'System Status' %}
 | 
			
		||||
                        {% trans 'Overview' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
@ -146,6 +146,12 @@
 | 
			
		||||
                        {% trans 'Groups' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </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>
 | 
			
		||||
        </nav>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <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>
 | 
			
		||||
                </div>
 | 
			
		||||
@ -64,18 +65,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Certificates.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </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>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <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-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-secondary" type="button">{% trans 'Import' %}</a>
 | 
			
		||||
@ -69,18 +70,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Flows.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </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-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 %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <div class="pf-c-toolbar__bulk-select">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}"
 | 
			
		||||
                        class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
 | 
			
		||||
@ -61,18 +62,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Groups.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </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>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <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>
 | 
			
		||||
                </div>
 | 
			
		||||
@ -47,30 +48,41 @@
 | 
			
		||||
                            {{ outpost.providers.all.select_subclasses|join:", " }}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    {% with states=outpost.state %}
 | 
			
		||||
                    {% if states|length > 0 %}
 | 
			
		||||
                        <td role="cell">
 | 
			
		||||
                        {% with health=outpost.deployment_health %}
 | 
			
		||||
                        {% if health %}
 | 
			
		||||
                            <i class="fas fa-check pf-m-success"></i> {{ health|naturaltime }}
 | 
			
		||||
                            {% for state in states %}
 | 
			
		||||
                            <div>
 | 
			
		||||
                                {% if state.last_seen %}
 | 
			
		||||
                                <i class="fas fa-check pf-m-success"></i> {{ state.last_seen|naturaltime }}
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                            <i class="fas fa-times pf-m-danger"></i> Unhealthy
 | 
			
		||||
                                <i class="fas fa-times pf-m-danger"></i> {% trans 'Unhealthy' %}
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                        {% endwith %}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td role="cell">
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {% with ver=outpost.deployment_version %}
 | 
			
		||||
                            {% if ver.outdated %}
 | 
			
		||||
                                {% if ver.version == "" %}
 | 
			
		||||
                                <i class="fas fa-times pf-m-danger"></i> -
 | 
			
		||||
                            {% for state in states %}
 | 
			
		||||
                                <div>
 | 
			
		||||
                                    {% if not state.version %}
 | 
			
		||||
                                    <i class="fas fa-question-circle"></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 %}
 | 
			
		||||
                                <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 %}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </td>
 | 
			
		||||
                    {% 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 %}
 | 
			
		||||
                    {% endwith %}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </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-danger" href="{% url 'passbook_admin:outpost-delete' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
 | 
			
		||||
@ -83,18 +95,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Outposts.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </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>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
{% extends "administration/base.html" %}
 | 
			
		||||
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<section class="pf-c-page__main-section pf-m-light">
 | 
			
		||||
@ -10,139 +11,123 @@
 | 
			
		||||
</section>
 | 
			
		||||
<section class="pf-c-page__main-section">
 | 
			
		||||
    <div class="pf-l-gallery pf-m-gutter">
 | 
			
		||||
        <a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-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-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 class="pf-c-card__body">
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ application_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <table class="pf-c-table pf-m-compact" role="grid">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                        <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>
 | 
			
		||||
        </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__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-middleware"></i> {% trans 'Sources' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <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 pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <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 'Providers' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <a href="{% url 'passbook_admin:providers' %}">
 | 
			
		||||
                    <i class="fa fa-external-link-alt"> </i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if providers_without_application.exists %}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                <p class="pb-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ provider_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                <p class="pb-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ provider_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </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__header">
 | 
			
		||||
                <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 pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <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-infrastructure"></i> {% trans 'Policies' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <a href="{% url 'passbook_admin:policies' %}">
 | 
			
		||||
                    <i class="fa fa-external-link-alt"> </i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if policies_without_binding %}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                <p class="pb-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ policy_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'Policies without binding exist.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                <p class="pb-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ policy_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </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__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-migration"></i> {% trans 'Invitation' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <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 pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <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-user"></i> {% trans 'Users' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <a href="{% url 'passbook_admin:users' %}">
 | 
			
		||||
                    <i class="fa fa-external-link-alt"> </i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                <p class="pb-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ user_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </a>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <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-bundle"></i> {% trans 'Version' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <a href="https://github.com/BeryJu/passbook/releases" target="_blank">
 | 
			
		||||
                    <i class="fa fa-external-link-alt"> </i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                <p class="pb-aggregate-card">
 | 
			
		||||
                    {% if version >= version_latest %}
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ version }}
 | 
			
		||||
                    {% else %}
 | 
			
		||||
@ -161,97 +146,192 @@
 | 
			
		||||
            </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-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if worker_count < 1 %}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ worker_count }}
 | 
			
		||||
            <fetch-fill-slot class="pf-c-card__body" url="{% url 'passbook_api:admin_overview-list' %}" key="worker_count">
 | 
			
		||||
                <div slot="value < 1">
 | 
			
		||||
                    <p class="pb-aggregate-card">
 | 
			
		||||
                        <i class="fa fa-exclamation-triangle"></i> <span data-value></span>
 | 
			
		||||
                    </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 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>
 | 
			
		||||
 | 
			
		||||
        <a class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <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-server"></i> {% trans 'Cached Policies' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <a data-target="modal" data-modal="clearPolicyCache">
 | 
			
		||||
                    <i class="fa fa-trash"> </i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if cached_policies < 1 %}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                <p class="pb-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ cached_policies }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'No policies cached. Users may experience slow response times.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                <p class="pb-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ cached_policies }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </a>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <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-server"></i> {% trans 'Cached Flows' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <a data-target="modal" data-modal="clearFlowCache">
 | 
			
		||||
                    <i class="fa fa-trash"> </i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if cached_flows < 1 %}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                <p class="pb-aggregate-card">
 | 
			
		||||
                    <span class="fa fa-exclamation-triangle"></span> {{ cached_flows }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'No flows cached.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                <p class="pb-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ cached_flows }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </section>
 | 
			
		||||
    </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-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 Cache' %}?</h1>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Policy Cache' %}?</h1>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-modal-box__body" id="modal-description">
 | 
			
		||||
                <form method="post" id="clearForm">
 | 
			
		||||
                <form method="post" id="clear_policies">
 | 
			
		||||
                    {% csrf_token %}
 | 
			
		||||
                    <input type="hidden" name="clear">
 | 
			
		||||
                    <input type="hidden" name="clear_policies">
 | 
			
		||||
                    <p>
 | 
			
		||||
                        {% 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 %}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <h3>
 | 
			
		||||
                        {% blocktrans %}
 | 
			
		||||
                        This will also log you out.
 | 
			
		||||
                        {% endblocktrans %}
 | 
			
		||||
                    </h3>
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <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>
 | 
			
		||||
            </footer>
 | 
			
		||||
        </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 %}
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <div class="pf-c-toolbar__bulk-select">
 | 
			
		||||
                    <div class="pf-c-dropdown">
 | 
			
		||||
                        <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
@ -78,18 +79,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Policies.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="pf-c-dropdown">
 | 
			
		||||
                    <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% else %}
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <div class="pf-c-toolbar__bulk-select">
 | 
			
		||||
                    <div class="pf-c-dropdown">
 | 
			
		||||
                        <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
@ -72,18 +73,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Property Mappings.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="pf-c-dropdown">
 | 
			
		||||
                    <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <div class="pf-c-toolbar__bulk-select">
 | 
			
		||||
                    <div class="pf-c-dropdown">
 | 
			
		||||
                        <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
@ -91,18 +92,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Providers.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="pf-c-dropdown">
 | 
			
		||||
                    <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <div class="pf-c-toolbar__bulk-select">
 | 
			
		||||
                    <div class="pf-c-dropdown">
 | 
			
		||||
                        <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
@ -85,18 +86,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Sources.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="pf-c-dropdown">
 | 
			
		||||
                    <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
 | 
			
		||||
@ -11,8 +11,7 @@
 | 
			
		||||
            <i class="pf-icon pf-icon-plugged"></i>
 | 
			
		||||
            {% trans 'Stages' %}
 | 
			
		||||
        </h1>
 | 
			
		||||
        <p>{% trans "Stages are single steps of a Flow that a user is guided through." %}
 | 
			
		||||
        </p>
 | 
			
		||||
        <p>{% trans "Stages are single steps of a Flow that a user is guided through." %}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
 | 
			
		||||
@ -20,6 +19,7 @@
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <div class="pf-c-toolbar__bulk-select">
 | 
			
		||||
                    <div class="pf-c-dropdown">
 | 
			
		||||
                        <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
@ -81,18 +81,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Stages.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="pf-c-dropdown">
 | 
			
		||||
                    <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
 | 
			
		||||
@ -81,7 +81,7 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% else %}
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <div class="pf-c-toolbar__bulk-select">
 | 
			
		||||
                    <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>
 | 
			
		||||
@ -54,18 +55,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Invitations.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </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>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <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>
 | 
			
		||||
                </div>
 | 
			
		||||
@ -80,18 +81,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Stage Prompts.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </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>
 | 
			
		||||
            </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 %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                {% include 'partials/pagination.html' %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
 | 
			
		||||
            <thead>
 | 
			
		||||
                <tr role="row">
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Token' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Identifier' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'User' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Expires?' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
 | 
			
		||||
@ -35,9 +36,7 @@
 | 
			
		||||
                {% for token in object_list %}
 | 
			
		||||
                <tr role="row">
 | 
			
		||||
                    <th role="columnheader">
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <div>{{ token.pk.hex }}</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div>{{ token.identifier }}</div>
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <td role="cell">
 | 
			
		||||
                        <span>
 | 
			
		||||
@ -65,18 +64,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Tokens.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </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 %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <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>
 | 
			
		||||
                </div>
 | 
			
		||||
@ -53,7 +54,11 @@
 | 
			
		||||
                    </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-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_core:impersonate-init' user_id=user.pk %}">{% trans 'Impersonate' %}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
@ -61,18 +66,27 @@
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </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' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% 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__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">
 | 
			
		||||
                    {% trans 'No Users.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <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.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </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>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
{% extends "administration/base.html" %}
 | 
			
		||||
{% extends container_template|default:"administration/base.html" %}
 | 
			
		||||
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load passbook_utils %}
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ from passbook.admin.views import (
 | 
			
		||||
    stages_bindings,
 | 
			
		||||
    stages_invitations,
 | 
			
		||||
    stages_prompts,
 | 
			
		||||
    tasks,
 | 
			
		||||
    tokens,
 | 
			
		||||
    users,
 | 
			
		||||
)
 | 
			
		||||
@ -191,10 +192,20 @@ urlpatterns = [
 | 
			
		||||
    ),
 | 
			
		||||
    # 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(
 | 
			
		||||
        "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(
 | 
			
		||||
        "flows/<uuid:pk>/execute/",
 | 
			
		||||
@ -202,10 +213,14 @@ urlpatterns = [
 | 
			
		||||
        name="flow-execute",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "flows/<uuid:pk>/export/", flows.FlowExportView.as_view(), name="flow-export",
 | 
			
		||||
        "flows/<uuid:pk>/export/",
 | 
			
		||||
        flows.FlowExportView.as_view(),
 | 
			
		||||
        name="flow-export",
 | 
			
		||||
    ),
 | 
			
		||||
    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
 | 
			
		||||
    path(
 | 
			
		||||
@ -233,6 +248,10 @@ urlpatterns = [
 | 
			
		||||
    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>/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(
 | 
			
		||||
        "users/<int:pk>/reset/",
 | 
			
		||||
        users.UserPasswordResetView.as_view(),
 | 
			
		||||
@ -273,9 +292,15 @@ urlpatterns = [
 | 
			
		||||
        name="certificatekeypair-delete",
 | 
			
		||||
    ),
 | 
			
		||||
    # Outposts
 | 
			
		||||
    path("outposts/", outposts.OutpostListView.as_view(), name="outposts",),
 | 
			
		||||
    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(
 | 
			
		||||
        "outposts/<uuid:pk>/update/",
 | 
			
		||||
@ -287,4 +312,10 @@ urlpatterns = [
 | 
			
		||||
        outposts.OutpostDeleteView.as_view(),
 | 
			
		||||
        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 (
 | 
			
		||||
    BackSuccessUrlMixin,
 | 
			
		||||
    DeleteMessageView,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
)
 | 
			
		||||
from passbook.core.forms.applications import ApplicationForm
 | 
			
		||||
@ -20,7 +21,11 @@ from passbook.lib.views import CreateAssignPermView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ApplicationListView(
 | 
			
		||||
    LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    PermissionListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    ListView,
 | 
			
		||||
):
 | 
			
		||||
    """Show list of all applications"""
 | 
			
		||||
 | 
			
		||||
@ -29,6 +34,15 @@ class ApplicationListView(
 | 
			
		||||
    ordering = "name"
 | 
			
		||||
    template_name = "administration/application/list.html"
 | 
			
		||||
 | 
			
		||||
    search_fields = [
 | 
			
		||||
        "name",
 | 
			
		||||
        "slug",
 | 
			
		||||
        "meta_launch_url",
 | 
			
		||||
        "meta_icon_url",
 | 
			
		||||
        "meta_description",
 | 
			
		||||
        "meta_publisher",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ApplicationCreateView(
 | 
			
		||||
    SuccessMessageMixin,
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
 | 
			
		||||
from passbook.admin.views.utils import (
 | 
			
		||||
    BackSuccessUrlMixin,
 | 
			
		||||
    DeleteMessageView,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
)
 | 
			
		||||
from passbook.crypto.forms import CertificateKeyPairForm
 | 
			
		||||
@ -20,7 +21,11 @@ from passbook.lib.views import CreateAssignPermView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CertificateKeyPairListView(
 | 
			
		||||
    LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    PermissionListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    ListView,
 | 
			
		||||
):
 | 
			
		||||
    """Show list of all keypairs"""
 | 
			
		||||
 | 
			
		||||
@ -29,6 +34,8 @@ class CertificateKeyPairListView(
 | 
			
		||||
    ordering = "name"
 | 
			
		||||
    template_name = "administration/certificatekeypair/list.html"
 | 
			
		||||
 | 
			
		||||
    search_fields = ["name"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CertificateKeyPairCreateView(
 | 
			
		||||
    SuccessMessageMixin,
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
 | 
			
		||||
from passbook.admin.views.utils import (
 | 
			
		||||
    BackSuccessUrlMixin,
 | 
			
		||||
    DeleteMessageView,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
)
 | 
			
		||||
from passbook.flows.forms import FlowForm, FlowImportForm
 | 
			
		||||
@ -28,7 +29,11 @@ from passbook.lib.views import CreateAssignPermView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FlowListView(
 | 
			
		||||
    LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    PermissionListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    ListView,
 | 
			
		||||
):
 | 
			
		||||
    """Show list of all flows"""
 | 
			
		||||
 | 
			
		||||
@ -36,6 +41,7 @@ class FlowListView(
 | 
			
		||||
    permission_required = "passbook_flows.view_flow"
 | 
			
		||||
    ordering = "name"
 | 
			
		||||
    template_name = "administration/flow/list.html"
 | 
			
		||||
    search_fields = ["name", "slug", "designation", "title"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FlowCreateView(
 | 
			
		||||
@ -100,7 +106,9 @@ class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
 | 
			
		||||
        plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user})
 | 
			
		||||
        self.request.session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
        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 (
 | 
			
		||||
    BackSuccessUrlMixin,
 | 
			
		||||
    DeleteMessageView,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
)
 | 
			
		||||
from passbook.core.forms.groups import GroupForm
 | 
			
		||||
@ -20,7 +21,11 @@ from passbook.lib.views import CreateAssignPermView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GroupListView(
 | 
			
		||||
    LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    PermissionListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    ListView,
 | 
			
		||||
):
 | 
			
		||||
    """Show list of all groups"""
 | 
			
		||||
 | 
			
		||||
@ -28,6 +33,7 @@ class GroupListView(
 | 
			
		||||
    permission_required = "passbook_core.view_group"
 | 
			
		||||
    ordering = "name"
 | 
			
		||||
    template_name = "administration/group/list.html"
 | 
			
		||||
    search_fields = ["name", "attributes"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GroupCreateView(
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
 | 
			
		||||
from passbook.admin.views.utils import (
 | 
			
		||||
    BackSuccessUrlMixin,
 | 
			
		||||
    DeleteMessageView,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
)
 | 
			
		||||
from passbook.lib.views import CreateAssignPermView
 | 
			
		||||
@ -23,7 +24,11 @@ from passbook.outposts.models import Outpost, OutpostConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostListView(
 | 
			
		||||
    LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    PermissionListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    ListView,
 | 
			
		||||
):
 | 
			
		||||
    """Show list of all outposts"""
 | 
			
		||||
 | 
			
		||||
@ -31,6 +36,7 @@ class OutpostListView(
 | 
			
		||||
    permission_required = "passbook_outposts.view_outpost"
 | 
			
		||||
    ordering = "name"
 | 
			
		||||
    template_name = "administration/outpost/list.html"
 | 
			
		||||
    search_fields = ["name", "_config"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostCreateView(
 | 
			
		||||
 | 
			
		||||
@ -1,19 +1,22 @@
 | 
			
		||||
"""passbook administration overview"""
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
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 packaging.version import LegacyVersion, Version, parse
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook import __version__
 | 
			
		||||
from passbook.admin.mixins import AdminRequiredMixin
 | 
			
		||||
from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version
 | 
			
		||||
from passbook.core.models import Application, Provider, Source, User
 | 
			
		||||
from passbook.flows.models import Flow, Stage
 | 
			
		||||
from passbook.audit.models import Event, EventAction
 | 
			
		||||
from passbook.core.models import Provider, User
 | 
			
		||||
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):
 | 
			
		||||
@ -23,31 +26,46 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
 | 
			
		||||
 | 
			
		||||
    def post(self, *args, **kwargs):
 | 
			
		||||
        """Handle post (clear cache from modal)"""
 | 
			
		||||
        if "clear" in self.request.POST:
 | 
			
		||||
            cache.clear()
 | 
			
		||||
            return redirect(reverse("passbook_flows:default-authentication"))
 | 
			
		||||
        if "clear_policies" in self.request.POST:
 | 
			
		||||
            keys = cache.keys("policy_*")
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
    def get_latest_version(self) -> Union[LegacyVersion, Version]:
 | 
			
		||||
        """Get latest version from cache"""
 | 
			
		||||
        version_in_cache = cache.get(VERSION_CACHE_KEY)
 | 
			
		||||
        if not version_in_cache:
 | 
			
		||||
            if not settings.DEBUG:
 | 
			
		||||
                update_latest_version.delay()
 | 
			
		||||
            return parse(__version__)
 | 
			
		||||
        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):
 | 
			
		||||
        kwargs["application_count"] = len(Application.objects.all())
 | 
			
		||||
        kwargs["policy_count"] = len(Policy.objects.all())
 | 
			
		||||
        kwargs["user_count"] = len(User.objects.all()) - 1  # Remove anonymous user
 | 
			
		||||
        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_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(
 | 
			
		||||
            application=None
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ from passbook.admin.views.utils import (
 | 
			
		||||
    InheritanceCreateView,
 | 
			
		||||
    InheritanceListView,
 | 
			
		||||
    InheritanceUpdateView,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
)
 | 
			
		||||
from passbook.policies.models import Policy, PolicyBinding
 | 
			
		||||
@ -29,7 +30,11 @@ from passbook.policies.process import PolicyProcess, PolicyRequest
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyListView(
 | 
			
		||||
    LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    PermissionListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    InheritanceListView,
 | 
			
		||||
):
 | 
			
		||||
    """Show list of all policies"""
 | 
			
		||||
 | 
			
		||||
@ -37,6 +42,7 @@ class PolicyListView(
 | 
			
		||||
    permission_required = "passbook_policies.view_policy"
 | 
			
		||||
    ordering = "name"
 | 
			
		||||
    template_name = "administration/policy/list.html"
 | 
			
		||||
    search_fields = ["name"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyCreateView(
 | 
			
		||||
 | 
			
		||||
@ -14,13 +14,18 @@ from passbook.admin.views.utils import (
 | 
			
		||||
    InheritanceCreateView,
 | 
			
		||||
    InheritanceListView,
 | 
			
		||||
    InheritanceUpdateView,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
)
 | 
			
		||||
from passbook.core.models import PropertyMapping
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PropertyMappingListView(
 | 
			
		||||
    LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    PermissionListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    InheritanceListView,
 | 
			
		||||
):
 | 
			
		||||
    """Show list of all property_mappings"""
 | 
			
		||||
 | 
			
		||||
@ -28,6 +33,7 @@ class PropertyMappingListView(
 | 
			
		||||
    permission_required = "passbook_core.view_propertymapping"
 | 
			
		||||
    template_name = "administration/property_mapping/list.html"
 | 
			
		||||
    ordering = "name"
 | 
			
		||||
    search_fields = ["name", "expression"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PropertyMappingCreateView(
 | 
			
		||||
 | 
			
		||||
@ -14,13 +14,18 @@ from passbook.admin.views.utils import (
 | 
			
		||||
    InheritanceCreateView,
 | 
			
		||||
    InheritanceListView,
 | 
			
		||||
    InheritanceUpdateView,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
)
 | 
			
		||||
from passbook.core.models import Provider
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProviderListView(
 | 
			
		||||
    LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    PermissionListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    InheritanceListView,
 | 
			
		||||
):
 | 
			
		||||
    """Show list of all providers"""
 | 
			
		||||
 | 
			
		||||
@ -28,6 +33,7 @@ class ProviderListView(
 | 
			
		||||
    permission_required = "passbook_core.add_provider"
 | 
			
		||||
    template_name = "administration/provider/list.html"
 | 
			
		||||
    ordering = "id"
 | 
			
		||||
    search_fields = ["id", "name"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProviderCreateView(
 | 
			
		||||
 | 
			
		||||